preface
This article is quite long, the main length is introduced in the DIFF algorithm, before understanding the diFF algorithm, it is necessary to understand why Vue needs the DIFF algorithm, and when Vue will use the DIFF algorithm, let’s take a look at these two problems.
Why does Vue need diff algorithm?
Diff algorithm is an inevitable product of virtual DOM. By comparing the old and new virtual DOM and updating the changes to the real DOM, unnecessary DOM operations can be reduced as much as possible and performance can be improved.
When the state is changed, the related Watcher instance will be notified to update. At this time, the corresponding component will render again and generate a new VNode. There will always be some parts of a component that do not need to be updated. If the whole component is DOM updated, the performance will be greatly affected. Therefore, the DIff algorithm is used to compare the old and new VNodes to find the nodes that need to be updated and then update them. This process is called patch.
In Vue, components are mounted, updated and removed through this patch stage. Before introducing patch, let’s know when Vue will execute patch.
Time when patch is executed
Vue.prototype.$mount
$mount(‘#app’); Vue. Prototype.$mount() actually calls mountComponent
// core/stance/init.js
Vue.prototype._init = function (options) {
// ...
const vm = this;
if(vm.$options.el) { vm.$mount(vm.$options.el); }}// platforms/web/runtime/index.js
Vue.prototype.$mount = function (el, hydrating) {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
Copy the code
mountComponent
As you know from its name, this function is used to mount components, and executing this function does several things
- Call the lifecycle function
beforeMount
- Define a function
updateComponent
This function is executed internallyvm._update
And thevm._update
The parameter isvm._render()
The result is that a new VNode is generated - To create a
Watcher
Instance, and willupdateComponent
As an argument, this function is called when the component is mounted and updated
Here is the main code involved in mountComponent and Watcher
function mountComponent (vm, el) {
callHook(vm, 'beforeMount')
updateComponent = () = > {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if(vm._isMounted && ! vm._isDestroyed) { callHook(vm,'beforeUpdate')}}},true /* isRenderWatcher */)}// src/core/observer/watcher.js
export default class Watcher {
constructor(vm, expOrFn, cb, options, isRenderWatcher) {
this.getter = expOrFn // updateComponent is stored as a expOrFn parameter in the getter property
this.value = this.lazy ? undefined : this.get() // called once when mounted
}
get () {
// ...
value = this.getter.call(vm, vm)
// ...
}
// Notifies the update, calling the getter
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this) // Run is also called at the end of an asynchronous queue update
}
}
run () {
// ...
const value = this.get()
}
}
Copy the code
vm._update
Now we know that both component mounts and updates call the _update method, which takes the vNode generated by rerender as an argument, internally retrieves the current vNode (preVnode), and then handles it in one of two ways
- No preVnode: currently there is no old VNode, is mount phase, called
__patch__
Passing in DOM elements$el
And the newvnode
- PreVnode: indicates that the current is the update phase, called
__patch__
The incomingpreVnode
And the newvnode
The same function is called in both cases, but the arguments passed in are different, so the case handling needs to be determined inside __Patch__
Vue.prototype._update = function (vnode, hydrating) {
var vm = this;
var prevEl = vm.$el;
var prevVnode = vm._vnode;
if(! prevVnode) {// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
} else {
// updatesvm.$el = vm.__patch__(prevVnode, vnode); }}Copy the code
vm.__patch__
Vm. __patch__ is also a method on the prototype object, as defined below
Patch is only needed in the browser, server rendering does not have the concept of DOM manipulation and mount, so patch is not needed
// src/platforms/web/runtime/index.js
Vue.prototype.__patch__ = inBrowser ? patch : noop
Copy the code
The Patch function is returned by calling createPatchFunction
import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'
// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)
export const patch: Function = createPatchFunction({ nodeOps, modules })
Copy the code
CreatePatchFunction accepts two parameters. What are they
nodeOps
Is the web platform under some DOM API encapsulation, in the patch stage, naturally, there is no need to operate DOMmodules
They are some core modules, such as ATTr, class, style, event, directive, ref, etc. These modules expose THE API of Create, Update, remove, destory, etc., which will be called in the patch phase
CreatePatchFunction has a lot of code and declared a lot of functions, which is easy to call in each phase of patch. Patch logic is very complex, but here we only focus on the implementation of diff algorithm, and there is no more to introduce. The following is the definition of Patch function
export function createPatchFunction (backend) {
return function patch (oldVnode, vnode, hydrating, removeOnly) {}}Copy the code
Patch receives oldVnode and VNode, and then processes them according to different values of the two parameters. Finally, it is time to explore the realization principle of Patch. Next, it is time to analyze the code step by step to understand how VUE implements diff algorithm.
Working principle of patch phase
Patch method has a lot of codes, but it is basically just conditional judgment and then different operations are performed. The core code of DIff algorithm is to continue to compare the children of old and new VNodes and how to achieve efficient comparison when they correspond to the same elements. Therefore, we only need to understand the general process here. The following is the flow chart of patch method execution
graph TD patch[Start] --> vnode{vnode is undef? }; vnode -->|undefined| oldVnode1{oldVnode is undef? }; OldVnode1 - > | No undef | remove oldVnode - > return; oldVnode1 -->|undefined| return; vnode -->|No undef| oldVnode2{oldVnode is undef}; oldVnode2 -->|undefined| createEl[createElm]; OldVnode2 - > | vnode && oldVnode | condition judgment} {conditions; Condition - > | ov is VNode instance and samenode | patchVnode; Condition - > | ov is $el | mount; Condition - > | ov is VNode instance and! Samenode | cr/creating a new DOM to remove the old node;
The following is the main code and comments involved. The core process is to qualify sameVnode(oldVnode, vNode) and then call patchVnode, which will be described later
function patch (oldVnode, vnode) {
// If there is no new vnode, delete the old vnode (if there is one) and return
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
// No oldVnode, no diff
if (isUndef(oldVnode)) {
createElm(vnode, insertedVnodeQueue)
} else {
// oldVnode and vnode are not undefined/null
// From the previous call to '__patch__', oldVnode may be a real DOM element or a Vnode instance
const isRealElement = isDef(oldVnode.nodeType)
// The core of diff algorithm. If oldVnode is not a real DOM, sameVnode is called to determine whether it corresponds to the same element. If it is, patchVnode is called
if(! isRealElement && sameVnode(oldVnode, vnode)) { patchVnode(oldVnode, vnode, insertedVnodeQueue,null.null, removeOnly)
} else {
// Insert a new element
if (isRealElement) { / *... * / }
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
createElm(vnode, insertedVnodeQueue, oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm))
// Destroy the old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0.0)}else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
}
Copy the code
sameVnode
SameVnode is used to determine whether old and new VNodes correspond to the same element. If so, in-place reuse can be considered, such as directly modifying textContent, or continuing recursive comparison of children if there are children, otherwise the comparison will not continue and direct replacement
function sameVnode (a, b) {
return (
a.key === b.key && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
)
)
)
}
Copy the code
The sameVnode core code is as follows. If all the following conditions are met, it is judged as sameVnode
- keyThe virtual DOM uses a unique key to distinguish elements, which means that the elements are completely independent. If the key is different, it is not sameVnode and is in use
v-for
Always add a key when rendering a list to prevent the display from being distorted when adding or deleting elements from the list - Tag The tag names must be the same
- isCommentWhether or not to annotate a node must be both yes and no, in the case of annotating a node, such as using dynamic components
component
When,is
If false, null, or undefined, vue generates a comment node<! ---->
- Data contains specific binding data such as style and attrs
- sameInputTypeIf the same as
input
Element, but also satisfytype
The same
patchVnode
When patchVnode is arrived, it means that the old and new nodes are already sameVnode, but it needs to be processed in various cases. The main code is as follows
function patchVnode (oldVnode, vnode) {
if (oldVnode === vnode) return
const elm = vnode.elm = oldVnode.elm
const oldCh = oldVnode.children
const ch = vnode.children
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) { updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); }
} else if (isDef(ch)) {
if(process.env.NODE_ENV ! = ='production') {
checkDuplicateKeys(ch);
}
if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, ' '); }
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
} else if (isDef(oldCh)) {
removeVnodes(oldCh, 0, oldCh.length - 1);
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, ' '); }}else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text);
}
}
Copy the code
This function does a couple of things
- if
oldVnode
和vnode
If the value is the same object, return is required - The next step is to determine the condition, first save the vNode corresponding element in
elm
- if
vnode
If there are text nodes, update text directly - if
vnode
There are no text nodes in the following cases- If the old and new,
vnode
There arechildren
, and does not point to the same arrayupdateChildren
, the sublevels are compared first - If only the VNode has one
children
The calladdVnodes
Create a child, if anyoldVnode.text
, to be empty - Otherwise, if only oldVnode has it
children
, the implementationremoveVnodes
Remove children - If no new or old VNode exists
children
, only oldVnode has text nodes, so the text is null
- If the old and new,
The diff algorithm is based on updateChildren, and the diff algorithm is based on updateChildren. The diff algorithm is based on updateChildren, and the diff algorithm is based on updateChildren
updateChildren
UpdateChildren is a node that does not need to be updated, reused, added or deleted in the subhierarchy of the old and new VNodes. If the node is directly searched in the two arrays, the time complexity is O(n^2), so direct traversal comparison is obviously not advisable. In order to improve efficiency, Vue adopts the following strategy optimization algorithms in turn
Compare heads and tails
Reordering or modifying an item in a list component does not replace the entire list, where the top and bottom nodes may remain the same, or elements may be moved to another location, or elements may be reused in place. In this case, Vue uses a head-to-tail comparison strategy. Here is the main code
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
// ...
constcanMove = ! removeOnlywhile (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 && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
/ /...}}}Copy the code
So what does this part of code do?
- Create four index subscripts to initialize the heads and tails of the new and old children
- Use four variables to store the node where the four index subscripts are traversed each time
- When both arrays are nodes to be compared, the loop makes the following judgments. If the following conditions are true, the loop skips the latter judgments and starts the next cycle directly
- Without oldStartVnode, simply move oldStartIdx one place back and update the value of oldStartVnode
- Without oldEndVnode, simply move oldEndIdx forward one position and update the oldEndVnode value
- Check whether the old and new header vNodes are samevNodes
patchVnode
, moves the two header Pointers back and updates the values of the corresponding variables - Check whether the old and new tail vNodes are samevNodes
patchVnode
, moves the two tail Pointers forward and updates the values of the corresponding variables - Check whether oldStartVnode and newEndVnode are sameVnode, if yes, it indicates that the node runs behind, and call
patchVnode
, then move the node to the new location, moving oldStartIdx back and newEndIdx forward - As above, determine oldEndVnode and newStartVnode
The following examples illustrate that p node has three child elements a, B and C, and component update is triggered after reordering (regardless of addition and deletion). At this time, the following steps are used to compare the sub-levels
oldStartVnode
与newStartVnode
The values are both A, consistent with sameVnode, and patchVnode is directly invoked to change oldStartVnode to B and newStartVnode to C. At this time, oldStartIdx = 1 and newStartIdx = 1, and other parameters remain unchanged- The second time, we found a match
sameVnode(oldStartVnode, newEndVnode)
First call patchVnode to compare the sub-level first. After the sub-level processing is completed, move B to the new position, and then change oldStartVnode to C and newEndVnode to C. At this point, oldStartIdx = 2. newEndIdx = 1 - The third time, we found a match
sameVnode(oldStartVnode, newStartVnode)
, call patchVnode, oldStartIdx = 3, newStartIdx = 2 - Exit the loop to complete the diff operation
p p
/ | \ ==> / | \
a b c a c b
Copy the code
Use keys to create mappings
Since only four nodes can be compared at a time, if the node moves to the middle, the above method will not be able to find the matching node. Instead, we can only find the node matching newStartVnode in oldch. We usually add a key. Establish oldch key to IDX map, directly through the newStartVnode key can find the corresponding oldVnode, other nodes with different keys naturally do not need to consider
Find the corresponding oldVnode by key, and then compare whether it is sameVnode. If yes, call patchVnode, move oldVnode to a new location after completing diff of subtree, otherwise create a new node
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if {
// ...
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key] // Use key directly to find the mapping table
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) // There is no key
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
}
Copy the code
Take another example to illustrate that the P node has four child elements a, B, C and D, and component updates are triggered after reordering. At this time, the following steps are used to compare the sub-levels
- In this case, the head and tail comparison can not find the condition
- Now I’m going to use key to find,
newStartVnode
Oldch = 2; oldch = 2; oldch = 2; oldch = 2newStartVnode
Change it to a, and the next round can quickly find the position of A in oldch by pairwise comparison - Compare until diff is done to exit the loop
p p
/ / \ \ ==> / / \ \
a b c d c a d b
Copy the code
Traversal search
Without the key, we would have to go through oldch one by one and call findIdxInOld for the search. Here the time complexity would be high, but with the key added, it would not go this far, just as a backstop
Additions or deletions
If oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx, there must be a condition that fails to exit the loop. If oldStartIdx > oldEndIdx, there must be a new node. Call addVnodes to batch create nodes. The other way around is to remove some old nodes.
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {}
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1])?null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
}
Copy the code
At this point, the diff operation of a component update process is completed, and nodes that are updated, added, and deleted are updated to the real DOM one by one. Then the next Watcher call is continued until all Watcher updates are complete, and the nextTick callback is executed.
conclusion
Diff algorithm is the optimal solution between the two extremes of full DOM update and O(n^3) precise search. It not only finds the nodes that need to be updated as much as possible and eliminates unnecessary DOM updates, but also takes into account the complexity of the algorithm to avoid performance problems. Diff algorithm has the following two characteristics:
- Depth-first: If the old and new vNodes are Samevnodes, the children are compared first. This operation is recursive. If the old and new VNodes are not Samevnodes, the comparison will not continue, because the probability of reusable children is low, and even if they exist, the continued comparison is not worth the loss
- Same-layer comparison: only the nodes of the same layer will be compared. If there is more than one layer in the middle after the update, this situation is directly judged as not comparable, directly update or delete all, but generally we will not write such code
After learning the principles of the DIFF algorithm, we can consciously write higher-quality code to help the DIFF algorithm execute more efficiently, for example
- use
v-for
Render the list, to add a key to improve the efficiency of judgment, search, but also to prevent elements from being incorrectly reused bugs - use
v-if/v-else
Control element switching without adding keys allows VUE to reuse elements efficiently - Combining the concepts of depth-first and same-layer comparison, the hierarchical relationship of nodes is optimized to improve the efficiency of patch as much as possible