由于最近半年的工作涉猎到react,因此最近一个月花了点时间了解react的核心原理,发现Fiber的设计很是精巧,因此做一篇记录。
Why
在React v15前,每次有更新发生时,react的Reconciler(协调器)会做如下一些事情
1、调用组件的render方法获取当前的Virtual Dom
2、将当前的Virtual Dom和上次更新的Virtual Dom通过Diff比较出差异
3、将结果告知Render(渲染器)做渲染
以上的过程是不可中断的,每次获取Virtual Dom都必须整个树遍历,只要当前节点有孩子节点就要一直遍历到结束(该过程也被成为Stack Reconciler)。
同时我们知道在浏览器上,javaScript线程和渲染线程是两个互斥线程,当JavaScript线程占用了大量的时间时,渲染线程就无法工作。就像下图是React团队介绍Fiber用到的一个图,woker就像我们的javaScript线程,他需要长时间离开自己的工作台,无法返回工作。
而人眼能够接受的最合适帧数是60FPS,即任何UI上的刷新变化如果能在16ms内完成,人眼就察觉不出卡顿的效果,这对JavaScipt线程每次任务的执行有很高的时效性要求。
What
从上面的介绍我们可以知道,React v15的版本有两个问题
1、Virtual Dom的构造不可中断
2、React工作的时间必须足够短,能够把主线程交还给渲染线程使用
为了解决上面说的问题,从react v16开始,设计了一种React Fiber架构解决上述问题。Fiber核心其实是对整个调度渲染的重新设计,他有如下3个特点
1、可中断
2、分时操作
3、优先级
不同于Stack Reconciler,Fiber机制下,Dom树被转换成Fiber结构,树的遍历不再是完整遍历,而是由一个一个的work unit组成,每个work unit处理一个Fiber节点,要处理下一个Fiber节点前都会判断下是否有足够时间继续做Dom树处理,没有的话就暂停自己,让主线程执行其他优先级更高操作,从而保证了主线程能随时去处理例如用户点击,动画等更高优先级的事。
How
首先我们看下React里定义的Fiber是一个什么数据结构,源代码里的数据字段比较多,我这里就精简写出跟Fiber树相关的几个字段
// A Fiber is work on a Component that needs to be done or was done. There can
// be more than one per component.
export type Fiber = {|
// The resolved function/class/ associated with this fiber.
type: any,
// 关联的真实element
stateNode: any,
// 父Fiber
return: Fiber | null,
// 第一个孩子Fiber
child: Fiber | null,
// 下一个兄弟Fiber
sibling: Fiber | null,
index: number,
//双缓冲下对应的Fiber节点
alternate: Fiber | null,
|};
每个Element元素对应到一个Fiber节点 ,通过return记录父节点,child记录第一个孩子节点,sibling记录下一个兄弟节点,这样原本的树结构就转换成Fiber树结构。
Fiber架构可以分3层
1、Scheduler(调度器)
它的核心是利用window.requestIdleCallback()这个方法(react团队对这个方法做了改造),它会在浏览器有空闲时间的时候执行对应的任务,这样子来保证浏览器能够有机会去处理更高优先级的事情,不会被react的计算任务卡住。
2、Reconciler(协调器)
协调器的任务就是找出Dom的变化准备好Diff数据,相比以前的树全量比较,v16下的Reconciler把对树的比较拆成一次又一次的performUnitOfWork,每次performUnitOfWork前还得判断下是否有足够的时间(shouldYield),由于dom树已经被转换成Fiber树 ,因此每次中断都是可跟踪的,只需要等到有足够时间就可以从上次中断的位置继续performUnitOfWork。
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
workInProgress = performUnitOfWork(workInProgress)
}
}
//shouldYield 最后调用的方法
function shouldYieldToHost() {
const timeElapsed = getCurrentTime() - startTime;
if (timeElapsed < frameInterval) {
// The main thread has only been blocked for a really short amount of time;
// smaller than a single frame. Don't yield yet.
return false;
}
// The main thread has been blocked for a non-negligible amount of time. We
// may want to yield control of the main thread, so the browser can perform
// high priority tasks. The main ones are painting and user input. If there's
// a pending paint or a pending input, then we should yield. But if there's
// neither, then we can yield less often while remaining responsive. We'll
// eventually yield regardless, since there could be a pending paint that
// wasn't accompanied by a call to `requestPaint`, or other main thread tasks
// like network events.
if (enableIsInputPending) {
if (needsPaint) {
// There's a pending paint (signaled by `requestPaint`). Yield now.
return true;
}
if (timeElapsed < continuousInputInterval) {
// We haven't blocked the thread for that long. Only yield if there's a
// pending discrete input (e.g. click). It's OK if there's pending
// continuous input (e.g. mouseover).
if (isInputPending !== null) {
return isInputPending();
}
} else if (timeElapsed < maxInterval) {
// Yield if there's either a pending discrete or continuous input.
if (isInputPending !== null) {
return isInputPending(continuousOptions);
}
} else {
// We've blocked the thread for a long time. Even if there's no pending
// input, there may be some other scheduled work that we don't know about,
// like a network event. Yield now.
return true;
}
}
// `isInputPending` isn't available. Yield now.
return true;
}
当这个Fiber树的节点都完成performUnitOfWork之后,才会继续下一步的Render操作,因此即使频繁中断,我们也不会看到更新一半的Dom树。
3、Render(渲染器)
根据前面两步得到的Diff标记(effectTag,一般有PLACEMENT,UPDATE,DELETION3种值)去做不同的更新操作,这个过程就是不可中断,同步执行的,和v15版本差别不大。
经过上面的3个过程,React的线程操作更像下图,不会大量抢走线程,很容易切回主线程去执行更高优先级的事情。
参考:
Why Fiber-Fiber 解决了什么问题,是怎么解决的
The how and why on React’s usage of linked list in Fiber to walk the component’s tree