因为有裁人消息流出+被打C的双重冲击,只好尽可能在被裁以前恶补一波本身的未知领域,随时作好准备html
本文是本身在阅读完react fiber的主流程及部分功能模块的一篇我的总结,有些措辞、理解上不免有误,若各位读到这篇文章,请不要吝啬你的评论,请提出任何质疑、批评,能够逐行提出任何问题vue
阅读以前须要了解react和fiber的基本知识,如fiber的链表结构,fiber的遍历,react的基本用法,react渲染两个阶段(reconcile和commit),fiber的反作用(effectTag)node
我的推荐的fiber阅读方法:react
react为实现并发模式(异步渲染)设计了一个带优先级的任务系统,若是咱们不知道这个任务系统的运做方式,永远也不会真正了解react,接下来的讲解默认开启react并发模式git
首先并发模式的功能:github
先从2提及,由于咱们要对react有一个全局的理解web
首先,要知道调度算法包含一切,每个调度任务,都须要完成reconcile和commit的流程,所以ReactFiberScheduler.js即为react最核心的模块,完成任务调度全靠他;任务调度须要有一个调度器,细节请移步下文中的Scheduler.js;这个调度器按优先级顺序保存着多个任务,firstCallbackNode为当前任务,从最高优先级任务开始,如图所示: 算法
你们想象一下,调度器要实现任务的优先级调度,当高优先级任务来临时,当前运行的任务(firstCallbackNode)须要打断,让位给高优先级任务,这个过程必须在macrotask中完成,为何?首先requestIdleCallback
为macrotask,并且这样的打断才是咱们须要的,由于若是在主线程来调度,用户的交互会被js运行卡住,你想打断都打断不了api
咱们通常会在哪调用setState?promise
componentWillMount
中调用setState;该生命周期的执行会运行在reconcile阶段,不加入任务调度器componentDidMount
中调用setState;该生命周期的执行会运行在commit阶段,react中有一个isRendering
标志,true表示reconcile+commit正在进行,任务加入调度器须要isRendering
为false,不加入任务调度器又因为初始化渲染不开启并发模式,所以调度器中只会有三种来源的任务:
unstable_scheduleCallback
以调用setState而建立的任务在1和3中,setState就真的成了异步更新了;对于1和3,react也会作一个合并的处理,将全部setState合并,如:
setTimeout(() => {
this.setState({
nums: this.state.nums + 1
})
this.setState({
nums: this.state.nums + 1
})
}, 0)
复制代码
若state.nums初始值为0,在非并发模式下,最终会更新到2,由于setState是同步的;而在并发模式下,nums最终仍然为1,由于第二个setState任务没法加入调度器;来源1和来源3都是调度任务,在react调度器中,调度任务不能同时出现两个或以上;为何有这个规则,咱们下文再谈。源码以下:
function scheduleCallbackWithExpirationTime(
root: FiberRoot,
expirationTime: ExpirationTime,
) {
if (callbackExpirationTime !== NoWork) {
// A callback is already scheduled. Check its expiration time (timeout).
// 低级别的任务直接 return
if (expirationTime < callbackExpirationTime) {
// Existing callback has sufficient timeout. Exit.
return;
} else {
if (callbackID !== null) {
// Existing callback has insufficient timeout. Cancel and schedule a
// new one.
// 不然会取消当前,再用新的替代
cancelDeferredCallback(callbackID);
}
}
// The request callback timer is already running. Don't start a new one. } else { startRequestCallbackTimer(); } ... callbackID = scheduleDeferredCallback(performAsyncWork, {timeout}); } 复制代码
调度器已经简单介绍完毕,实现细节请移步下文的Scheduler.js;咱们能够把调度器想象成一个黑盒,当咱们每调用一次并发模式的setState时,就向调度器加入一个任务,它会自动帮咱们按优先级运行
每个并发模式调用的setState都会产生一个调度任务,而每个调度任务都会完成一次渲染过程,所以我预测在并发模式正式推出后,会有大量文章针对并发模式下的setState的优化文章,至少咱们如今能够知道并发模式下的setState可不能滥用;搞清楚了调度器和任务框架咱们再来深刻一下调度器中的每一个任务
有了上文的总体框架,我以为这个时候你们能够本身去看看源码了,我只要告诉你performAsyncWork
表明向调度器加入一个异步调度任务,而performSyncWork
表明了主线程开始运行一个同步任务,源码阅读就不困难了
先简单思考一下,一个调度任务须要完成什么工做:
requestIdleCallback
就好了,它只能帮咱们尽可能分配好时间来运行JS,详见下文;若是你的callback运行时间太长,它是没办法的,由于只要JS运行就会卡交互先验知识:
ReactDOM.render(<App/>, document.getElementById('root'))
所建立,正常来讲一个项目应该只有一个fiber.expirationTime
属性表明该fiber的优先级,数值越大,优先级越高,经过当前时间减超时时间得到,同步任务优先级默认为最高fiber.childExpirationTime
属性表明该fiber的子节点优先级,该属性能够用来判断fiber的子节点还有没有任务或比较优先级,更新时若没有任务(fiber.childExpirationTime===0
)或者本次渲染的优先级大于子节点优先级,那么没必要再往下遍历 当某组件(fiber)触发了任务时,会往上遍历,将fiber.return.expirationTime
和fiber.return.childExpirationTime
所有更新为该组件的expirationTime,一直到root,能够理解为在root上收集更新先看看调度任务的总流程:
从setState开始,区分同步或异步,同步则直接运行performWork
,异步则将performWork
加入到macrotask运行(调度器);再根据isYieldy
(是否能打断,同步则不能打断,为false) 来调用不一样的performRoot
循环体;图中绿线表明异步任务,红框表示该过程可被打断;任务未执行完毕的话(被打断),这里会重复向调度器加入任务
注意:这里的打断表明macrotask中该任务已运行完毕,会把js运行交还给主线程,也是用户交互能获得喘息的惟一机会
再看看performRoot循环体:
循环判断是否还有任务以及!(didYield && currentRendererTime > nextFlushedExpirationTime)
,didYield
表示是否已经被调度器叫停;currentRendererTime
能够理解为任务运行的当前时间,经过recomputeCurrentRendererTime()
获得,上文说过,随着时间流逝,该值愈来愈小;nextFlushedExpirationTime
表示将要渲染的任务的时间(root.expirationTime);当两个表达式都为true时,循环才退出,didYield
为true说明任务被调度器叫停,须要被打断,currentRendererTime > nextFlushedExpirationTime
为true代表任务未超时
这里我认为判断有些重复,由于调度器已经为咱们判断了是否超时,超时则不会打断,我认为react在这里是一个双保险机制,具体缘由未知
进入循环,执行performWorkOnRoot()
,这个稍后再讲;接下来是findHighestPriorityRoot()
,其实就是找最高优先级的root,并获得root的expirationTime,root的expirationTime即为将要执行的任务的时间即这里的nextFlushedExpirationTime
;最后是算当前时间
再看看performWorkOnRoot()
:
我已合并同步异步的状况,绿线表示异步多出来的部分;代码很简单,就是判断finishedWork
是否为空,为空则renderRoot()
,不为空则completeRoot()
,这里renderRoot
即为reconcile过程,completeRoot
即为commit过程,接下来看reconcile过程
renderRoot
其实只作两件事:
workLoop
循环体nextUnitOfWork
是否为空,若不为空则任务未完成,下次再继续,为空则代表reconcile完成,赋值root.finishedWork
,这时候才能commitworkLoop循环体:
看的出来workLoop
即为循环求nextUnitOfWork
的过程,直到nextUnitOfWork
为空或者被打断;nextUnitOfWork
是一个全局变量,就是遍历所在的fiber,那么workLoop
就是不断地遍历,求出下一个fiber;先执行beginWork
,beginWork
作了什么?
processUpdateQueue
(参考后文)获得state,调用render
渲染函数以获得JSX表示的虚拟节点,标记componentDidMount等反作用若是beginWork
的结果为空,说明这个节点已经没有儿子了,接下来就该轮到completeUnitOfWork
出场了,completeUnitOfWork
须要作到:
reconcile完成之后,接下来再回到performWorkOnRoot
中的commitRoot
,主要工做以下:
react调度任务细节总结完毕,我并无说太多reconcile和commit的细节,由于我认为这部分写多了就不叫总结了,远不如本身读来的清楚
将整个react项目比做一个大型矿场,用户是老板,调度器是包工头,调度任务是矿工,不一样的矿场表明着不一样的root;一个项目只能有一个矿坑(nextUnitOfWork
),虚线底部表明矿已挖完,reconcile结束
由于一个矿坑只产出一种矿,不一样矿工来挖同一个矿坑,有的矿工挖的是金矿,有的矿工挖的是煤矿,不容许;这里可能也有react15中setState合并的考虑
commit阶段要执行componentDidMount这种react彻底失控的反作用,以及其它生命周期,固然不能打断,否则打断再运行,岂不是会重复调用屡次? 在reconcile阶段,react一样避免调用任何失控的代码,如componentWillReceiveProps,componentWillReceiveProps,用户在这些生命周期里面调用setState,reconcile被打断后从新开始岂不是要调用屡次setState?
若是从目标fiber开始更新,如这里的fiber2,那么咱们的矿坑就能够从fiber2开始挖,节省了时间;可是你没有想过,root的优先级是会更新的,若是这时候fiber3拥有了更高优先级,那么会从fiber3开始遍历,因为遍历只能向下或向右,咱们会忽视fiber2的更新;因此不如把全部更新提到root,这样惟一的坏处就是被打断以后要从root开始遍历,可是至少不会漏掉更新
unstable_scheduleCallback
时的timeout值很是有讲究,当用户以滚键盘的极快速度输入1-9时,timeout值设得太低,中间不少数字将不会被渲染! 举例来讲,当输入1时,以unstable_scheduleCallback
来调用setState,调度器中存在的任务是setState任务,而后setState任务又建立了一个调度任务,这个调度任务不断地打断重连,咱们的交互获得喘息,输入了2,3,4...调度器中又放入了多个setState任务,由于按得太快,这些任务在调度器中被链接到了一块儿;在第一个任务打断重连完毕后,接下来的几个setState任务所有执行并转成了调度任务,因为这几个调度任务expirationTime相等,执行的倒是不一样的setState任务,所以调度任务被合并,只会剩下最后一个执行的调度任务; 不过,当timeout值设置得够大时,问题将获得解决,由于这时候加入调度器的setState任务的expirationTime会很是大,它们的执行会很是靠后,在它们建立的每个调度任务执行完以后,所以输入框的数字将渲染得很完整,不过依然没法摆脱4的问题react在这部分的内容不少很杂,可是我认为对主流程而言不必讲的太细,何况我也没看太仔细,这里更多细节只须要参考这篇文章React事件系统和源码浅析 - 掘金
简单来讲,react实现了一套事件系统;在更新props阶段,就为全部拥有事件回调的fiber绑定好事件(react事件系统),事件绑定在document上;触发事件时,进入事件系统,事件系统建立一个SyntheticEvent
用来代替原生的e对象;接着,以冒泡/捕获的顺序收集全部fiber和其中的事件回调;再按冒泡/捕获顺序触发绑定在fiber上的回调函数
这里须要注意几点:
interactiveUpdates()
里的判断让人费解,找不到其场景if (
!isBatchingUpdates &&
!isRendering &&
lowestPriorityPendingInteractiveExpirationTime !== NoWork
) {
// Synchronously flush pending interactive updates.
performWork(lowestPriorityPendingInteractiveExpirationTime, false);
lowestPriorityPendingInteractiveExpirationTime = NoWork;
}
复制代码
关键词:requestAnimationFrame、frameDeadline、activeFrameTime、timeout、unstable_scheduleCallback、unstable_cancelCallback、unstable_shouldYield
核心模块。react fiber的任务调度全靠它,我认为搞懂这个模块才能搞懂react schedule的过程,unstable_scheduleCallback
、unstable_cancelCallback
、unstable_shouldYield
,三个api可以分别实现将任务加入任务列表,将任务从任务列表中删除,以及判断任务是否应该被打断
主要实现方法是运用requestAnimationFrame + MessageChannel + 双向链表的插入排序,最后暴露出unstable_scheduleCallback
和unstable_shouldYield
两个api。
对第一位的理解须要看一下这篇文章,简单来讲是屏幕显示是显示器在不断地刷新图像,如60Hz的屏幕,每16ms刷新一次,而1帧表明的是一个静止的画面,若一个dom元素从左到右移动,而咱们须要这个dom每一帧向右移动1px,60Hz的屏幕,咱们须要在16ms之内,完成向右移动的js运行和dom绘制,这样在第二帧(17ms时)开始的时候,dom已经右移了1px,而且被屏幕给刷了出来,咱们的眼睛才会感受到动画的连续性,也就是常说的不掉帧。requestAnimationFrame
则给了咱们十分精确且可靠的服务。
requestIdleCallback
的功能是在每一帧的空闲时间(完成dom绘制、动画等以后)来运行js,若这一帧的空闲时间不足,则分配到下一帧执行,再不足,分配到下下帧完成,直到超过规定的timeout时间,则直接运行js。requestAnimationFrame
能尽可能保证回调函数在一帧内运行一次且dom绘制一次,这样也保证了动画等效果的流畅度,然而却没有超时运行机制,react polyfill的主要是超时功能。
requestAnimationFrame
一般的用法:
function callback(currentTime) {
// 动画操做
...
window.requestAnimationFrame(callback)
}
window.requestAnimationFrame(callback)
复制代码
其表明的是每一帧都尽可能运行一次callback,并完成动画绘制,若运行不完,也没办法,就掉帧。
Scheduler.js使用animationTick
做为requestAnimationFrame
的callback,用以计算frameDeadline
和调用传入的回调函数,在react中即为调度函数;frameDeadline
表示的是运行到当前帧的帧过时时间,计算方法是当前时间 + activeFrameTime
,activeFrameTime
表示的是一帧的时间,默认为33ms,可是会根据设备动态调整,好比在刷新频率更高的设备上,连续运行两帧的当前时间比运行到该帧的过时时间frameDeadline
都小,说明咱们一帧中的js任务耗时也小,一帧时间充足且requestAnimationFrame
调用比预设的33ms频繁,那么activeFrameTime
会下降以达到最佳性能
有了frameDeadline
与用户自定义的过时时间timeoutTime
,那么咱们很容易获得polyfill requestIdleCallback的原理:用户定义的callback在这一帧有空就去运行,超过帧过时时间frameDeadline
就到下一帧去运行,你能够超过帧过时时间,可是你不能超过用户定义的timeoutTime
,一旦超过,我啥也无论,直接运行callback。
Scheduler.js将每一次unstable_scheduleCallback
的调用根据用户定义的timeout来为任务分配优先级,timeout越小,优先级越高。具体实现为:用双向链表结构来表示任务列表,且按优先级从高到低的顺序进行排列,当某个任务插入时,从头结点开始循环遍历,若遇到某个任务结点node的expirationTime > 插入任务的expirationTime,说明插入任务比node优先级高,则退出循环,并在node前插入,expirationTime = 当前时间 + timeout;这样就实现了按优先级排序的任务插入功能,animationTick
会循环调用这些任务链表。
function unstable_shouldYield() {
return (
!currentDidTimeout &&
((firstCallbackNode !== null &&
firstCallbackNode.expirationTime < currentExpirationTime) ||
shouldYieldToHost())
);
}
shouldYieldToHost = function() {
return frameDeadline <= getCurrentTime();
};
复制代码
unstable_shouldYield
被用来判断在任务列表中是否有更高级的任务,在react中用来判断是否能打断当前任务,是schedule中的一个核心api。
首先判断currentDidTimeout
,currentDidTimeout
为false说明任务没有过时,你们要知道过时任务拥有最高优先级,那么即便有更高级的任务依然没法打断,直接return false; 再判断firstCallbackNode.expirationTime < currentExpirationTime
,这里其实是照顾一种特殊的状况,那就是一个最高优先级的任务插入以后,低优先级的任务还在运行中,这种状况是仍然须要打断的;这里firstCallbackNode
实际上是那个插入的高优先级任务,而currentExpirationTime
实际上是上一个任务的expirationTime,只是还没结算
最后是一个shouldYieldToHost()
,很简单,就是看任务在帧内是否过时,注意到这边任务帧内过时的话是return true,表明直接就能被打断;
关键词:enqueueUpdate、processUpdateQueue
react15中,全部经交互事件触发的setState更新都会被收集到dirtyComponents,收集好了再批量更新;react16因为加入了优先级策略,在调度时连setState操做都被赋予不一样的优先级,在同一组件针对带优先级的调度任务及setState操做,是该模块的核心功能
首先贴两个数据结构(已删去部分不关注的属性):
export type Update<State> = {
expirationTime: ExpirationTime,
payload: any,
callback: (() => mixed) | null,
next: Update<State> | null,
nextEffect: Update<State> | null,
};
export type UpdateQueue<State> = {
baseState: State,
// 头节点
firstUpdate: Update<State> | null,
// 尾节点
lastUpdate: Update<State> | null,
// callback 处理
firstEffect: Update<State> | null,
lastEffect: Update<State> | null,
};
复制代码
fiber上有个updateQueue
属性,就是来自上述数据结构。每次调用setState的时候,会新建一个updateQueue
,queue中存储了baseState
,用于记录state,该属性服务于优先级调度,后面会说;另外记录头节点、尾节点及用于callback的effect头尾指针;还有以链表形式链接的update,如图所示:
每当调用一次setState,会调用enqueueUpdate
,就会在链表以后插入一个update,这个插入是无序的,然而不一样的update是带优先级的,用一个属性expirationTime来表示,payload即为调用setState的第一个参数。
当调度任务依次执行时,会调用processUpdateQueue
计算最终的state,咱们不要忘了调度任务是带有优先级的任务,执行的时候有前后顺序,对应的是processUpdateQueue
的前后执行顺序;而update也是优先级任务的一部分,当咱们按链表顺序从头至尾执行时,须要优先执行高优先级的update,跳太低优先级的update;react的注释为咱们阐明了这一过程:
假设有一updateQueue为A1 - B2 - C1 - D2;
A一、B2等表明一个update,其中字母表明state,数字大小表明优先级,1为高优先级;
调度任务按高低优先级依次执行,第一次调度是高优先级任务,从头结点firstUpdate开始处理,processUpdateQueue会跳太低优先级的update;
则执行的update为A1 - C1,本次调度获得的最终state为AC,baseState为A,queue的firstUpdate指针指向B2,以供下次调度使用;
第二次调度是低优先级任务,此时firstUpdate指向B2,则从B2开始,执行的update为 B2 - C1 - D2,最终state将与baseState:A合并,获得ABCD
以上即为processUpdateQueue
的处理过程,咱们须要注意几点:
processUpdateQueue
从头结点firstUpdate
开始遍历update,并对state进行合并 对于低优先级的update,遍历时会跳过baseState
会定格在被跳过的update以前的resultStatebaseState
主要做用在于记录好被跳过的update以前的state,以便在下一次更加低优先级的调度任务时合并statefirstUpdate
和lastUpdate
指向null,updateQueue
完成使命再看一个例子:
A1-B1-C2-D3-E2-F1 第一次调度:baseState:AB,resultState:ABF,firstUpdate:C2 第二次调度:baseState:ABC,resultState:ABCEF,firstUpdate:D3 第三次调度:baseState:ABC,resultState:ABCDEF,firstUpdate:null