This article is the fourth in a series on VUE in detail, which is supposedly a separate, larger chapter from the previous article on VUe-Transition in Detail. However goose, the length of the article is too long, so have to carry it out to write a separate. Those interested in previous articles in this series can click on the link below to send them in
- Vue Core – VDOM
- In Detail on Vue-Slot
- In Detail on Vue-Transition
The
In the real world, however, many elements needed to be displayed using transition effects, and it became clear that the
First, take an example
As usual, let’s start with an official example
<template>
<div id="list-demo">
<button v-on:click="add">Add</button>
<button v-on:click="remove">Remove</button>
<transition-group name="list" tag="p">
<span v-for="item in items" v-bind:key="item" class="list-item">
{{ item }}
</span>
</transition-group>
</div>
</template>
<script>
export default {
name: 'home',
data () {
return {
items: [1.2.3.4.5.6.7.8.9].nextNum: 10}},methods: {
randomIndex: function () {
return Math.floor(Math.random() * this.items.length)
},
add: function () {
this.items.splice(this.randomIndex(), 0.this.nextNum++)
},
remove: function () {
this.items.splice(this.randomIndex(), 1)}}}</script>
<style lang="scss">
.list-item {
display: inline-block;
margin-right: 10px;
}
.list-enter-active..list-leave-active {
transition: all 1s;
}
.list-enter..list-leave-to {
opacity: 0;
transform: translateY(30px);
}
</style>
Copy the code
The effect is shown below.
Next, I’ll take you through the design of the
Transition-group implementation
In contrast to the
However, their design concept is the same. They also provide a props and a series of hook functions for us to use as the interface for input => output conversion or binding
export default {
props,
beforeMount () {
// ...
},
render (h: Function) {
// ...
},
updated () {
// ...
},
methods: {
// ...}}Copy the code
1. Props & other import
// props
import { transitionProps, extractTransitionData } from './transition'
const props = extend({
tag: String.moveClass: String
}, transitionProps)
delete props.mode
// other import
import { warn, extend } from 'core/util/index'
import { addClass, removeClass } from '.. /class-util'
import { setActiveInstance } from 'core/instance/lifecycle'
import {
hasTransition,
getTransitionInfo,
transitionEndEvent,
addTransitionClass,
removeTransitionClass
} from '.. /transition-util'
Copy the code
2, render
First, we need to define a set of variables to facilitate subsequent operations
tag
: We can see from the overall context of the design above,<transition-group>
noabstract
Property that it will render a real node, then nodetag
Is required, and its default value isspan
.map
: Creates an empty objectprevChildren
: is used to store the last child noderawChildren
: get<transition-group>
The child node of the packagechildren
: stores the current child nodetransitionData
: Gets render data on the component
const tag: string = this.tag || this.$vnode.data.tag || 'span'
const map: Object = Object.create(null)
const prevChildren: Array<VNode> = this.prevChildren = this.children
const rawChildren: Array<VNode> = this.$slots.default || []
const children: Array<VNode> = this.children = []
const transitionData: Object = extractTransitionData(this)
Copy the code
This is followed by node traversal, where the binding of the transition animation is performed on each node in the list
- right
rawChildren
Go through and place eachvnode
Node extraction; - If the node contains any__vlistThe character
key
, it willvnode
Put it in thechildren
; - Then the transition data will be extracted
transitionData
Added to thevnode.data.transition
To animate the transitions of individual elements in the list
for (let i = 0; i < rawChildren.length; i++) {
const c: VNode = rawChildren[i]
if (c.tag) {
if(c.key ! =null && String(c.key).indexOf('__vlist')! = =0) { children.push(c) map[c.key] = c ; (c.data || (c.data = {})).transition = transitionData }else if(process.env.NODE_ENV ! = ='production') {
constopts: ? VNodeComponentOptions = c.componentOptionsconst name: string = opts ? (opts.Ctor.options.name || opts.tag || ' ') : c.tag
warn(`<transition-group> children must be keyed: <${name}> `)}}}Copy the code
PrevChildren is then processed
- if
prevChildren
If it exists, it is traversed and willtransitionData
Assigned tovnode.data.transition
After that, whenvnode
Child nodesenter
和leave
If there is a transition animation, the corresponding transition animation will be executed - Call native
getBoundingClientRect
Gets the element’s position information and records it tovnode.data.pos
中 - And then determine
map
Exists invnode.key
, if exists, willvnode
In thekept
Medium, otherwise throw toremoved
In the queue - Finally, place the rendered elements in
this.kept
,this.removed
Is used to record the nodes removed
if (prevChildren) {
const kept: Array<VNode> = []
const removed: Array<VNode> = []
for (let i = 0; i < prevChildren.length; i++) {
const c: VNode = prevChildren[i]
c.data.transition = transitionData
c.data.pos = c.elm.getBoundingClientRect()
if (map[c.key]) {
kept.push(c)
} else {
removed.push(c)
}
}
this.kept = h(tag, null, kept)
this.removed = removed
}
Copy the code
Finally,
return h(tag, null, children)
Copy the code
Update & methods
Now that we’ve tied the transition to the render phase for each element in the list, we’re going to update the list dynamically as each element changes. So how does that work? So let’s get this logic straight
I. Check whether you need to move
- First of all in
update
In the hook function, we get the last child nodeprevChildren
和moveClass
; Then judgechildren
Does it exist andchildren
Whether or nothas moveIf,children
Does not exist, orchildren
There is nomove
State, then there is no need to continueupdate
的move
Transition, directreturn
Can be
const children: Array<VNode> = this.prevChildren
const moveClass: string = this.moveClass || ((this.name || 'v') + '-move')
if(! children.length || !this.hasMove(children[0].elm, moveClass)) {
return
}
Copy the code
hasMove()
: This method is mainly used to judgeel
Whether the node hasmove
In the state.- The current setting
return
If not, it clones a DOM node and then removes all of the elements to avoid CSS transitions inside the elementtransitionClasses
- Next, add the clone node again
moveClass
And should bedisplay
Set tonone
And then add tothis.$el
上 - And then go through
getTransitionInfo
To get ittransition
Relevant information and then fromthis.$el
Remove it. At this point we’ve already gotten whether the node hastransform
The information of the
export consthasTransition = inBrowser && ! isIE9 hasMove (el: any,moveClass: string): boolean {
// If transition is not in your browser, or the browser does not support transition, return false
if(! hasTransition) {return false
}
// If the current instance context has _hasMove, return the value of _hasMove
if (this._hasMove) {
return this._hasMove
}
const clone: HTMLElement = el.cloneNode()
if (el._transitionClasses) {
el._transitionClasses.forEach((cls: string) = > { removeClass(clone, cls) })
}
addClass(clone, moveClass)
clone.style.display = 'none'
this.$el.appendChild(clone)
const info: Object = getTransitionInfo(clone)
this.$el.removeChild(clone)
return (this._hasMove = info.hasTransform)
}
Copy the code
Ii. Move Transition implementation
- Then, a wave of pre-processing is carried out for the child nodes. Here, three cycles are used for the processing of the child nodes, mainly to avoid the chaos of reading and writing DOM in each cycle and to help prevent the layout disorder
children.forEach(callPendingCbs)
children.forEach(recordPosition)
children.forEach(applyTranslation)
Copy the code
The three functions are treated as follows
callPendingCbs()
: Judge whether the transition animation of the previous frame of each node is completed. If not, it will be executed in advance_moveCb()
和_enterCb()
recordPosition()
: Records the new location of each nodeapplyTranslation()
: Obtain the old and new node positions respectively, and calculate the difference. If there is a difference, set the node’stransform
Property offsets the node to be moved to its previous position, which is the listmove
To prepare for
function callPendingCbs (c: VNode) {
if (c.elm._moveCb) {
c.elm._moveCb()
}
if (c.elm._enterCb) {
c.elm._enterCb()
}
}
function recordPosition (c: VNode) {
c.data.newPos = c.elm.getBoundingClientRect()
}
function applyTranslation (c: VNode) {
const oldPos = c.data.pos
const newPos = c.data.newPos
const dx = oldPos.left - newPos.left
const dy = oldPos.top - newPos.top
if (dx || dy) {
c.data.moved = true
const s = c.elm.style
s.transform = s.WebkitTransform = `translate(${dx}px,${dy}px)`
s.transitionDuration = '0s'}}Copy the code
- Next, a traversal implementation is performed on the child elements
move
Transition. It passes the fetch before iteratingdocument.body.offsetHeight
, which causes the calculation to occur, triggering backflow and allowing the browser to redraw - And then you start right
children
Traversal, period ifvnode.data.moved
为true
, the implementationaddTransitionClass
Plus the child nodemoveClass
And should bestyle.transform
Property clearing, since we have shifted the child node to the previous old position in the child node preprocessing, it will transition from the old position to the current position, that is what we wantmove
Transitional effect - And then it adds to the node
transitionend
Transition to the end of the listening event, in the event to do some cleaning operations
this._reflow = document.body.offsetHeight
children.forEach((c: VNode) = > {
if (c.data.moved) {
const el: any = c.elm
const s: any = el.style
addTransitionClass(el, moveClass)
s.transform = s.WebkitTransform = s.transitionDuration = ' '
el.addEventListener(transitionEndEvent, el._moveCb = function cb (e) {
if(e && e.target ! == el) {return
}
if(! e ||/transform$/.test(e.propertyName)) {
el.removeEventListener(transitionEndEvent, cb)
el._moveCb = null
removeTransitionClass(el, moveClass)
}
})
}
})
Copy the code
Note: I’ll summarize the browser backflow triggers, such as browser window changes, computing styles, adding or removing elements to the DOM, changing element classes, and so on
- Add or remove visible DOM elements
- Element position change
- Element size changes — margins, padding, borders, width, and height
- Content changes, such as changes in the width and height of calculated values caused by changes in the size of text, text, or images entered by the user in the input field
- Page render initialization
- Browser window size changes – when the resize event occurs
- Calculate the offsetWidth and offsetHeight properties
- Sets the value of the style property
4, beforeMount
Since VDOM does not guarantee a relative position of the removed element when the node diff is updated. So we need to override the update rendering logic in the beforeMount hook function to achieve the desired effect
- Get the instance itself first
update
Method to cache - We know that from up here
this.kept
Is the last node cached, and the number of nodes in it has been increasedtransition
Transition properties. So let’s go throughsetActiveInstance
Cache the current instance, and thenvnode
for__patch__
Operation to remove what needs to be removedvnode
And then executerestoreActiveInstance
Point the example to recovery - then
this.kept
Assigned tothis._vnode
To trigger the transition - Last to perform cache
update
Rendering node
beforeMount () {
const update = this._update
this._update = (vnode, hydrating) = > {
const restoreActiveInstance = setActiveInstance(this)
// force removing pass
this.__patch__(
this._vnode,
this.kept,
false.// hydrating
true // removeOnly (! important, avoids unnecessary moves)
)
this._vnode = this.kept
restoreActiveInstance()
update.call(this, vnode, hydrating)
}
}
Copy the code
setActiveInstance
export let activeInstance: any = null
export function setActiveInstance(vm: Component) {
const prevActiveInstance = activeInstance
activeInstance = vm
return () = > {
activeInstance = prevActiveInstance
}
}
Copy the code
The last
That’s about it, and we’re done parsing the transition related built-in components
Front end communication group: 731175396, warmly welcome all girls, Hanzi enthusiastically join