Three goals for this content:
1. Understand the patch process logically
2. Do a manual diFF process for a complex case
3. Debug VUE source code
Virtual DOM is to abstract out the structure and information of the real DOM tree and simulate the tree structure in the form of objects, as follows:
True DOM.
<div>
<p>Hello World</p>
</div>
Copy the code
The corresponding virtual DOM is:
let vnode = {
tag: 'div'.children:[ {tag:'p'.text:'Hello World'}}]Copy the code
Rendering the real DOM has some overhead. If the real DOM is rendered every time the data is modified, the DOM tree will be redrawn and rearranged, resulting in high performance overhead. Is it possible to modify only a small amount of data without rendering the entire DOM? Virtual DOM and Diff algorithms can be implemented.
How to do that? First, a virtual DOM tree is generated according to the real DOM. When the data of a DOM node changes, a new Vnode is generated and the new Vnode is compared with the old oldVnode. The patch function is used to patch the real DOM or create vNodes or remove oldvNodes
Diff algorithm
The traditional Diff algorithm traverses each node in the two trees and makes a comparison between each node.
Vue optimized Diff algorithm Vue’s Diff algorithm only compares elements at the same level, not across levels
Diff is a comparison. It is a broad concept, such as Linux diff, Git diff, etc. Diff in vDOM is diff between two trees. The time complexity of tree diff is O(n^3). The time complexity is too high to use the algorithm. However, the framework creators later optimized the time complexity of diff algorithm to O(n), and the specific changes were as follows: Only comparing the same level, but not comparing different tags across levels, then deleting the reconstruction directly, and no longer comparing tags and keys in depth. If both are identical, it will be considered as the same node, and no further comparison will be made
3. Realization of Diff algorithm in Vue
EmptyVNode: annotation node without content TextVNode: text node ElementVNode: common element node ComponentVNode: component node CloneVNode: Clone nodes can be any of the above types, the only difference being that the empirical attribute of isverification is true
The patch function accepts the following parameters: oldVnode: old virtual node Vnode: new virtual node hydrating: whether to mix with the real DOM removeOnly: special flag used for transition-group
The processing process is roughly divided into the following steps:
Vnode does not exist. If oldVnode does exist, remove oldVnode. Create vNode. If both Vnode and oldVnode exist, if vnode and oldVnode are the same node (sameVnode is used to compare the following details), patchVnode is used for subsequent comparison. If vnode and oldVnode are not the same node, Create a new element based on vNode and mount it under the oldVnode parent element. If the component root node is replaced, iterate to update the parent element. Then remove the old node. If oldVnode is a server-side render element node, you need to map the virtual DOM to the real DOM using the Hydrate function
return function patch(oldVnode, vnode, hydrating, removeOnly) {
// If the vnode does not exist but the oldVnode does exist, remove the oldVnode
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
Copy the code
let isInitialPatch = false
const insertedVnodeQueue = []
// If oldVnode does not exist, but vNode does exist, create vNode
if (isUndef(oldVnode)) {
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
// Both vNodes and oldvNodes exist
// Check if it is a real DOM element
const isRealElement = isDef(oldVnode.nodeType)
if(! isRealElement && sameVnode(oldVnode, vnode)) {// If vNode and oldVnode are the same (use sameVnode to compare)
// The patchVnode function is used for the subsequent comparison.
patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
} else {
// VNode and oldVnode are not the same case
if (isRealElement) {
// If there is a real node, there is a data-server-render attribute
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
// When the old Vnode is a server render element, hydrating is set to true
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
// The hydrate function is needed to map the virtual DOM to the real DOM
if (isTrue(hydrating)) {
// Need to merge into the real DOM
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
// Call the insert hook
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
} else if(process.env.NODE_ENV ! = ='production') {
warn(
'The client-side rendered virtual DOM tree is not matching ' +
'server-rendered content. This is likely caused by incorrect ' +
'HTML markup, for example nesting block-level elements inside ' +
'<p>, or missing <tbody>. Bailing hydration and performing ' +
'full client-side render.')}}// If the server does not render the element or fails to merge into the real DOM, an empty Vnode is created to replace it
oldVnode = emptyNodeAt(oldVnode)
}
// Get the parent of the oldVnode
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// Create a real DOM node based on vNode and mount it to oldVnode's parent node
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// If the component root node is replaced, iterate over and update the parent Element
if (isDef(vnode.parent)) {
let ancestor = vnode.parent
const patchable = isPatchable(vnode)
while (ancestor) {
for (let i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](ancestor)
}
ancestor.elm = vnode.elm
if (patchable) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, ancestor)
}
/ / # 6513
// invoke insert hooks that may have been merged by create hooks.
// e.g. for directives that uses the "inserted" hook.
const insert = ancestor.data.hook.insert
if (insert.merged) {
// start at index 1 to avoid re-invoking component mounted hook
for (let i = 1; i < insert.fns.length; i++) {
insert.fns[i]()
}
}
} else {
registerRef(ancestor)
}
ancestor = ancestor.parent
}
}
// Destroy the old node
if (isDef(parentElm)) {
// Remove the old node
removeVnodes(parentElm, [oldVnode], 0.0)}else if (isDef(oldVnode.tag)) {
// Call the destroy hook
invokeDestroyHook(oldVnode)
}
}
}
// Call the INSERT hook and return the node
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
Copy the code
The biggest purpose of VNode is to generate virtual DOM nodes corresponding to the real DOM before and after data changes. Then, you can compare the old and new VNodes to find out the differences, and then update the DOM nodes with the differences, and finally achieve the purpose of updating the view with the least operation on the real DOM.
In Vue, the DOM-diff process is called the patch process. Patch the old VNode to create a new VNode.
When data changes, the set method will call dep. notify to notify all subscribers Watcher, and the subscribers will call Patch to patch the real DOM and update the corresponding view.Using the new VNode as a baseline, retrofitting the old oldVNode to look like the new VNode is what the patch process does.
Create a node: the new VNode has one and the old oldVNode does not. Create the node in the old oldVNode. Delete node: if the new VNode does not exist and the old oldVNode does, it is deleted from the old oldVNode. Update nodes: Both the new VNode and the old oldVNode exist. Update the old oldVNode using the new VNode.
function patch (oldVnode, vnode) {
// some code
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode)
} else {
const oEl = oldVnode.el // The real element node corresponding to the current oldVnode
let parentEle = api.parentNode(oEl) / / the parent element
createEle(vnode) // Generate new elements based on Vnode
if(parentEle ! = =null) {
api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // Add the new element to the parent element
api.removeChild(parentEle, oldVnode.el) // Remove the old element node
oldVnode = null}}// some code
return vnode
}
Copy the code
Determine whether the two nodes are worthy of comparison, and then execute patchVnode
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)
) || (
isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)
)
)
)
}
Copy the code
How does Vue determine if it is the same node? The process is as follows: Check whether the Key value is the same as the tag value isComment. SameInputType () is used to determine the input of the form. SameInputType () is used to determine the input of the form. SameInputType () is used to determine the input of the form.
If you use index as the key, the key changes every time you shuffle the order, invalidating this judgment and reducing the efficiency of the Diff.
PatchVnode and oldVnode are the same node
Return If oldVnode isAsyncPlaceholder property is true, skip checking asynchronous components. Return If oldVnode and VNode are static nodes, Copy oldvNode. elm and oldvNode. child to vNode if the vnode is a clone or controlled by the V-once command with the same key. Return If vnode is not a text stander or comment node. If both vNode and oldVnode have child nodes and their child nodes are inconsistent, updateChildren is called to update the child node. Call addVnodes to create child nodes. If only oldVnode has child nodes, call removeVnodes to remove those child nodes. If the vnode text is undefined, If vnode is a text node but different from oldVnode text content, just update the text.Copy the code
If not, replace oldVnode with Vnode
When we determine that the two nodes are worth comparing, we will specify the patchVnode method for both nodes. So what does this method do?
patchVnode (oldVnode, vnode) {
const el = vnode.el = oldVnode.el
let i, oldCh = oldVnode.children, ch = vnode.children
if (oldVnode === vnode) return
if(oldVnode.text ! = =null&& vnode.text ! = =null&& oldVnode.text ! == vnode.text) { api.setTextContent(el, vnode.text) }else {
updateEle(el, vnode, oldVnode)
if(oldCh && ch && oldCh ! == ch) { updateChildren(el, oldCh, ch) }else if (ch){
createEle(vnode) //create el's children dom
}else if (oldCh){
api.removeChildren(el)
}
}
}
Copy the code
function patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) {
// If the new and old nodes reference the same, return directly.
if (oldVnode === vnode) {
return
}
const elm = vnode.elm = oldVnode.elm
// If the oldVnode isAsyncPlaceholder property is true, skip checking asynchronous components
if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
} else {
vnode.isAsyncPlaceholder = true
}
return
}
// If old and new are static nodes, the key of vNode is the same
// The new vNode is cloned or the new vnode has the V-once attribute
// Perform an assignment and return. Vnode's componentInstance remains unchanged
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance
return
}
let i
const data = vnode.data
// Execute data.hook. Prepatch hook
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
// Get the list of child elements
const oldCh = oldVnode.children
const ch = vnode.children
if (isDef(data) && isPatchable(vnode)) {
// Iterate over cbs.update hook function to update all oldVnode properties
// Including attrs, class, domProps, events, style, ref, and directives
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
// Execute data.hook. Update hook
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
// The Vnode text option is undefined
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
// The children of the new and old nodes are different, execute the updateChildren method
if(oldCh ! == ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) }else if (isDef(ch)) {
// oldVnode children does not have the addVnodes method executed
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, ' ')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
// The removeVnodes method does not exist on vNode
removeVnodes(elm, oldCh, 0, oldCh.length - 1)}else if (isDef(oldVnode.text)) {
// The old and new nodes are undefined, and the old node has text, empty the text.
nodeOps.setTextContent(elm, ' ')}}else if(oldVnode.text ! == vnode.text) {// The text content of the new and old nodes is different
nodeOps.setTextContent(elm, vnode.text)
}
if (isDef(data)) {
// Execute data.hook. Postpatch hook
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
Copy the code
Prerequisites: Children of vNode and oldVnode are not equal
This function does the following: finds the corresponding real DOM, called EL, and determines whether Vnode and oldVnode point to the same object. If so, return. If they both have text nodes and are not equal, set the text node of EL to the text node of Vnode. If oldVnode has children and Vnode does not, remove the children of EL. If oldVnode does not have children and Vnode does, add the children of Vnode to EL after realism. If both have children, perform the updateChildren function to compare the children
The other points are easy to understand, so let’s talk in detail about the updateChildren vNode header versus the oldVnode header
Compare the vnode tail with the oldVnode tail
The vNode head is compared with the oldVnode tail
The vnode tail compares with the oldVnode header
Patch, move nodes, move subscripts and other operations are not correct as long as they meet one of the conditions. If a node with the same key and newStart cannot be found in oldChild, create a new one.
Find, get this node, and determine whether it is the same node as newStartVnode
If it is the same node, patch and insert the node before oldStart, newStart subscript continues to move. If it is not the same node, execute createElm to create a new element
Extract the child node Vch of Vnode and oldCh of oldVnode. OldCh and oldCh each have two starting and ending variables StartIdx and EndIdx, and compare their two variables with each other. There are altogether four ways of comparison. If none of the four comparisons match, if the key is set, the comparison will be performed with the key. During the comparison, the variable will move towards the middle, and the comparison will end as soon as StartIdx>EndIdx indicates that at least one of the oldCh and vCh have been traversed.
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
// Define variables
let oldStartIdx = 0 // the Child header of the old node
let newStartIdx = 0 // New node Child header subscript
let oldEndIdx = oldCh.length - 1 // the old node Child subscript
let oldStartVnode = oldCh[0] // The old node Child is the header
let oldEndVnode = oldCh[oldEndIdx] // End of the old node Child
let newEndIdx = newCh.length - 1 // New node Child subscript
let newStartVnode = newCh[0] // New node Child header
let newEndVnode = newCh[newEndIdx] // The end of the new node Child
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
constcanMove = ! removeOnlyif(process.env.NODE_ENV ! = ='production') {
checkDuplicateKeys(newCh)
}
// Define the loop
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// Check exists
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
If the Child header of the old node is the same as the Child header of the new node
} else if (sameVnode(oldStartVnode, newStartVnode)) {
/ / patch
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
// Patch is completed to move the node position and continue to compare with the next node
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
// If the Child end of the old node is the same as the Child end of the new node
} else if (sameVnode(oldEndVnode, newEndVnode)) {
/ / patch
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
// Patch is completed to move the node position and continue to compare with the next node
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
// If the old Child header is the same as the new Child header
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
/ / patch
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
// Place oldStart after oldEnd
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
// Patch is completed to move the node position and continue to compare with the next node
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
// If the Child end of the old node is the same as the Child head of the new node
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
/ / patch
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
// Place the oldEnd node before the oldStart node
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
// Patch is completed to move the node position and continue to compare with the next node
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// If there is no identical Key, execute createElm to create the element
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key) ?
oldKeyToIdx[newStartVnode.key] :
findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
// Both nodes are samenodes with the same Key
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
// If it is the same node, perform patch and lift to insert oldStart before oldStart, newStart subscript continues to move
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
If the node is not the same, execute createElm to create a new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
// oldStartIdx > oldEndIdx indicates that oldChild is traversed first. Use the addVnode method to add the node pointed by newStartIdx to the node of 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) {
// If newStartIdx > newEndIdx indicates that newChild is traversed first, remove the node that oldChild is not traversed
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
Copy the code
4. Practical debugging cases
<html>
<header>
<script src=".. /.. /dist/vue.js"></script>
</header>
<body>
<div id="demo">
<p v-for="item in items" :key="item">{{ item }}</p>
</div>
<div>
["a", "b", "c", "d", "e", "f", "g"] => ["f", "d", "a", "h", "e", "c", "b", "g"]
</div>
<script>
const app = new Vue({
el: "#demo".data: {
items: ["a"."b"."c"."d"."e"."f"."g"]},mounted() {
setTimeout(() = > {
this.items = ["f"."d"."a"."h"."e"."c"."b"."g"]},2000)}})</script>
</body>
</html>
Copy the code
Debug in vUE to observe code direction and flow
Correct use of key can quickly perform sameVnode comparison, accelerate the efficiency of Diff, and can be used as a point of performance optimization. DIff does peer comparisons only, using the sameVnode function, and text nodes replace text content directly. Diff of the list of child elements, comparing head to head, tail to tail, head to tail, etc., until traversing the list of child elements of both elements. AddVnode/removeVnode if a list is traversed first.
Patch juejin.cn/post/696414…