1. Virtual dom
patch(vnode, h('div#app', obj.foo))
2. Cross platform: convert virtual dom updates to different runtime special operations to achieve cross platform
const patch = init([snabbdom_style.default]) patch(vnode, h('div#app', {style:{color:'red'}}, obj.foo))
three Compatibility: you can also add compatibility codes to enhance the compatibility of operations
1.1 comparison between real dom and virtual dom
Real dom
<div> <p>Hello World</p> </div>
Virtual dom
let vnode = { tag: 'div', children:[ {tag:'p', text:'Hello World'}] }
2. diff algorithm
2.1 patch function
2.1.1 patch implementation
File patch core\vdom\patch.js
return function patch (oldVnode, vnode, hydrating, removeOnly) { // If the new virtual dom tree does not exist, it is deleted if (isUndef(vnode)) { if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return } let isInitialPatch = false const insertedVnodeQueue = [] // If the old vdom does not exist, add a new one if (isUndef(oldVnode)) { // empty mount (likely as component), create new root element isInitialPatch = true createElm(vnode, insertedVnodeQueue) } else { // If the real node is passed in, the initialization operation is performed const isRealElement = isDef(oldVnode.nodeType) if (!isRealElement && sameVnode(oldVnode, vnode)) { // patch existing root node // There are old and new vdom s patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly) } else { // During initialization, create a new dom, append it to the body, and delete the array if (isRealElement) { // mounting to a real element // check if this is server-rendered content and if we can perform // a successful hydration. // If there is a real node, there is the data server render attribute if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) { // When the old Vnode is a rendering element on the server side, hydrating is marked as true oldVnode.removeAttribute(SSR_ATTR) hydrating = true } // You need to map the virtual DOM to the real DOM with the hydrate function if (isTrue(hydrating)) { // It needs to be merged into the real DOM if (hydrate(oldVnode, vnode, insertedVnodeQueue)) { // Call 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.' ) } } // either not server-rendered, or hydration failed. // create an empty node and replace it // If it is not a server-side rendering element or merging to the real DOM fails, create an empty Vnode node to replace it oldVnode = emptyNodeAt(oldVnode) } // replacing existing element const oldElm = oldVnode.elm const parentElm = nodeOps.parentNode(oldElm) // create new node 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 } } // destroy old node if (isDef(parentElm)) { removeVnodes([oldVnode], 0, 0) } else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode) } } } invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) return vnode.elm }
3. patchVnode
let i const data = vnode.data if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) { i(oldVnode, vnode) } // Check whether the old and new nodes have child queues const oldCh = oldVnode.children const ch = vnode.children // Attribute update if (isDef(data) && isPatchable(vnode)) { 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) } // Judge whether it is an Element. If there is no text, it is an Element if (isUndef(vnode.text)) { // All have children if (isDef(oldCh) && isDef(ch)) { if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) } else if (isDef(ch)) { // Only new nodes have children if (process.env.NODE_ENV !== 'production') { checkDuplicateKeys(ch) } // Empty old node text if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '') // Add child addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) } else if (isDef(oldCh)) { // Only the old node has children removeVnodes(oldCh, 0, oldCh.length - 1) } else if (isDef(oldVnode.text)) { // Old node has text nodeOps.setTextContent(elm, '') } } else if (oldVnode.text !== vnode.text) { // Both old and new are text and different nodeOps.setTextContent(elm, vnode.text) } if (isDef(data)) { if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode) } }
4. updateChildren
Here comes the point!!!!
There are four situations:
1. The old node is the same as the new header node, and directly patchVnode
2. The old tail node is the same as the new tail node, directly patchVnode
3. The old node is the same as the new tail node. While patchVnode, the old node should be placed behind the old tail node
4. The old tail node is the same as the new head node. While patchVnode, place the old tail node in front of the old node
If none of the above four conditions are met:
In the old node queue, look for the node that satisfies sameVnode with the new head node, and then move it to the front of the old node, and the subscript of the new head node moves back one bit
Of course, if such a node cannot be found, create a new node and put it at the front of the old node queue
Finally, the queue lengths of the old node and the new node may be different, and there will be one queue that has not been traversed, so there will be the following two cases:
Batch addition (after traversing the old nodes, new nodes that have not been traversed will be created and added in batch)
Batch deletion (after traversing the new nodes, delete the old nodes that have not been traversed in batch)
Direct source 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] 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 const canMove = !removeOnly if (process.env.NODE_ENV !== 'production') { checkDuplicateKeys(newCh) } /** * Both sides close to the middle, dfs */ while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { // The old start node does not exist. Move the subscript one bit back oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left } else if (isUndef(oldEndVnode)) { // The old tail node does not exist. The subscript moves forward one bit oldEndVnode = oldCh[--oldEndIdx] } else if (sameVnode(oldStartVnode, newStartVnode)) { // The old and new start nodes are the same and + 1 at the same time patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] } else if (sameVnode(oldEndVnode, newEndVnode)) { // The old and new end nodes are the same, and - 1 patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newEndVnode)) { // The old head node is the same as the new tail node. Move the old head node to the old tail to improve the similarity // 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)) { // The old tail node is the same as the new head node // Vnode moved left patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] } else { // The four conjectures did not find the same, so they were forced to search circularly if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // Find the index key in the old array idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) // If there is no such element in the old child node array, a new one will be created if (isUndef(idxInOld)) { // New element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } else { // If the element is found to be reusable, further comparison is made vnodeToMove = oldCh[idxInOld] if (sameVnode(vnodeToMove, newStartVnode)) { // Is exactly the same element patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) oldCh[idxInOld] = undefined canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) } else { // Only the key is the same, but the content is different // same key but different element. treat as new element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } } newStartVnode = newCh[++newStartIdx] } } // Finally, finish the work if (oldStartIdx > oldEndIdx) { refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm // Batch creation addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) } else if (newStartIdx > newEndIdx) { // Batch delete removeVnodes(oldCh, oldStartIdx, oldEndIdx) } }