React 算法之调和算法

概念

调和函数(源码)是在fiber树构(对比更新)过程中对旧fiber节点新reactElement进行比较, 判定旧fiber节点是否可以复用的一个比较函数.

调和函数仅是fiber树构造过程中的一个环节, 所以在深入理解这个函数之前, 建议对fiber树构造有一个宏观的理解(可以参考前文fiber 树构造(初次创建), fiber 树构造(对比更新)), 本节重点探讨其算法的实现细节.

它的主要作用:

  1. 给新增,移动,和删除节点设置fiber.flags(新增, 移动: Placement, 删除: Deletion)
  2. 如果是需要删除的fiber, 除了自身打上Deletion之外, 还要将其添加到父节点的effects链表中(正常副作用队列的处理是在completeWork函数, 但是该节点(被删除)会脱离fiber树, 不会再进入completeWork阶段, 所以在beginWork阶段提前加入副作用队列).

特性

算法复杂度低, 从上至下比较整个树形结构, 时间复杂度被缩短到 O(n)

基本原理

  1. 比较对象: fiber对象与ReactElement对象相比较.

    • 注意: 此处有一个误区, 并不是两棵 fiber 树相比较, 而是旧fiber对象与新ReactElement对象向比较, 结果生成新的fiber子节点.
    • 可以理解为输入ReactElement, 经过reconcileChildren()之后, 输出fiber.
  2. 比较方案:
    • 单节点比较
    • 可迭代节点比较

单节点比较

单节点的逻辑比较简明, 先直接看源码:


// 只保留主干逻辑function reconcileSingleElement(returnFiber: Fiber,currentFirstChild: Fiber | null,element: ReactElement,lanes: Lanes,): Fiber {const key = element.key;let child = currentFirstChild;while (child !== null) {// currentFirstChild !== null, 表明是对比更新阶段if (child.key === key) {// 1. key相同, 进一步判断 child.elementType === element.typeswitch (child.tag) {// 只看核心逻辑default: {if (child.elementType === element.type) {// 1.1 已经匹配上了, 如果有兄弟节点, 需要给兄弟节点打上Deletion标记deleteRemainingChildren(returnFiber, child.sibling);// 1.2 构造fiber节点, 新的fiber对象会复用current.stateNode, 即可复用DOM对象const existing = useFiber(child, element.props);existing.ref = coerceRef(returnFiber, child, element);existing.return = returnFiber;return existing;}break;}}// Didn't match. 给当前节点点打上Deletion标记deleteRemainingChildren(returnFiber, child);break;} else {// 2. key不相同, 匹配失败, 给当前节点打上Deletion标记deleteChild(returnFiber, child);}child = child.sibling;}{// ...省略部分代码, 只看核心逻辑}// 新建节点const created = createFiberFromElement(element, returnFiber.mode, lanes);created.ref = coerceRef(returnFiber, currentFirstChild, element);created.return = returnFiber;return created;}
  1. 如果是新增节点, 直接新建 fiber, 没有多余的逻辑
  2. 如果是对比更新
    • 如果keytype都相同(即: ReactElement.key === Fiber.key 且 Fiber.elementType === ReactElement.type), 则复用
    • 否则新建

注意: 复用过程是调用useFiber(child, element.props)创建新的fiber对象, 这个新fiber对象.stateNode = currentFirstChild.stateNode, 即stateNode属性得到了复用, 故 DOM 节点得到了复用.

可迭代节点比较(数组类型, [Symbol.iterator]=fn,[@@iterator]=fn)

可迭代节点比较, 在源码中被分为了 2 个部分:


function reconcileChildFibers(returnFiber: Fiber,currentFirstChild: Fiber | null,newChild: any,lanes: Lanes,): Fiber | null {if (isArray(newChild)) {return reconcileChildrenArray(returnFiber,currentFirstChild,newChild,lanes,);}if (getIteratorFn(newChild)) {return reconcileChildrenIterator(returnFiber,currentFirstChild,newChild,lanes,);}}

