When learning becomes a habit, knowledge becomes common sense. Thank you for your attention, likes, collections and comments.
The new video and articles will be sent to WeChat official account for the first time. Li Yongning
The article has been included in github warehouse liyongning/blog , welcome to Watch and Star.
preface
As mentioned earlier, when the component is updated, the updateComponent method passed when instantiating the render watcher will be executed:
const updateComponent = () => { // Execute VM_ Render() function, get the virtual VNode, and pass the VNode to VM_ Update method, and then it's time to go to the patch stage vm._update(vm._render(), hydrating) }
First, VM_ Render() function, get the VNode of the component, and pass the VNode to VM_ Update method, and then it's time to enter the patch phase. Today, let's have an in-depth understanding of the execution process of patch when updating components.
history
The 1.x version of Vue does not have VNode and diff algorithms. The core of that version of Vue is only the responsive principle: object defineProperty,Dep,Watcher.
- Object.defineProperty: responsible for data interception. The dependency collection is performed during getter and dep notifies the watcher to update during setter
- dep: the object returned by Vue data option. The key and dep of the object correspond to each other one by one
- Watcher: there is a one to many relationship between key and watcher. Every time a key is used in the component template, a Watcher will be generated
<template> <div class="wrapper"> <!-- Each time responsive data is referenced in the template, a watcher --> <!-- watcher 1 --> <div class="msg1">{{ msg }}</div> <!-- watcher 2 --> <div class="msg2">{{ msg }}</div> </div> </template> <script> export default { data() { return { // One to one correspondence with dep and one to many correspondence with watcher msg: 'Hello Vue 1.0' } } } </script>
When the data is updated, dep informs the watcher to directly update the dom. Because this version of the watcher and the DOM have a one-to-one correspondence, the watcher can clearly know the position of the key in the component template, so it can achieve directional update, so its update efficiency is very high.
Although the update efficiency is high, it also produces serious problems, which makes it impossible to complete an enterprise application. The reason is very simple: when your page is complex enough, it will contain many components. Under this architecture, it means that this page will produce a large number of watcher s, which is very resource consuming.
At this time, Vue 2.0 introduces VNode and diff algorithm to solve 1 Problems in X. Enlarge the granularity of the watcher and turn it into a component into a watcher (that is, the rendering watcher). At this time, no matter how large your page is, there are few watchers, which solves the problem of performance degradation caused by too many watchers on complex pages.
When the responsive data is updated, dep informs the watcher to update. At this time, the problem comes. Vue 1 The watcher and key in X correspond one by one, so you can know exactly where to update. However, the watcher in Vue 2.0 corresponds to an entire component, and the watcher does not know where the updated data is in the component. At this time, VNode needs to solve the problem.
By introducing VNode, when the data in the component is updated, a new VNode will be generated for the component. By comparing the new and old vnodes, find out the differences, and then execute DOM operation to update the changed node. This process is known as diff.
The above is the historical reason why Vue 2.0 introduces VNode and diff algorithms, and also Vue 1.0 X to 2 A development of X.
target
- Deeply understand Vue's patch stage and the principle of its diff algorithm.
Source code interpretation
entrance
/src/core/instance/lifecycle.js
const updateComponent = () => { // Execute VM_ Render() function, get VNode and pass VNode to_ update method, and then it's time to go to the patch stage vm._update(vm._render(), hydrating) }
vm._update
/src/core/instance/lifecycle.js
/** * The entry location of the first rendering and subsequent updates of the page is also the entry location of the patch */ Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { const vm: Component = this // Page mount point, real element const prevEl = vm.$el // Old VNode const prevVnode = vm._vnode const restoreActiveInstance = setActiveInstance(vm) // New VNode vm._vnode = vnode // Vue.prototype.__patch__ is injected in entry points // based on the rendering backend used. if (!prevVnode) { // The old VNode does not exist, which means that it is rendered for the first time, that is, when initializing the page vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) } else { // When the responsive data is updated, that is, when the page is updated, go here vm.$el = vm.__patch__(prevVnode, vnode) } restoreActiveInstance() // update __vue__ reference if (prevEl) { prevEl.__vue__ = null } if (vm.$el) { vm.$el.__vue__ = vm } // if parent is an HOC, update its $el as well if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) { vm.$parent.$el = vm.$el } // updated hook is called by the scheduler to ensure that children are // updated in a parent's updated hook. }
vm.\_\_patch\_\_
/src/platforms/web/runtime/index.js
/ stay Vue Installation on prototype chain web Platform patch function Vue.prototype.__patch__ = inBrowser ? patch : noop
patch
/src/platforms/web/runtime/patch.js
// Patch factory function, pass in some platform specific operations for it, and then return a patch function export const patch: Function = createPatchFunction({ nodeOps, modules })
nodeOps
src/platforms/web/runtime/node-ops.js
/** * web DOM operation API of platform */ /** * Create an element node with the tag name tagName */ export function createElement (tagName: string, vnode: VNode): Element { // Create element node const elm = document.createElement(tagName) if (tagName !== 'select') { return elm } // false or null will remove the attribute but undefined will not // If it is a select element, set the multiple attribute for it if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) { elm.setAttribute('multiple', 'multiple') } return elm } // Create element node with namespace export function createElementNS (namespace: string, tagName: string): Element { return document.createElementNS(namespaceMap[namespace], tagName) } // Create text node export function createTextNode (text: string): Text { return document.createTextNode(text) } // Create annotation node export function createComment (text: string): Comment { return document.createComment(text) } // Insert node before specifying node export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) { parentNode.insertBefore(newNode, referenceNode) } /** * Remove the specified child node */ export function removeChild (node: Node, child: Node) { node.removeChild(child) } /** * Add child node */ export function appendChild (node: Node, child: Node) { node.appendChild(child) } /** * Returns the parent node of the specified node */ export function parentNode (node: Node): ?Node { return node.parentNode } /** * Returns the next sibling node of the specified node */ export function nextSibling (node: Node): ?Node { return node.nextSibling } /** * Returns the label name of the specified node */ export function tagName (node: Element): string { return node.tagName } /** * Sets the text for the specified node */ export function setTextContent (node: Node, text: string) { node.textContent = text } /** * Set the specified scopeId attribute for the node with the attribute value '' */ export function setStyleScope (node: Element, scopeId: string) { node.setAttribute(scopeId, '') }
modules
/src/platforms/web/runtime/modules and / src/core/vdom/modules
Some platform specific operations, such as attr, class, style, event, and the core directive and ref, will expose some specific methods, such as create, activate, update, remove, destroy. These methods will be called in the patch phase to perform corresponding operations, such as creating attr and instructions. There are too many contents in this part, so I won't list them one by one here. You can go back and read in depth if necessary during the process of reading patch. For example, when operating the attributes of nodes, read the code related to attr.
createPatchFunction
Tip: due to the large amount of code of this function, the code structure has been adjusted to facilitate reading and understanding
/src/core/vdom/patch.js
const hooks = ['create', 'activate', 'update', 'remove', 'destroy'] /** * Factory function, inject some function operations unique to the platform, define some methods, and then return the patch function */ export function createPatchFunction (backend) { let i, j const cbs = {} /** * modules: { ref, directives, Some platform specific manipulations, such as attr, class, style, etc.} * nodeOps: { API} */ const { modules, nodeOps } = backend /** * hooks = ['create', 'activate', 'update', 'remove', 'destroy'] * Traverse these hooks, and then find the corresponding methods from each module of modules, such as create, update and destroy methods in directives * Put these methods into cb[hook] = [hook method], for example: CB create = [fn1, fn2, ...] * Then call the corresponding hook method at the appropriate time to complete the corresponding operation */ for (i = 0; i < hooks.length; ++i) { // Like CBS create = [] cbs[hooks[i]] = [] for (j = 0; j < modules.length; ++j) { if (isDef(modules[j][hooks[i]])) { // Traverse each module, find the Create method in each module, and then add it to CBS In the create array cbs[hooks[i]].push(modules[j][hooks[i]]) } } } /** * vm.__patch__ * 1,If the new node does not exist and the old node exists, call destroy to destroy the old node * 2,If oldVnode is a real element, it means rendering for the first time, creating a new node, inserting the body, and then removing the old node * 3,If oldVnode is not a real element, it indicates the update phase and executes patchVnode */ return patch }
patch
src/core/vdom/patch.js
/** * vm.__patch__ * 1,If the new node does not exist and the old node exists, call destroy to destroy the old node * 2,If oldVnode is a real element, it means rendering for the first time, creating a new node, inserting the body, and then removing the old node * 3,If oldVnode is not a real element, it indicates the update phase and executes patchVnode */ function patch(oldVnode, vnode, hydrating, removeOnly) { // If the new node does not exist and the old node exists, call destroy to destroy the old node if (isUndef(vnode)) { if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return } let isInitialPatch = false const insertedVnodeQueue = [] if (isUndef(oldVnode)) { // The new VNode exists and the old VNode does not exist. This situation will occur when a component is rendered for the first time, such as: // <div id="app"><comp></comp></div> // The comp component here will go here when rendering for the first time // empty mount (likely as component), create new root element isInitialPatch = true createElm(vnode, insertedVnodeQueue) } else { // Determine whether oldVnode is a real element const isRealElement = isDef(oldVnode.nodeType) if (!isRealElement && sameVnode(oldVnode, vnode)) { // If it is not a real element, but the old node and the new node are the same node, it is the update phase and execute patch to update the node patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly) } else { // If it is a real element, it indicates the first rendering if (isRealElement) { // Mount to real elements and process server-side rendering // mounting to a real element // check if this is server-rendered content and if we can perform // a successful hydration. if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) { oldVnode.removeAttribute(SSR_ATTR) hydrating = true } if (isTrue(hydrating)) { if (hydrate(oldVnode, vnode, insertedVnodeQueue)) { 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.' ) } } // Go here to explain that it is not the server-side rendering, or if the hydration fails, create a vnode node according to oldVnode // either not server-rendered, or hydration failed. // create an empty node and replace it oldVnode = emptyNodeAt(oldVnode) } // Get the real element of the old node const oldElm = oldVnode.elm // Get the parent element of the old node, that is, body const parentElm = nodeOps.parentNode(oldElm) // Create a whole DOM tree based on the new vnode and insert it under the body element createElm( vnode, insertedVnodeQueue, // extremely rare edge case: do not insert if old element is in a // leaving transition. Only happens when combining transition + // keep-alive + HOCs. (#4590) oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm) ) // Update parent placeholder node element recursively 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 } } // Remove old nodes if (isDef(parentElm)) { removeVnodes([oldVnode], 0, 0) } else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode) } } } invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) return vnode.elm }
invokeDestroyHook
src/core/vdom/patch.js
/** * Destroy node: * Execute the destroy hook of the component, that is, execute the $destroy method * Execute the destroy method of each module (style, class, directive, etc.) of the component * If the vnode still has child nodes, invokeDestroyHook is called recursively */ function invokeDestroyHook(vnode) { let i, j const data = vnode.data if (isDef(data)) { if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode) for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode) } if (isDef(i = vnode.children)) { for (j = 0; j < vnode.children.length; ++j) { invokeDestroyHook(vnode.children[j]) } } }
sameVnode
src/core/vdom/patch.js
/** * Judge whether the two nodes are the same */ function sameVnode (a, b) { return ( // The key s must be the same. Note that undefined = = = undefined = > true a.key === b.key && ( ( // Same label a.tag === b.tag && // Are annotation nodes a.isComment === b.isComment && // All have data attribute isDef(a.data) === isDef(b.data) && // input tag sameInputType(a, b) ) || ( // Asynchronous placeholder node isTrue(a.isAsyncPlaceholder) && a.asyncFactory === b.asyncFactory && isUndef(b.asyncFactory.error) ) ) ) }
emptyNodeAt
src/core/vdom/patch.js
/** * Create an empty vnode for the element (elm) */ function emptyNodeAt(elm) { return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm) }
createElm
src/core/vdom/patch.js
/** * Create a whole DOM tree based on vnode and insert it into the parent node */ function createElm( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) { if (isDef(vnode.elm) && isDef(ownerArray)) { // This vnode was used in a previous render! // now it's used as a new node, overwriting its elm would cause // potential patch errors down the road when it's used as an insertion // reference node. Instead, we clone the node on-demand before creating // associated DOM element for it. vnode = ownerArray[index] = cloneVNode(vnode) } vnode.isRootInsert = !nested // for transition enter check /** * a key * 1,If vnode is a component, execute the init hook, create a component instance and mount it, * Then execute the create hook of each module for the component * If the component is wrapped by keep alive, activate the component * 2,If it's a normal element, nothing is bad */ if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { return } // Get data object const data = vnode.data // All child nodes const children = vnode.children const tag = vnode.tag if (isDef(tag)) { // Unknown tag if (process.env.NODE_ENV !== 'production') { if (data && data.pre) { creatingElmInVPre++ } if (isUnknownElement(vnode, creatingElmInVPre)) { warn( 'Unknown custom element: <' + tag + '> - did you ' + 'register the component correctly? For recursive components, ' + 'make sure to provide the "name" option.', vnode.context ) } } // Create a new node vnode.elm = vnode.ns ? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag, vnode) setScope(vnode) // Recursively create all child nodes (common elements, components) createChildren(vnode, children, insertedVnodeQueue) if (isDef(data)) { invokeCreateHooks(vnode, insertedVnodeQueue) } // Insert node into parent node insert(parentElm, vnode.elm, refElm) if (process.env.NODE_ENV !== 'production' && data && data.pre) { creatingElmInVPre-- } } else if (isTrue(vnode.isComment)) { // Annotation node, create annotation node and insert parent node vnode.elm = nodeOps.createComment(vnode.text) insert(parentElm, vnode.elm, refElm) } else { // Text node, create a text node and insert the parent node vnode.elm = nodeOps.createTextNode(vnode.text) insert(parentElm, vnode.elm, refElm) } }
createComponent
src/core/vdom/patch.js
/** * If vnode is a component, execute init hook, create component instance and mount it * Then execute the create method of each module for the component * @param {*} vnode Component new vnode * @param {*} insertedVnodeQueue array * @param {*} parentElm oldVnode Parent node of * @param {*} refElm oldVnode Next sibling node of * @returns If vnode is a component and the component is created successfully, return true; otherwise, return undefined */ function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) { // Get vnode Data object let i = vnode.data if (isDef(i)) { // Verify whether the component instance already exists & & wrapped by keep alive const isReactivated = isDef(vnode.componentInstance) && i.keepAlive // Execute vnode data. Init hook function, which was mentioned when talking about render helper // If it is a component wrapped by keep alive: execute the prepatch hook and update the relevant attributes on oldVnode with the attributes on vnode // If the component is not wrapped by keep alive or rendered for the first time, initialize the component and enter the mount phase if (isDef(i = i.hook) && isDef(i = i.init)) { i(vnode, false /* hydrating */) } // after calling the init hook, if the vnode is a child component // it should've created a child instance and mounted it. the child // component also has set the placeholder vnode's elm. // in that case we can just return the element and be done. if (isDef(vnode.componentInstance)) { // If vnode is a sub component, a component instance will be created and mounted after calling the init hook // At this time, you can execute the create hook of each module for the component initComponent(vnode, insertedVnodeQueue) // Insert the DOM node of the component into the parent node insert(parentElm, vnode.elm, refElm) if (isTrue(isReactivated)) { // When the component is wrapped by keep alive, activate the component reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm) } return true } } }
insert
src/core/vdom/patch.js
/** * Insert node to parent node */ function insert(parent, elm, ref) { if (isDef(parent)) { if (isDef(ref)) { if (nodeOps.parentNode(ref) === parent) { nodeOps.insertBefore(parent, elm, ref) } } else { nodeOps.appendChild(parent, elm) } } }
removeVnodes
src/core/vdom/patch.js
/** * Removes nodes within the specified index range (startIdx -- endIdx) */ function removeVnodes(vnodes, startIdx, endIdx) { for (; startIdx <= endIdx; ++startIdx) { const ch = vnodes[startIdx] if (isDef(ch)) { if (isDef(ch.tag)) { removeAndInvokeRemoveHook(ch) invokeDestroyHook(ch) } else { // Text node removeNode(ch.elm) } } } }
patchVnode
src/core/vdom/patch.js
/** * Update node * Full attribute update * If both old and new nodes have children, diff is executed recursively * If the new node has children and the old node has no children, these child nodes of the new node will be added * If the old node has children and the new node has no children, these children of the old node will be deleted * Update text node */ function patchVnode( oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly ) { // The old node is the same as the new node, which is returned directly if (oldVnode === vnode) { return } if (isDef(vnode.elm) && isDef(ownerArray)) { // clone reused vnode vnode = ownerArray[index] = cloneVNode(vnode) } const elm = vnode.elm = oldVnode.elm // Asynchronous placeholder node if (isTrue(oldVnode.isAsyncPlaceholder)) { if (isDef(vnode.asyncFactory.resolved)) { hydrate(oldVnode.elm, vnode, insertedVnodeQueue) } else { vnode.isAsyncPlaceholder = true } return } // Skip updates to static nodes // reuse element for static trees. // note we only do this if the vnode is cloned - // if the new node is not cloned it means the render functions have been // reset by the hot-reload-api and we need to do a proper re-render. if (isTrue(vnode.isStatic) && isTrue(oldVnode.isStatic) && vnode.key === oldVnode.key && (isTrue(vnode.isCloned) || isTrue(vnode.isOnce)) ) { // If the old and new nodes are static, and the key s of the two nodes are the same, and the new node is clone d or the new node has a v-once instruction, this part of the nodes will be reused vnode.componentInstance = oldVnode.componentInstance return } // Execute the prepatch hook of the component let i const data = vnode.data if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) { i(oldVnode, vnode) } // Children of old nodes const oldCh = oldVnode.children // Children of new nodes const ch = vnode.children // Fully update the attributes of new nodes. Vue 3.0 has done a lot of optimization here if (isDef(data) && isPatchable(vnode)) { // Perform all attribute updates for the new node for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode) if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode) } if (isUndef(vnode.text)) { // The new node is not a text node if (isDef(oldCh) && isDef(ch)) { // If both old and new nodes have children, the diff process is executed recursively if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) } else if (isDef(ch)) { // If the old child does not exist and the new child exists, create these new child nodes 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)) { // If the old child exists and the new child does not exist, remove these old child nodes removeVnodes(oldCh, 0, oldCh.length - 1) } else if (isDef(oldVnode.text)) { // If the old node is a text node, leave the text content blank nodeOps.setTextContent(elm, '') } } else if (oldVnode.text !== vnode.text) { // If the new node is a text node, the text node is updated nodeOps.setTextContent(elm, vnode.text) } if (isDef(data)) { if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode) } }
updateChildren
src/core/vdom/patch.js
/** * diff Process: * diff Optimization: four assumptions are made. It is assumed that there are the same nodes at the beginning and end of the new and old nodes. Once the assumption is hit, a cycle is avoided to improve the execution efficiency * If the assumption is unfortunately missed, the traversal is performed to find the new start node from the old node * If the same node is found, execute patchVnode, and then move the old node to the correct position * If the old node traverses before the new node, the remaining new nodes perform the operation of adding new nodes * If the traversal of the new node ends before the old node, the remaining old nodes will be deleted to remove these old nodes */ function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { // Start index of old node let oldStartIdx = 0 // Start index of new node let newStartIdx = 0 // End index of old node let oldEndIdx = oldCh.length - 1 // First old node let oldStartVnode = oldCh[0] // Last old node let oldEndVnode = oldCh[oldEndIdx] // End index of the new node let newEndIdx = newCh.length - 1 // First new node let newStartVnode = newCh[0] // Last new node let newEndVnode = newCh[newEndIdx] let oldKeyToIdx, idxInOld, vnodeToMove, refElm // removeOnly is a special flag used only by < transition group > to ensure that the removed elements remain in the correct relative position during the transition const canMove = !removeOnly if (process.env.NODE_ENV !== 'production') { // Check whether the key of the new node is duplicate checkDuplicateKeys(newCh) } // Traverse the new and old groups of nodes. As long as one group is traversed (the start index exceeds the end index), it will jump out of the loop while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { // If the node is moved, it may not exist on the current index. Detect this situation. If the node does not exist, adjust the index oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left } else if (isUndef(oldEndVnode)) { oldEndVnode = oldCh[--oldEndIdx] } else if (sameVnode(oldStartVnode, newStartVnode)) { // The old start node and the new start node are the same node. Execute patch patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) // After the patch ends, the indexes of the old start and the new start are added by 1 respectively oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] } else if (sameVnode(oldEndVnode, newEndVnode)) { // The old end and the new end are the same node, and the patch is executed patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) // After the patch ends, the indexes of the old end and the new end are reduced by 1 respectively oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right // The old start and the new end are the same node, and the patch is executed patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) // Used when handling components wrapped by transition group canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) // After the patch ends, the old start index increases by 1 and the new end index decreases by 1 oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left // The old end and the new start are the same node, and the patch is executed patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) // After the patch is completed, the old ending index is reduced by 1 and the new beginning index is increased by 1 oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] } else { // If the above four assumptions are not true, the location index of the new start node in the old node can be found through traversal // Find the relationship mapping between the key and index of each node in the old node = > oldkeytoidx = {key1: idx1,...} if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // Find the location index of the new start node in the old node in the mapping idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) if (isUndef(idxInOld)) { // New element // If the new start node is not found in the old node, it indicates that it is a newly created element, and the creation is executed createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } else { // Found the new start node in the old node vnodeToMove = oldCh[idxInOld] if (sameVnode(vnodeToMove, newStartVnode)) { // If the two nodes are the same, execute patch patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) // After the patch is completed, set the old node to undefined oldCh[idxInOld] = undefined canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) } else { // Finally, if the node is found, but the two nodes are not the same node, it will be regarded as a new element and created createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } } // Move the old node backward one newStartVnode = newCh[++newStartIdx] } } // Here, it means that the old sister node or the new node has been traversed if (oldStartIdx > oldEndIdx) { // It indicates that the old node has been traversed and the new node has a surplus, it indicates that the remaining nodes in this part are new nodes, and then add these nodes refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) } else if (newStartIdx > newEndIdx) { // It indicates that the new node has been traversed and the old node has a surplus. It indicates that this part of the node has been deleted, then remove these nodes removeVnodes(oldCh, oldStartIdx, oldEndIdx) } }
checkDuplicateKeys
src/core/vdom/patch.js
/** * Check whether the key s of a group of elements are repeated */ function checkDuplicateKeys(children) { const seenKeys = {} for (let i = 0; i < children.length; i++) { const vnode = children[i] const key = vnode.key if (isDef(key)) { if (seenKeys[key]) { warn( `Duplicate keys detected: '${key}'. This may cause an update error.`, vnode.context ) } else { seenKeys[key] = true } } } }
addVnodes
src/core/vdom/patch.js
/** * Adds a node within the specified index range (startIdx -- endIdx) */ function addVnodes(parentElm, refElm, vnodes, startIdx, endIdx, insertedVnodeQueue) { for (; startIdx <= endIdx; ++startIdx) { createElm(vnodes[startIdx], insertedVnodeQueue, parentElm, refElm, false, vnodes, startIdx) } }
createKeyToOldIdx
src/core/vdom/patch.js
/** * Get the relationship mapping between the key and index of the node in the specified range (beginIdx -- endIdx) = > {key1: idx1,...} */ function createKeyToOldIdx(children, beginIdx, endIdx) { let i, key const map = {} for (i = beginIdx; i <= endIdx; ++i) { key = children[i].key if (isDef(key)) map[key] = i } return map }
findIdxInOld
src/core/vdom/patch.js
/** * Find the location index of the new node (vnode) in the old node (oldCh) */ function findIdxInOld(node, oldCh, start, end) { for (let i = start; i < end; i++) { const c = oldCh[i] if (isDef(c) && sameVnode(node, c)) return i } }
invokeCreateHooks
src/core/vdom/patch.js
/** * Call the create method of each module, such as creating attributes, creating styles, instructions, etc., and then execute the mounted life cycle method of the component */ function invokeCreateHooks(vnode, insertedVnodeQueue) { for (let i = 0; i < cbs.create.length; ++i) { cbs.create[i](emptyNode, vnode) } // Component hook i = vnode.data.hook // Reuse variable if (isDef(i)) { // The component does not seem to have a create hook if (isDef(i.create)) i.create(emptyNode, vnode) // Call the insert hook of the component and execute the mounted life cycle method of the component if (isDef(i.insert)) insertedVnodeQueue.push(vnode) } }
createChildren
src/core/vdom/patch.js
/** * Create all child nodes and insert the child nodes into the parent node to form a DOM tree */ function createChildren(vnode, children, insertedVnodeQueue) { if (Array.isArray(children)) { // children is an array, representing a group of nodes if (process.env.NODE_ENV !== 'production') { // Check whether the key s of this group of nodes are duplicate checkDuplicateKeys(children) } // Traverse this group of nodes, create these nodes in turn, and then insert the parent node to form a DOM tree for (let i = 0; i < children.length; ++i) { createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i) } } else if (isPrimitive(vnode.text)) { // Description is a text node. Create a text node and insert the parent node nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text))) } }
summary
The interviewer asked: can you talk about Vue's patch algorithm?
Answer:
Vue's patch algorithm has three functions: it is responsible for the first rendering and subsequent updating or destroying components
- If the old VNode is a real element, it means rendering for the first time, creating the whole DOM tree, inserting the body, and then removing the old template node
If the vpat element does not exist, it indicates that the vpat element is not a new one, and if the update element is not a new one
- The first is to update all attributes in full
If both old and new vnodes have children, update children is executed recursively to perform the diff process
According to the characteristics of front-end operation DOM node, the following optimization is carried out:
- Same layer comparison (reduce time complexity) depth first (recursion)
- Moreover, the front end rarely completely disrupts the order of nodes, so four assumptions are made. It is assumed that there are the same nodes at the beginning and end of new and old vnodes. Once the assumption is hit, a cycle is avoided, the time complexity of diff is reduced, and the execution efficiency is improved. If the assumption is unfortunately missed, the traversal is performed to find the starting node of the new VNode from the old VNode
- If the same node is found, execute patchVnode, and then move the old node to the correct position
- If the old VNode traverses before the new VNode, the remaining new vnodes perform the operation of adding new nodes
- If the traversal of the new VNode ends before the old VNode, the remaining old vnodes execute the delete operation to remove these old nodes
- If the new VNode has children and the old VNode has no children, these new child nodes will be added
- If the old VNode has children and the new VNode has no children, delete these old child nodes
- The remaining one is to update the text node
- If the new VNode does not exist and the old VNode exists, call destroy to destroy the old node
Well, here, the Vue source code interpretation series is over. If you seriously read the whole series of articles, I believe you are quite familiar with the Vue source code. No matter from the macro level or the detailed explanation of some details, there should be no problem. Even though some details are not clear now, when you encounter a problem, you can see at a glance where to go to the source code to find the answer.
Here you can try to retell the whole execution process of Vue in your mind. The process is very important, but the summary is the final sublimation moment. If you get stuck in any link, you can go back and read the corresponding part.
Remember the goals mentioned in the first article in the series? I believe that after reading it several times, you can write in your resume: proficient in the source code principle of Vue framework.
Next, we will start Vue's handwriting series.
link
- Supporting video, WeChat official account reply Get "proficient in Vue technology stack source code principle video version":
- Proficient in Vue technology stack source code principle column
- github warehouse liyongning/Vue Welcome, Star
Thank you for your attention, likes, collections and comments. See you next time.
When learning becomes a habit, knowledge becomes common sense. Thank you for your attention, likes, collections and comments.
The new video and articles will be sent to WeChat official account for the first time. Li Yongning
The article has been included in github warehouse liyongning/blog , welcome to Watch and Star.