认识
一、认识
React
渲染更新工作流 主要有初始化、Scheduler
调度、Render
调和、Commit
提交 阶段。
二、初始化
一、创建 Virtual DOM
: 在 Webpack
、RsPack
或者 Vite
等构建编译下, 通过显示引入 createElement
或者隐式引入 jsx
将 JSX
语法转换为 ReactElement
。ReactElement
就是 React
中的 Virtual DOM
。通过 ReactElement
来创建 Fiber
。ReactElement
与 Fiber
一一对应。
-
*
ReactElement
创建原理: 在React.js 17
之前, 应用程序通过@babel/preset-react
将JSX
语法转换为React.createElement
的js
代码,因此需要显式将React
引入,才能正常调用createElement
。React.js 17
版本之后,官方与babel
进行了合作,直接通过将react/jsx-runtime
对jsx
语法进行了新的转换而不依赖React.createElement
, 称为Runtime Automatic
(自动运行时)。在自动运行时模式下,JSX
会被转换成新的入口函数,import {jsx as _jsx} from 'react/jsx-runtime';
和import {jsxs as _jsxs} from 'react/jsx-runtime';
。React
中的createElement
以及JSX
都会返回一个ReactElement
对象, 用于后续创建Fiber
对象。由上所述,React
是纯运行时前端框架, 在运行前, 已经将JSX
全部转换为ReactElement
虚拟DOM
, 在运行中没有机会进行编译优化。 -
ReactElement
数据结构:const ReactElement = function (type, key, ref, props) {
const element = {
$$typeof: REACT_ELEMENT_TYPE,
type,
key,
ref,
props
};
return element;
};
二、React
应用初始化: 1. 为传入的 DOM
容器创建一个 FiberRootNode
对象, 这个对象作为整个应用的根容器; 2. 在 FiberRootNode
的基础上,创建一个特殊的 Fiber
节点——HostRoot Fiber
。它代表整个应用的根 Fiber
节点,是连接虚拟 DOM
与实际渲染的桥梁。 3. 将 FiberRootNode
和 HostRoot Fiber
关联起来,形成完整的 Fiber
树结构。4. 初始化更新队列, 在 HostRoot Fiber
上,React
会初始化一个更新队列(UpdateQueue
),用于存储各种待处理的更新任务, 个更新队列采用环形链表的形式存储 Update
节点,不仅便于插入和遍历,还能有效管理任务的优先级和调度顺序; 4. 绑定全局事件代理, 在创建根容器的过程中,React
会将所有需要的事件(如点击、输入等)绑定到传入的 DOM
容器上,实现事件委托机制。5. 为新根容器启用并发模式, React
会启动时间切片(Time Slicing
), 调度器(Scheduler
)会根据任务的优先级对更新进行细粒度的调度,必要时允许任务中断与恢复。
-
FiberRootNode
: 是React
应用的根节点, 它的作用是负责应用加载相关的内容,比如应用加载模式mode
,存储本次应用更新的回调任务以及优先级,存储创建完成的FiberTree
等。 -
HostRootFiber
: 是虚拟DOM
树的根节点,类型是FiberNode
, 针对普通DOM
元素或者组件创建的Fiber
对象,是虚拟DOM
的真实体现
三、Mount Scheduler 调度
Scheduler
调度执行: Schedule
中分别提供了 unstable_runWithPriority
和 unstable_scheduleCallback
方法, 用于支持调度。在 Mount
初始挂载阶段, 调用 unstable_runWithPriority
以同步的优先级, 调用执行初始渲染回调。在回调中, 将 HostRoot Fiber
的 UpdateQueue
加入到更新队列, 调用 scheduleUpdateOnFiber
开始进行调度更新。在 Mount
初始挂载阶段 , 从当前 HostRoot Fiber
向上遍历, 更新每个祖先节点的 childLanes
属性,表明该分支中存在待调和的更新任务, 然后返回 rootFiber
。由于 HostRoot Fiber
是根节点, 直接返回自己, 进行渲染流程。unstable_runWithPriority
: 接受一个优先级与一个回调函数。在函数中, 执行回调函数之前, 记录当前优先级为传入的优先级, 开始执行回调函数并返回函数执行结果, 最后记录当前优先级为之前的优先级。
四、Mount Render 调和阶段
Render
调和阶段 的主要目标是生成一棵新的 Fiber
树,并为后续的 DOM
更新准备好 副作用列表(effect list
)。这个阶段不直接操作 DOM
,而是进行计算和比较。render
阶段以 DFS
深度优先 的顺序遍历 ReactElement
, 如果有子节点, 遍历子节点, 如果没有子节点遍历兄弟节点。Render
阶段 可以中断和恢复, 简单来说,由于 Render
阶段的操作对用户来说其实是不可见的,所以就算打断再重启,对用户来说也是零感知。而 commit
阶段的操作则涉及真实 DOM
的渲染,所以这个过程必须用同步渲染来求稳。Render
调和阶段 是一个递归的过程, 存在 递
和 归
两个阶段。
BeginWork
递阶段: 从根 Fiber
开始, 对每个 Fiber
节点,调用对应的处理函数。处理逻辑如下(主要以函数组件为例): 遇到函数组件, 调用 renderWithHooks
执行函数组件。在执行过程中, React
按照 Hook
调用的顺序遍历组件内部所有 Hook
, 在 Mount
阶段的每一个 Hook
会调用 MountHookXX
来为每一个 Hook
创建一个 Hook
对象, 存储在 当前处理的 fiber.memoizedState
中(注意, 如果 Hook
不在函数组件中调用, 此时获取不到当前正在处理的 Fiber
, 无法进行后续流程) 。 Hook
对象中也有一个 memoizedState
, 另外还有 next
、baseQueue
、baseState
、updateQueue
。此时的 Hook.memoizedState
为传入的初始值。最终, 组件函数最终返回一个新的 ReactElement
, 拿到对应的 ReactElement
之后, 调用 reconcileChildren
, 在 Mount
挂载阶段, 会直接生成新的 Fiber
。
-
Hook.memoizedState
:-
useRef Hook.memoizedState
: 保存{current: xxx}
-
useState Hook.memoizedState
: 保存state
的值 -
useEffect Hook.memoizedState
: 保存包含useEffect
回调函数、依赖项等的链表数据结构effect
。 -
useReducer Hook.memoizedState
: 保存state
的值 -
useMemo Hook.memoizedState
与useCallback Hook.memoizedState
:useCallback
保存的是callback
函数本身,而useMemo
保存的是callback
函数的执行结果
-
-
Fiber.memoizedState
: 存储Hooks
链表
CompleteWork
归阶段: 当某个 Fiber
节点没有子节点或者子节点已处理完成后,开始从叶子节点向上回溯。对于 HostComponent
(DOM
元素节点),在这一步骤会创建真实 DOM
元素的描述(还未实际挂载), 对于文本节点、Portal
等, 也会进行相应处理。将当前节点产生的副作用(比如需要插入、更新或删除 DOM
节点的操作)沿着树向上冒泡到父节点。构建一份完整的副作用列表,供 Commit
阶段统一处理。将每个节点的副作用合并到父节点上,最终从根节点可以获得整个更新所需的所有副作用。
五、Mount Commit 提交阶段
Commit
提交阶段 是 React
更新流程中唯一会直接操作 DOM
的阶段,它根据 Render
阶段生成的副作用列表执行实际的更新操作。这个阶段是同步执行的,确保所有副作用在一次提交中完成, 这个阶段是同步且不可中断的,确保 UI
的最终状态与最新的 React
元素描述完全一致。简单来说,由于 Render
阶段的操作对用户来说其实是不可见的,所以就算打断再重启,对用户来说也是零感知。而 commit
阶段的操作则涉及真实 DOM
的渲染,所以这个过程必须用同步渲染来求稳。通常分为以下几个子阶段:
beforeMutation
变更前提交阶段: 对于需要捕获 DOM
状态的组件,会在实际 DOM
变更前调用 getSnapshotBeforeUpdate
, 记录必要的信息,为后续可能的布局修正提供数据。
mutation
变更提交阶段: 根据 effect list
中标记的副作用,对 DOM
进行实际的操作:创建新的 DOM
节点、更新现有节点、删除不再需要的节点等。在这一阶段,React
同时会处理事件绑定的更新以及 ref
的挂载或卸载,确保新的 DOM
状态与组件状态一致。对于需要删除的节点,执行相应的卸载操作,如移除事件监听器、释放内存资源等。
layout
布局提交阶段: 真实 DOM
挂载完成后,会依次调用诸如 componentDidMount
、componentDidUpdate
等生命周期方法。在函数组件中,相关的 layout effect
(useLayoutEffect
)也会在这一阶段执行。这一阶段确保所有 DOM
更新完成后,再进行可能依赖最新 DOM 布局信息的操作,比如同步测量、滚动调整等。
问题:
commit
阶段执行的生命周期或者effect
钩子是 先子后父? 为什么呢? 本质上commit
阶段处理的事情和dom
元素有关系,commit
阶段生命周期是可以改变真实dom
元素的状态的,所以如果在子组件生命周期内改变dom
状态,并且想要在父组件的生命周期中同步状态,就需要确保父组件的生命周期执行时机要晚于子组件。
六、Update Scheduler 调度
调用 setState
、useState
、useReducer
和 forceUpdate
之后,其实都会走入 React
的更新流程。
-
setState
: 当在类组件中调用this.setState(partialState, [callback])
时,会进入组件实例的updater
对象。updater
内部会调用类似enqueueSetState
的方法,将本次更新的信息封装成一个Update
对象。Update
对象包含了传入的部分状态(可能是对象或函数)、更新优先级(Lane
)、以及可能的回调函数。将该Update
插入到对应的fiber.updateQueue.shared.pending
更新队列中。调用scheduleUpdateOnFiber
,标记当前Fiber
需要更新,并由调度器安排后续渲染任务。 -
forceUpdate
: 当调用组件实例的forceUpdate()
时,会通过组件内部的updater
调用类似enqueueForceUpdate
的方法。 -
useState dispatchSetState
: 确定该更新所属的优先级(即Lane
), 并生成一个Update
对象, 其中包含了此次更新的action
(可能是新状态值或用于计算状态的函数)。将该Update
插入到对应Hook
的更新队列中Hook.UpdateQueue
中。调用scheduleUpdateOnFiber
,标记当前Fiber
需要更新,并由调度器安排后续渲染任务。在将更新添加到更新队列之前,基于eagerState
优化策略, 尝试立即计算出新状态, 如果计算结果与当前状态完全一致,就可以跳过后续的调度和渲染工作,从而避免不必要的组件更新,提升性能。React
会同步计算最新状态,React
会立即以当前组件的memoizedState
为基础,调用更新函数或直接合并传入的部分状态,从而得到一个eagerState
。对于类组件,这通常是在setState
调用中直接合并对象或执行updater
函数。对于函数组件,虽然Hook
更新也经过类似处理,但内部逻辑会在Hooks
链表中记录更新队列。计算出eagerState
后,React
会将其与当前的状态通过Object.is
进行比较, 如果eagerState
与当前状态完全一致,说明此次更新不会引起UI
变化。此时,React
可以选择跳过调度,避免无谓的渲染过程。如果eagerState
与当前状态有差异, 加入更新队列。调用scheduleUpdateOnFiber
,标记当前Fiber
需要更新,并由调度器安排后续渲染任务。 -
useReducer dispatchSetState
: 确定该更新所属的优先级(即Lane
), 并生成一个Update
对象, 其中包含了此次更新的action
(可能是新状态值或用于计算状态的函数)。将该Update
插入到对应Hook
的更新队列中Hook.UpdateQueue
中。在将更新添加到更新队列之前,基于eagerState
优化策略, 尝试立即计算出新状态, 如果计算结果与当前状态完全一致,就可以跳过后续的调度和渲染工作,从而避免不必要的组件更新,提升性能。React
会同步计算最新状态,React
会立即以当前组件的memoizedState
为基础,调用更新函数或直接合并传入的部分状态,从而得到一个eagerState
。对于类组件,这通常是在setState
调用中直接合并对象或执行updater
函数。对于函数组件,虽然Hook
更新也经过类似处理,但内部逻辑会在Hooks
链表中记录更新队列。计算出eagerState
后,React
会将其与当前的状态通过Object.is
进行比较, 如果eagerState
与当前状态完全一致,说明此次更新不会引起UI
变化。此时,React
可以选择跳过调度,避免无谓的渲染过程。如果eagerState
与当前状态有差异, 加入更新队列。调用scheduleUpdateOnFiber
,标记当前Fiber
需要更新,并由调度器安排后续渲染任务。
因此, 无论是调用 setState
、useState/useReducer
还是 forceUpdate
,其本质都是通过创建一个 Update
对象并将其插入到对应的更新队列中, 在插入期间, 需要进行对应的优化策略判断, 比如 eagerState
策略。而且, 无论是类组件将 Update
插入 Fiber.UpdateQueue
也好, 函数组件将 Update
插入 Hook.UpdateQueue
也好, 都会形成一个单向环形链表。之所以设计为单向环形链表是因为: 1. 做成环形链表可以只需要利用一个指针,便能找到头节点与尾节点; 2. 更加方便地找到最后一个 Update
对象,同时插入新的 Update
对象也非常方便; 3. 如果使用普通的线性链表,就需要同时记录第一个和最后一个节点的位置,维护成本相对较高。 当然, Update
加入更新队列还需要进行
在没有命中对应的性能优化策略之后, 调用 scheduleUpdateOnFiber
进入调度阶段。scheduleUpdateOnFiber
冒泡标记更新, 从当前 Fiber
向上遍历,更新每个祖先节点的 childLanes
属性,表明该分支中存在待调和的更新任务。在冒泡更新标记的过程中,最终会找到对应的根节点(通常为 FiberRoot
),这个根节点是整个更新流程的入口, 并返回 FiberRoot
, 并调用 unstable_scheduleCallback
将更新任务以对应的优先级加入任务队列。在函数中, 不同的优先级意味着不同时长的任务过期时间。同时会比较 startTime
与 currentTime
, 如果 startTime > currentTime
, 表示当前任务未就绪, 存入 timerQueue
。并根据开始时间重新排列 timerQueue
中任务的顺序。当 timerQueue
中有任务就绪,即startTime <= currentTime
,我们将其取出并加入taskQueue
。 timerQueue
和 taskQueue
两个队列为了能在 O(1)
的时间复杂度里找到两个队列中时间最早的那个任务, 采用的是最小堆的数据结构, 每次存入任务的过程就是最小堆自动调整的过程, 时间最小的, 也就是最早的任务。 Scheduler
调度更新任务时的表现为: 1. 优先级高的任务会打断低优先级任务的执行, 每次执行的任务是所有任务中优先级最高的; 2. 如果两个任务的优先级相同, 不会开启新的调度; 3. 低优先级的任务存在过期时间, 等到失效后, 以同步任务高优执行。防止低优先级任务一直得不到执行, 导致饥饿问题的出现
-
通过
RootFiber
是如何找到需要更新的组件的:Root Fiber
是通过对比每一级的childLanes
与当前更新优先级, 逐渐向下对比调和找到需要更新的组件的。 -
为什么要从触发更新的
Fiber
递归向上形成一个childLanes
链, 而不可以从从触发的Fiber
直接向下调和呢?:React
并不知道一次state
变化, 影响的范围, 比如一次更新发生, 父子Fiber
上都有更新, 如果直接向下调和, 那么是不合理的, 所以采用了从rootFiber
统一向下的方式。
七、Update Render 调和
通过 Scheduler unstable_shouldYield
判断当前浏览器是否处于空闲状态, 如果处于空闲状态。开始进入 Render
调和 阶段。此时, React
中存在两颗树。双缓存 Fiber
树: React
构建了两棵 Fiber
树——当前树(current fiber tree
)和工作中树(work-in-progress fiber tree
),以实现高效的更新和必要时的回滚。这种双缓冲设计使得在进行并发更新时,能够分阶段地进行调度与中断,同时保证界面的一致性。
BeginWork
递阶段, 同 Mount
阶段, 从根 Fiber
开始, 对每个 Fiber
节点,调用对应的处理函数。处理逻辑如下(主要以函数组件为例): 遇到函数组件, 调用 renderWithHooks
执行函数组件。在 Update
阶段, 从 当前处理的 Fiber.memoizedState
中获取 Hook
链表, React
按照 Hook
调用的顺序遍历组件内部所有 Hook
, 调用 updateHookXX
检查其更新队列, 对于每个 Hook
, 如果队列中有更新, 从 Hook.baseState
触发, 遍历并处理 UpdateQueue
中的所有更新,根据优先级选择性地应用更新,并合并计算出最终的新状态,同时保留未处理的低优先级更新作为下次更新的基础。最终的计算结果会赋值给组件 Fiber
的 memoizedState
。 最后, renderWithHooks
会返回新的 ReactElement
, 这时, 新生成的 ReactElement
就已经包含了更新后的状态信息。拿到最新的 ReactElement
之后, 首先判断是否命中 bailOut
优化策略, bailout
性能优化策略, 通过对比current
树与 workInProgress
树 Fiber
的 props
、context
、state
等来命中 bailout
性能优化策略的组件可以不通过 reconcile
生成 workInProgress.child
, 而是复用上次更新生成的 workInProgress.child
, 减少不必要的子组件 render
。如果未命中, 调用 reconcileChildren
, 在 Update
更新阶段, 通过 Diff
算法来对比 当前的旧 Fiber
和最新的 ReactElement
。根据新旧差异决定是复用已有的 Fiber
还是创建新的 Fiber
节点, 在对比过程中,为需要更新的节点打上相应的 flag
(如 Placement
、Update
或 Deletion
)。同时创建或更新当前节点的子 Fiber
链,为后续 CompleteWork
阶段做准备。
CompleteWork
归阶段: 当某个 Fiber
节点没有子节点或者子节点已处理完成后,开始从叶子节点向上回溯。对于 HostComponent
(DOM
元素节点),在这一步骤会创建真实 DOM
元素的描述(还未实际挂载), 对于文本节点、Portal
等, 也会进行相应处理。将当前节点产生的副作用(比如需要插入、更新或删除 DOM
节点的操作)沿着树向上冒泡到父节点。构建一份完整的副作用列表,供 Commit
阶段统一处理。将每个节点的副作用合并到父节点上,最终从根节点可以获得整个更新所需的所有副作用。
八、Update Commit 提交
Commit
提交阶段 是 React
更新流程中唯一会直接操作 DOM
的阶段,它根据 Render
阶段生成的副作用列表执行实际的更新操作。这个阶段是同步执行的,确保所有副作用在一次提交中完成, 这个阶段是同步且不可中断的,确保 UI
的最终状态与最新的 React
元素描述完全一致。通常分为以下几个子阶段:
beforeMutation
变更前提交阶段: 对于需要捕获 DOM
状态的组件,会在实际 DOM
变更前调用 getSnapshotBeforeUpdate
, 记录必要的信息,为后续可能的布局修正提供数据。
mutation
变更提交阶段: 根据 effect list
中标记的副作用,对 DOM
进行实际的操作:创建新的 DOM
节点、更新现有节点、删除不再需要的节点等。在这一阶段,React
同时会处理事件绑定的更新以及 ref
的挂载或卸载,确保新的 DOM
状态与组件状态一致。对于需要删除的节点,执行相应的卸载操作,如移除事件监听器、释放内存资源等。
layout
布局提交阶段: 真实 DOM
挂载完成后,会依次调用诸如 componentDidMount
、componentDidUpdate
等生命周期方法。在函数组件中,相关的 layout effect
(useLayoutEffect
)也会在这一阶段执行。这一阶段确保所有 DOM
更新完成后,再进行可能依赖最新 DOM 布局信息的操作,比如同步测量、滚动调整等。
问题:
commit
阶段执行的生命周期或者effect
钩子是 先子后父? 为什么呢? 本质上commit
阶段处理的事情和dom
元素有关系,commit
阶段生命周期是可以改变真实dom
元素的状态的,所以如果在子组件生命周期内改变dom
状态,并且想要在父组件的生命周期中同步状态,就需要确保父组件的生命周期执行时机要晚于子组件。
九、问题
9.1 说说 React JSX 转换成真实 DOM 过程?
一、JSX
转换为虚拟 DOM
, 在 Webpack
、RsPack
或者 Vite
等构建编译下, 通过显示引入 createElement
或者隐式引入 jsx
将 JSX
语法转换为 ReactElement
。ReactElement
就是 React
中的 Virtual DOM
。通过 ReactElement
来创建 Fiber
。ReactElement
与 Fiber
一一对应。
二、初始化渲染或者状态更新触发调度器调度, 创建一个 Update
对象并将其插入到对应的更新队列中, 在插入期间, 需要进行对应的优化策略判断, 比如 eagerState
策略, 在没有命中对应的性能优化策略之后, 调用 scheduleUpdateOnFiber
进入调度阶段。scheduleUpdateOnFiber
冒泡标记更新, 从当前 Fiber
向上遍历, 更新每个祖先节点的 childLanes
属性,表明该分支中存在待调和的更新任务。在冒泡更新标记的过程中,最终会找到对应的根节点(通常为 FiberRoot
),这个根节点是整个更新流程的入口, 并返回 FiberRoot
, 并调用调度器的调度方法 unstable_scheduleCallback
将更新任务以对应的优先级加入任务队列。
三、进入 Render
调和阶段, 生成 Fiber
树, 通过 Scheduler unstable_shouldYield
判断当前浏览器是否处于空闲状态, 如果处于空闲状态。开始进入 Render
调和 阶段。Render
调和阶段 的主要目标是生成一棵新的 Fiber
树,并为后续的 DOM
更新准备好 副作用列表(effect list
)。这个阶段不直接操作 DOM
,而是进行计算和比较。render
阶段以 DFS
深度优先 的顺序遍历 ReactElement
, 如果有子节点, 遍历子节点, 如果没有子节点遍历兄弟节点。Render
阶段 可以中断和恢复, 简单来说,由于 Render
阶段的操作对用户来说其实是不可见的,所以就算打断再重启,对用户来说也是零感知。而 commit
阶段的操作则涉及真实 DOM
的渲染,所以这个过程必须用同步渲染来求稳。Render
调和阶段 是一个递归的过程, 存在 递
和 归
两个阶段。
-
BeginWork
递阶段, 从根Fiber
开始, 对每个Fiber
节点,调用对应的处理函数, 处理完成后会返回新的ReactElement
虚拟DOM
。获取新的ReactElement
后会进入Reconcile
协调 进行Diff
最小差异比对, 根据差异决定是否复用旧Fiber
, 最后返回Fiber
。 -
CompleteWork
归阶段: 当某个Fiber
节点没有子节点或者子节点已处理完成后,开始从叶子节点向上回溯。对于HostComponent
(DOM
元素节点),在这一步骤会创建真实DOM
元素的描述(还未实际挂载)
四、Commit
提交阶段 是 React
更新流程中唯一会直接操作 DOM
的阶段,它根据 Render
阶段生成的副作用列表执行实际的更新操作。这个阶段是同步执行的,确保所有副作用在一次提交中完成, 这个阶段是同步且不可中断的,确保 UI
的最终状态与最新的 React
元素描述完全一致。通常分为以下几个子阶段:
beforeMutation
变更前提交阶段: 对于需要捕获 DOM
状态的组件,会在实际 DOM
变更前调用 getSnapshotBeforeUpdate
, 记录必要的信息,为后续可能的布局修正提供数据。
mutation
变更提交阶段: 根据 effect list
中标记的副作用,对 DOM
进行实际的操作:创建新的 DOM
节点、更新现有节点、删除不再需要的节点等。在这一阶段,React
同时会处理事件绑定的更新以及 ref
的挂载或卸载,确保新的 DOM
状态与组件状态一致。对于需要删除的节点,执行相应的卸载操作,如移除事件监听器、释放内存资源等。
layout
布局提交阶段: 真实 DOM
挂载完成后,会依次调用诸如 componentDidMount
、componentDidUpdate
等生命周期方法。在函数组件中,相关的 layout effect
(useLayoutEffect
)也会在这一阶段执行。这一阶段确保所有 DOM
更新完成后,再进行可能依赖最新 DOM
布局信息的操作,比如同步测量、滚动调整等。
9.2 说说 React render 方法的原理 ? 在什么时候会被触发 ?
当 初始化渲染、调用 setState()
更新状态、父组件重新渲染导致 props
更新、上下文 Context
发生变化时, 都会进入组件实例的 updater
对象。updater
内部会调用类似 enqueueSetState
的方法,将本次更新的信息封装成一个 Update
对象。Update
对象包含了传入的部分状态(可能是对象或函数)、更新优先级(Lane
)、以及可能的回调函数。将该 Update
插入到对应的 fiber.updateQueue.shared.pending
更新队列中。调用 scheduleUpdateOnFiber
,标记当前 Fiber
需要更新,并由调度器安排后续渲染任务。
进入 Render
阶段, 它是一个递归的过程, 存在 递
和 归
两个阶段。在 BeginWork
递阶段, 从根 Fiber
开始, 对每个 Fiber
节点,调用对应的处理函数。处理逻辑如下, 遇到类组件时, 实例化类组件, 调用组件的构造函数 constructor
, 初始化组件的 state
、refs
等。React
执行 getDerivedStateFromProps
静态方法来处理 props
和 state
。如果为更新阶段, 接下来会调用 shouldComponentUpdate(nextProps, nextState)
判断组件是否需要更新。 如果是初始化渲染或者通过 shouldComponentUpdate
判断需要更新时, 调用组件实例的 render()
方法, render
方法应该是一个纯函数, 不允许有副作用, 用于生成新的虚拟 DOM
ReactElement
。获取最新的 ReactElement
之后, 根据 Reconcile
协调 Diff
, 寻求最小差异更新。然后继续下一个 Fiber
的处理。当某个 Fiber
节点没有子节点或者子节点已处理完成后,开始从叶子节点向上回溯进入 CompleteWork
阶段。
9.3 React Schedule 调度器中, 为什么不能直接用 requestIdleCallback ? 那么如何替代原生 requestIdleCallback 呢?
浏览器的原生 API
requestIdleCallback
用于在浏览器空闲时执行一些低优先级任务。但是由于以下原因, React
放弃使用: 1. 浏览器兼容性; 2. 触发频率不稳定, 受很多因素影响, 比如当我们的浏览器切换 tab
后, 之前 tab
注册的 requestIdleCallback
触发的频率会变得很低。3. requestIdleCallback
没有提供足够的控制粒度,例如无法定义任务优先级、不能随时中断、重启、继续等。基于以上原因, React
实现了功能更完备的requestIdleCallbackPolyfill
,这就是 Scheduler
。除了在空闲时触发回调的功能外, Scheduler
还提供了多种调度优先级供任务设置, 精确控制渲染的优先级和时间片。
React Scheduler
替代原生 requestIdleCallback
的实现: React
模拟 requestIdleCallback
的策略为: setImmediate
、 MessageChannel
、setTimeout
。 由于setTimeout
嵌套过深会有>= 4ms
的延迟, 所以如果当前宿主环境不支持 setImmediate
和 MessageChannel
, React
会优雅降级,选择setTimeout
来模拟, 这个调度器支持: 1. 根据任务优先级来灵活调度渲染任务; 2. 在每一帧内精确地控制任务执行时长(通常控制在 ~5ms
左右,避免掉帧); 3. 可中断、重启渲染任务,实现了精确的 Time Slicing
(时间切片)特性; 4. 避免了浏览器兼容性和原生 API
饥饿问题。
MessageChannel
模拟实现 requestIdleCallback
function requestIdleCallbackOfMessageChannel() {
return (cb, { timeout } = {}) => {
const start = performance.now();
const channel = new MessageChannel();
channel.port2.onmessage = () => {
const now = performance.now();
const didTimeout = timeout && now - start >= timeout;
if (didTimeout || now - start < 50) {
cb({
didTimeout,
timeRemaining() { return Math.max(0, 50 - (now - start)); },
});
} else {
channel.port1.postMessage(undefined);
}
};
channel.port1.postMessage(undefined);
};
}
setTimeout
模拟实现 requestIdleCallback
function requestIdleCallbackOfSetTimeout() {
return (cb, { timeout } = {}) => {
const start = performance.now();
const callbackWrapper = () => {
const now = performance.now();
const didTimeout = timeout && now - start >= timeout;
if (didTimeout || now - start < 50) {
cb({
didTimeout,
timeRemaining() { return Math.max(0, 50 - (now - start)); },
});
} else {
window.setTimeout(callbackWrapper);
}
};
window.setTimeout(callbackWrapper);
};
}