React React16
之前是直接递归渲染 vdom
setState
会触发重新渲染 对比渲染出的新旧vdom
对差异部分进行 dom 操作
React16
之后 为了性能优化 会先将 vdom
转换为fiber
(从树转换成链表) 然后在渲染
render
阶段: 从vdom
转换为 fiber
并且对需要 dom 操作的节点打上 effectTag
的标记
commit
阶段: 对有 effectTag
标记的 fiber
节点进行 dom 操作 并执行所有的effect
副作用函数
从vdom
转换成fiber
这个阶段叫做调和(reconcile
) 由scheduler
调度执行 是可以打断的
diff
算法作用在reconcile
阶段 第一次渲染不需要diff
直接vdom
转 fiber
再次渲染的时候 会产生新的vdom
这个时候就要和之前的 fiber
进行对比决定如何产生新的fiber
对于可复用的节点打上修改标记 剩余的旧节点打上删除的标记 新节点打上新增标记
打上effectTag
可以标识这个fiber
发生了怎么样的变化(新增、更新、删除) 这些被打上标记的fiber
会在complete
阶段被收集起来 形成一个effectList
链表 只包含这些需要操作的fiber
最后在commit
阶段被更新掉
Diff 的主体 1 2 3 4 5 6 workInProgress.child = reconcileChildFibers ( workInProgress, current.child , nextChildren, renderLanes );
Diff 的基本原则 场景有节点自身更新 节点增删 节点移动等三种情况
即使两个元素的子树完全一样 但前后父级元素不同 按照规则 div 元素及其子树会完全销毁 并重建新的一个 p 元素及其子树 不会尝试复用子树
使用标签名(tag
)和key
识别节点 区分出前后的节点是否变化 以达到尽量复用无变化的节点(React 可以根据 tag
和key
来判断节点只是位置发生了变化来复用)
每轮循环都是自左向右进行查找比对的
Diff 使用场景 单节点 单节点更新以及增删 即在tag
和key
相同的情况下 节点的属性发生了变化 就是节点更新 如果节点的tag | key
不同 则新旧节点没有关系
单节点指newChildren
为单一节点 但是oldFiber
的数量不一定
1 2 3 1. old : A new : A2. old : A - B - C new : B3. old : -- new : A
对于单节点 其实只有更新操作 对 React 来说只有两种情况 即 oldFiber 是否为空
多节点 多节点分为节点更新、新增节点、删除节点、节点移动四种情况 调用 reconcileChildrenArray
进行diff
它会以newChildren
为主体进行最多三轮遍历 即第一轮从头开始 之后每一轮都是上一轮结束的断点继续
在平时的实践中 节点自身的更新是最频繁的 所以diff
算法会优先处理更新的节点 所以第一轮是针对节点自身属性的更新 后面两轮依次处理节点的新增、移动
节点更新 第一轮从头开始遍历 newChildren
逐个和 oldFiber
链中的节点进行比较 判断节点的 key |tag
是否有变化
没变则从oldFiber
节点clone
一个props
被更新的fiber
节点 新的节点来自于newChildren
中的新节点 实现节点更新
有变化则说明不满足复用条件 立即中断遍历进入下边的遍历 (diff 算法的复杂度也因为这个操作大幅降低)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 const newFiber = updateSlot ( returnFiber, oldFiber, newChildren[newIdx], lanes, ); if (newFiber === null ) { if (oldFiber === null ) { oldFiber = nextOldFiber; } break ; } ... 1. 第一轮遍历中 ab都是一样的 可以复用 在d这里跳出第一轮循环 这里的最后一个可复用的节点 即b(lastPlacedIndex )是下一轮循环的移动节点参照物.... lastPlacedIndex = placedChild (newFiber, lastPlacedIndex, newIndex)
节点删除 遍历 newChildren
当它遍历结束但是 oldFiber
链还没遍历完 说明剩下的节点都要被删除 可以直接在oldFiber
的节点上打上 Delection
的删除标记理删除
1 2 3 4 5 6 if (newIdx === newChildren.length ) { deleteRemainingChildren (returnFiber, oldFiber); return resultingFirstChild; }
节点新增 当oldFiber
链遍历完 但newChildren
还没遍历完 那么余下的节点都属于新插入的节点 会新建fiber
节点并以sibling
为指针连成 fiber
链
节点移动 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 //old: a-b-c-d-e-f new: a-b-d-c-e 第一轮 发现固定节点为ab 此时oldFiber为CDEF newChildren为 DCE 之后将oldFiber节点放入一个以key为键的map中 existingChildren newChildren中剩余的节点 都是不确定是否要移动的 所以遍历它们 每一个去看看这个节点在oldFiber中的旧位置 遍历到它在newChildren中的新位置 如果旧位置在lastPlacedIndex的右边 说明这个节点不变 如果旧节点在左边 而新节点在右边 说明位置没变化 更新新的lastPlacedIndex 如果旧位置在右边 而新位置也在右边 说明位置不变 更新lasyPlacedIndex 如果旧位置在lastPlacedIndex的左边,当前这个节点的位置要往右挪。 原因是旧位置在lastPlacedIndex的左边,新位置却在lastPlacedIndex的右边,所以它要往右挪,但它不是固定节点。此时无需更新lastPlacedIndex //过程 1. 最右侧固定节点为b lastPlacedIndex为1 existingChildren {c: '节点c' d: '节点d', e: "节点e", f: “节点f”} 2. newchildren DCE继续遍历 首先遍历D D在oldFiber中为3 3>1 oldFiber中D的位置在B的右边 newCHildren也是如此 所以D的位置不动 此时最新的节点变为D lasyPlacedIndex为3 并从existingChildren删除D 3.再遍历到c c在oldFiber中的位置是2 2 < 3 c原来在d的左边 而newChildren中的c在d的右边 所以要将它移动到右边 并从 existingChildren 中删除c 4.再遍历到e e在oldFiber中的位置是4 4 > 3 所以e的位置不懂 最新的固定点变成e 从 existingChildren中删除e 5.newChildren中的节点都处理完了 针对移动节点的遍历结束 existingFiber中还有f节点 直接删除就好
总结
第一轮遍历 一一对比vdom
和oldFiber
如果可以复用就处理下一节点 否则就结束遍历
如果所有新的vdom
都处理完了 就把剩下的fiber
节点删除掉就行
如果还有vdom
没有处理 就就行第二次遍历
第二轮遍历 把oldFiber
中的lastPlacedIndex
之后的节点放入 existingChildren
的 map
中 遍历剩下的vdom
从map
中查找 如果找到了 在判断是否需要移动 (是否需要移动都需要在查找后从map
中删除该节点)
第二轮遍历后 就将剩余的oldFiber
删除掉 剩余的vdom
新增
源码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 function reconcileChildrenArray ( returnFiber: Fiber, currentFirstChild: Fiber | null , newChildren: Array <*>, lanes: Lanes ): Fiber | null { let resultingFirstChild : Fiber | null = null ; let previousNewFiber : Fiber | null = null ; let oldFiber = currentFirstChild; let lastPlacedIndex = 0 ; let newIdx = 0 ; let nextOldFiber = null ; for (; oldFiber !== null && newIdx < newChildren.length ; newIdx++) { if (oldFiber.index > newIdx) { nextOldFiber = oldFiber; oldFiber = null ; } else { nextOldFiber = oldFiber.sibling ; } const newFiber = updateSlot ( returnFiber, oldFiber, newChildren[newIdx], lanes ); if (newFiber === null ) { if (oldFiber === null ) { } break ; } if (shouldTrackSideEffects) { if (oldFiber && newFiber.alternate === null ) { deleteChild (returnFiber, oldFiber); } } lastPlacedIndex = placeChild (newFiber, lastPlacedIndex, newIdx); if (previousNewFiber === null ) { resultingFirstChild = newFiber; } else { previousNewFiber.sibling = newFiber; } previousNewFiber = newFiber; oldFiber = nextOldFiber; } if (newIdx === newChildren.length ) { deleteRemainingChildren (returnFiber, oldFiber); return resultingFirstChild; } if (oldFiber === null ) { for (; newIdx < newChildren.length ; newIdx++) { const newFiber = createChild (returnFiber, newChildren[newIdx], lanes); if (newFiber === null ) { continue ; } lastPlacedIndex = placeChild (newFiber, lastPlacedIndex, newIdx); if (previousNewFiber === null ) { resultingFirstChild = newFiber; } else { previousNewFiber.sibling = newFiber; } previousNewFiber = newFiber; } return resultingFirstChild; } const existingChildren = mapRemainingChildren (returnFiber, oldFiber); for (; newIdx < newChildren.length ; newIdx++) { const newFiber = updateFromMap ( existingChildren, returnFiber, newIdx, newChildren[newIdx], lanes ); if (newFiber !== null ) { if (shouldTrackSideEffects) { if (newFiber.alternate !== null ) { existingChildren.delete ( newFiber.key === null ? newIdx : newFiber.key ); } } lastPlacedIndex = placeChild (newFiber, lastPlacedIndex, newIdx); if (previousNewFiber === null ) { resultingFirstChild = newFiber; } else { previousNewFiber.sibling = newFiber; } previousNewFiber = newFiber; } } if (shouldTrackSideEffects) { existingChildren.forEach ((child ) => deleteChild (returnFiber, child)); } return resultingFirstChild; }
vue vue2Diff 双端比较 即新列表和旧列表两个列表的头尾互相比对 在比对的过程中逐渐向内靠拢 知道某一个列表的所有节点全部遍历过 对比停止(即将复杂的变化情况留在中间位置)
算法规则
如果新节点有子节点而老节点没有 则判断老节点是否有文本内容 如果有就清空老节点的文本内容 然后为其新增子节点
如果新节点没有子节点而老节点有子节点 则先删除老节点的子节点 然后设置文本内容
如果新节点没有子节点 而老节点也没有子节点 则进行文本的对比 然后设置其文本内容
如果新节点有子节点 老节点也有子节点 则进行新老子节点的比对 然后进行新增 移动 删除 修改等操作 也是diff
算法操作的情况
执行时机 在页面首次渲染时会调用一次patch
并创建新的 vdom
但是这个时候不会使用diff
算法进行更深层次的比较
之后在组件的状态数据发生改变时 会触发setter
并通过 Notify 通知Watcher
对应的Watcher
会通知更新并执行更新函数 会执行render
函数获取新的vDom
然后执行patch
对比上次渲染的结果和oldVdom
并计算出最小的变化 然后再更根据这个最小的变化去更新真实的dom
实现描述 1 2 3 4 //vue/src/core/vdom/path.js let oldStartIdx, oldEndIdx, newStartIdx, newEndIdx ;//分别用来表示新旧vnode的首尾索引 //当 oldStartIdx > oldEndIdx 表示旧vdom已经遍历完成 接下来是新增vdom的操作 //当 newStartIdx > newEndIdx 表示新vdom已经遍历完成 直接对剩余的旧vdom进行删除操作就行
先对以下四种情况做优化策略
oldStartIdx / newStartIdx
oldEndIdx / newEndIdx
oldStartIdx / newEndIdx
oldEndIdx / newStartIdx
如果以上的情况都没找到 则从新数组的第一个节点去老数组中查找 找到了就递归更新 没找到就创建新节点
最后循环结束后 需要对未处理的节点进行处理 如果是老节点列表先循环完毕 这个时候如果新节点列表还有剩余的节点 则说明这些节点直接创建并插入到Dom
中就行了 如果新节点列表先循环完毕 而老节点列表还有剩余节点 则说明这些节点都是要被废弃的节点 直接批量删除就好了
Diff 算法的优化 1.只比较同一层级 不跨级比较 Diff
过程只会对同一层级的DOM
进行比较来简化比较次数
2.通过 tag 以及 key 1.tag 如果同一层的 tag 不同 则直接移除老的DOM
对应的节点
2.key 如果tag && key
相同 则认为是相同的节点 不会继续按照这个树结构做深度比较
key 的作用
key
可以精确的找到相同的新旧虚拟节点 可以加快 patch 过程 更高效地更新虚拟DOM
如果patch
的时候发现key
值不同 则会直接认为不是相同的节点 所以如果key
值不唯一 可能会有一些隐藏的 bug 比如对列表数据进行增删 还有使用相同标签元素进行过度切换的时候 会导致只替换内部属性而不会触发过渡效果
源码解读 源码分析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 function patch (oldVnode, vnode, hydrating, removeOnly ) { if (isUndef (vnode)) { if (isDef (oldVnode)) invokeDestroyHook (oldVnode); return ; } let isInitialPatch = false ; const insertedVnodeQueue = []; if (isUndef (oldVnode)) { isInitialPatch = true ; createElm (vnode, insertedVnodeQueue); } else { const isRealElement = isDef (oldVnode.nodeType ); if (!isRealElement && sameVnode (oldVnode, vnode)) { patchVnode (oldVnode, vnode, insertedVnodeQueue, null , null , removeOnly); } else { if (isRealElement) { 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." ); } } oldVnode = emptyNodeAt (oldVnode); } const oldElm = oldVnode.elm ; const parentElm = nodeOps.parentNode (oldElm); createElm ( vnode, insertedVnodeQueue, oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling (oldElm) ); 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); } const insert = ancestor.data .hook .insert ; if (insert.merged ) { for (let i = 1 ; i < insert.fns .length ; i++) { insert.fns [i](); } } } else { registerRef (ancestor); } ancestor = ancestor.parent ; } } if (isDef (parentElm)) { removeVnodes ([oldVnode], 0 , 0 ); } else if (isDef (oldVnode.tag )) { invokeDestroyHook (oldVnode); } } } invokeInsertHook (vnode, insertedVnodeQueue, isInitialPatch); return vnode.elm ; } function sameVnode (a, b ) { return ( (a.key === b.key && (a.asyncFactory == b.asyncFactory ) & (a.tag === b.tag && a.isComment == b.isComment && isDef (a.data ) == isDef (b.data ) && sameInputeType (a, b))) || (isTrue (a.isAsyncPlaceholder ) && isUndef (b.isAsyncFactory .error )) ); }
patchVnode 方法的源码
这是在新的 vnode 和 oldVnode 是同一节点的情况下 才会执行的函数 主要对比节点文本变化或子节点变化
如果oldVnode
和 vnode
的引用地址是一样的 就表示节点没有变化 直接返回
如果 oldVnode && isAsyncPlaceholder
则跳过异步组件的检查 直接返回
如果oldVnode
和vnode
都是静态节点且有唯一的key
并且vnode
是克隆节点或者v-once
控制节点时 将oldVnode.elm
和 oldVnode.child
都复制到vnode
上 然后返回
如果vnode
不是文本节点也不是注释的情况下
如果vnode
和 oldVnode
都有子节点 并且子节点不一样 就使用 updateChildren
更新节点
如果只有vnode
有子节点 就直接使用addVnodes
创建子节点
如果只有``oldVnode有子节点 就调用
removeVnodes`删除子节点
如果vnode
文本为undefined
就删掉vnode.elm
如果vnode
是文本节点但是和oldVnode
文本的内容不一样 就更新文本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 function patchVnode ( oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly ) { if (oldVnode === vnode) return ; if (isDef (vnode.elm ) && isDef (ownerArray)) { vnode = ownerArray[index] = cloneVNode (vnode); } const elm = (vnode.elm = oldVnode.elm ); if (isTrue (oldVnode.isAsyncPlaceholder )) { if (isDef (vnode.asyncFactory .resolved )) { hydrate (oldVnode.elm , vnode, insertedVnodeQueue); } else { vnode.isAsyncPlaceholder = true ; } return ; } 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 ; if (isDef (data) && isDef ((i = data.hook )) && isDef ((i = i.prepatch ))) { i (oldVnode, vnode); } const oldCh = oldVnode.children ; const ch = vnode.children ; 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); } if (isUndef (vnode.text )) { if (isDef (oldCh) && isDef (ch)) { if (oldCh !== ch) updateChildren (elm, oldCh, ch, insertedVnodeQueue, removeOnly); } else if (isDef (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 ); } if (isDef (data)) { if (isDef ((i = data.hook )) && isDef ((i = i.postpatch ))) i (oldVnode, vnode); } }
updateChildren 新的vnode
和oldVnode
都有子节点并且子节点不一样的时候进行对比子节点的函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 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; const canMove = !removeOnly; if (process.env .NODE_ENV !== "production" ) { checkDuplicateKeys (newCh); } while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef (oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx]; } 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)) { 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)) { patchVnode ( oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx ); 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 ] : findIdxInOld (newStartVnode, oldCh, oldStartIdx, oldEndIdx); if (isUndef (idxInOld)) { 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 { createElm ( newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm , false , newCh, newStartIdx ); } } 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 (oldCh, oldStartIdx, oldEndIdx); } }
vue3 diff 的作用域 存在 children 的 vnode 类型 1.element 元素类型 vnode patchElement
用于处理element
类型的vnode
2.fragment 碎片类型的 vnode fragment
是虚拟的 并不会在DOM
树中呈现 这样我们就可以将组件的功能绑定到单一的元素中 而不需要创建一个多余的DOM
节点 使用 processFragment
处理Fragment
类型的vnode
1 2 3 4 5 if (patchFlag && PatchFlag .KEYD_FRAGMENT ) {... } else if (patch && PatchFlags .UNKEYED_FRAGMENT ) { }
patchChildren
根据是否存在key
进行真正的 diff
或者直接patch
diff 算法具体操作 不存在 key 的时候 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 c1 = c1 || EMPTY_ARR c2 = c2 || EMPTY_ARR const oldLength = c1.length const newLength = c2.length const commonLength = Math .min (oldLength, newLength) let i for (i = 0 ; i < commonLength; i++) { const nextChild = (c2[i] = optimized ? cloneIfMounted (c2[i] as VNode ) : normalizeVNode (c2[i])) patch ( c1[i], nextChild, container, null , parentComponent, parentSuspense, isSVG, optimized ) } if (oldLength > newLength) { unmountChildren (c1, parentComponent, parentSuspense, true , commonLength) } else { mountChildren ( c2, container, anchor, parentComponent, parentSuspense, isSVG, optimized, commonLength ) }
存在 key 的情况 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 /* c1 老的vnode c2 新的vnode */ let i = 0 /* 记录索引 */ const l2 = c2.length /* 新vnode的数量 */ let e1 = c1.length - 1 /* 老vnode 最后一个节点的索引 */ let e2 = l2 - 1 /* 新节点最后一个节点的索引 */ //首先新旧children首首对比 发现不同立马跳出 while(i <= e1 && i <= e2) { ..... } //从新旧children的尾部开始向前对比 发现不同立马退出 while( i <= e1 && i <= e2) { //..... e1--; e2--; } //如果老节点全部patch 新节点没有patch完 则创建新的vnode插入 if(i > e1) { if(i <= e2) while(i <= e2) { patch(...) } } //如果新的节点全部patch 而节点有剩余 则卸载所有老节点 else if(i > e2) { while(i <= e1) { unmount(...); i++; } } //如果双端对比后 没有处理完 则需要继续处理 即diff的核心 最长连续稳定序列算法 const s1 = i //第一步遍历到的index const s2 = i const keyToNewIndexMap: Map<string | number, number> = new Map() /* 把没有比较过的新的vnode节点,通过map保存 */ for (i = s2; i <= e2; i++) { if (nextChild.key != null) { keyToNewIndexMap.set(nextChild.key, i) } } let j;//记录剩下新的节点的索引 let patched = 0 ;//记录这个过程patch的节点的数量 const toBePatched = e2 - s2 + 1 /* 没有经过 path 新的节点的数量 */ let moved = false /* 证明是否移动过 */ let maxNewIndexSoFar = 0 const newIndexToOldIndexMap = new Array(toBePatched);//用来存放新节点的索引和老节点的索引的数组 for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0 /* 建立一个数组,每个子元素都是0 [ 0, 0, 0, 0, 0, 0, ] */ --------------------------------------- //接下来是diff算法的核心 //第一步 通过老节点的key找到对应新节点的index 开始遍历老的节点 判断有没有key 如果存在key通过新节点的keyToNewIndexMNap找到新节点index 如果不存在则会遍历生下来的新节点试图找到对应index //如果存在index证明有对应的老节点 则直接复用老节点进行patch 如果没有找到老节点对应的新节点 则删除当前的老节点 //newIndexToOldIndexMap找到对应新老节点关系 for (i = s1; i <= e1; i++) { /* 开始遍历老节点 */ const prevChild = c1[i] if (patched >= toBePatched) { /* ①如果已经patch的大于等于需要patch的,说明新children已经处理完成,那么统一卸载老的节点 */ unmount(prevChild, parentComponent, parentSuspense, true) continue } let newIndex /* ② 如果,老节点的key存在 ,通过key找到对应的index */ if (prevChild.key != null) { newIndex = keyToNewIndexMap.get(prevChild.key) } else { /* ③ 如果,老节点的key不存在 */ for (j = s2; j <= e2; j++) { /* 遍历剩下的所有新节点 */ if ( newIndexToOldIndexMap[j - s2] === 0 && /* newIndexToOldIndexMap[j - s2] === 0 新节点没有被patch */ isSameVNodeType(prevChild, c2[j] as VNode) ) { /* 如果找到与当前老节点对应的新节点那么 ,将新节点的索引,赋值给newIndex */ newIndex = j break } } } if (newIndex === undefined) { /* ①没有找到与老节点对应的新节点,删除当前节点,卸载所有的节点 */ unmount(prevChild, parentComponent, parentSuspense, true) } else { /* ②把老节点的索引,记录在存放新节点的数组中, */ newIndexToOldIndexMap[newIndex - s2] = i + 1 if (newIndex >= maxNewIndexSoFar) { maxNewIndexSoFar = newIndex } else { /* 证明有节点已经移动了 */ moved = true } /* 找到新的节点进行patch节点 */ patch( prevChild, c2[newIndex] as VNode, container, null, parentComponent, parentSuspense, isSVG, optimized ) patched++ } } //虽然已经patch过所有的老节点 可是对于已经发生移动的节点 要怎么真正移动dom元素 以及对于新增的节点 要怎么处理 /*移动老节点创建新节点*/ /* 根据最长稳定序列移动相对应的节点 */ const increasingNewIndexSequence = moved ? getSequence(newIndexToOldIndexMap)//通过getSequece获取一个最长稳定序列 对于index === 0的情况也就是新增节点 需要重新创建一个vnode 然后对于发生移动的节点进行统一的移动 : EMPTY_ARR j = increasingNewIndexSequence.length - 1 for (i = toBePatched - 1; i >= 0; i--) { const nextIndex = s2 + i const nextChild = c2[nextIndex] as VNode const anchor = nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor if (newIndexToOldIndexMap[i] === 0) { /* 没有老的节点与新的节点对应,则创建一个新的vnode */ patch( null, nextChild, container, anchor, parentComponent, parentSuspense, isSVG ) } else if (moved) { if (j < 0 || i !== increasingNewIndexSequence[j]) { /*如果没有在长*/ /* 需要移动的vnode */ move(nextChild, container, anchor, MoveType.REORDER) } else { j-- }
Q: 为什么要得到最长稳定序列
A: 我们需要一个序列作为基础的参照序列 其他未在稳定序列的节点进行移动
总结
新旧children
首部开始对比 发现不同立马跳出
如果第一步没有patch
完 立即从后往前开始patch
发现不同立马跳出循环
如果新的节点大于老的节点数 对于剩下的节点全部以新的 vnode 处理
对于老的节点大于新的节点 则将超出的节点卸载
前面四轮没有 patch 完的元素与 3,4 对立关系
将没有比较过的新节点通过map
保存 记录已经 patch 过的新节点的数量 patched
没有进过patch
新的节点的数量toBePatched
进阿里一个数组 newIndexToOldIndexMap 来记录老节点的索引 数组的索引就是新节点的索引
开始遍历老节点
如果toBePatched
新的节点数量为 0 说明已经比较完成 可以统一卸载老节点
如果老节点的key
存在 则通过key
找到对应的新节点的index
如果老节点的key
不存在
遍历剩下的所有新节点
如果找到与当前老节点对应的新节点 那么将新节点的索引 赋值给 newIndex
如果没有找到与老节点对应的新节点 卸载当前老节点
如果找到与老节点对应的新节点 则将老节点的索引 记录在存放新节点的数组中
如果节点发生移动 用moved
记录移动了
根据newIndexOldIndexMap
新节点索引列表找到最长稳定序列
对于newIndexToOldIndexMap - item == 0
证明不存在老节点 直接创建新的vnode
对于发生移动的节点进行移动处理
patch
新老节点 找到新的节点进行patch
遍历结束
vue3 相比 vue2 的优化 事件缓存 将事件进行缓存,这样如果多次用到相同的事件,就不用重复去创建以及绑定
1 2 3 4 5 6 7 <button @click={toClickSec}>clickSecondBtn</button> export function render (_ctx, _cache, $props, $setup, $data, $options ) { return (_openBlock (), _createElementBlock ("div" , _hoisted_1, [ _createElementVNode ("button" , { onClick : _cache[1 ] || (_cache[1 ] = $event => ({toClickSec : _ctx.toClickSec })) }, "clickSecondBtn" ) ]))
静态标记 对于一些属性或者内容固定的节点,对其打上静态节点的标记,这样在后续的diff
过程中,可以跳过这些不必要的比较来优化diff
算法 ,同样的 还有类似于class绑定、props
等,也是同样的道理,利用标记来避免不必要的比较
1 2 3 4 5 6 7 8 <h2>i am olddog</h2> <div > i am div</div > <div :class ={style} > i am class div</div > <div :pro ="props" :class ={style} > i am props div </div > onst _hoisted_1 = { id : "app" } const _hoisted_2 = _createElementVNode ("h2" , null , "i am olddog" , -1 )const _hoisted_3 = _createElementVNode ("div" , null , "i am div" , -1 )const _hoisted_4 = ["pro" ]
可以看到,这些标记其实就是一些位运算
不仅 vue。react 使用的也是位运算来标记,除此之外,位运算还可以用于在权限系统中(例如一个系统拥有多个用户,而每个用户只有几个权限的情况下 但是需注意这种情况下的权限查找较不方便)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 export const enum PatchFlags { TEXT = 1 , CLASS = 1 << 1 , STYLE = 1 << 2 , PROPS = 1 << 3 , FULL_PROPS = 1 << 4 , HYDRATE_EVENTS = 1 << 5 , STABLE_FRAGMENT = 1 << 6 , KEYED_FRAGMENT = 1 << 7 , UNKEYED_FRAGMENT = 1 << 8 , NEED_PATCH = 1 << 9 , DYNAMIC_SLOTS = 1 << 10 , }
vue2
是全量Diff
(即不管任何节点都会进行diff
) vue3
是 静态标记 + 非全量DIff
(即通过静态标记标记不同Vdom
的类型,以此跳过不必要的比较来优化diff
的效率)
静态提升 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <h2>i am olddog</h2> <div > i am div</div > <div :class ={style} > i am class div</div > <div :pro ="props" :class ={style} > i am props div </div > onst _hoisted_1 = { id : "app" } const _hoisted_2 = _createElementVNode ("h2" , null , "i am olddog" , -1 )const _hoisted_3 = _createElementVNode ("div" , null , "i am div" , -1 )const _hoisted_4 = ["pro" ]export function render (_ctx, _cache, $props, $setup, $data, $options ) { return (_openBlock (), _createElementBlock ("div" , _hoisted_1, [ _hoisted_2, _hoisted_3, _createElementVNode ("div" , { class : _normalizeClass ({style : _ctx.style }) }, "i am class div" , 2 ),
可以看到 对于一些内容或属性固定的静态节点 vue3
将它们定义在 render
函数外部来做到 静态提升
这样当多次用到这些节点的时候 可以直接引用而不是重复创建 大大提高了性能
使用最长稳定子序列优化对比流程 vue2
旧头和新头相比
旧头和新尾相比
旧尾和新头相比
旧尾和新尾相比
都没有命中的对比
vue3
旧头和新头对比
旧尾和新尾对比
基于最长递增子序列进行移动/增加/删除 (使用最长递增子序列可以最大程度的减少DOM
的移动 达到最少的DOM
操作)