其中reconcileChildrenArray函数(针对数组类型)和reconcileChildrenIterator(针对可迭代类型)的核心逻辑几乎一致, 下文将分析reconcileChildrenArray()函数. 如果是新增节点, 所有的比较逻辑都无法命中, 只有对比更新过程, 才有实际作用, 所以下文重点分析对比更新的情况.


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;// 1. 第一次循环: 遍历最长公共序列(key相同), 公共序列的节点都视为可复用for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {// 后文分析}if (newIdx === newChildren.length) {// 如果newChildren序列被遍历完, 那么oldFiber序列中剩余节点都视为删除(打上Deletion标记)deleteRemainingChildren(returnFiber, oldFiber);return resultingFirstChild;}if (oldFiber === null) {// 如果oldFiber序列被遍历完, 那么newChildren序列中剩余节点都视为新增(打上Placement标记)for (; newIdx < newChildren.length; newIdx++) {// 后文分析}return resultingFirstChild;}// ==================分割线==================const existingChildren = mapRemainingChildren(returnFiber, oldFiber);// 2. 第二次循环: 遍历剩余非公共序列, 优先复用oldFiber序列中的节点for (; newIdx < newChildren.length; newIdx++) {}if (shouldTrackSideEffects) {// newChildren已经遍历完, 那么oldFiber序列中剩余节点都视为删除(打上Deletion标记)existingChildren.forEach(child => deleteChild(returnFiber, child));}return resultingFirstChild;}

reconcileChildrenArray函数源码看似很长, 梳理其主干之后, 其实非常清晰.

通过形参, 首先明确比较对象是currentFirstChild: Fiber | nullnewChildren: Array<*>:

  • currentFirstChild: 是一个fiber节点, 通过fiber.sibling可以将兄弟节点全部遍历出来. 所以可以将currentFirstChild理解为链表头部, 它代表一个序列, 源码中被记为oldFiber.
  • newChildren: 是一个数组, 其中包含了若干个ReactElement对象. 所以newChildren也代表一个序列.

所以reconcileChildrenArray实际就是 2 个序列之间的比较(链表oldFiber数组newChildren), 最后返回合理的fiber序列.

上述代码中, 以注释分割线为界限, 整个核心逻辑分为 2 步骤:

  1. 第一次循环: 遍历最长公共序列(key 相同), 公共序列的节点都视为可复用

    • 如果newChildren序列被遍历完, 那么oldFiber序列中剩余节点都视为删除(打上Deletion标记)
    • 如果oldFiber序列被遍历完, 那么newChildren序列中剩余节点都视为新增(打上Placement标记)
  2. 第二次循环: 遍历剩余非公共序列, 优先复用 oldFiber 序列中的节点
    • 在对比更新阶段(非初次创建fiber, 此时shouldTrackSideEffects被设置为true). 第二次循环遍历完成之后, oldFiber序列中没有匹配上的节点都视为删除(打上Deletion标记)

假设有如下图所示 2 个初始化序列:

接下来第一次循环, 会遍历公共序列A,B, 生成的 fiber 节点fiber(A), fiber(B)可以复用.

最后第二次循环, 会遍历剩余序列E,C,X,Y:

  • 生成的 fiber 节点fiber(E), fiber(C)可以复用. 其中fiber(C)节点发生了位移(打上Placement标记).
  • fiber(X), fiber(Y)是新增(打上Placement标记).
  • 同时oldFiber序列中的fiber(D)节点确定被删除(打上Deletion标记).

整个主干逻辑就介绍完了, 接下来贴上完整源码

第一次循环


