每日一博 | react 原理之 lane 优先级和 diff 更新
React 16
中处理优先级的模型是expirationTime
,它使用一个时间长度来描述任务的优先级。而React 17
则使用Lane
模型来处理任务的优先级,它通过将不同优先级赋值给一个位,通过31位的位运算来操作优先级,能够覆盖更多的边界条件。简言之:使用二进制数来表示任务的优先级。
为了理解的完整性,本文从 React
架构上 schedule
, diff
和 commit
三个阶段的流程进行展开
1. Schedule
1.1 创建更新任务
在首次渲染和用户触发事件时会创建一个更新任务,并分配一个优先级,接着放入 fiber.updateQueue
更新对列中,交给 Scheduler
调度更新
fiber.updateQueue
是一个环形结构pending
指针指向最后一个 update
,新的 update
插入过程:
const pending = sharedQueue.pending if (pending === null) { // 第一个 update 进入队列,创建一个环形结构 update.next = update } else { // 最新的update插入到首尾中间 update.next = pending.next pending.next = update } // pending 指向最新的 update sharedQueue.pending = update
创建环形结构是为了能一次找到首节点和尾节点
1.2 优先级
优先级本质就是比较大小,对于区间优先级,react
使用了二进制运算来判断是否处于区间中,最多有 31 位,每一位都是一条车道,
如判断 lane 是否在一个区间中:
// 0b0000000001111111111111111000000 & 0b0000000000000000000000001000000 === 0b0000000000000000000000001000000 export function isSubsetOfLanes(set: Lanes, subset: Lanes | Lane) { return (set & subset) === subset }
合并 lane:
// 0b0000000000000000000000010000000 | 0b0000000000000000000000100000000 === 0b0000000000000000000000110000000 export function mergeLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes { return a | b }
1.3 updateQueue 执行
- 遍历
updateQueue
链表,收集在当前更新任务区间的任务,即计算lane
是否在当前区间内,若不满足条件,则放进newFirstBaseUpdate...newLastBaseUpdate
中推迟执行。 - 执行
update
,即通过getStateFromUpdate
计算新的state
,将其结果存到newState
,剩余update
插入到lastBaseUpdate
后面 - 当
updateQueue
都处理完成后,将最后结果存入baseState
1.4 通过时间分片实现并发
通过时间切片的方式,即将任务分解为多个工作单元。每完成一个工作单元,判断是否有高优作业,若有,则让浏览器中断渲染
可使用 requestIdleCallback
简单实现时间分片效果:
function workLoop(deadline: IdleDeadline) { // 如果存在空闲时间 while (workInProgress && deadline.timeRemaining() > 0) { workInProgress = performUnitOfWork(workInProgress) } } // 当js线程空闲时执行 requestIdleCallback(workLoop)
为了减少commit
执行(这过程用户可感知),react
设计了跟踪 fiber root
,也被称为 progress root
或者 wipRoot
,一旦完成所有的工作,即没有下一个工作单元时,才将 fiber
提交给 dom
。
2. Diff
2.1 构建树
通过 CreateElement
函数生成 element
节点
/** * @param {string} type HTML标签类型 或 函数组件 * @param {object} props 具有JSX属性中的所有键和值 * @param {string | array} children JSX子节点列表 */ function CreateElement(type, props, ...children) { return { $$typeOf, // ReactElement, FragmentElement... tag, // ClassComponent, FunctionComponent, HostComponent... type, // "div", [[Function]], [[constructor]]... props: { ...props, children, }, ... } }
常见的 JSX
节点有如下几点
- 函数组件,其生成的
element
节点为:
{ $$typeOf: ReactElement, tag: FunctionComponent, type: 函数本身 }
- Class 组件,其生成的
element
节点为:
{ $$typeOf: ReactElement, tag: ClassComponent, type: class 构造函数 }
- 原生标签或文本节点,其生成的
element
节点为:
{ $$typeOf: ReactElement, tag: HostComponent, type: "div"或"text" }
2.1.1 模拟CreateElement
函数执行的过程
下面的函数组件对于开发过react
的同学应该不陌生,我们就以它为例子,学习学习
function App(props) { return <h1 title="el_title">Hi {props.name}</h1> // HTML标签类型 } const element = <App name="foo" /> // FC
转换为 CreateElement
函数调用
function App(props) { return CreateElement("h1", {title:"el_title"}, "Hi ", props.name) } const element = CreateElement(App, {name:"foo"})
执行并生成的 element
节点
[ { "type": App, // 函数组件本身 "props": { "name": "foo" // key-value "children": [] // 函数组件执行结果 }, { "type": "h1", "props": { "title": "el_title" // key-value "children": ["Hi", props.name] // 注意喔,是数组类型 } } ]
注意喔,函数组件的 children
是来自于函数的运行结果而不是props
, 即 children = type(props)
在这个过程中,返回的TreeNode
树结构为[{...},{...}]
,是一棵普通的树结构,基于递归遍历,无法实现断点回溯,而构建的Fiber链表
,其每个 fiber
节点都有 3 个指针 ,链接到其第一个子节点 child
,下一个兄弟姐妹节点 sibling
和父节点 return
,且每个 fiber
都将成为一个工作单元
2.2 更新、删除
当我们需要实现 更新 和 删除 节点时,即调用setState
,则需将 render
函数中收到的元素与提交给 dom
的最后的 FiberTree
进行比较。因此,我们需要保存最后一次提交给 FiberTree
的引用 currentRoot
,同时,为每个fiber
添加alternate
属性,记录上一阶段提交的old fiber
let currentRoot = null function Render(el, container) { wipRoot = { alternate: currentRoot, } } function CommitRoot() { currentRoot = wipRoot wipRoot = null }
注意喔,子代创建的过程伴随着比对,即为元素的子代创建fiber
的同时,将old fiber
与new fiber
进行比对
-
如果
old fiber
与new fiber
具有相同的type
,保留dom
节点并更新其props
,并设置标签effectTag
为UPDATE
-
若
type
不同,且为new fiber
,意味着要创建新的DOM
节点,设置标签effectTag
为PLACEMENT
;若为old fiber
,则需要删除节点,设置标签effectTag
为DELETION
为了快速检测到变化,React
使用了 key
。使其 更快速的检测到子元素何时更改了在元素数组中的位置key->fiber
3. commit 阶段
3.1 创建操作
提交创建操作会进行真实 dom
生成和 ref
初始化:
const stateNode = document.createElement(fiber.type) fiber.stateNode = stateNode // 保存dom节点 fiber.props.ref(stateNode) // 执行props中的ref函数,传入dom节点
3.2 更新操作
更新阶段会将 props
设置到 dom
const oldProps = fiber.alternate.props const newProps = fiber.props // 对比oldProps和newProps,找到变化的key value然后设置到dom
3.3 替换操作
利用旧的节点找到父节点,然后替换 dom
节点为新的 dom
节点
const parent = fiber.alternate.return.stateNode const oldDom = fiber.alternate.stateNode const newDom = fiber.stateNode parent.replaceChild(newDom, oldDom)
3.4 删除操作
新节点不存在,说明当前节点被删除
const oldDom = fiber.alternate.stateNode oldDom.remove()

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Apache Ignite 2.12.0 版本发布,分布式内存数据库
Apache Ignite 版本发布说明: Apache Ignite 分布式内存数据库 2.12.0 (!) 警告: 社区在未来的版本中会废弃如下的功能:CacheMode#LOCAL、CacheAtomicityMode#TRANSACTIONAL_SNAPSHOT、CacheConfiguration#rebalanceDelay; GCE、AWS、Azure模块,CacheSpringStoreSessionListener和TcpDiscoveryZookeeperIpFinder移植到了Ignite扩展库; 现有的服务网格实现在下一版本中会被删除。 Ignite: 新增分布式环境测试能力; 新增IndexQuery API,KV接口支持索引查询; 新增KubernetesConnectionConfiguration.discoveryPort; 分布式缓存查询汇总支持MergeSort; 控制脚本新增在特定分区上的读修复; GridRestProcessor新增追踪请求处理的能力; 新增一个显式的方法,以支持注册基于类的二进制类型; 新增批量缓存处理直方图指标; 新增缓存...
- 下一篇
Dory-Engine —— 简单的应用上云引擎
Dory-Engine 是一个简单得非常吓人的应用上云引擎 DORY = DevOps Orechstration YML,一种DevOps编排定义语言DSL的简称。 应用开发者无需掌握复杂的DevOps和Kubernetes云原生知识,即可实现应用从源代码交付到Kubernetes云原生环境。 给应用开发者一种全新的ServerLess风格的使用体验,无需编写复杂的Kubernetes应用部署配置文件,仅需要几项所见即所得的配置,即可轻松把应用从源代码编译打包发布到Kubernetes云原生环境。 DORY的极简设计理念: 简化复杂的技术: 应用开发者无需了解云原生基础设施的如何部署应用,即可自己动手发布应用 简化复杂的流程: 应用开发者无需编写各种复杂的脚本,通过几项简单的配置,即可实现代码构建、制品打包、应用发布 简化复杂的权限: 解决让人头痛的多租户云存储隔离问题,真正实现一套容器云与云存储多租户共用 DORY的架构如下: 分布式: Dory-Engine使用无状态设计架构,轻松实现分布式水平扩缩容。 远程步骤执行器(Docker)可以根据工作负载,进行水平扩缩容实现高弹...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS8编译安装MySQL8.0.19
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- Mario游戏-低调大师作品
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- Linux系统CentOS6、CentOS7手动修改IP地址
- CentOS8安装Docker,最新的服务器搭配容器使用
- CentOS7安装Docker,走上虚拟化容器引擎之路