react-source-learn icon indicating copy to clipboard operation
react-source-learn copied to clipboard

React源码系列(三): ReactRoot的创建以及调度工作scheduleWork的执行

Open jsonz1993 opened this issue 6 years ago • 1 comments

本文讲的主要部分是 从 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当一个点抽出来说)。

demo-code

首先我们看一下 React.render(<T />, document.getElementById('container')),这里面的<T />首先会被babel调用 React.createElement生成为ReactElement,具体可以看第二篇 React源码系列(二): 从jsx到createElement

t-react-element

第一阶段 准备ReactRoot和后续需要的基本属性

ReactRoot

ReactDOM.render 首先会调用 legacyCreateRootFromDOMContainer 创建一个ReactRoot的实例,我们叫他root

react-root

这个 root 有什么用呢? 我们可以看到 ReactRoot原型链上有 render 方法,这个方法会开始执行我们root的整个render操作。 _internalRoot 属性指向一个 root obj,挂载着我们后面渲染所需的一些基本信息,比如 current 指向我们的 fiber tree的根节点 HostRootcontainerInfo 指向 dom容器,也就是本例中的 document.getElementById('container')

这些属性创建绑定大都可以在createFiberRoot找到。创建完ReactRoot之后,调用原型链上的ReactRoot.render方法。

scheduleRootUpdate 调度Root上的更新操作

ReactRoot.render 会先调用 requestCurrentTime 获取当前已经花费的时间, 然后调用computeExpirationForFiber来确定我们的优先级, computeExpirationForFiber 会根据我们当前的工作类型,比如isWorking, isCommit, isAsync 等等来返回对应的优先级,第一次渲染默认都是Sync的情况。如果不是同步渲染的情况,既Interactive 或者 Async,这时候会根据我们 前面requestCurrentTime获取的已花费时间 调用相应的算法来确定优先级。

动态获取异步、交互式优先级算法 expiration

获取优先级函数 computeexpirationforfiber

这里我们就可以看出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

update

到这一步,我们已经完成了HostRoot(fiber tree root)的创建、context的设置、优先级(更新模式异步还是高优先级还是低优先级)和更新队列的设置。

first

第二阶段 准备调度需要的优先级,渲染模式(异步同步),需要渲染的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 的创建和对应的操作

second

第三阶段 创建一条用于工作的fiber workInProgress,处理同步异步控制,调用beginWork

RenderRoot

RenderRoot会根据 current 拷贝创建另外一条单向链表 workInProgress 作为工作的Fiber,后面的工作都会在workInProgress上面进行,而不是current。为什么呢?

然后调用workLoop进行create fiber、diff 等操作。

work-loop

workLoop

workLoop 顾名思义,就是不断循环工作。判断有没有nextUnitOfWork,既下一个工作单元,我们都知道fiber系统给予React渲染阶段可中断的特性,所以这里每次都是只执行一个工作单元,而不是像React15一样一把梭,调用performUnitOfWork去处理这个工作单元。假如我们这次的work是可以被中断的,既如果不是Sync同步的情况,那么每一次循环调用performUnitOfWork之前,会调用shouleYield来判断是否有时间继续执行,有的话就继续执行,没有就等下一次有空闲时间再执行。

performunitofwork

performUnitOfWork 调用 beginWork,如果beginWork返回null,意味着我们这次工作已经做完了,这时候就调用 completeUnitOfWork ,开始commit的操作。

third

jsonz1993 avatar Nov 13 '18 12:11 jsonz1993

请问 requestWork的工作很简单,就是维护一条 scheduledRoot 的单向链表,比如说 lastScheduleRoot == null,意味着我们当前已经没有要处理的 root,这时候就把 firstScheduleRoot、lastScheduleRoot、root.nextScheduleRoot 都设置为 root。如果 lastScheduleRoot !== null,则把 lastScheduledRoot.nextScheduledRoot设置为root,等 lastScheduledRoot调度完就会开始处理当前 root。 这么一条单链表有什么意义啊?first last roo.next都是root

guoyaojie avatar Jul 24 '19 09:07 guoyaojie