理解React Fiber

由于最近半年的工作涉猎到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的线程操作更像下图,不会大量抢走线程,很容易切回主线程去执行更高优先级的事情。

参考:

浅谈对React Fiber的理解

Why Fiber-Fiber 解决了什么问题,是怎么解决的

The how and why on React’s usage of linked list in Fiber to walk the component’s tree

关于chenzujie

非著名码农一枚,认真工作,快乐生活
此条目发表在前端分类目录,贴了, 标签。将固定链接加入收藏夹。

发表评论

邮箱地址不会被公开。 必填项已用*标注