React 理念
快速响应
制约快速响应的因素:
- 当遇到大计算量的操作或设备性能不足使页面掉帧,导致卡顿 (CPU 瓶颈)
- 发送网络请求,由于需要等待数据返回才能进一步操作(IO 瓶颈)
CPU 的瓶颈
JS 可以操作 DOM,但是GUI渲染线程
和JS线程
是互斥的,JS 脚本执行和浏览器布局、绘制不能同时执行。
如果 JS 执行时间太长,超出了 16.6ms,这次刷新就没有时间执行样式布局和样式绘制了。
React 使用时间切片
来解决。在浏览器的每一帧的时间中,预留一些时间给 JS 线程,React 利用这部分时间(5ms)更新组件。当预留的时间不够时,React 把线程控制权交还给浏览器使其有时间渲染 UI,React 则等待下一帧时间来继续被中断的工作。时间切片
的关键是 将同步的更新变为可中断的异步更新。
IO 的瓶颈
React 给出的答案是将人机交互研究的结果整合到真实的 UI 中
拿业界人机交互最顶尖的苹果举例,在 IOS 系统中,在“设置”里进入“通用”界面,然后再进入“siri 与搜索”界面,会发现,点击“siri 与搜索”后,先在当前页面停留了一小段时间,这一小段时间被用来请求数据。当“这一小段时间”非常短时,用户是无感知的。如果请求时间超过一个返回,再显示loading
效果。
如果直接显示loading
,即使数据请求时间很短,loading
效果一闪而过,用户也是可以感知到的。为此,React 实现了 Suspense
功能以及配套的useDeferredValue
解决上述问题的实现,就是将 同步的更新改为 可中断的异步更新
老的 React 架构
React15 架构可以分为两层:
- Reconciler( 协调器 )——负责找出变化的组件
每当有更新时,Reconciler 会做如下工作:- 调用函数组件、或 class 组件的 render 方法,将返回的 jsx 转换为虚拟 DOM
- 将虚拟 DOM 和上次更新的虚拟 DOM 对比
- 通过对比找出本次更新中变化的虚拟 DOM
- 通知 renderer 将变化的虚拟 DOM 渲染到页面上
- Renderer( 渲染器 )——负责将变化的组件渲染到页面上
React15 架构的缺点:mount
的组件会调用mountComponent
,update
的组件会调用updateConponent
,这两个方法都会递归更新子组件。
递归的缺点:
更新一旦开始,就无法中断。当层级很深时,递归更新时间超过 16ms,用户交互就会卡顿。
React15 的 Reconciler 和 Renderer 是交替工作的,由于整个过程是同步的,所以用户看来所有 DOM 是同时更新的。
新的 React 架构
React16 架构可以分为三层:
- Scheduler( 调度器 )——调度任务的优先级,高优任务优先进入 Reconciler
- Reconciler( 协调器 )——负责找出变化的组件
- Renderer( 渲染器 )——负责将变化的组件渲染到页面上
既然以浏览器是否有剩余时间作为任务中断的标准,那么我们需要当浏览器有剩余时间时通知我们。
部分浏览器已经实现了这个 API——requestIdleCallback
。由于以下原因,React 放弃使用:
- 浏览器兼容性
- 触发频率不稳定,受很多因素影响。比如当我们的浏览器切换 tab 后,之前 tab 注册的
requestIdleCallback
触发频率会变得很低。
Scheduler 就是requestIdleCallback
的 polyfill。
React16 的 Reconciler 的更新工作从递归变成了可中断的循环过程。每次循环会调用shouldYield
判断当前是否有剩余的时间。
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
workInProgress = performUnitOfWork(workInProgress);
}
}
那么为了解决中断更新时 DOM 渲染不完全的问题,Reconciler 和 Renderer 不再是交替工作,而是打上带有增/删/改/更新的标记。
整个 Scheduler 和 Reconciler 的工作都在内存中进行。只有当组件都完成了 Reconciler 的工作,才会统一交给 Renderer。
Renderer 根据 Reconciler 为虚拟 DOM 打的标记,同步执行对应的 DOM 操作。
Fiber
作为架构来说:React15 的 Reconciler 称为stack Reconciler
。React16 的 Reconciler 成为Fiber Reconciler
。
作为静态的数据结构来说:每个 Fiber 节点对应一个 React element,保存了该组件的类型(函数组件/类组件/原生组件)、对应 DOM 节点信息。
作为动态工作单元来讲:每个 Fiber 节点保存了本次更新中该组件改变的状态、要执行的工作(需要被删除/被插入页面/被更新…)
作为架构来说
每个 Fiber 节点依靠this.return
、this.child
、this.sibling
来连接成树
为什么用return
表示父节点?——因为作为一个工作单元,return
指节点执行完completeWork
后会返回的下一个节点,子 Fiber 节点及其兄弟节点完成工作后都会返回其父节点,所以用return
代指父节点。
作为静态的数据结构
保存了组件相关的信息:
this.tag = tag; // Fiber对应组件的类型 Function/Class/Host
this.key = key; // key属性
this.elementType = null; // 大部分情况同type,某些情况不同,比如FunctionComponent用React.memo包裹
this.type = null; // 对于FunctionComponent,指函数本身,对于ClassComponent,指Class;对于HostComponent,指DOM节点tagName
this.stateNode = null; // Fiber节点对应的真实DOM节点
作为动态的工作单元
Fiber 保存了本次更细相关的信息:
// 保存本次更新造成的状态改变相关信息
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
// 保存本次更新会造成的DOM操作
this.effectTag = NoEffect;
this.nextEffect = null;
this.firstEffect = null;
this.lastEffect = null;
// 调度优先级相关
this.lanes = NoLanes;
this.childLanes = NoLanes;
双缓存 Fiber 树
双缓存:在内存中构建并直接替换的技术
在 React 中最多会同时存在两颗树。当前屏幕上显示内容对应的 Fiber 树叫current Fiber树
,在内存中构建的 Fiber 树叫workInProgress Fiber树
。current Fiber树
中的 Fiber 节点称为current fiber
;workInProgress Fiber树
对应的 Fiber 节点称为workInProgress fiber
。它们通过**alternate**
连接
currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;
React 应用的根节点通过使_current 指针_
在不同的 Fiber 树的_rootFiber_
间切换来完成_current Fiber_
树指向的切换。
当workInProgress Fiber树
构建完成交给 Renderer 渲染在页面上之后,应用根节点的current指针
指向workInProgress Fiber树
,此时workInProgress Fiber树就变为current Fiber树
。
mount 时
首次执行 ReactDOM.render
会创建 fiberRootNode
(源码中叫fiberRoot
)和 rootFiber
。其中**fiberRootNode**
是整个应用的根节点,**rootFiber**
是**<App/>**
所在组件树的根节点。
之所以要区分
fiberRootNode
与rootFiber
,是因为在应用中我们可以多次调用ReactDOM.render
渲染不同的组件树,他们会拥有不同的rootFiber
。但是整个应用的根节点只有一个,那就是fiberRootNode
。
fiberRootNode
的 current
会指向当前页面上已渲染内容对应 Fiber树
,即 current Fiber树
。
由于是首次渲染,页面中还没有挂载任何 DOM
,所以 fiberRootNode.current
指向的 rootFiber
没有任何子Fiber节点
(即 current Fiber树
为空)。
接下来进入 _render阶段_
,根据组件返回的 JSX
在内存中依次创建 Fiber节点
并连接在一起构建 Fiber树
,被称为 workInProgress Fiber树
。
在构建 workInProgress Fiber树
时会尝试复用 current Fiber树
中已有的 Fiber节点
内的属性,在 首次渲染
时只有 rootFiber
存在对应的 current fiber
(即rootFiber.alternate
)。
已构建完的 workInProgress Fiber树
在 _commit阶段_
渲染到页面。
此时DOM
更新为右侧树对应的样子。fiberRootNode
的 current
指针指向 workInProgress Fiber树
使其变为 current Fiber 树
。
update 时
这会开启一次新的 render阶段
并构建一棵新的 workInProgress Fiber 树
。和 mount
时一样,workInProgress fiber
的创建可以复用 current Fiber树
对应的节点数据。
这个决定是否复用的过程就是 Diff 算法
workInProgress Fiber 树
在 render阶段
完成构建后进入 commit阶段
渲染到页面上。渲染完毕后,workInProgress Fiber 树
变为 current Fiber 树
。
Render 阶段
render 阶段会分为“递”和“归”两个阶段。 “递”阶段会执行
beginWork
, “归”阶段会执行completeWork
beginWork
参数:
- current: 当前组件的 Fiber 节点在上一次更新的 Fiber 节点,
workInProgress.alternate
- workInPorgress:当前组件的 Fiber 节点
- renderLanes:优先级相关
组件 mout 时,由于是首次渲染,除了 rootFiber,是不存在 current 的,即 current === null update 时,由于组件 mount 过了,所以 current 不为 null 所以可以判断 current 是否为 null 来判断是哪个阶段
update时: 如果 current 存在,在一定条件下可以复用 current 节点,这样就可以克隆current.child
作为`workInProgress.child
。
可以复用的情况:
- oldProps === newProps && workInProgress.type === current.type
- !includesSomeLane(renderLanes, updateLanes),即 Fiber 节点的优先级不够
mount 时:除了fiberRootNode
外,current 为 null。根据 fiber.tag
创建不同类型的子 Fiber 节点。
reconcileChildren:
对于常见的 FunctionComponent / ClassComponent / HostComponent 类型,会进入 reconcileChildren 方法
功能:
- 对于 mount 组件,会创建新的 Fiber 节点 mountChildFibers
- 对于 update 组件,会将当前组件与上次更新的组件对应的 Fiber 节点对比(Diff 算法),将比较的结果生成新的 Fiber 节点 reconcileChildFibers
这个方法内也是通过 current === null ? 来判断是 mount 阶段还是 update 阶段的。
最终会生成新的子 Fiber 节点并赋值给`workInProgress.child
,作为本次 beginWork 的返回值。并作为下次performUnitOfWork
执行时 workInProgress 的传参
mountChildFibers 与 reconcileChildFibers 的唯一区别是:reconcileChildFibers 会为生成的 Fiber 节点上添加
effectTag
属性,而 mountChildFibers 不会
effectTagrender
工作是在内存中进行的,当工作结束后,会通知Renderer
需要执行的 DOM 操作。
需要执行的 DOM 操作的具体类型就保存在fiber.effectTag
中。通过二进制表示。
在 mount 时,只有 rootFiber 会赋值 Placement effectTag,在commit阶段
只会执行一次
completeWork
同样根据current === null ?
判断是 mount 还是 update
针对 HostComponent
,判断 update
时还要考虑 workInProgress.stateNode !== null ?
(即该 Fiber 节点是否存在对应的 DOM 节点)
update 时
Fiber 节点已经存在对应的 DOM 节点,所以不需要再生成 DOM 节点了。需要做的主要是处理 props,比如:
- onClick、onChange 等回调函数的注册
- 处理 style prop
- 处理 DANGEROUSLY_SET_INNER_HTML prop
- 处理 children prop
主要的逻辑就是调用updateHostComponent
方法,在这个内部,被处理完的 props 会被赋值给 workInProgress.updateQueue,并最终赋值在页面上。
mount 时
- 为 Fiber 节点生成对应的 DOM 节点
- 将子孙 DOM 节点插入刚生成的 DOM 节点中
- 与 update 逻辑中的 updateHostComponent 类似的处理 props 的过程
那么commit阶段
是如何通过一次插入DOM
操作(对应一个Placement effectTag
)将整棵DOM树
插入页面的呢??
答:由于completeWork
属于 “归” 阶段调用的函数,每次调用 appendAllChildren 都会将已生成的子孙 DOM 节点插入当前生成的 DOM 节点下。那么当“归”到 rootFiber 时,我们已经有了一个构建好的离屏 DOM 树。
effectList
在 completeWork 的上层函数 completeUnitOfWork 中,每个执行完 completeWork 且存在effectTag
的 Fiber 节点都会被保存在effectList
的单向链表中。effectTag
的第一个节点保存在fiber.firstEffect
,最后一个保存在fiber.lastEffect
。
类似于 appendAllChildren,在归的阶段,所有的 effectTag 对应的 Fiber 节点都会保存在 effectList 中,最终形成一条以 rootFiber.firstEffect
为起点的单项链表。这样,只需要遍历effectList
就能执行 effect 而不需要遍历 Fiber 树了。
Commit 阶段
commit 阶段是以 commitRoot 方法为起点的,fiberRootNode 作为传参
在
rootFiber.firstEffect
上保存了一条需要执行副作用的 Fiber 节点的单向链表effectList
,这些Fiber
节点的updateQueue
中保存了变化的 props
这些副作用对应的 DOM 操作在 commit 阶段执行。还有一些生命周期钩子比如componentDidxxx
,hook 比如 useEffect 需要在 commit 阶段完成。
commit 阶段的主要工作是:
- before Mutation (DOM 操作前)
- Mutation(DOM 操作)
- layout(DOM 操作之后)
在 before Mutation 之前,layout 之后,还需要一些额外的工作,比如
useEffect
的触发,优先级相关的重置,ref 的绑定/解绑。
before Mutation 之前主要是一些变量赋值,状态重置的工作
layout 之后主要包括三点:
- useEffect 相关的处理
- 性能追踪相关 ( interaction 相关的变量)
- 在 commit 阶段会触发一些生命周期钩子(componentDidxxx),hook (useLayoutEffect, useEffect)
这些回调方法中可能会触发新的更新。
before Mutation
整个过程就是遍历 effectList 并调用 commitBeforeMutationEffects 函数处理。
commitBeforeMutationEffects
- 处理 DOM 节点 渲染/删除 后的 autoFocus,blur 等逻辑
- 调用 getSnapshotBeforeUpdate 生命周期钩子
- 处理 useEffect
调度 getSnapshotBeforeUpdate
从React
v16 开始,componentWillXXX
钩子前增加了UNSAFE_
前缀。
原因是因为**stack Reconciler**
重构为**Fiber Reconciler**
之后,render 阶段的任务可能中断/重新开始,对应在 render 阶段的生命周期钩子(即 componentWillxxx)可能触发多次。
所以替代为getSnapshotBeforeUpdate
,这个是在 commit 阶段触发的,commit 是同步的,所以不会有多次调用的问题。
调度 useEffectscheduleCallback
方法由Scheduler
模块提供,用以某个优先级异步调度一个回调函数。
useEffect 如何被异步调度?
遍历 effectList 执行 effect 回调函数。
当一个 FunctionComponent 含有 useEffect 或者 useLayoutEffect,对应的 Fiber 节点也会被赋值 effectTag。
详细一点,整个 useEffect 异步调用分为三步:
- before mutation 阶段在 scheduleCallback 中调度 flushPassiveEffects
- layout 阶段之后将
effectList
赋值给 rootWithPendingPassiveEffects - scheduleCallback 触发 flushPassiveEffects,flushPassiveEffects 内部遍历 rootWithPendingPassiveEffects
为什么需要异步调用?
与 componentDidMount、componentDidUpdate 不同的是,在浏览器完成布局和绘制之后,传给 useEffect 的函数会延迟调用。这使得它适用于许多常见的副作用场景,比如设置订阅和事件处理等情况,因此不应在函数中执行阻塞浏览器更新屏幕的操作。
所以:useEffect 异步执行的原因主要是防止 同步执行阻塞浏览器渲染。
mutation 阶段
类似 before mutation 阶段, mutation 阶段也是遍历 effectList,执行函数。这里执行的是 commitMutationEffects
commitMutationEffects 会遍历 effectList,对每个 Fiber 节点执行如下操作:
- 根据 ContentReset effectTag 重置文字节点
- 更新 ref
- 根据 effectTag 分别处理,其中 effectTag 包括(Placement | Update | Deletion | Hydrating)
Placement effect
当 Fiber 节点含有 Placement effectTag,意味着该 Fiber 节点对应的 dom 节点需要插入页面中。调用 commitPlacement
- 获取父级 DOM 节点。
const parentFiber = getHostParentFiber(finishedWork); // finishedWork 为传入的Fiber节点
// 父级 DOM 节点
const parentStateNode = parentFiber.stateNode;
- 获取 Fiber 节点的 DOM 兄弟节点
const before = getHostSibling(finishedWork); // finishedWork 为传入的Fiber节点
- 根据 DOM 兄弟节点是否存在调用 parentNode.insertBefore 或 parentNode.appendChild 执行 DOM 插入操作
值得注意的是: getHostSibling (获取兄弟节点)的执行很耗时,当在同一个父 Fiber 节点下依次执行多个插入操作,getHostSibling 算法复杂度为指数级
这是由于 Fiber 节点不只包括 HostComponent,所以 Fiber 树和渲染的 DOM 树不是一一对应的。要从 Fiber 节点找到 DOM 节点很可能跨层级操作
Update effect
当 Fiber 节点含有 Update effectTag,意味着需要更新。会调用 commitWork,会根据 Fiber.tag 分别处理
对于 FunctionComponent,会调用 commitHookEffectListUnmount。该方法会遍历 effectList,执行所有 useLayoutEffect hook 的销毁函数
对于 HostComponent,会调用 commitUpdate,最终会在 updateDOMProperties 中将 render 阶段 completeWork 中为 Fiber 节点赋值的 updateQueue 对应的内容渲染到页面上
Deletion effect
当 Fiber 节点含有 Deletion effectTag,意味着该 Fiber 节点对应的 DOM 节点需要删除。调用的方法是 commitDeletion。
- 递归调用 Fiber 节点 及其子孙 Fiber 节点中 fiber.tag 为 ClassComponent 的 componentWillUnmount 生命周期钩子,从页面移除 Fiber 节点对应 DOM 节点
- 解绑 ref
- 调度 useEffect 销毁函数
layout 阶段
与前两个阶段类似, layout 阶段也是遍历 effectList,执行函数( commitLayoutEffects)
commitLayoutEffectOnFiber(调用生命周期钩子和 hook 相关操作)、
- 对于 ClassComponent,会通过 current === null ? 区分 mount 还是 update, 调用 componentDidMount 或 componentDidUpdate
如果 this.setState 赋值了第二个参数回调函数,也会在此时调用 对于 FunctionComponent 以及相关类型(ForwardRef、React.memo 包裹的 FunctionComponent),会调用 useLayoutEffect hook 的回调函数,调度 useEffect 的销毁和回调函数
useLayoutEffect hook 从上一次更新的销毁函数调用到本次更新的回调函数调用是同步执行,是在 layout 阶段执行的,这时候属于 commit 阶段,而这个阶段就是同步执行的。===
ComponentDidMount
useEffect 需要先调度,在 Layout 阶段完成后再异步执行。 这就是 useLayoutEffect 和 useEffect 的区别对于 HostRoot,即 rootFiber,如果赋值了第三个参数回调函数,也会在此时调用。
- 对于 ClassComponent,会通过 current === null ? 区分 mount 还是 update, 调用 componentDidMount 或 componentDidUpdate
- commitAttachRef(赋值 ref)
获取 DOM 实例,更新 ref
current Fiber 树的切换
root.current = finishedWork;
在 mutation 阶段结束后,layout 阶段开始前执行。
原因:
componentWillUnmount 会在 mutation 阶段执行。此时 current Fiber 树还是指向前一次更新的 Fiber 树,在生命周期钩子内获取的 DOM 还是更新前的
componentDidMount 和 componentDidUpdate 会在 layout 阶段执行。此时 current Fiber 树已经指向更新后的 Fiber 树,在生命周期钩子内获取的 DOM 就是更新后的。
状态更新
流程
触发状态更新(根据场景调用不同方法)
|
|
v
创建Update对象(接下来三节详解)
|
|
v
从fiber到root(`markUpdateLaneFromFiberToRoot`)
|
|
v
调度更新(`ensureRootIsScheduled`)
|
|
v
render阶段(`performSyncWorkOnRoot` 或 `performConcurrentWorkOnRoot`)
|
|
v
commit阶段(`commitRoot`)
Update
可以触发更新的方法:
- ReactDOM.render——HostRoot
- this.setState——ClassComponent
- this.forceUpdate——ClassComponent
- useState/useReducer——FunctionComponent
由于不同类型组件工作方式不同,所以存在两种不同结构的 Update,其中 ClassComponent 和 HostComponent 共用同一套 Update
const update: Update<*> = {
eventTime, // 任务时间, 通过 performance.now() 获取的毫秒数
lane, // 优先级相关字段
suspenseConfig, // Suspense 相关
tag: UpdateState, // 更新的类型: UpdateState | ReplaceState | ForceUpdate | CaptureUpdate
payload: null, // 更新挂载的数据;对于ClassComponent,是this.setState第一个参数;对于HostRoot,是ReactDOM.render第一个参数
callback: null, // 更新的回调函数:commit的layout阶段的回调函数
next: null, // 与其他 Update 形成链表
};
fiber 的多个 Update 会形成链表并包含在 fiber.updateQueue 中。
什么情况下会存在多个 Update ?
比如点击一次里面调用了两次 this.setState 。
Fiber 节点最多同时存在两个 updateQueue:
- current Fiber 保存的 updateQueue——current updateQueue
- workInProgress Fiber 保存的 updateQueue —— workInProgress updateQueue
在 commit 阶段结束后,workInProgress updateQueue 就变成了 current updateQueue
ClassComponent 与 HostRoot 使用的 UpdateQueue 结构如下:
const queue: UpdateQueue<State> = {
baseState: fiber.memoizedState, // 本次更新前该fiber节点的state,Update 基于该 state 计算更新后的 state
// 本次更新前Fiber节点已经保存的Update,以链表形式存在,链表头为 firstBaseUpdate, 链表尾为 lastBaseUpdate。
// 之所以更新产生前就有Update,是由于某些Update优先级较低,在上次render阶段由Update计算state时被跳过了
firstBaseUpdate: null,
lastBaseUpdate: null,
shared: {
pending: null, // 触发更新时,产生的Update会保存在这形成单向环链表。当由Update计算state时会被剪开连接在lastBaseUpdate后面
},
effects: null, // 数组,保存 effects.callback !== null 的 Update
};
updateQueue 的流程
假设有一个 fiber 刚经历完 commit 阶段完成渲染,该 fiber 有两个由于优先级过低而在上次的 render 阶段没有被处理,所以会形成下一个更新的 baseUpdate,称其为 u1 和 u2,且 u1.next === u2 则:
fiber.updateQueue.firstBaseUpdate = u1;
fiber.updateQueue.lastBaseUpdate = u2;
u1.next === u2;
// fiber.updateQueue.baseUpdate: u1 --> u2
现在我们在 fiber 上触发两次更新,会产生两个新的 Update,u3 和 u4
每个 update 都会通过 enqueueUpdate 插入到 updateQueue 队列上
插入 u3 后
fiber.updateQueue.shared.pending = u3
u3.next = u3;
即
fiber.updateQueue.shared.pending: u3 ─────┐
^ |
└──────┘
插入 u4 后
fiber.updateQueue.shared.pending = u4;
u4.next = u3;
u3.next = u4;
即
fiber.updateQueue.shared.pending: u4 ──> u3
^ |
└──────┘
更新调度后进入 render 阶段
此时 shared.pending 的环被剪开连接到 updateQueue.lastBaseUpdate 后面
fiber.updateQueue.baseUpdate: u1 --> u2 --> u3 --> u4
接下来遍历 fiber.updateQueue.baseUpdate 链表,以 fiber.updateQueue.baseState 为 初始 state,依次与遍历到的 Update 计算产生新的 state
在遍历时如果有优先级低的Update
会被跳过。
当遍历完成后获得的 state,就是该 Fiber 节点在本次更新的 state(memoizedState)
state 的变化在 render 阶段产生不同的 JSX 对象,通过 Diff 算法产生不同的 effectTag ,在 commit 阶段渲染在页面上
渲染完成后 workInProgress Fiber 树变为 current Fiber 树。整个更新流程结束。
优先级
状态更新由用户交互产生,用户对交互执行顺序有个预期。React 根据人机交互研究结果中用户对交互的预期顺序将交互产生的状态更新赋予不同的优先级
- 生命周期方法: 同步执行
- 受控的用户输入:比如输入框输入文字,同步执行。
- 交互事件:比如动画,高优先级执行
- 其他:比如数据请求,低优先级执行
由于优先级的关系,比如 本来是要执行 u1 的,但是 u1 还没执行完,突然来了个高优先级的 u2,这时候会中断 u1 的执行,等 u2 执行完之后再执行 u1.但是由于 u1 和 u2 之前可能会有依赖关系,所以,执行顺序是 u1 —> u2。所以,u2 会执行两遍。这也就是什么 componentWillxxx 会被标记为 **unsafe_**
render 阶段可能被中断,如何保证 updateQueue 中的 Update 不会丢失?
在 render 阶段,shared.pending 环被剪开并连接在 updateQueue.baseUpdate 后面,实际上 shared.pending 会同时连接在 workInProgress.updateQueue.lastBaseUpdate 和 current.updateQueue.lastBaseUpdate 后面。
当 render 阶段被中断重新开始后,会基于 current.updateQueue 克隆出 workInProgress.updateQueue。由于 current.updateQueue.lastBaseUpdate 保存了上一次的 Update。所以不会被丢失。
当 commit 阶段渲染完成,由于 workInProgress.updateQueue.lastBaseUpdate 保存了上一次更新的 Update,所以当 workInProgress Fiber 树变成 current Fiber 树时,也不会造成 Update 丢失。
如何保证状态依赖的连续性?
当 Update 由于优先级过低被跳过之后,保存在 baseUpdate 中的不仅仅是当前 Update,还包括链表中该 Update 之后所有的 Update。