// 1. 第一次循环: 遍历最长公共序列(key相同), 公共序列的节点都视为可复用for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {if (oldFiber.index > newIdx) {nextOldFiber = oldFiber;oldFiber = null;} else {nextOldFiber = oldFiber.sibling;}// new槽位和old槽位进行比较, 如果key不同, 返回null// key相同, 比较type是否一致. type一致则执行useFiber(update逻辑), type不一致则运行createXXX(insert逻辑)const newFiber = updateSlot(returnFiber,oldFiber,newChildren[newIdx],lanes,);if (newFiber === null) {// 如果返回null, 表明key不同. 无法满足公共序列条件, 退出循环if (oldFiber === null) {oldFiber = nextOldFiber;}break;}if (shouldTrackSideEffects) {// 若是新增节点, 则给老节点打上Deletion标记if (oldFiber && newFiber.alternate === null) {deleteChild(returnFiber, oldFiber);}}// lastPlacedIndex 记录被移动的节点索引// 如果当前节点可复用, 则要判断位置是否移动.lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);// 更新resultingFirstChild结果序列if (previousNewFiber === null) {resultingFirstChild = newFiber;} else {previousNewFiber.sibling = newFiber;}previousNewFiber = newFiber;oldFiber = nextOldFiber;}

第二次循环


// 1. 将第一次循环后, oldFiber剩余序列加入到一个map中. 目的是为了第二次循环能顺利的找到可复用节点const existingChildren = mapRemainingChildren(returnFiber, oldFiber);// 2. 第二次循环: 遍历剩余非公共序列, 优先复用oldFiber序列中的节点for (; newIdx < newChildren.length; newIdx++) {const newFiber = updateFromMap(existingChildren,returnFiber,newIdx,newChildren[newIdx],lanes,);if (newFiber !== null) {if (shouldTrackSideEffects) {if (newFiber.alternate !== null) {// 如果newFiber是通过复用创建的, 则清理map中对应的老节点existingChildren.delete(newFiber.key === null ? newIdx : newFiber.key);}}lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);// 更新resultingFirstChild结果序列if (previousNewFiber === null) {resultingFirstChild = newFiber;} else {previousNewFiber.sibling = newFiber;}previousNewFiber = newFiber;}}// 3. 善后工作, 第二次循环完成之后, existingChildren中剩余的fiber节点就是将要被删除的节点, 打上Deletion标记if (shouldTrackSideEffects) {existingChildren.forEach(child => deleteChild(returnFiber, child));}

结果

无论是单节点还是可迭代节点的比较, 最终的目的都是生成下级子节点. 并在reconcileChildren过程中, 给一些有副作用的节点(新增, 删除, 移动位置等)打上副作用标记, 等待 commit 阶段(参考fiber 树渲染)的处理.

总结

本节介绍了 React 源码中, fiber构造循环阶段用于生成下级子节点的reconcileChildren函数(函数中的算法被称为调和算法), 并演示了可迭代节点比较的图解示例. 该算法十分巧妙, 其核心逻辑把newChildren序列分为 2 步遍历, 先遍历公共序列, 再遍历非公共部分, 同时复用oldFiber序列中的节点.

