Browser rendering engine process
Create DOM tree - > create StyleRules - > create Render tree - > Layout - > drawing
virtual Dom
First, virtual DOM is not unique to Vue, including React and other front-end frameworks. As long as the entity DOM is calculated before operation, it can be collectively referred to as virtual DOM based.
It is designed to solve the performance problem of browser.
snabbdom
Vue's virtual DOM technology is mainly based on snabbdom in GitHub:
snabbdom: a virtual DOM library that focuses on simplicity, modularity, power, and performance.
https://github.com/snabbdom/snabbdom
It can be said that we have understood the specific methods in snabbdom, that is, we have understood the core idea of virtual dom in vue:
When the status is updated, compare the new JavaScript object with the old JavaScript object (i.e. diff algorithm), calculate the difference between them, and apply the difference to the real DOM, so as to reduce the DOM operation.
diff strategy
- Only compare the same level, not cross level
- If the tag names are different, you can delete them directly without further depth comparison
- If the tag name is the same and the key is the same, it is considered to be the same node. Do not continue the comparison
patch
patch method is the core of virtual dom. It will be called after vnode (virtual node) is changed and before initialization.
The main logic is as follows:
- Call pre hook in module (life cycle related)
- If an Element is passed in, it will be converted into an empty vnode (here, a dom Element will be passed in when it is first created)
- If the old and new nodes are sameVnode, call patchVnode to update vnode, otherwise create a new node
- Create new nodes, insert new nodes, and remove old nodes
- Call insert hook on element (life cycle related)
- Call post hook on element (life cycle related)
Source code analysis:
function patch(oldVnode: VNode | Element, vnode: VNode): VNode { let i: number, elm: Node, parent: Node; const insertedVnodeQueue: VNodeQueue = []; // Call pre hook in module for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i](); // If an Element is passed in, it will be converted to an empty vnode if (!isVnode(oldVnode)) { //Create an empty vnode and associate the DOM element emptyNodeAt oldVnode = emptyNodeAt(oldVnode); } // patchVnode is called when sameVnode (sel and key are the same) // Judge whether the two parameters are the same vnode // Compare tag name key if (sameVnode(oldVnode, vnode)) { patchVnode(oldVnode, vnode, insertedVnodeQueue); } else { elm = oldVnode.elm as Node; parent = api.parentNode(elm); // Create a new dom node vnode elm createElm(vnode, insertedVnodeQueue); if (parent !== null) { // Insert dom api.insertBefore(parent, vnode.elm as Node, api.nextSibling(elm)); // Remove old dom removeVnodes(parent, [oldVnode], 0, 0); } } // Call the insert hook on the element. Note that the insert hook is not supported on the module for (i = 0; i < insertedVnodeQueue.length; ++i) { (((insertedVnodeQueue[i].data as VNodeData).hook as Hooks).insert as any)(insertedVnodeQueue[i]); } // Call module post hook for (i = 0; i < cbs.post.length; ++i) cbs.post[i](); return vnode; } function emptyNodeAt(elm: Element) { const id = elm.id ? '#' + elm.id : ''; const c = elm.className ? '.' + elm.className.split(' ').join('.') : ''; return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm); } // The key is the same as the selector function sameVnode(vnode1: VNode, vnode2: VNode): boolean { return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel; }
patchVnode
The main logic is as follows:
- Call the prepatch hook in the module (life cycle related)
- Make differentiation according to the text and children differences between the old and new vnodes
- It should be noted that when children exist in both old and new nodes and are different, diff the children and call updateChildren
function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) { let i: any, hook: any; // Call the prepatch hook lifecycle hook if (isDef((i = vnode.data)) && isDef((hook = i.hook)) && isDef((i = hook.prepatch))) { i(oldVnode, vnode); } //Set the dom Association for the new vnode const elm = (vnode.elm = oldVnode.elm as Node); let oldCh = oldVnode.children; let ch = vnode.children; if (oldVnode === vnode) return; if (vnode.data !== undefined) { // Call the update hook life cycle hook on the module for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode); i = vnode.data.hook; // Call the update hook lifecycle hook on vnode if (isDef(i) && isDef((i = i.update))) i(oldVnode, vnode); } if (isUndef(vnode.text)) { if (isDef(oldCh) && isDef(ch)) { // diff the children when the old and new nodes have children and they are different if (oldCh !== ch) updateChildren(elm, oldCh as Array<VNode>, ch as Array<VNode>, insertedVnodeQueue); } else if (isDef(ch)) { // The old node does not have children, and the new node has children // Old node exists and text is null if (isDef(oldVnode.text)) api.setTextContent(elm, ''); // Add a new vnode addVnodes(elm, null, ch as Array<VNode>, 0, (ch as Array<VNode>).length - 1, insertedVnodeQueue); } else if (isDef(oldCh)) { // The new node does not have children. The old node has children. Remove the children of the old node removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1); } else if (isDef(oldVnode.text)) { // Old node exists and text is null api.setTextContent(elm, ''); } } else if (oldVnode.text !== vnode.text) { // Update text api.setTextContent(elm, vnode.text as string); } // Call the postpatch hook lifecycle hook if (isDef(hook) && isDef((i = hook.postpatch))) { i(oldVnode, vnode); } }
Core method updateChildren
When both the new Vnode and the old Vnode have children, enter the method.
- Assign newStartIdx oldStartIdx newEndIdx oldEndIdx to the old and new node s respectively for pointer loop traversal
- Increase the number of nodes that do not conform to the same, and replace other patchvnodes
- At the end of the cycle, process the remaining nodes for batch deletion or batch addition
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] let oldKeyToIdx, idxInOld, elmToMove, refElm // 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 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) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] } else if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue) 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) canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] } else { if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null if (isUndef(idxInOld)) { // New element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm) newStartVnode = newCh[++newStartIdx] } else { elmToMove = oldCh[idxInOld] if (sameVnode(elmToMove, newStartVnode)) { patchVnode(elmToMove, newStartVnode, insertedVnodeQueue) oldCh[idxInOld] = undefined canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm) newStartVnode = newCh[++newStartIdx] } else { // same key but different element. treat as new element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm) newStartVnode = newCh[++newStartIdx] } } } } 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(parentElm, oldCh, oldStartIdx, oldEndIdx) } }
Map summary
https://juejin.cn/post/6844903671906435080#heading-0
https://segmentfault.com/a/1190000017519084
https://www.cnblogs.com/lilicat/p/13448827.html
The source code comments of this article refer to articles such as nuggets Xunlei front end and segment fault-chen4342024