虚拟DOM
虚拟DOM(virtual DOM)用js对象结构表示DOM树结构,然后用这个树构建一个真正的DOM树,插到文档中,当状态变更时,重新构造一颗新的对象树,然后将新的树和旧的树进行比较,记录差异,并把差异以打补丁的方式应用到真正的DOM树上来更新视图。
diff算法就是调用名为patch的函数,比较新旧节点,一边比较一边给真实的DOM打补丁。原始的diff算法是遍历循环比较的,不分层级的,而优化后的diff算法同层比较,不跨层级的。
vue中的diff算法
当data发生变化时,set方法会调用Dep.notify通知所有订阅者,此时订阅者会调用patch方法给dom打补丁来更新视图:
patch方法
function patch (oldVnode, vnode) {
// ......
if (sameVnode(oldVnode, vnode)) { // 节点一样的话深入检查子节点
patchVnode(oldVnode, vnode)
} else {
const oEl = oldVnode.el // 当前oldVnode对应的真实元素节点
let parentEle = api.parentNode(oEl) // 父元素
createEle(vnode) // 根据Vnode生成新元素
if (parentEle !== null) {
api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 将新元素添加进父元素
api.removeChild(parentEle, oldVnode.el) // 移除以前的旧元素节点
oldVnode = null
}
}
// ......
return vnode
}
该方法接受两个参数分别是旧节点oldVnode和新节点Vnode。用sameVnode方法判断两个节点是否可以进行比较,如果可以的话调用patchVnode方法,否则直接用Vnode替换oldVnode。
function sameVnode (a, b) {
return (
a.key === b.key && // key值
a.tag === b.tag && // 标签名
a.isComment === b.isComment && // 是否为注释节点
// 是否都定义了data,data包含一些具体信息,例如onclick , style
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b) // 当标签是<input>的时候,type必须相同
)
}
patchVnode方法
function patchVnode (
oldVnode, // 旧节点
vnode, // 新节点
insertedVnodeQueue, // 插入节点的队列
ownerArray, // 节点 数组
index, // 当前 节点的
removeOnly // 只有在 patch 函数中被传入,当老节点不是真实的 dom 节点,当新老节点是相同节点的时候
) {
// 如果新节点和旧节点 相等(使用了 同一个地址,直接返回不进行修改)
// 这里就是 当 props 没有改变的时候,子组件不会做渲染,而是直接复用
if (oldVnode === vnode) {
return
}
if (isDef(vnode.elm) && isDef(ownerArray)) {
// clone reused vnode
vnode = ownerArray[index] = cloneVNode(vnode)
}
const elm = vnode.elm = oldVnode.elm
// 当 当前节点 是 注释节点(被 v-if )了,或者是一个 异步函数节点,那不执行
if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
} else {
vnode.isAsyncPlaceholder = true
}
return
}
// 当前节点 是一个静态节点的时候,或者 标记了 once 的时候,那不执行
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
// 调用 prepatch 的钩子函数
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
const oldCh = oldVnode.children
const ch = vnode.children
// 调用 update 钩子函数
if (isDef(data) && isPatchable(vnode)) {
// 这里 的 update 钩子函数式 vnode 本身的钩子函数
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
// 这里的 update 钩子函数 是 用户传过来的 钩子函数
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
// 新节点 没有 text 属性
if (isUndef(vnode.text)) {
// 如果都有子节点,对比更新子节点
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) { // 新节点存在子节点,但是老节点不存在子节点
// 如果老节点是 text, 清空
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)) { // 如果老节点是 text, 清空
nodeOps.setTextContent(elm, '')
}
// 新旧节点 text 属性不一样
} else if (oldVnode.text !== vnode.text) {
// 将 text 设置为 新节点的 text
nodeOps.setTextContent(elm, vnode.text)
}
if (isDef(data)) {
// 执行 postpatch 钩子函数
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
该方法会首先看是否是同一个节点、是否是注释节点、是否是异步函数组件、是否是静态节点,是否是 once 的节点,是的话,不执行;接着查看是否存在子节点,新老节点都存在子节点,就执行updateChildren函数深入对比;新节点存在子节点但是老节点不存在子节点,增加,新节点不存在子节点但是老节点存在子节点,删除,检查新老节点的text属性是否一致,否的话,就更新。
updateChildren (parentElm, oldCh, newCh) {
let oldStartIdx = 0, 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
let idxInOld
let elmToMove
let before
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode == null) { // 对于vnode.key的比较,会把oldVnode = null
oldStartVnode = oldCh[++oldStartIdx]
}else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx]
}else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx]
}else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx]
}else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
}else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
}else if (sameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode)
api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
}else if (sameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode)
api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
}else {
// 使用key时的比较
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 有key生成index表
}
idxInOld = oldKeyToIdx[newStartVnode.key]
if (!idxInOld) {
api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
newStartVnode = newCh[++newStartIdx]
}
else {
elmToMove = oldCh[idxInOld]
if (elmToMove.sel !== newStartVnode.sel) {
api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
}else {
patchVnode(elmToMove, newStartVnode)
oldCh[idxInOld] = null
api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el)
}
newStartVnode = newCh[++newStartIdx]
}
}
}
if (oldStartIdx > oldEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx)
}else if (newStartIdx > newEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
该方法会从双端比较新旧节点的各个子节点,一共有四种比较方法,如果四种比较都不匹配并且设置了key值,那么就会用key值进行比较。
oldS与S匹配,真实DOM的节点不变。(对应的两个指针都向中间靠)oldE与E匹配,真实DOM的节点不变。oldS与E匹配,真实DOM的第一个节点移到最后。oldE与S匹配,真实DOM的最后一个节点移到最前方。- 如果新旧子节点都存在
key,那么会根据oldChild的key生成一张hash表,用S的key与hash表做匹配,匹配成功就判断S和匹配节点是否为sameNode,如果是,就在真实dom中将成功的节点移到最前面,否则,将S生成对应的节点插入到dom中对应的oldS位置,S指针向中间移动,被匹配old中的节点置为null。 - 如果没有
key,则直接将S生成新的节点插入真实DOM(有了key,可以尽可能地重用元素,主要是为了高效地更新虚拟DOM。)
例子
真实DOM: b d c a
old: b d c a
new: a e b f
-
oldS = b, oldE = a; S = a, E = f;oldE与S匹配,将a节点放到第一个,此时真实
DOM:a b d c; -
oldS = b, oldE = c; S = e, E = f;四种匹配都不行,看key值,将e节点移到oldS所在的位置,此时真实
DOM:a e b d c; -
oldS = b, oldE = c; S = b, E = f;oldS与S匹配,节点不变,此时真实
DOM:a e b d c; -
oldS = d, oldE = c; S = f, E = f;四种匹配都不行,看key值,将f节点移到oldS所在的位置,此时真实
DOM:a e b f d c; -
S > E,结束比较,删除旧节点,此时真实
DOM:a e b f;
react中的diff算法
Fiber双缓存
react的源码主要分为三个部分:
Scheduler(调度器):根据任务优先级,从高到低依次安排任务进行reconcile;(16之前没有这一个部分。任务只能同步执行,不能中断)Reconciler(协调器):找出改变的节点,打上增删改的effectTag;Renderer(渲染器):将打上effectTag的节点渲染到视图上;
Fiber是保存DOM信息的一种数据结构,通过child、sibling和return形成Fiber树。
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// 作为静态数据结构的属性
this.tag = tag;
this.key = key;
this.elementType = null;
this.type = null;
this.stateNode = null;
// 用于连接其他Fiber节点形成Fiber树
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
this.ref = null;
// 作为动态的工作单元的属性
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
this.effectTag = NoEffect;
this.nextEffect = null;
this.firstEffect = null;
this.lastEffect = null;
// 调度优先级相关
this.lanes = NoLanes;
this.childLanes = NoLanes;
// 指向该fiber在另一次更新时对应的fiber
this.alternate = null;
}
举个例子,如果一个组件是以下结构:
function App() {
return (
<div>
mm,
<ul>
<li>hello</li>
<li>i'm wgg</li>
</ul>
</div>
)
}
那么对应的Fiber树结构为:
react中存在两棵Fiber树:current Fiber树描述了当前展现的DOM树,workInProgress Fiber树是正在更新的Fiber树。
首次渲染的话,直接根据jsx返回的对象构建current Fiber树应用到真实DOM上,更新的话,会将jsx返回的对象与current Fiber树做比较形成workInProgress Fiber树,然后workInProgress Fiber树变成current Fiber树应用到真实DOM上。这个对比的过程就叫diff算法。
diff算法
经典diff算法的时间复杂度为O(n^3),而react通过三个策略将diff算法优化到 O(n) 的时间复杂度:
- 同级元素比较,跨级层的不复用。
- 不同类型节点生成的
DOM树不同,此时会直接销毁老节点及子孙节点,并新建节点。 - 对同一层级的子节点,开发者可以通过
key来确定哪些子元素可以在不同渲染中保持稳定。
export function reconcileChildren(
current: Fiber | null, // 当前 fiber 节点
workInProgress: Fiber, // 父 fiber 节点
nextChildren: any, // 新生成的 ReactElement 内容
renderLanes: Lanes, // 渲染的优先级
) {
if (current === null) {
// 如果当前 fiber 节点为空,则直接将新的 ReactElement 内容生成新的 fiber
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes,
);
} else {
// 当前 fiber 节点不为空,则与新生成的 ReactElement 内容进行 diff
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes,
);
}
}
该方法根据当前Fiber是否存在决定是否直接渲染ReactElement内容或者是否与current Fiber树做比较进行diff。
function reconcileChildFibers(
returnFiber: Fiber, // 父 Fiber
currentFirstChild: Fiber | null, // 父 fiber 下要对比的第一个子 fiber
newChild: any, // 更新后的 React.Element 内容
lanes: Lanes, // 更新的优先级
): Fiber | null {
// 对新创建的 ReactElement 最外层是 fragment 类型单独处理,比较其 children
const isUnkeyedTopLevelFragment =
typeof newChild === 'object' &&
newChild !== null &&
newChild.type === REACT_FRAGMENT_TYPE &&
newChild.key === null;
if (isUnkeyedTopLevelFragment) {
newChild = newChild.props.children;
}
// 对更新后的 React.Element 是单节点的处理
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
// 常规 react 元素
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
// react.portal 类型
case REACT_PORTAL_TYPE:
return placeSingleChild(
reconcileSinglePortal(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
// react.lazy 类型
case REACT_LAZY_TYPE:
if (enableLazyElements) {
const payload = newChild._payload;
const init = newChild._init;
return reconcileChildFibers(
returnFiber,
currentFirstChild,
init(payload),
lanes,
);
}
}
// 更新后的 React.Element 是多节点的处理
if (isArray(newChild)) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
// 迭代器函数的单独处理
if (getIteratorFn(newChild)) {
return reconcileChildrenIterator(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
throwOnInvalidObjectType(returnFiber, newChild);
}
// 纯文本节点的类型处理
if (typeof newChild === 'string' || typeof newChild === 'number') {
return placeSingleChild(
reconcileSingleTextNode(
returnFiber,
currentFirstChild,
'' + newChild,
lanes,
),
);
}
if (__DEV__) {
if (typeof newChild === 'function') {
warnOnFunctionType(returnFiber);
}
}
// 不符合以上情况都视为 empty,直接从父节点删除所有旧的子 Fiber
return deleteRemainingChildren(returnFiber, currentFirstChild);
}
该方法主要根据节点类型的不同来进行不同的处理,我们主要讲一下一下几个类型:
-
单节点diff(同级只有一个节点)
function reconcileSingleElement( returnFiber: Fiber, // 父 fiber currentFirstChild: Fiber | null, // 父 fiber 下第一个开始对比的旧的子 fiber element: ReactElement, // 当前的 ReactElement内容 lanes: Lanes, // 更新的优先级 ): Fiber { const key = element.key; let child = currentFirstChild; // 处理旧的 fiber 由多个节点变成新的 fiber 一个节点的情况 // 循环遍历父 fiber 下的旧的子 fiber,直至遍历完或者找到 key 和 type 都与新节点相同的情况 while (child !== null) { if (child.key === key) { // key相同 const elementType = element.type; if (elementType === REACT_FRAGMENT_TYPE) { //...... } else { if ( // 如果新的 ReactElement 和旧 Fiber 的 key 和 type 都相等 child.elementType === elementType || (__DEV__ ? isCompatibleFamilyForHotReloading(child, element) : false) || (enableLazyElements && typeof elementType === 'object' && elementType !== null && elementType.$$typeof === REACT_LAZY_TYPE && resolveLazy(elementType) === child.type) ) { // 对旧 fiber 后面的所有兄弟节点添加 Deletion 副作用标记,用于 dom 更新时删除 deleteRemainingChildren(returnFiber, child.sibling); // 通过 useFiber 复用新节点并返回 const existing = useFiber(child, element.props); existing.ref = coerceRef(returnFiber, child, element); existing.return = returnFiber; if (__DEV__) { existing._debugSource = element._source; existing._debugOwner = element._owner; } return existing; } } // 若 key 相同但是 type 不同说明不匹配,移除旧 fiber 及其后面的兄弟 fiber deleteRemainingChildren(returnFiber, child); break; } else { // 若 key 不同,对当前的旧 fiber 添加 Deletion 副作用标记,继续对其兄弟节点遍历 deleteChild(returnFiber, child); } child = child.sibling; } // 都遍历完之后说明没有匹配到 key 和 type 都相同的 fiber if (element.type === REACT_FRAGMENT_TYPE) { // ...... } else { // createFiberFromElement 创建 fiber 并返回 const created = createFiberFromElement(element, returnFiber.mode, lanes); created.ref = coerceRef(returnFiber, currentFirstChild, element); created.return = returnFiber; return created; } }-
旧
Fiber和新内容(jsx形成的vDom,以下称vDom)key和type相同,可以复用节点。旧
Fiber和vDom的key相同type不同,节点及其兄弟节点都不能复用,打上Deletion标签,创建新的节点,打上Placement的标记。 -
旧
Fiber和vDom的key和type都不相同,直接打上删除Deletion标签,创建新的节点,打上Placement的标记。
-
-
多节点diff(同级有多个节点)
function reconcileChildrenArray( returnFiber: Fiber, currentFirstChild: Fiber | null, newChildren: Array<*>, lanes: Lanes, ): Fiber | null { // ...... let resultingFirstChild: Fiber | null = null; // 最终要返回的第一个子 fiber let previousNewFiber: Fiber | null = null; let oldFiber = currentFirstChild; let lastPlacedIndex = 0; let newIdx = 0; let nextOldFiber = null; // 因为在实际的应用开发中,react 发现更新的情况远大于新增和删除的情况,所以这里优先处理更新 // 根据 oldFiber 的 index 和 newChildren 的下标,找到要对比更新的 oldFiber for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) { if (oldFiber.index > newIdx) { nextOldFiber = oldFiber; oldFiber = null; } else { nextOldFiber = oldFiber.sibling; } // 通过 updateSlot 来 diff oldFiber 和新的 child,生成新的 Fiber // updateSlot 与上面单节点的 diff 类似,如果 oldFiber 可复用,则根据 oldFiber 和 child 的 props 生成新的 fiber;否则返回 null const newFiber = updateSlot( returnFiber, oldFiber, newChildren[newIdx], lanes, ); // newFiber 为 null 说明不可复用,退出第一轮的循环 if (newFiber === null) { if (oldFiber === null) { oldFiber = nextOldFiber; } break; } if (shouldTrackSideEffects) { if (oldFiber && newFiber.alternate === null) { deleteChild(returnFiber, oldFiber); } } // 记录复用的 oldFiber 的 index,同时给新 fiber 打上 Placement 副作用标签 lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); if (previousNewFiber === null) { // 如果上一个 newFiber 为 null,说明这是第一个生成的 newFiber,设置为 resultingFirstChild resultingFirstChild = newFiber; } else { // 否则构建链式关系 previousNewFiber.sibling = newFiber; } previousNewFiber = newFiber; oldFiber = nextOldFiber; } if (newIdx === newChildren.length) { // newChildren遍历完了,说明剩下的 oldFiber 都是待删除的 Fiber // 对剩下 oldFiber 标记 Deletion deleteRemainingChildren(returnFiber, oldFiber); return resultingFirstChild; } if (oldFiber === null) { // olderFiber 遍历完了 // newChildren 剩下的节点都是需要新增的节点 for (; newIdx < newChildren.length; newIdx++) { // 遍历剩下的 child,通过 createChild 创建新的 fiber const newFiber = createChild(returnFiber, newChildren[newIdx], lanes); if (newFiber === null) { continue; } // 处理dom移动,// 记录 index,同时给新 fiber 打上 Placement 副作用标签 lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); // 将新创建 fiber 加入到 fiber 链表树中 if (previousNewFiber === null) { resultingFirstChild = newFiber; } else { previousNewFiber.sibling = newFiber; } previousNewFiber = newFiber; } return resultingFirstChild; } // oldFiber 和 newChildren 都未遍历完 // mapRemainingChildren 生成一个以 oldFiber 的 key 为 key, oldFiber 为 value 的 map const existingChildren = mapRemainingChildren(returnFiber, oldFiber); // 对剩下的 newChildren 进行遍历 for (; newIdx < newChildren.length; newIdx++) { // 找到 mapRemainingChildren 中 key 相等的 fiber, 创建新 fiber 复用 const newFiber = updateFromMap( existingChildren, returnFiber, newIdx, newChildren[newIdx], lanes, ); if (newFiber !== null) { if (shouldTrackSideEffects) { if (newFiber.alternate !== null) { // 删除当前找到的 fiber existingChildren.delete( newFiber.key === null ? newIdx : newFiber.key, ); } } // 处理dom移动,记录 index,同时给新 fiber 打上 Placement 副作用标签 lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); // 将新创建 fiber 加入到 fiber 链表树中 if (previousNewFiber === null) { resultingFirstChild = newFiber; } else { previousNewFiber.sibling = newFiber; } previousNewFiber = newFiber; } } if (shouldTrackSideEffects) { // 剩余的旧 fiber 的打上 Deletion 副作用标签 existingChildren.forEach(child => deleteChild(returnFiber, child)); } return resultingFirstChild; }从源码里可以看到多节点
diff要经历3个遍历:- 先对
newChildren进行第一轮遍历,将当前的oldFiber与 当前newIdx下标的newChild通过updateSlot进行diff,diff的流程和上面单节点的diff类似,然后返回diff后的结果:- 如果
diff后oldFiber和newIdx的key和type一致,说明可复用。根据oldFiber和newChild的props生成新的fiber,通过placeChild给新生成的fiber打上Placement副作用标记,同时新fiber与之前遍历生成的新fiber构建链表树关系。然后继续执行遍历,对下一个oldFiber和下一个newIdx下标的newFiber继续diff。 - 如果
diff后oldFiber和newIdx的key或type不一致,那么说明不可复用,返回的结果为null,第一轮遍历结束
- 如果
- 第一轮遍历结束后,可能会执行以下几种情况:
- 若
newChildren遍历完了,那剩下的oldFiber都是待删除的,对剩下的oldFiber打上Deletion副作用标记 - 若
oldFiber遍历完了,那剩下的newChildren都是需要新增的,遍历剩下的newChildren, 创建新的fiber,给新生成的fiber打上Placement副作用标记并添加到fiber链表树中。 - 若
oldFiber和newChildren都未遍历完,通过mapRemainingChildren创建一个以剩下的oldFiber的key为key,oldFiber为value的map。然后对剩下的newChildren进行遍历,通过updateFromMap在map中寻找具有相同key创建新的fiber(若找到则基于oldFiber和newChild的props创建,否则直接基于newChild创建),则从map中删除当前的key,然后placeChild给新生成的fiber打上Placement副作用标记并添加到fiber链表树中。遍历完之后则existingChildren还剩下oldFiber的话,则都是待删除的fiber,deleteChild对其打上Deletion副作用标记。
- 若
- 先对
简单来讲:
第一轮遍历,一一对比 vdom 和老的 fiber,如果可以复用就处理下一个节点,否则就结束遍历。
如果所有的新的 vdom 处理完了,那就把剩下的老 fiber 节点删掉就行。
如果还有 vdom 没处理,那就进行第二次遍历:
第二轮遍历,把剩下的老 fiber 放到 map 里,遍历剩下的 vdom,从 map 里查找,如果找到了,就移动过来。
第二轮遍历完了之后,把剩余的老 fiber 删掉,剩余的 vdom 新增。
例子
一一对比新的 vdom 和 老的 fiber,发现b是可以复用的,那就创建新 fiber 节点,打上更新标记。
c不可复用,所以结束第一轮遍历,进入第二轮遍历。
e在map中无法匹配,所以直接创建一个新的fiber,打上新增的标签,d可以在map中找到,所以直接复用,打上更新的标签,此时vDom已经遍历完毕,还剩下c,打上删除的标签。