宏观包结构 基础包结构
react
react 基础包, 只提供定义 react 组件(ReactElement
)的必要函数, 一般来说需要和渲染器(react-dom
,react-native
)一同使用. 在编写react
应用的代码时, 大部分都是调用此包的 api.
react-dom
react 渲染器之一, 是 react 与 web 平台连接的桥梁(可以在浏览器和 nodejs 环境中使用), 将react-reconciler
中的运行结果输出到 web 界面上 . 在编写react
应用的代码时,大多数场景下, 能用到此包的就是一个入口函数ReactDOM.render(<App/>, document.getElementById('root'))
, 其余使用的 api, 基本是react
包提供的.
react-reconciler
react 得以运行的核心包(综合协调react-dom
,react
,scheduler
各包之间的调用与配合 ). 管理 react 应用状态的输入和结果的输出. 将输入信号最终转换成输出信号传递给渲染器.
接受输入(scheduleUpdateOnFiber
), 将fiber
树生成逻辑封装到一个回调函数中(涉及fiber
树形结构, fiber.updateQueue
队列, 调和算法等),
把此回调函数(performSyncWorkOnRoot
或performConcurrentWorkOnRoot
)送入scheduler
进行调度
scheduler
会控制回调函数执行的时机, 回调函数执行完成后得到全新的 fiber 树
再调用渲染器(如react-dom
, react-native
等)将 fiber 树形结构最终反映到界面上
scheduler
调度机制的核心实现, 控制由react-reconciler
送入的回调函数的执行时机 , 在concurrent
模式下可以实现任务分片. 在编写react
应用的代码时, 同样几乎不会直接用到此包提供的 api.
核心任务就是执行回调(回调函数由react-reconciler
提供)
通过控制回调函数的执行时机, 来达到任务分片的目的, 实现可中断渲染(concurrent
模式下才有此特性)
宏观总览 为了便于理解, 可将 react 应用整体结构分为接口层(api
)和内核层(core
)2 个部分
接口层(api)
react
包, 平时在开发过程中使用的绝大部分api
均来自此包(不是所有). 在react
启动之后, 正常可以改变渲染的基本操作有 3 个.
class 组件中使用setState()
function 组件里面使用 hook,并发起dispatchAction
去改变 hook 对象
改变 context(其实也需要setState
或dispatchAction
的辅助才能改变)
以上setState
和dispatchAction
都由react
包直接暴露. 所以要想 react 工作, 基本上是调用react
包的 api 去与其他包进行交互.
内核层(core)
整个内核部分, 由 3 部分构成:
调度器(scheduler)
核心职责只有 1 个, 就是执行回调.
把react-reconciler
提供的回调函数, 包装到一个任务对象中.
在内部维护一个任务队列, 优先级高的排在最前面.
循环消费任务队列, 直到队列清空.
构造器(react-reconciler)
有 3 个核心职责:
装载渲染器, 渲染器必须实现HostConfig
协议 (如: react-dom
), 保证在需要的时候, 能够正确调用渲染器的 api, 生成实际节点(如: dom
节点).
接收react-dom
包(初次render
)和react
包(后续更新setState
)发起的更新请求.
将fiber
树的构造过程包装在一个回调函数中, 并将此回调函数传入到scheduler
包等待调度.
渲染器(react-dom)
有 2 个核心职责:
引导react
应用的启动(通过ReactDOM.render
).
实现HostConfig
协议 (源码在 ReactDOMHostConfig.js 中 ), 能够将react-reconciler
包构造出来的fiber
树表现出来, 生成 dom 节点(浏览器中), 生成字符串(ssr).
内核关系
总结 react主要有四个包,react、react-dom(渲染器)、react-reconciler(构造器)、scheduler(调度器)。
react提供了一些我们平时编写代码时常用的API。
react-dom 将 react-reconciler 包构造出来的 fiber 树表现出来, 生成 dom 节点。当然还负责通过 ReactDOM.render 来启动 React 应用。
react-reconciler 主要用于接受更新的请求(请求可能来源于react-dom的初次render,也可能来源于react的setState),将 fiber 树的构造或更新过程包装为一个回调,然后将这个回调传递给 scheduler 调度。
schduler 接收到 reconciler 传过来的回调,将其包装为一个 task。内部会维护一个任务队列,优先级高的排在最前面,循环消费任务队列,直到任务队列为空。(schduler 是可以脱离 react 的,从包的名字上也可以看出来,本质上它就是一个根据优先级消费任务的算法)
工作循环与主干逻辑
任务调度循环(调度任务) 位于 scheduler 中 ,它是react
应用得以运行的保证,它需要循环调用,控制所有任务(task
)的调度。
优先级任务队列以二叉堆为数据结构,循环执行堆,直到堆被清空。
调度每一个任务,但不关系这个任务干什么,只需要执行回调函数 performSyncWorkOnRoot
或performConcurrentWorkOnRoot
。
fiber 构造循环(任务的一部分) 位于 react-reconciler 中,控制 fiber 树的构造。
fiber 构造循环是以树为数据结构,从上至下执行深度优先遍历。
fiber 构造循环是任务的一部分,只负责 fiber 树的构造。实际上一个任务可能包括fiber树的构造,DOM渲染,调度检测。
主干逻辑 1.将每一次更新(如DOM变更)视为一次更新需求
2.react-reconciler 收到更新需求后,会去 scheduler 将更新需求转化一个任务
3.scheduler 通过任务调度循环来执行 task
4.task的实际执行过程发生在 react-reconciler 中
fiber构造循环,循环完成后构造出最新的 fiber 树
commitRoot,把最新的 fiber 树最终渲染到页面上,任务完成
主干逻辑就是从DOM更新到实际渲染这一条链路, 为了更好的性能(如批量更新, 可中断渲染等功能),react在输入到输出的链路上做了很多优化策略,比如本文讲述的任务调度循环和fiber构造循环相互配合就可以实现可中断渲染 。
==高频对象== react包 ReactElement 1 2 ReactDOM .render (<App /> , document .getElementById ('root' ));
包括<App/>
及其所有子节点都是ReactElement
对象,每个ReactElement
对象的区别在于 type 不同。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 export type ReactElement = { $typeof : any, type : any, key : any, ref : any, props : any, _owner : any, _store : {validated : boolean, ...}, _self : React $Element<any>, _shadowChildren : any, _source : Source , };
key
属性在reconciler
阶段会用到,所有的ReactElement
对象都有 key 属性,默认值为 null。(diff算法使用)
type
属性决定了节点的种类:
它的值可以是字符串(代表div,span
等 dom 节点),函数(代表function, class
等节点),或者 react 内部定义的节点类型(portal,context,fragment
等)
在reconciler
阶段, 会根据 type 执行不同的逻辑(在 fiber 构建阶段详细解读)。
如 type 是一个字符串类型, 则直接使用.
如 type 是一个ReactComponent
类型, 则会调用其 render 方法获取子节点.
如 type 是一个function
类型,则会调用该方法获取子节点
…
ReactComponent(class类型组件) 对于ReactElement
来讲,ReactComponent
仅仅是诸多type
类型中的一种。
对于开发者来讲,ReactComponent
使用非常高频(在状态组件章节中详细解读)。
ReactComponent
是 class 类型,继承父类Component
,拥有特殊的方法(setState
,forceUpdate
)和特殊的属性(context
,updater
等)。
在reconciler
阶段,会依据ReactElement
对象的特征,生成对应的 fiber 节点。当识别到ReactElement
对象是 class 类型的时候,会触发ReactComponent
对象的生命周期,并调用其 render
方法,生成ReactElement
子节点。
其他 ReactElement function
类型的组件和class
类型的组件一样, 是诸多ReactElement
形式中的一种.
react-reconciler包 Fiber对象 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 export type Fiber = { tag : WorkTag , key : null | string, elementType : any, type : any, stateNode : any, return : Fiber | null , child : Fiber | null , sibling : Fiber | null , index : number, ref : | null | (((handle: mixed ) => void ) & { _stringRef : ?string, ... }) | RefObject , pendingProps : any, memoizedProps : any, updateQueue : mixed, memoizedState : any, dependencies : Dependencies | null , mode : TypeOfMode , flags : Flags , subtreeFlags : Flags , deletions : Array <Fiber > | null , nextEffect : Fiber | null , firstEffect : Fiber | null , lastEffect : Fiber | null , lanes : Lanes , childLanes : Lanes , alternate : Fiber | null , actualDuration?: number, actualStartTime?: number, selfBaseDuration?: number, treeBaseDuration?: number, };
fiber树和ReactElement结构:
其中<App/>
,<Content/>
为ClassComponent
类型的fiber
节点, 其余节点都是普通HostComponent
类型节点.
<Content/>
的子节点在ReactElement
树中是React.Fragment
, 但是在fiber
树中React.Fragment
并没有与之对应的fiber
节点(reconciler
阶段对此类型节点做了单独处理, 所以ReactElement
节点和fiber
节点不是一对一匹配).
reconciler运作流程 react-reconciler的主要作用
输入: 暴露api
函数(如: scheduleUpdateOnFiber
), 供给其他包(如react
包)调用。
注册调度任务: 与调度中心(scheduler
包)交互, 注册调度任务task
, 等待任务回调。
执行任务回调: 在内存中构造出fiber树
, 同时与与渲染器(react-dom
)交互, 在内存中创建出与fiber
对应的DOM
节点。
输出: 与渲染器(react-dom
)交互, 渲染DOM
节点。
输入 无论是首次渲染还是后续更新,最后都会间接调用scheduleUpdateOnFiber
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 export function scheduleUpdateOnFiber ( fiber: Fiber, lane: Lane, eventTime: number, ) { const root = markUpdateLaneFromFiberToRoot (fiber, lane); if (lane === SyncLane ) { if ( (executionContext & LegacyUnbatchedContext ) !== NoContext && (executionContext & (RenderContext | CommitContext )) === NoContext ) { performSyncWorkOnRoot (root); } else { ensureRootIsScheduled (root, eventTime); } } else { ensureRootIsScheduled (root, eventTime); } }
逻辑进入到scheduleUpdateOnFiber
之后, 后面有 2 种可能:
不经过调度, 直接进行fiber构造
.
注册调度任务, 经过Scheduler
包的调度, 间接进行fiber构造
.
注册调度任务 与输入环节紧密相连, scheduleUpdateOnFiber
函数之后, 立即进入ensureRootIsScheduled
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 function ensureRootIsScheduled (root: FiberRoot, currentTime: number ) { const existingCallbackNode = root.callbackNode ; const nextLanes = getNextLanes ( root, root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes , ); const newCallbackPriority = returnNextLanesPriority (); if (nextLanes === NoLanes ) { return ; } if (existingCallbackNode !== null ) { const existingCallbackPriority = root.callbackPriority ; if (existingCallbackPriority === newCallbackPriority) { return ; } cancelCallback (existingCallbackNode); } let newCallbackNode; if (newCallbackPriority === SyncLanePriority ) { newCallbackNode = scheduleSyncCallback ( performSyncWorkOnRoot.bind (null , root), ); } else if (newCallbackPriority === SyncBatchedLanePriority ) { newCallbackNode = scheduleCallback ( ImmediateSchedulerPriority , performSyncWorkOnRoot.bind (null , root), ); } else { const schedulerPriorityLevel = lanePriorityToSchedulerPriority (newCallbackPriority); newCallbackNode = scheduleCallback ( schedulerPriorityLevel, performConcurrentWorkOnRoot.bind (null , root), ); } root.callbackPriority = newCallbackPriority; root.callbackNode = newCallbackNode; }
ensureRootIsScheduled
的逻辑很清晰, 分为 2 部分:
前半部分: 判断是否需要注册新的调度(如果无需新的调度, 会退出函数)
后半部分: 注册调度任务
performSyncWorkOnRoot
或performConcurrentWorkOnRoot
被封装到了任务回调(scheduleCallback
)中
等待调度中心执行任务, 任务运行其实就是执行performSyncWorkOnRoot
或performConcurrentWorkOnRoot
执行任务回调 实际上就是执行performSyncWorkOnRoot
或performConcurrentWorkOnRoot
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function performSyncWorkOnRoot (root ) { let lanes; let exitStatus; lanes = getNextLanes (root, NoLanes ); exitStatus = renderRootSync (root, lanes); if (root.tag !== LegacyRoot && exitStatus === RootErrored ) { } const finishedWork : Fiber = (root.current .alternate : any); root.finishedWork = finishedWork; root.finishedLanes = lanes; commitRoot (root); ensureRootIsScheduled (root, now ()); return null ; }
performSyncWorkOnRoot
的逻辑很清晰, 分为 3 部分:
fiber 树构造
异常处理: 有可能 fiber 构造过程中出现异常
调用输出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 function performConcurrentWorkOnRoot (root ) { const originalCallbackNode = root.callbackNode ; const didFlushPassiveEffects = flushPassiveEffects (); if (didFlushPassiveEffects) { if (root.callbackNode !== originalCallbackNode) { return null ; } else { } } let lanes = getNextLanes ( root, root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes , ); let exitStatus = renderRootConcurrent (root, lanes); if ( includesSomeLane ( workInProgressRootIncludedLanes, workInProgressRootUpdatedLanes, ) ) { prepareFreshStack (root, NoLanes ); } else if (exitStatus !== RootIncomplete ) { if (exitStatus === RootErrored ) { }. const finishedWork : Fiber = (root.current .alternate : any); root.finishedWork = finishedWork; root.finishedLanes = lanes; finishConcurrentRender (root, exitStatus, lanes); } ensureRootIsScheduled (root, now ()); if (root.callbackNode === originalCallbackNode) { return performConcurrentWorkOnRoot.bind (null , root); } return null ; }
performConcurrentWorkOnRoot
的逻辑与performSyncWorkOnRoot
的不同之处在于, 对于可中断渲染的支持 :
调用performConcurrentWorkOnRoot
函数时, 首先检查是否处于render
过程中, 是否需要恢复上一次渲染。
如果本次渲染被中断, 最后返回一个新的 performConcurrentWorkOnRoot 函数, 等待下一次调用。
commit提交 在输出阶段,commitRoot
的实现逻辑是在commitRootImpl
函数中, 其主要逻辑是处理副作用队列, 将最新的 fiber 树结构反映到 DOM 上 .
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 function commitRootImpl (root, renderPriorityLevel ) { const finishedWork = root.finishedWork ; const lanes = root.finishedLanes ; root.finishedWork = null ; root.finishedLanes = NoLanes ; root.callbackNode = null ; let firstEffect = finishedWork.firstEffect ; if (firstEffect !== null ) { const prevExecutionContext = executionContext; executionContext |= CommitContext ; nextEffect = firstEffect; do { commitBeforeMutationEffects (); } while (nextEffect !== null ); nextEffect = firstEffect; do { commitMutationEffects (root, renderPriorityLevel); } while (nextEffect !== null ); root.current = finishedWork; nextEffect = firstEffect; do { commitLayoutEffects (root, lanes); } while (nextEffect !== null ); nextEffect = null ; executionContext = prevExecutionContext; } ensureRootIsScheduled (root, now ()); return null ; }
核心逻辑分为 3 个步骤:
commitBeforeMutationEffects
dom 变更之前, 主要处理副作用队列中带有Snapshot
,Passive
标记的fiber
节点.
commitMutationEffects
dom 变更, 界面得到更新. 主要处理副作用队列中带有Placement
, Update
, Deletion
, Hydrating
标记的fiber
节点.
commitLayoutEffects
dom 变更后, 主要处理副作用队列中带有Update | Callback
标记的fiber
节点.
==总结== 无论是首次渲染还是后续更新,都会进入一个函数 scheduleUpdateOnFiber,这个函数是 react-reconciler 暴露给其它包的,这个函数主要是根据上下文来直接或间接的执行 fiber 构造。所谓间接就是要注册调度任务,然后经过 Scheduler 的调度。
注册调度任务就是将 performSyncWorkOnRoot 或 performConcurrentWorkOnRoot 封装为回调,放到任务队列,等待执行,执行任务也就是执行这两个函数,当这两个函数被执行时,就进入 render 阶段,会以dfs的方式对比老的 Fiber 节点和新的组件返回的 JSX 对象,生成新的 Fiber 节点以及 effectList。
这两个函数的区别就在于对于可中断渲染的支持,performConcurrentWorkOnRoot 支持可中断渲染,因此会在每次调用时检查是否处于 render 的过程中,如果是就恢复上一次渲染。
然后进入 commit 阶段,基于 effectList 生成新的DOM。
优先级管理 可中断渲染,时间切片(time slicing),异步渲染(suspense) 等特性, 在源码中得以实现都依赖于优先级管理。
React
内部对于优先级
的管理, 贯穿运作流程的 4 个阶段(从输入到输出), 根据其功能的不同, 可以分为 3 种类型:
fiber
优先级(LanePriority
): 位于react-reconciler
包, 也就是Lane(车道模型)
.
调度优先级(SchedulerPriority
): 位于scheduler
包.
优先级等级(ReactPriorityLevel
) : 位于react-reconciler
包中的SchedulerWithReactIntegration.js
, 负责上述 2 套优先级体系的转换.
Lane Lane是什么 使用31位的二进制表示31条赛道,位数越小的赛道优先级越高 ,某些相邻的赛道拥有相同优先级
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 export const NoLanes : Lanes = 0b0000000000000000000000000000000 ;export const NoLane : Lane = 0b0000000000000000000000000000000 ;export const SyncLane : Lane = 0b0000000000000000000000000000001 ;export const SyncBatchedLane : Lane = 0b0000000000000000000000000000010 ;export const InputDiscreteHydrationLane : Lane = 0b0000000000000000000000000000100 ;const InputDiscreteLanes : Lanes = 0b0000000000000000000000000011000 ;const InputContinuousHydrationLane : Lane = 0b0000000000000000000000000100000 ;const InputContinuousLanes : Lanes = 0b0000000000000000000000011000000 ;export const DefaultHydrationLane : Lane = 0b0000000000000000000000100000000 ;export const DefaultLanes : Lanes = 0b0000000000000000000111000000000 ;const TransitionHydrationLane : Lane = 0b0000000000000000001000000000000 ;const TransitionLanes : Lanes = 0b0000000001111111110000000000000 ;const RetryLanes : Lanes = 0b0000011110000000000000000000000 ;export const SomeRetryLane : Lanes = 0b0000010000000000000000000000000 ;export const SelectiveHydrationLane : Lane = 0b0000100000000000000000000000000 ;const NonIdleLanes = 0b0000111111111111111111111111111 ;export const IdleHydrationLane : Lane = 0b0001000000000000000000000000000 ;const IdleLanes : Lanes = 0b0110000000000000000000000000000 ;export const OffscreenLane : Lane = 0b1000000000000000000000000000000 ;
Lanes 可以看到其中有几个变量占用了几条赛道,比如:
1 2 3 const InputDiscreteLanes : Lanes = 0b0000000000000000000000000011000 ;export const DefaultLanes : Lanes = 0b0000000000000000000111000000000 ;const TransitionLanes : Lanes = 0b0000000001111111110000000000000 ;
这就是批
的概念,被称作lanes
(区别于优先级
的lane
)。
其中InputDiscreteLanes
是“用户交互”触发更新会拥有的优先级
范围。
DefaultLanes
是“请求数据返回后触发更新”拥有的优先级
范围。
TransitionLanes
是Suspense
、useTransition
、useDeferredValue
拥有的优先级
范围。
这其中有个细节,越低优先级
的lanes
占用的位越多。比如InputDiscreteLanes
占了2个位,TransitionLanes
占了9个位。
原因在于:越低优先级
的更新
越容易被打断,导致积压下来,所以需要更多的位。相反,最高优的同步更新的SyncLane
不需要多余的lanes
。
为什么这么设计 主要是方便我们使用位操作来实现一些比较、合并、重置等功能。
Lane有关操作 生成 lane 我们可以使用左移操作来快速生成一个对应的 lane,比如我们想要生成一个 DefaultLane
我们只需要用下面的代码即可
筛选 lanes 我们有时候需要判断哪些 lane 已经有任务了,哪些 lane 没有任务,这个时候我们需要使用我们的 lanes 和我们预定义的一些 lanes 内容进行按位与的操作,得到的内容就是我们哪些对应的车道上有任务,比如我们现在想要知道在非闲置车道上哪些车道已经有任务了,我们只需要用 NonIdleLanes 和我们 lanes 做按位与操作即可
1 const nonIdlePendingLanes = pendingLanes & NonIdleLanes ;
如果我们想要筛选出不包含某些车道的任务,我们只需要先把筛选模型按位取反再按位与即可:
1 const IdlePendingLanes = pendingLanes & ~NonIdleLanes ;
合并 lanes 如果想把 lane 合并到 lanes 中,我们只需要进行按位或的操作就行了
1 root.pendingLanes |= updateLan
取出优先级最高的已经使用的 lane 取出优先级最高的任务,也就是二进制中最右边的那个 1。
操作是 lanes 与自己的负数进行按位与操作,因为我们的负数是使用补码保存的,如果最后一位是 1 ,那么补码就会变成 1,与操作就会返回 1;如果最后一位是 0 ,补码就是 0,但是会产生一位的进位,这个进位会持续到遇到 0 为止,而这个 0 就是 1的补码,此时就会产生 1 了,与操作就会返回 1。
删除对应的 lane 我们有时候会从我们总的 lanes 里取出我们需要 lane 的然后重置它,我们可以对指定的车道的值按位取反,然后和我们的 lanes 进行按位与操作,这时除了我们需要的那个车道是 0 其他都是 1 ,按位与的时候,我们重置的车道必定变成 0,而其他的车道会变成保留自己原来的值:
LanePriority LanePriority
: 属于react-reconciler
包, 定义于 ReactFiberLane.js
1 2 3 4 5 6 7 8 9 10 export const SyncLanePriority : LanePriority = 15 ;export const SyncBatchedLanePriority : LanePriority = 14 ;const InputDiscreteHydrationLanePriority : LanePriority = 13 ;export const InputDiscreteLanePriority : LanePriority = 12 ;const OffscreenLanePriority : LanePriority = 1 ;export const NoLanePriority : LanePriority = 0 ;
与fiber
构造过程相关的优先级(如fiber.updateQueue
,fiber.lanes
)都使用LanePriority
SchedulerPriority SchedulerPriority
, 属于scheduler
包, 定义于SchedulerPriorities.js
中
1 2 3 4 5 6 export const NoPriority = 0 ;export const ImmediatePriority = 1 ;export const UserBlockingPriority = 2 ;export const NormalPriority = 3 ;export const LowPriority = 4 ;export const IdlePriority = 5 ;
与scheduler
调度中心相关的优先级使用SchedulerPriority
.
ReactPriorityLevel reactPriorityLevel
, 属于react-reconciler
包, 定义于SchedulerWithReactIntegration.js
中
用于转换 SchedulerPriority 和 LanePriority,协同调度中心(scheduler包)和 fiber 树构造(react-reconciler包)中对优先级的使用。
1 2 3 4 5 6 7 export const ImmediatePriority : ReactPriorityLevel = 99 ;export const UserBlockingPriority : ReactPriorityLevel = 98 ;export const NormalPriority : ReactPriorityLevel = 97 ;export const LowPriority : ReactPriorityLevel = 96 ;export const IdlePriority : ReactPriorityLevel = 95 ;export const NoPriority : ReactPriorityLevel = 90 ;
参考 https://blog.csdn.net/weixin_46463785/article/details/130402847
调度原理(Scheduler) 原理图
调度相关核心函数 1 2 3 4 5 6 7 8 export let requestHostCallback; export let cancelHostCallback; export let requestHostTimeout; export let cancelHostTimeout; export let shouldYieldToHost; export let requestPaint; export let getCurrentTime; export let forceFrameRate;
调度 请求或取消调度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 const performWorkUntilDeadline = ( ) => { if (scheduledHostCallback !== null ) { const currentTime = getCurrentTime (); deadline = currentTime + yieldInterval; scheduledHostCallback (hasTimeRemaining, currentTime); } else { isMessageLoopRunning = false ; } };const channel = new MessageChannel ();const port = channel.port2 ; channel.port1 .onmessage = performWorkUntilDeadline; requestHostCallback = function (callback ) { scheduledHostCallback = callback; if (!isMessageLoopRunning) { isMessageLoopRunning = true ; port.postMessage (null ); } }; cancelHostCallback = function ( ) { scheduledHostCallback = null ; };
请求回调之后scheduledHostCallback = callback
, 然后通过MessageChannel
发消息的方式触发performWorkUntilDeadline
函数, 最后执行回调scheduledHostCallback
.
此处需要注意: MessageChannel
在浏览器事件循环中属于宏任务
, 所以调度中心永远是异步执行
回调函数.
关于MessageChannel https://juejin.cn/post/6889314677528985614?searchId=202402272149034FDB259ADEE976088AD9
时间切片 执行时间分割, 让出主线程(把控制权归还浏览器, 浏览器可以处理用户输入, UI 绘制等紧急任务).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 const localPerformance = performance; getCurrentTime = () => localPerformance.now ();let yieldInterval = 5 ;let deadline = 0 ;const maxYieldInterval = 300 ;let needsPaint = false ;const scheduling = navigator.scheduling ; shouldYieldToHost = function ( ) { const currentTime = getCurrentTime (); if (currentTime >= deadline) { if (needsPaint || scheduling.isInputPending ()) { return true ; } return currentTime >= maxYieldInterval; } else { return false ; } }; requestPaint = function ( ) { needsPaint = true ; }; forceFrameRate = function (fps ) { if (fps < 0 || fps > 125 ) { console ['error' ]( 'forceFrameRate takes a positive int between 0 and 125, ' + 'forcing frame rates higher than 125 fps is not supported' , ); return ; } if (fps > 0 ) { yieldInterval = Math .floor (1000 / fps); } else { yieldInterval = 5 ; } };
完整回调的实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const performWorkUntilDeadline = ( ) => { if (scheduledHostCallback !== null ) { const currentTime = getCurrentTime (); deadline = currentTime + yieldInterval; const hasTimeRemaining = true ; try { const hasMoreWork = scheduledHostCallback (hasTimeRemaining, currentTime); if (!hasMoreWork) { isMessageLoopRunning = false ; scheduledHostCallback = null ; } else { port.postMessage (null ); } } catch (error) { port.postMessage (null ); throw error; } } else { isMessageLoopRunning = false ; } needsPaint = false ; };
任务队列管理 调度的目的是为了消费任务, 接下来就具体分析任务队列是如何管理与实现的。
在Scheduler.js 中, 维护了一个taskQueue , 任务队列管理就是围绕这个taskQueue
展开.
1 2 3 var taskQueue = [];var timerQueue = [];
创建任务 在unstable_scheduleCallback
函数中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 function unstable_scheduleCallback (priorityLevel, callback, options ) { var currentTime = getCurrentTime (); var startTime; if (typeof options === 'object' && options !== null ) { } else { startTime = currentTime; } var timeout; switch (priorityLevel) { case ImmediatePriority : timeout = IMMEDIATE_PRIORITY_TIMEOUT ; break ; case UserBlockingPriority : timeout = USER_BLOCKING_PRIORITY_TIMEOUT ; break ; case IdlePriority : timeout = IDLE_PRIORITY_TIMEOUT ; break ; case LowPriority : timeout = LOW_PRIORITY_TIMEOUT ; break ; case NormalPriority : default : timeout = NORMAL_PRIORITY_TIMEOUT ; break ; } var expirationTime = startTime + timeout; var newTask = { id : taskIdCounter++, callback, priorityLevel, startTime, expirationTime, sortIndex : -1 , }; if (startTime > currentTime) { } else { newTask.sortIndex = expirationTime; push (taskQueue, newTask); if (!isHostCallbackScheduled && !isPerformingWork) { isHostCallbackScheduled = true ; requestHostCallback (flushWork); } } return newTask; }
任务对象 1 2 3 4 5 6 7 8 9 var newTask = { id : taskIdCounter++, callback, priorityLevel, startTime, expirationTime, sortIndex : -1 , }; newTask.sortIndex = expirationTime;
消费任务 创建任务之后, 最后请求调度requestHostCallback(flushWork)
, flushWork
函数作为参数被传入调度中心内核等待回调。
在调度中心中, 只需下一个事件循环就会执行回调, 最终执行flushWork
。flushWork中会执行 workLoop。
队列消费的主要逻辑是在workLoop
函数中,这部分就是任务调度循环。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 function workLoop (hasTimeRemaining, initialTime ) { let currentTime = initialTime; currentTask = peek (taskQueue); while (currentTask !== null ) { if ( currentTask.expirationTime > currentTime && (!hasTimeRemaining || shouldYieldToHost ()) ) { break ; } const callback = currentTask.callback ; if (typeof callback === 'function' ) { currentTask.callback = null ; currentPriorityLevel = currentTask.priorityLevel ; const didUserCallbackTimeout = currentTask.expirationTime <= currentTime; const continuationCallback = callback (didUserCallbackTimeout); currentTime = getCurrentTime (); if (typeof continuationCallback === 'function' ) { currentTask.callback = continuationCallback; } else { if (currentTask === peek (taskQueue)) { pop (taskQueue); } } } else { pop (taskQueue); } currentTask = peek (taskQueue); } if (currentTask !== null ) { return true ; } else { return false ; } }
时间切片(time slicing) 和 fiber树的可中断渲染。 这 2 大特性的实现, 都集中于这个while
循环。
每一次while
循环的退出就是一个时间切片, 深入分析while
循环的退出条件:
队列被完全清空: 这种情况就是很正常的情况, 一气呵成, 没有遇到任何阻碍.
执行超时: 在消费 taskQueue 时, 在执行 task.callback 之前, 都会检测是否超时, 所以超时检测是以 task 为单位.
如果某个task.callback
执行时间太长(如: fiber树
很大, 或逻辑很重)也会造成超时
所以在执行task.callback
过程中, 也需要一种机制检测是否超时, 如果超时了就立刻暂停task.callback
的执行.
时间切片原理
消费任务队列的过程中, 可以消费1~n
个 task, 甚至清空整个 queue. 但是在每一次具体执行task.callback
之前都要进行超时检测, 如果超时可以立即退出循环并等待下一次调用.
可中断渲染原理
在时间切片的基础之上, 如果单个task.callback
执行时间就很长(假设 200ms). 就需要task.callback
自己能够检测是否超时, 所以在 fiber 树构造过程中, 每构造完成一个单元, 都会检测一次超时(源码链接 ), 如遇超时就退出fiber树构造循环
, 并返回一个新的回调函数(就是此处的continuationCallback
)并等待下一次回调继续未完成的fiber树构造
.
总结 scheduler 会维护一个基于大顶堆的任务队列,接受回调并包装成 task 对象(可能是组件的初次渲染、更新或提交),根据优先级被添加到任务队列中,schduler 有自己的优先级 schdulerPriority。为了避免任务过长而导致页面卡顿,就采用了时间分片的思想,将任务拆分成多个时间片段,初始是5ms,如果任务超时了,就中断并将控制权还给浏览器,去做layout、paint等事情,这样就基本能保证不会掉帧卡顿了。然后在下一帧再恢复到之前中断的地方继续往下执行。
scheduler 经常需要执行 reconciler 传递过来的构建 fiber 的任务。fiber 的引入正是为了支持可中断性和恢复,如果使用原先的递归比较虚拟 DOM 的方法来做 diff,那是没有办法在中端之后恢复原位的。Fiber架构采用了深度优先遍历的方式,允许在遍历树的过程中暂停和恢复,每个Fiber节点都包含了大量上下文的信息,因此可以非常方便的复位。
Fiber是怎么实现中断和恢复的?
在每处理完成一个 Fiber 节点时,会检查时间片是否到时,如果到了,则会中断此次 “渲染”
注:Reconciliation 阶段React Fiber会找出需要更新哪些DOM, 是可以被打断的,但是到了第二阶段Commit Phase ,那就一鼓作气把DOM更新完,绝不会被打断。