在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
React中diff算法的理解
虚拟DOM
<div class="root" name="root"> <p>1</p> <div>11</div> </div> 如果使用 { type: "div", props: { className: "root" name: "root", children: [{ type: "p", props: { children: [{ type: "text", props: { text: "1" } }] } },{ type: "div", props: { children: [{ type: "text", props: { text: "11" } }] } }] } } 实际上在
操作虚拟DOM与操作原生DOM的比较在这里直接引用了尤大的话( 原生 DOM 操作 vs 通过框架封装操作这是一个性能 对 React 的 Virtual DOM 的误解
MVVM vs Virtual DOM相比起
可以看到, 性能比较也要看场合在比较性能的时候,要分清楚初始渲染、小量数据更新、大量数据更新这些不同的场合,
不要天真地以为 总结以上这些比较,更多的是对于框架开发研究者提供一些参考,主流的框架 diff算法
时间复杂度首先进行一次完整的 diff策略上边提到的
通俗点说就是:
比较后会出现几种情况,然后进行相应的操作:
分析在分析时会简单引用一下在 // react-reconciler/src/ReactChildFiber.js line 1246 export function reconcileChildren( current: Fiber | null, workInProgress: Fiber, nextChildren: any, renderExpirationTime: ExpirationTime, ) { if (current === null) { // 首次渲染 创建子节点的`Fiber`实例 workInProgress.child = mountChildFibers( workInProgress, null, nextChildren, renderExpirationTime, ); } else { // 否则调用`reconcileChildFibers`去做`diff` workInProgress.child = reconcileChildFibers( workInProgress, current.child, nextChildren, renderExpirationTime, ); } } 对比一下 // react-reconciler/src/ReactChildFiber.js line 1370 export const reconcileChildFibers = ChildReconciler(true); export const mountChildFibers = ChildReconciler(false); function ChildReconciler(shouldTrackSideEffects) { // ... function deleteChild(){ // ... } function useFiber(){ // ... } function placeChild(){ // ... } function placeSingleChild(){ // ... } function updateTextNode(){ // ... } function updateElement(){ // ... } function updatePortal(){ // ... } function updateFragment(){ // ... } function createChild(){ // ... } function updateSlot(){ // ... } function updateFromMap(){ // ... } function warnOnInvalidKey(){ // ... } function reconcileChildrenArray(){ // ... } function reconcileChildrenIterator(){ // ... } function reconcileSingleTextNode(){ // ... } function reconcileSingleElement(){ // ... } function reconcileSinglePortal(){ // ... } function reconcileChildFibers(){ // ... } return reconcileChildFibers; }
首先看
分两种情况原因就是为了复用节点,第一种情况, if (currentFirstChild !== null && currentFirstChild.tag === HostText) { // We already have an existing node so let's just update it and delete // the rest. deleteRemainingChildren(returnFiber, currentFirstChild.sibling); // 删除兄弟 const existing = useFiber(currentFirstChild, textContent, expirationTime); existing.return = returnFiber; return existing; // 复用 } 第二种情况, // The existing first child is not a text node so we need to create one // and delete the existing ones. // 创建新的Fiber节点,将旧的节点和旧节点的兄弟都删除 deleteRemainingChildren(returnFiber, currentFirstChild); const created = createFiberFromText( textContent, returnFiber.mode, expirationTime, ); 接下来是 // react-reconciler/src/ReactChildFiber.js line 1132 const key = element.key; let child = currentFirstChild; while (child !== null) { // TODO: If key === null and child.key === null, then this only applies to // the first item in the list. if (child.key === key) { if ( child.tag === Fragment ? element.type === REACT_FRAGMENT_TYPE : child.elementType === element.type || // Keep this check inline so it only runs on the false path: (__DEV__ ? isCompatibleFamilyForHotReloading(child, element) : false) ) { deleteRemainingChildren(returnFiber, child.sibling); // 因为当前节点是只有一个节点,而老的如果是有兄弟节点是要删除的,是多余的 const existing = useFiber( child, element.type === REACT_FRAGMENT_TYPE ? element.props.children : element.props, expirationTime, ); existing.ref = coerceRef(returnFiber, child, element); existing.return = returnFiber; // ... return existing; } else { deleteRemainingChildren(returnFiber, child); break; } } else { deleteChild(returnFiber, child); // 从child开始delete } child = child.sibling; // 继续从子节点中找到一个可复用的节点 } 接下来就是没有找到可以复用的节点因而去创建节点了,对于 // react-reconciler/src/ReactChildFiber.js line 1178 if (element.type === REACT_FRAGMENT_TYPE) { const created = createFiberFromFragment( element.props.children, returnFiber.mode, expirationTime, element.key, ); created.return = returnFiber; return created; } else { const created = createFiberFromElement( element, returnFiber.mode, expirationTime, ); created.ref = coerceRef(returnFiber, currentFirstChild, element); created.return = returnFiber; return created; }
// react-reconciler/src/ReactChildFiber.js line 756 function reconcileChildrenArray( returnFiber: Fiber, currentFirstChild: Fiber | null, newChildren: Array<*>, expirationTime: ExpirationTime, ): Fiber | null { // 机翻注释 // 这个算法不能通过两端搜索来优化,因为我们在光纤上没有反向指针。我想看看我们能用这个模型走多远。如果最终不值得权衡,我们可以稍后再添加。 // 即使是双端优化,我们也希望在很少有变化的情况下进行优化,并强制进行比较,而不是去寻找地图。它想探索在前进模式下首先到达那条路径,并且只有当我们注意到我们需要很多向前看的时候才去地图。这不能处理反转以及两个结束的搜索,但这是不寻常的。此外,要使两端优化在Iterables上工作,我们需要复制整个集合。 // 在第一次迭代中,我们只需在每次插入/移动时都碰到坏情况(将所有内容添加到映射中)。 // 如果更改此代码,还需要更新reconcileChildrenIterator(),它使用相同的算法。 let oldFiber = currentFirstChild; let lastPlacedIndex = 0; let newIdx = 0; let nextOldFiber = null; // 第一个for循环,按照index一一对比,当新老节点不一致时退出循环并且记录退出时的节点及oldFiber节点 for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) { if (oldFiber.index > newIdx) { // 位置不匹配 nextOldFiber = oldFiber; // 下一个即将对比的旧节点 oldFiber = null; // 如果newFiber也为null(不能复用)就退出当前一一对比的for循环 } else { nextOldFiber = oldFiber.sibling; //正常的情况下 为了下轮循环,拿到兄弟节点下面赋值给oldFiber } // //如果节点可以复用(key值匹配),就更新并且返回新节点,否则返回为null,代表节点不可以复用 const newFiber = updateSlot( // 判断是否可以复用节点 returnFiber, oldFiber, newChildren[newIdx], expirationTime, ); // 节点无法复用 跳出循环 if (newFiber === null) { // TODO: This breaks on empty slots like null children. That's // unfortunate because it triggers the slow path all the time. We need // a better way to communicate whether this was a miss or null, // boolean, undefined, etc. if (oldFiber === null) { oldFiber = nextOldFiber; // 记录不可以复用的节点并且退出对比 } break; // 退出循环 } if (shouldTrackSideEffects) { // 没有复用已经存在的节点,就删除掉已经存在的节点 if (oldFiber && newFiber.alternate === null) { // We matched the slot, but we didn't reuse the existing fiber, so we // need to delete the existing child. deleteChild(returnFiber, oldFiber); } } // 本次遍历会给新增的节点打 插入的标记 lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); if (previousNewFiber === null) { // TODO: Move out of the loop. This only happens for the first run. resultingFirstChild = newFiber; } else { // TODO: Defer siblings if we're not at the right index for this slot. // I.e. if we had null values before, then we want to defer this // for each null value. However, we also don't want to call updateSlot // with the previous one. previousNewFiber.sibling = newFiber; } previousNewFiber = newFiber; oldFiber = nextOldFiber; // 重新给 oldFiber 赋值继续遍历 } 在 // react-reconciler/src/ReactChildFiber.js line 544 function updateSlot( returnFiber: Fiber, oldFiber: Fiber | null, newChild: any, expirationTime: ExpirationTime, ): Fiber | null { // Update the fiber if the keys match, otherwise return null. const key = oldFiber !== null ? oldFiber.key : null; if (typeof newChild === 'string' || typeof newChild === 'number') { // 对于新的节点如果是 string 或者 number,那么都是没有 key 的, // 所有如果老的节点有 key 的话,就不能复用,直接返回 null。 // 老的节点 key 为 null 的话,代表老的节点是文本节点,就可以复用 if (key !== null) { return null; } return updateTextNode( returnFiber, oldFiber, '' + newChild, expirationTime, ); } // ... return null; }
// react-reconciler/src/ReactChildFiber.js line 569 if (typeof newChild === 'object' && newChild !== null) { switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: { // ReactElement if (newChild.key === key) { if (newChild.type === REACT_FRAGMENT_TYPE) { return updateFragment( returnFiber, oldFiber, newChild.props.children, expirationTime, key, ); } return updateElement( returnFiber, oldFiber, newChild, expirationTime, ); } else { return null; } } case REACT_PORTAL_TYPE: { // 调用 updatePortal // ... } } if (isArray(newChild) || getIteratorFn(newChild)) { if (key !== null) { return null; } return updateFragment( returnFiber, oldFiber, newChild, expirationTime, null, ); } } 让我们回到最初的遍历,当我们遍历完成了之后,就会有两种情况,即老节点已经遍历完毕,或者新节点已经遍历完毕,如果此时我们新节点已经遍历完毕,也就是没有要更新的了,这种情况一般就是从原来的数组里面删除了元素,那么直接把剩下的老节点删除了就行了。如果老的节点在第一次循环的时候就被复用完了,新的节点还有,很有可能就是新增了节点的情况,那么这个时候只需要根据把剩余新的节点直接创建 // react-reconciler/src/ReactChildFiber.js line 839 // 新节点已经更新完成,删除多余的老节点 if (newIdx === newChildren.length) { // We've reached the end of the new children. We can delete the rest. deleteRemainingChildren(returnFiber, oldFiber); return resultingFirstChild; } // 新节点已经更新完成,删除多余的老节点 if (oldFiber === null) { // If we don't have any more existing children we can choose a fast path // since the rest will all be insertions. for (; newIdx < newChildren.length; newIdx++) { const newFiber = createChild( returnFiber, newChildren[newIdx], expirationTime, ); if (newFiber === null) { continue; } lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); if (previousNewFiber === null) { // TODO: Move out of the loop. This only happens for the first run. resultingFirstChild = newFiber; } else { previousNewFiber.sibling = newFiber; } previousNewFiber = newFiber; } return resultingFirstChild; } 接下来考虑移动的情况如何进行节点复用,即如果老的数组和新的数组里面都有这个元素,而且位置不相同这种情况下的复用, // react-reconciler/src/ReactChildFiber.js line 872 // Add all children to a key map for quick lookups. // 从oldFiber开始将已经存在的节点的key或者index添加到map结构中 const existingChildren = mapRemainingChildren(returnFiber, oldFiber); // Keep scanning and use the map to restore deleted items as moves. // 剩余没有对比的新节点,到旧节点的map中通过key或者index一一对比查看是否可以复用。 for (; newIdx < newChildren.length; newIdx++) { // 主要查看新旧节点的key或者index是否有相同的,然后再查看是否可以复用。 const newFiber = updateFromMap( existingChildren, returnFiber, newIdx, newChildren[newIdx], expirationTime, ); if (newFiber !== null) { if (shouldTrackSideEffects) { if (newFiber.alternate !== null) { // The new fiber is a work in progress, but if there exists a // current, that means that we reused the fiber. We need to delete // it from the child list so that we don't add it to the deletion // list. existingChildren.delete( // 在map中删除掉已经复用的节点的key或者index newFiber.key === null ? newIdx : newFiber.key, ); } } lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); // 添加newFiber到更新过的newFiber结构中。 if (previousNewFiber === null) { resultingFirstChild = newFiber; } else { previousNewFiber.sibling = newFiber; } previousNewFiber = newFiber; } } // react-reconciler/src/ReactChildFiber.js line 299 // 将旧节点的key或者index,旧节点保存到map结构中,方便通过key或者index获取 function mapRemainingChildren( returnFiber: Fiber, currentFirstChild: Fiber, ): Map<string | number, Fiber> { // Add the remaining children to a temporary map so that we can find them by // keys quickly. Implicit (null) keys get added to this set with their index // instead. const existingChildren: Map<string | number, Fiber> = new Map(); let existingChild = currentFirstChild; while (existingChild !== null) { if (existingChild.key !== null) { existingChildren.set(existingChild.key, existingChild); } else { existingChildren.set(existingChild.index, existingChild); } existingChild = existingChild.sibling; } return existingChildren; } 至此新数组遍历完毕,也就是同一层的
每日一题 https://github.com/WindrunnerMax/EveryDay 参考 https://zhuanlan.zhihu.com/p/89363990 以上就是深入浅析React中diff算法的详细内容,更多关于React中diff算法的资料请关注极客世界其它相关文章! |
请发表评论