start
This time the question seems a bit strange: put two nouns that have no correlation together. As you know, transition-group is one of the built-in components of Vue, mainly used in list animations, but will it have any special connection with the Virtual Dom Diff algorithm? There is obviously a answer, so the next step is code decomposition.
origin
Mainly, I have been a little confused about Vue's Virtual Dom Diff algorithm recently, and then I turned on the computer to review the past and learn about the new ones; but I quickly noticed the code:
// removeOnly is a special flag used only by <transition-group> // to ensure removed elements stay in correct relative positions // during leaving transitions const canMove = !removeOnly
What the hell is removeOnly? Why do you feel that you didn't have any impression of this variable before? Let's look at the comments: removeOnly is only used on the transition-group component, the purpose is to ensure that the removed elements can maintain the correct relative position during the animation (please forgive me for the Zhazha translation); well, I ignored some details when I was reading the source code. But this aroused my great curiosity. In order to make the transition-group component, it actually has to deal with the Diff algorithm. What is the necessity of this component?
In-depth
First of all, if there is no interference from removeOnly, that is, when canMove is true, what will the normal Diff algorithm behave like:
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left } else if (isUndef(oldEndVnode)) { oldEndVnode = oldCh[--oldEndIdx] } else if (sameVnode(oldStartVnode, newStartVnode)) { patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] } else if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) canMove && (parentElm, , ()) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) canMove && (parentElm, , ) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] } else { if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) idxInOld = isDef() ? oldKeyToIdx[] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) if (isUndef(idxInOld)) { // New element createElm(newStartVnode, insertedVnodeQueue, parentElm, , false, newCh, newStartIdx) } else { vnodeToMove = oldCh[idxInOld] if (sameVnode(vnodeToMove, newStartVnode)) { patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) oldCh[idxInOld] = undefined canMove && (parentElm, , ) } else { // same key but different element. treat as new element createElm(newStartVnode, insertedVnodeQueue, parentElm, , false, newCh, newStartIdx) } } newStartVnode = newCh[++newStartIdx] } }
- First, the oldStartVnode will be compared with newStartVnode. Of course, if they are of the same type, they will enter the patch process;
- Then try to compare oldEndVnode with newEndVnode and continue to skip;
- Obviously, there is no canMove in the previous two judgments, because there is no need to move elements after patching, both head and tail, but the latter is different; then continue to compare oldStartVnode and newEndVnode, canMove begins to appear, and the old head node is moved from the head to the tail. After patching, oldStartElem also needs to move behind oldEndElem;
- Similarly, if you skip the previous judgment and continue to compare oldEndVnode with newStartVnode, the same movement will also occur, but this time you move oldEndElm to the front of oldStartElm;
- If you skip the above judgment, you need to establish an oldKeyToIdx map on the old Vnode node (it is obvious that not all Vnodes will have keys, so there are not necessarily all old Vnodes on this map, and it is even likely that they are empty). Then if the key is defined on the newStartVnode, try to find the corresponding oldVnode location in the map (if it does not exist, you can take it as a matter of course to think that this is a new element); and if the newStartVnode does not define the key, it will brutally traverse all the old Vnode nodes to see if it can find a VNode with the same type that can be patched; it means that it is still very important to define the key. Now Vue templates require that the key be defined when the for loop list is defined. You can imagine what would happen if we directly use the subscript as the key; according to the sameVnode method:
function sameVnode (a, b) { return ( === && ( ( === && === && isDef() === isDef() && sameInputType(a, b) ) || ( isTrue() && === && isUndef() ) ) ) }
First, we will determine whether the key is consistent, then the tag type, the input type, etc.
So when subscripting as keys, it is obvious that the key will easily be judged as consistent, and the second thing is to look at the tag type, etc.
If you find the corresponding old Vnode in the map, you will continue to move the Dom node corresponding to this Vnode to the front of the old oldStartElem.
In summary, the movement of the Diff algorithm is carried out on the old Vnode, while the new Vnode only updates the elm property.
At the end of the Diff algorithm, you can imagine a situation where the elements will move to both ends, and the rest are elements that will be removed later, and you need to execute the exit animation, but this effect must be bad, because the list at this time is disrupted, and the animation we expect is obviously that the elements execute the exit animation from the original position, which is the meaning of the existence of removeOnly.
Magic of transition-group
How does transition-group use removeOnly? Skip directly to the source code of transition-group, and it is directly a comment:
// Provides transition support for list items. // supports move transitions using the FLIP technique. // Because the vdom's children update algorithm is "unstable" - . // it doesn't guarantee the relative positioning of removed elements, // we force transition-group to update its children into two passes: // in the first pass, we remove all nodes that need to be removed, // triggering their leaving transition; in the second pass, we insert/move // into the final desired state. This way in the second pass removed // nodes will remain where they should be.
The general idea is:
This component is to provide animation support for lists, and the animation provided by the component uses FLIP technology;
Because the Diff algorithm cannot guarantee the relative position of the element to be removed (as we summarized above), we have to make the transition-group update go through two stages. The first stage: we first remove all the elements to be removed to trigger their departure animation; in the second stage: we only move the elements to the correct position.
After knowing the general logic, how to implement transition-group?
First of all, the transition-group inherits the props related to the transiton component, so they are really a real brother.
const props = extend({ tag: String, moveClass: String }, transitionProps)
Then the first focus comes to the beforeMount method
beforeMount () { const update = this._update this._update = (vnode, hydrating) => { const restoreActiveInstance = setActiveInstance(this) // force removing pass this.__patch__( this._vnode, , false, // hydrating true // removeOnly (!important, avoids unnecessary moves) ) this._vnode = restoreActiveInstance() (this, vnode, hydrating) } }
The transition-group has made special processing on the _update method, and first force patching it, and then execute the original update method. This is the two stages of processing mentioned just now;
Then, when the transition-group caches VNode tree, and then tracks the code and finds that the render method has also been specially processed:
render (h: Function) { const tag: string = || this.$ || 'span' const map: Object = (null) const prevChildren: Array<VNode> = = const rawChildren: Array<VNode> = this.$ || [] const children: Array<VNode> = = [] const transitionData: Object = extractTransitionData(this) for (let i = 0; i < ; i++) { const c: VNode = rawChildren[i] if () { if ( != null && String().indexOf('__vlist') !== 0) { (c) map[] = c ;( || ( = {})).transition = transitionData } else if (.NODE_ENV !== 'production') { const opts: ?VNodeComponentOptions = const name: string = opts ? ( || || '') : warn(`<transition-group> children must be keyed: <${name}>`) } } } if (prevChildren) { const kept: Array<VNode> = [] const removed: Array<VNode> = [] for (let i = 0; i < ; i++) { const c: VNode = prevChildren[i] = transitionData = () if (map[]) { (c) } else { (c) } } = h(tag, null, kept) = removed } return h(tag, null, children) },
The processing here is to first traverse the VNode list contained in the transition-group, collect all the VNodes into the children array and map, and inject transition-related attributes into the VNode so that the corresponding animation will be triggered when the VNode is removed.
Then if prevChildren exists, that is, when the render triggers the second time, it will traverse the old children list. First, the latest transition attribute will be updated to the old VNode, and then it will be very important to obtain the location of the DOM node corresponding to the VNode (very important!) and record it; then judge which VNodes need to be maintained based on the map (the same VNode in the old and new lists), and which ones need to be removed, and finally point to the VNode list that needs to be maintained; so in the first stage of the pacth process, the VNode to be removed can be accurately removed first, and the new VNode will not be inserted, nor the DOM node will not be moved; only after executing the subsequent update method will do the next two steps.
Let’s look at the updated method, how to use FLIP to implement mobile animation:
updated () { const children: Array<VNode> = const moveClass: string = || (( || 'v') + '-move') if (! || !(children[0].elm, moveClass)) { return } // we divide the work into three loops to avoid mixing DOM reads and writes // in each iteration - which helps prevent layout thrashing. (callPendingCbs) (recordPosition) (applyTranslation) // force reflow to put everything in position // assign to this to avoid being removed in tree-shaking // $flow-disable-line this._reflow = ((c: VNode) => { if () { const el: any = const s: any = addTransitionClass(el, moveClass) = = = '' (transitionEndEvent, el._moveCb = function cb (e) { if (e && !== el) { return } if (!e || /transform$/.test()) { (transitionEndEvent, cb) el._moveCb = null removeTransitionClass(el, moveClass) } }) } }) },
The processing here will first check whether there is a transform attribute after adding the move class. If so, it means there is a moving animation; then process:
- Resetting the pending callback, mainly to remove the monitoring of animation events
- Record the latest relative position of the node
- Compare the old and new positions of nodes, whether there is any change. If there is any change, apply transform on the node and move the node to the old position; then force reflow to update the dom node location information; so the list we see may not have changed on the surface, but in fact we moved the node to the original position again;
- Finally, we add the nodes with changing positions and add the move class to trigger the moving animation;
This is the black magic that transition-group has, and it has indeed helped us do a lot of things behind our backs.
at last
I have learned the new through reviewing the past. In the process of writing, I actually found that there were still many vague aspects of my previous understanding, which shows that I am still not careful enough to read the code and have not done enough to understand it. I must pay more attention to it in the future. If there are any mistakes and omissions in the end, I hope everyone can correct it.
The above is all the content of this article. I hope it will be helpful to everyone's study and I hope everyone will support me more.