【React源码】(十八)React 算法之调和算法
React 算法之调和算法
概念
调和函数(源码)是在fiber树构(对比更新)
过程中对旧fiber节点
与新reactElement
进行比较, 判定旧fiber节点
是否可以复用的一个比较函数.
调和函数仅是fiber树构造
过程中的一个环节, 所以在深入理解这个函数之前, 建议对fiber树构造
有一个宏观的理解(可以参考前文fiber 树构造(初次创建), fiber 树构造(对比更新)), 本节重点探讨其算法的实现细节.
它的主要作用:
- 给新增,移动,和删除节点设置
fiber.flags
(新增, 移动:Placement
, 删除:Deletion
) - 如果是需要删除的
fiber
, 除了自身打上Deletion之外, 还要将其添加到父节点的effects链表中(正常副作用队列的处理是在completeWork
函数, 但是该节点(被删除)会脱离fiber
树, 不会再进入completeWork
阶段, 所以在beginWork
阶段提前加入副作用队列).
特性
算法复杂度低, 从上至下比较整个树形结构, 时间复杂度被缩短到 O(n)
基本原理
- 比较对象:
fiber
对象与ReactElement
对象相比较.- 注意: 此处有一个误区, 并不是两棵 fiber 树相比较, 而是
旧fiber
对象与新ReactElement
对象向比较, 结果生成新的fiber子节点
. - 可以理解为输入
ReactElement
, 经过reconcileChildren()
之后, 输出fiber
.
- 注意: 此处有一个误区, 并不是两棵 fiber 树相比较, 而是
- 比较方案:
- 单节点比较
- 可迭代节点比较
单节点比较
单节点的逻辑比较简明, 先直接看源码:
// 只保留主干逻辑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;}
- 如果是新增节点, 直接新建 fiber, 没有多余的逻辑
- 如果是对比更新
- 如果
key
和type
都相同(即: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 | null
和newChildren: Array<*>
:
currentFirstChild
: 是一个fiber
节点, 通过fiber.sibling
可以将兄弟节点全部遍历出来. 所以可以将currentFirstChild
理解为链表头部, 它代表一个序列, 源码中被记为oldFiber
.newChildren
: 是一个数组, 其中包含了若干个ReactElement
对象. 所以newChildren
也代表一个序列.
所以reconcileChildrenArray
实际就是 2 个序列之间的比较(链表oldFiber
和数组newChildren
), 最后返回合理的fiber
序列.
上述代码中, 以注释分割线为界限, 整个核心逻辑分为 2 步骤:
- 第一次循环: 遍历最长
公共
序列(key 相同), 公共序列的节点都视为可复用- 如果
newChildren序列
被遍历完, 那么oldFiber序列
中剩余节点都视为删除(打上Deletion
标记) - 如果
oldFiber序列
被遍历完, 那么newChildren序列
中剩余节点都视为新增(打上Placement
标记)
- 如果
- 第二次循环: 遍历剩余
非公共
序列, 优先复用 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 算法之调和算法相关推荐
- React源码解读之React Fiber
开始之前,先讲一下该文章能帮你解决哪些问题? 开始之前,先讲一下该文章能帮你解决哪些问题? facebook为什么要使用重构React React Fiber是什么 React Fiber的核心算法 ...
- React源码分析7 — React合成事件系统
1 React合成事件特点 React自己实现了一套高效的事件注册,存储,分发和重用逻辑,在DOM事件体系基础上做了很大改进,减少了内存消耗,简化了事件逻辑,并最大化的解决了IE等浏览器的不兼容问题. ...
- webuploader 怎么在react中_另辟蹊径搭建阅读React源码调试环境支持所有React版本细分文件断点调试...
引言(为什么写这篇文章) 若要高效阅读和理解React源码,搭建调试环境是必不可少的一步.而常规方法:使用react.development.js和react-dom.development.js调试 ...
- 从 Preact 源码一窥 React 原理(二):Diff 算法
从 Preact 源码一窥 React 原理(二):Diff 算法 前言 Diff 算法 渲染 diffChildren 函数 diff 函数 diffElementNodes 函数 diffProp ...
- React 源码剖析系列 - 不可思议的 react diff
目前,前端领域中 React 势头正盛,使用者众多却少有能够深入剖析内部实现机制和原理.本系列文章希望通过剖析 React 源码,理解其内部的实现原理,知其然更要知其所以然. React diff 作 ...
- React源码分析与实现(一):组件的初始化与渲染
原文链接地址:github.com/Nealyang 转载请注明出处 前言 战战兢兢写下开篇...也感谢小蘑菇大神以及网上各路大神的博客资料参考~ 阅读源码的方式有很多种,广度优先法.调用栈调试法等等 ...
- 推荐一个 React 技术揭秘的项目,自顶向下的 React 源码分析
大家好,我是你们的 猫哥,那个不喜欢吃鱼.又不喜欢喵 的超级猫 ~ just-react 这本书的宗旨是打造一本严谨.易懂的 React 源码分析教程. 为了达到这个目标,在行文上,本书会遵循: 不预 ...
- react 组件遍历】_从 Context 源码实现谈 React 性能优化
(给前端大全加星标,提升前端技能) 转自:魔术师卡颂 学完这篇文章,你会收获: 了解Context的实现原理 源码层面掌握React组件的render时机,从而写出高性能的React组件 源码层面了解 ...
- react源码解析之stack reconciler
关于源码解读的系列文章,可以关注我的github的这个仓库, 现在才刚刚写,后续有空就写点.争取把react源码剖析透学习透.有不正确的地方希望大家帮忙指正.大家互相学习,共同进步. 本篇文章是官方文 ...
- react 源码概览
React 采用 monorepo 的管理方式.仓库中包含多个独立的包,以便于更改可以一起联调,并且问题只会出现在同一地方. react React "Core" 中包含所有全局 ...
最新文章
- Conda安装Glossary词汇表
- SAP Hybris Commerce的页面路由实现
- css3轮播不用jpuery_15款效果很酷的最新jQuery/CSS3特效
- 一个网页设计师应该考虑的9件事
- linux shell 字符串操作详解 (长度,读取,替换,截取,连接,对比,删除,位置 )...
- B-TrunC标准加入ITU集群国际标准
- android加载obj文件,android – 从SD卡Rajawali解析.obj模型
- js原生代码实现轮播图淡入淡出和滚动的效果
- 2021-7-28-自动化运维Ansible
- Guided Anchoring:在线稀疏anchor生成方案,嵌入即提2AP | CVPR 2019
- 模拟信号高速数据采集回放设备
- linux视频对话框,抖音对话框视频怎么做?如何在视频画面上添加对话气泡框?视频加对话气泡的方法...
- win10 机械硬盘 开机速度缓慢的原因排查与优化
- PS图片无法保存ICO格式解决方法
- word停止工作 怎么解决
- 用A4纸当屏幕「播放」宫崎骏动画,随意抖动都毫无破绽
- ASP.NET Lambda表达式
- 计算机网络的功能有哪些?
- MATLAB导入mat文件的方法
- mt6572 开发板环境建立笔记
热门文章
- js拖拽(二)仿iGoogle自定义首页模块拖拽
- “AI四小龙”神话破灭?依图终止IPO,云从大裁员,旷视巨亏不止
- 地图,GPS位置地图坐标系:WGS-84(GPS)、GCJ-02(Google地图)、BD-09(百度地图),OpenGIS
- 微软,招 Java 了!
- office高级应用与python综合案例教程_《office高级应用案例教程.ppt
- 5G牌照发放 ,手机产业将迎来第二春。
- office2007安装失败2902_Office2007安装出错怎么办?安装出错原因及解决方法分享
- 二元二次不定方程(佩尔方程)
- pc即时聊天所用表情插件的记录
- 你如何理解软件测试?