在看源码时,可以看到有这么一个方法,从函数名来看,是「找到优先级最高的 root
」,但每个应用只有一个 root
,那为什么还要去「找」呢?
因为 react
支持多个实例,即支持调用多次 ReactDOM.render
。如果他们同时发生更新,也要有个先后关系,这个方法,就是用来从多个实例对应的 root
中,找出最先进行更新的那个 root
。
该博客适合对源码有一定了解的同学
- 知道
root
表示什么- 知道整个渲染流程的调用链
- 知道批量更新与非批量更新
概述
findHighestPriorityRoot
方法其实是一个从「链结构」查找特定节点的算法。root
视为一个节点,root.nextScheduledRoot
指向下一个节点,并且最后一个节点指向第一个节点。
firstScheduledRoot
是该链结构的一个节点,lastScheduledRoot
是该链结构的最后一个节点。每次先从第一个节点开始找。
简单实现
这个算法其实很简单,每个节点上有 value
字段,找到链结构中 value
为最大值的那个节点
1 | class Root { |
1 | const root1 = new Root(200); |
其他影响查找的逻辑
实际的 findHighestPriorityRoot
方法,还有其他逻辑。
1、当 value
为 0 时,该节点就视为无效,需要将该节点从链结构中移除,考虑到这个因素,上面的代码需要进行修改
1 | function findHighestPriorityRoot() { |
2、当 value === 1000
时,不需要再查找,该值必然为最大值。
1 | function findHighestPriorityRoot() { |
对照实际函数
附上 16.8.6 版本该方法实际源码 https://github.com/facebook/react/blob/16.8.6/packages/react-reconciler/src/ReactFiberScheduler.js#L2138
expirationTime
对应 value
highestPriorityWork
对应 maxValue
highestPriorityRoot
对应 maxValueRoot
lastScheduledRoot
对应 lastRoot
firstScheduledRoot
对应 firstRoot
NoWork === 0
Sync === 1000
实际用例
正常业务场景下,只会调用一次 ReactDOM.render
,所以不会出现需要「寻找优先级最高的 root
」这种情况。不过为了能证实上述的说法,现在运行一个实际用例,断点查看运行过程中那些全局变量的变化
1 | class App1 extends React.Component { |
点击按钮会更新 app1
,并且将 app2
渲染到页面上。
setState
执行后,调用链路简单来说是setState
-> enqueueSetState
-> scheduleWork
-> requestWork
-> 省略…
第一次调用 findHighestPriorityRoot
第一次调用是在创建 update
时的 requestCurrentTime
方法内,但第一次调用由于 firstScheduledRoot
好 lastScheduledRoot
都是 null
,就等于不存在任何节点用于查找,所以这次调用没有任何意义。
第二次调用
requestWork
方法方法会调用 addRootToSchedule
,调用参数为 app1Root
(表示 app1
生成的 root
,后面同理),调用后会将 firstScheduledRoot
和 lastScheduledRoot
这两个全局变量都赋值为 app1Root
。
然后调用栈回到 requestWork
方法内,由于此时是「批量更新」(isBatchingUpdates === true
),所以不会继续往下执行,而是中断,并等到该批次结尾再执行。
「该批次」可以理解为一次同步任务执行,上面例子就是
handleClick
该方法执行完后。
所以接下来是调用 ReactDOM.render(<App2 />)
,render
方法的调用链路和 setState
差别不是很大,同样会调用 scheduleWork
并且之后就是一样的了。render
-> updateContainer
-> scheduleRootUpdate
-> scheduleWork
。
render
和 setState
一样也会创建 update
,所以同样会调用 requestCurrentTime
,所以也会调用 findHighestPriorityRoot
方法,此时由于 lastScheduledRoot
和 firstScheduledRoot
都已经被赋值为 app1Root
了,所以是可以进行查找的,但由于此时只存在 app1Root
,所以肯定只能找到该节点,并且实际的该方法并不返回值,而是会将 nextFlushedRoot
全局变量置为找到的节点。
然后在 requestWork
方法内同样会调用 addRootToSchedule
,不过这次和 setState
过程中调用时就不同了,此时lastScheduledRoot === firstScheduledRoot === app1Root
,所以会走另外一个逻辑
1 | // root === app2Root、lastScheduledRoot === firstScheudledRoot === app1Root |
即将 app2Root
保存到 app1Root
上,然后将 lastScheduledRoot
覆盖为 app2Root
,再将 app1Root
保存到 app2Root
上,用代码表示是这样的
1 | firstScheduledRoot === app1Root; |
这和我们之前自己实现 findHighestPriorityRoot
时使用的用例很类似,可以说就是在初始化这个链结构。
1 | const root1 = new Root(200); |
再次从 addRootToSchedule
方法回到 requestWork
中,此时会走非批量更新,即1
2
3
4
5
6
7
8
9
10
11
12// setState 过程中置为 true
if (isBatchingUpdates) {
// ReactDOM.render 过程中置为 true
if (isUnbatchingUpdates) {
// 所以会调用这里
// root === app2Root
nextFlushedRoot = root;
nextFlushedExpirationTime = Sync;
performWorkOnRoot(root, Sync, false);
}
return;
}
performWorkOnRoot
之后的调用链是 renderRoot
,再之后的,也不会再调用 findHighestPriorityRoot
方法了。
第三次调用
app2 render
完成后,接下来会回到 app1
的更新,即调用 performSyncWork
方法,这是前面提到的「批处理结尾」时会调用的方法。
该方法就是调用 performWork(Sync, false)
,该方法内,先调用一次 findHighestPriorityRoot
,此时仍然
1 | firstScheduledRoot === app1Root; |
app2Root
已经「完成工作」,或者说「已经无效」,所以需要被移除,我们前面也讲到如何从链中移除一个节点,所以 app1Root
就变成了 lastScheduledRoot
,同时还是 firstScheduledRoot
,并且 nextFlushedRoot
也置为了 app1Root
。
然后再调用 performWork(nextFlushedRoot)
实现对 app1
的更新。
第四次调用
实际上,在 performWork(nextFlushedRoot)
下面,还会调用一次 findHighestPriorityRoot
,目的是找到下一个需要更新的 root
进行更新。
1 | while (nextFlushedRoot !== null) { |
当然,这次调用时,app1Root
已经「无效」,所以被移除,链上不再存在节点,自然 nextFlushedRoot === null
,从而中止了循环。
可能存在的疑问
1、为什么不直接调用两次 ReactDOM.render
1 | ReactDOM.render(<App1 />, document.getElementById('app1')); |
为什么这么写就不行?
因为 render
是非批量更新,即它会直接走完整个流程,如果想要出现链中有两个以上的节点,需要控制 addRootToSchedule
方法的调用顺序。
即,在第一个 app1
执行完 addRootToSchedule
方法后,需要中断当前 app1
的更新,开始下一个 app2
的更新。
所以,我们的实际用例可以改成先完成初始化,在点击按钮后,调用 app1
和 app2
的 setState
。
还有一些遗漏,后面再补。