react-source-learn
react-source-learn copied to clipboard
React源码系列(三): ReactRoot的创建以及调度工作scheduleWork的执行
本文讲的主要部分是 从 ReactDOM.render
调用到 fiber tree
创建之间的准备步骤,主要包括了 ReactRoot
的创建,优先级以及同异步渲染方式的确定还有 FiberTreeRoot
的准备。
对一些比较经典的东西可能会简单带过,因为目前主要是梳理整个 React 的运行机制,而关于某一些细点,后面可能会独立出来说会比较清晰。
在开始之前,我们要先知道一个事情就是:平时我们写React,都要引入多一个 'react-dom',用 ReactDOM.render
来把React生成的ReactElement
渲染到 DOM Tree 中。
也就是说我们引入的React负责ReactElement组件相关的东西,渲染层面抽出来交给,ReactDOM、 ReactNative,这种分离这也赋予了React跨终端渲染的能力。
我们先来写个小demo,后面顺着这个demo断点来看代码会容易很多,本文以第一次渲染的情况做为探讨的情况(这也意味着这个阶段不会进行diff的阶段,后面可能会把diff当一个点抽出来说)。
首先我们看一下 React.render(<T />, document.getElementById('container'))
,这里面的<T />
首先会被babel调用 React.createElement
生成为ReactElement,具体可以看第二篇 React源码系列(二): 从jsx到createElement
第一阶段 准备ReactRoot和后续需要的基本属性
ReactRoot
ReactDOM.render 首先会调用 legacyCreateRootFromDOMContainer
创建一个ReactRoot
的实例,我们叫他root
。
这个 root
有什么用呢? 我们可以看到 ReactRoot原型链上有 render
方法,这个方法会开始执行我们root的整个render操作。 _internalRoot
属性指向一个 root obj
,挂载着我们后面渲染所需的一些基本信息,比如 current
指向我们的 fiber tree的根节点 HostRoot, containerInfo
指向 dom容器,也就是本例中的 document.getElementById('container')
。
这些属性创建绑定大都可以在createFiberRoot
找到。创建完ReactRoot之后,调用原型链上的ReactRoot.render
方法。
scheduleRootUpdate 调度Root上的更新操作
ReactRoot.render
会先调用 requestCurrentTime
获取当前已经花费的时间, 然后调用computeExpirationForFiber
来确定我们的优先级, computeExpirationForFiber
会根据我们当前的工作类型,比如isWorking, isCommit, isAsync 等等来返回对应的优先级,第一次渲染默认都是Sync的情况。如果不是同步渲染的情况,既Interactive 或者 Async,这时候会根据我们 前面requestCurrentTime获取的已花费时间 调用相应的算法来确定优先级。
动态获取异步、交互式优先级算法
获取优先级函数
这里我们就可以看出React的更新优先级: Sync、Interactive、Async加上offscreen只留意到有四种。 但是之前看到网上很多说有六种情况,应该是我哪里漏了或者还没抽象成这种可以执行优先级的层面。
网上看到的六种更新情况 synchronous 与之前的Stack reconciler操作一样,同步执行 task 在next tick之前执行 animation 下一帧之前执行 high 在不久的将来立即执行 low 稍微延迟(100-200ms)执行也没关系 offscreen 下一次render时或scroll时才执行
获取该任务的优先级之后,给ReactRoot设置一个默认的context。
根据优先级之后创建一个update任务,这个update对象的payload指向我们的<T />组件,既这个update的任务是处理我们的组件,callback指向一个ReactWork._onCommit
,实际上就是执行完render
之后调用我们在ReactDOM.render
传入的回调函数。
把上述update任务加到fiber的更新队列中(fiber.updateQueue
),后面会在某个阶段统一处理updateQueue
。
到这一步,我们已经完成了HostRoot(fiber tree root)的创建、context的设置、优先级(更新模式异步还是高优先级还是低优先级)和更新队列的设置。

第二阶段 准备调度需要的优先级,渲染模式(异步同步),需要渲染的Root等
在第一阶段,我们更新完 fiber的 updateQueue之后,就调用 scheduleWork
开始调度这次的工作。
scheduleWork 调度工作(准备工作)
scheduleWork
主要的事情就是找到我们要处理的 root
设置刚才获取到的执行优先级,然后调用 requestWork
。
requestWork 申请工作 处理scheduleRoot单向链表,根据执行方式调用 performWork
requestWork的工作很简单,就是维护一条 scheduledRoot
的单向链表,比如说 lastScheduleRoot == null
,意味着我们当前已经没有要处理的 root,这时候就把 firstScheduleRoot、lastScheduleRoot、root.nextScheduleRoot
都设置为 root。如果 lastScheduleRoot !== null
,则把 lastScheduledRoot.nextScheduledRoot
设置为root,等 lastScheduledRoot
调度完就会开始处理当前 root。
然后就根据执行方式(是否是同步的方式)传不同的参数调用performWork
,设置isRendering
开关,开始render工作。
到这里我们算是Render的第二个阶段,设置调度的root,优先级,异步同步模式,接下来就是根据这些开关和模式,调用RenderRoot去执行fiber tree 的创建和对应的操作

第三阶段 创建一条用于工作的fiber workInProgress,处理同步异步控制,调用beginWork
RenderRoot
RenderRoot会根据 current
拷贝创建另外一条单向链表 workInProgress 作为工作的Fiber,后面的工作都会在workInProgress上面进行,而不是current。为什么呢?
然后调用workLoop进行create fiber、diff 等操作。
workLoop
workLoop 顾名思义,就是不断循环工作。判断有没有nextUnitOfWork
,既下一个工作单元,我们都知道fiber系统给予React渲染阶段可中断的特性,所以这里每次都是只执行一个工作单元,而不是像React15一样一把梭,调用performUnitOfWork
去处理这个工作单元。假如我们这次的work是可以被中断的,既如果不是Sync同步
的情况,那么每一次循环调用performUnitOfWork
之前,会调用shouleYield
来判断是否有时间继续执行,有的话就继续执行,没有就等下一次有空闲时间再执行。
performUnitOfWork
调用 beginWork
,如果beginWork
返回null,意味着我们这次工作已经做完了,这时候就调用 completeUnitOfWork
,开始commit
的操作。
请问 requestWork的工作很简单,就是维护一条 scheduledRoot 的单向链表,比如说 lastScheduleRoot == null,意味着我们当前已经没有要处理的 root,这时候就把 firstScheduleRoot、lastScheduleRoot、root.nextScheduleRoot 都设置为 root。如果 lastScheduleRoot !== null,则把 lastScheduledRoot.nextScheduledRoot设置为root,等 lastScheduledRoot调度完就会开始处理当前 root。 这么一条单链表有什么意义啊?first last roo.next都是root