【React源码】(十八)React 算法之调和算法相关推荐

  1. React源码解读之React Fiber

    开始之前,先讲一下该文章能帮你解决哪些问题? 开始之前,先讲一下该文章能帮你解决哪些问题? facebook为什么要使用重构React React Fiber是什么 React Fiber的核心算法 ...

  2. React源码分析7 — React合成事件系统

    1 React合成事件特点 React自己实现了一套高效的事件注册,存储,分发和重用逻辑,在DOM事件体系基础上做了很大改进,减少了内存消耗,简化了事件逻辑,并最大化的解决了IE等浏览器的不兼容问题. ...

  3. webuploader 怎么在react中_另辟蹊径搭建阅读React源码调试环境支持所有React版本细分文件断点调试...

    引言(为什么写这篇文章) 若要高效阅读和理解React源码,搭建调试环境是必不可少的一步.而常规方法:使用react.development.js和react-dom.development.js调试 ...

  4. 从 Preact 源码一窥 React 原理(二):Diff 算法

    从 Preact 源码一窥 React 原理(二):Diff 算法 前言 Diff 算法 渲染 diffChildren 函数 diff 函数 diffElementNodes 函数 diffProp ...

  5. React 源码剖析系列 - 不可思议的 react diff

    目前,前端领域中 React 势头正盛,使用者众多却少有能够深入剖析内部实现机制和原理.本系列文章希望通过剖析 React 源码,理解其内部的实现原理,知其然更要知其所以然. React diff 作 ...

  6. React源码分析与实现(一):组件的初始化与渲染

    原文链接地址:github.com/Nealyang 转载请注明出处 前言 战战兢兢写下开篇...也感谢小蘑菇大神以及网上各路大神的博客资料参考~ 阅读源码的方式有很多种,广度优先法.调用栈调试法等等 ...

  7. 推荐一个 React 技术揭秘的项目,自顶向下的 React 源码分析

    大家好,我是你们的 猫哥,那个不喜欢吃鱼.又不喜欢喵 的超级猫 ~ just-react 这本书的宗旨是打造一本严谨.易懂的 React 源码分析教程. 为了达到这个目标,在行文上,本书会遵循: 不预 ...

  8. react 组件遍历】_从 Context 源码实现谈 React 性能优化

    (给前端大全加星标,提升前端技能) 转自:魔术师卡颂 学完这篇文章,你会收获: 了解Context的实现原理 源码层面掌握React组件的render时机,从而写出高性能的React组件 源码层面了解 ...

  9. react源码解析之stack reconciler

    关于源码解读的系列文章,可以关注我的github的这个仓库, 现在才刚刚写,后续有空就写点.争取把react源码剖析透学习透.有不正确的地方希望大家帮忙指正.大家互相学习,共同进步. 本篇文章是官方文 ...

  10. react 源码概览

    React 采用 monorepo 的管理方式.仓库中包含多个独立的包,以便于更改可以一起联调,并且问题只会出现在同一地方. react React "Core" 中包含所有全局 ...

最新文章

  1. Conda安装Glossary词汇表
  2. SAP Hybris Commerce的页面路由实现
  3. css3轮播不用jpuery_15款效果很酷的最新jQuery/CSS3特效
  4. 一个网页设计师应该考虑的9件事
  5. linux shell 字符串操作详解 (长度,读取,替换,截取,连接,对比,删除,位置 )...
  6. B-TrunC标准加入ITU集群国际标准
  7. android加载obj文件,android – 从SD卡Rajawali解析.obj模型
  8. js原生代码实现轮播图淡入淡出和滚动的效果
  9. 2021-7-28-自动化运维Ansible
  10. Guided Anchoring:在线稀疏anchor生成方案,嵌入即提2AP | CVPR 2019
  11. 模拟信号高速数据采集回放设备
  12. linux视频对话框,抖音对话框视频怎么做?如何在视频画面上添加对话气泡框?视频加对话气泡的方法...
  13. win10 机械硬盘 开机速度缓慢的原因排查与优化
  14. PS图片无法保存ICO格式解决方法
  15. word停止工作 怎么解决
  16. 用A4纸当屏幕「播放」宫崎骏动画,随意抖动都毫无破绽
  17. ASP.NET Lambda表达式
  18. 计算机网络的功能有哪些?
  19. MATLAB导入mat文件的方法
  20. mt6572 开发板环境建立笔记

热门文章

  1. js拖拽(二)仿iGoogle自定义首页模块拖拽
  2. “AI四小龙”神话破灭?依图终止IPO,云从大裁员,旷视巨亏不止
  3. 地图,GPS位置地图坐标系:WGS-84(GPS)、GCJ-02(Google地图)、BD-09(百度地图),OpenGIS
  4. 微软,招 Java 了!
  5. office高级应用与python综合案例教程_《office高级应用案例教程.ppt
  6. 5G牌照发放 ,手机产业将迎来第二春。
  7. office2007安装失败2902_Office2007安装出错怎么办?安装出错原因及解决方法分享
  8. 二元二次不定方程(佩尔方程)
  9. pc即时聊天所用表情插件的记录
  10. 你如何理解软件测试?