# React 源码解读(二) Fiber Root

在react 16之前,组件渲染是不能被打断的,这就导致当项目非常大的时候,只能等待渲染完才能做其他事。 而在 16 之后,React.render 新增了 fiber 机制。 可以在更新过程中中断任务来执行优先级更高的任务。

fiber 分为两个阶段

  • reconciliation 阶段 fiberNode为单位,调度的时候,每更新一个fiber,返回去询问,是否有优先级更高的任务,接下来的任务会中断,不会实际渲染到页面,update,打上 effectTag
  • commit 阶段 处理这些标记,渲染到页面,这个过程是不能被打断的

它的流程具体是怎么样的? 优先级更高的任务,如何判断优先级?

# 创建 FiberRoot

顺着流程走,代码会在 ReactDOM.render 中执行 在源码中可以看到依次执行了以下几个方法

  • legacyRenderSubtreeIntoContainer
  • legacyCreateRootFromDOMContainer
  • ReactRoot
  • 进入 react-reconciler 库
    • 具体通过 createContainer -> createFiberRoot 创建 fiberRoot。

legacyRenderSubtreeIntoContainer

function legacyRenderSubtreeIntoContainer(
  parentComponent: ?React$Component<any, any>,
  children: ReactNodeList,
  container: DOMContainer,
  forceHydrate: boolean,  // forceHydrate 的作用是是否需要复用节点,在服务端渲染中它会被设为true
  callback: ?Function,
) {

  let root: Root = (container._reactRootContainer: any);
  // 第一次root 不存在
  if (!root) {
    // Initial mount  
    // 创建reactRoot,在dom元素上挂载, 
    root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
      container,
      forceHydrate,
    );
  }
  // Initial mount should not be batched.
    // 首次不用批量更新, batchedUpdate是等view更新完之后在去统一更新状态,
    // 有一个等待的过程, 第一次不需要,可以更快
    unbatchedUpdates(() => {
      if (parentComponent != null) {
        root.legacy_renderSubtreeIntoContainer(
          parentComponent,
          children,
          callback,
        );
      } else {
        // ReactRoot.prototype.render
        root.render(children, callback);
      }
    });
  // 省略代码
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

在 legacyRenderSubtreeIntoContainer 方法中,第一次root 不存在时,创建了reactRoot 在dom元素上挂载 forceHydrate 参数被传入到了 legacyCreateRootFromDOMContainer中

legacyCreateRootFromDOMContainer

function legacyCreateRootFromDOMContainer(
  container: DOMContainer,
  forceHydrate: boolean,
): Root {
  // 是否需要复用节点
  const shouldHydrate =
    forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
  // First clear any existing content.
  if (!shouldHydrate) {
    let warned = false;
    let rootSibling;
    // 删除子节点
    while ((rootSibling = container.lastChild)) {
      container.removeChild(rootSibling);
    }
  }
  // Legacy roots are not async by default.
  const isConcurrent = false;
  // 返回一个新创建的ReactRoot
  return new ReactRoot(container, isConcurrent, shouldHydrate);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

ReactRoot 通过 createContainer 创建了 FiberRoot ReactRoot

function ReactRoot(
  container: DOMContainer,
  isConcurrent: boolean,
  hydrate: boolean,
) {
  // 创建FiberRoot
  const root = createContainer(container, isConcurrent, hydrate);
  // 把创建的节点绑在_internalRoot属性上
  this._internalRoot = root;
}
1
2
3
4
5
6
7
8
9
10

创建fiber tree 的过程

cmd-react-children

这里我们再来捋一下

ReactDOM.render 通过 legacyCreateRootFromDOMContainer 创建了 ReactRoot

ReactRoot 又创建了 FiberRoot

FiberRoot 又有如下几个比较重要的属性

  • current
  • containerInfo
  • finishedWork 在reconciliation 阶段,把一个update 推到 finishedWork,在 commit 阶段处理它
  • expirationTime

# expirationTime

ReactRoot.prototype.render => updateContainer() => ExpirationTime render 的过程中调用了 updateContainer,和前面 createContainer 相对应,进行更新操作,而 updateContainer 返回的是个 ExpirationTime

updateContainer

// updateContainer为啥返回一个ExpirationTime?
export function updateContainer(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function,
): ExpirationTime {
  
  const current = container.current;
  // 通过 msToExpirationTime 得到currentTime
  const currentTime = requestCurrentTime();
  // 根据给任务分优先级,来得到不同的过期时间
  const expirationTime = computeExpirationForFiber(currentTime, current);
  return updateContainerAtExpirationTime(
    element,
    container,
    parentComponent,
    expirationTime,
    callback,
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

在返回之前先获取了 expirationTime 那就先看看 expirationTime 是什么 computeExpirationForFiber => 计算不同的过期时间 找到 computeExpirationForFiber 方法

这里有一个设置优先级的操作,也就是 getCurrentPriorityLevel,这里最后又调到了 Scheduler.js 的东西,也是 Scheduler 中最重要的部分

通过设置了5个等级的优先级,分别对应着超时时间,优先级越高,ExpirationTime 超时时间越低

Scheduler.js

// TODO: Use symbols?
var ImmediatePriority = 1;
var UserBlockingPriority = 2;
var NormalPriority = 3;
var LowPriority = 4;
var IdlePriority = 5;

// Max 31 bit integer. The max integer size in V8 for 32-bit systems.
// Math.pow(2, 30) - 1
// 0b111111111111111111111111111111
var maxSigned31BitInt = 1073741823;

// Times out immediately, 比0还小,立即执行
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
// Eventually times out
var USER_BLOCKING_PRIORITY = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
// Never times out
var IDLE_PRIORITY = maxSigned31BitInt;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

下面是 computeExpirationForFiber 方法,用来计算不同的过期时间

computeExpirationForFiber

// 计算不同的过期时间
function computeExpirationForFiber(currentTime: ExpirationTime, fiber: Fiber) {
  // var ImmediatePriority = 1; 最高优先级, 直接走messageChannel 直接处理
  // var UserBlockingPriority = 2;
  // var NormalPriority = 3;  默认
  // var LowPriority = 4;
  // var IdlePriority = 5;
  // 优先级越高,ExpirationTime 超时时间越低
  const priorityLevel = getCurrentPriorityLevel();

  let expirationTime;
  if ((fiber.mode & ConcurrentMode) === NoContext) {
    // Outside of concurrent mode, updates are always synchronous.
    // 在并发模式之外,更新始终是同步的。
    expirationTime = Sync;
    // isWorking 在renderRoot或者CommitRoot
    // isCommitting CommitRoot
  } else if (isWorking && !isCommitting) {
    // isWorking 在 commitRoot 和 renderRoot 时被设置为true
    // isCommitting 在 commitRoot 时被设置为true
    // 所以在render阶段,优先级 expirationTime 设置为下次渲染的到期时间
    // During render phase, updates expire during as the current render.
    expirationTime = nextRenderExpirationTime;
  } else {
    // 在commit阶段,根据priorityLevel进行expirationTime更新
    switch (priorityLevel) {
      case ImmediatePriority:
        // 立即执行
        expirationTime = Sync;
        break;
      case UserBlockingPriority:
        // 因用户交互阻塞的优先级
        expirationTime = computeInteractiveExpiration(currentTime);
        break;
      case NormalPriority:
        // 一般,默认优先级, 异步执行
        // This is a normal, concurrent update
        expirationTime = computeAsyncExpiration(currentTime);
        break;
      case LowPriority:
      case IdlePriority:
        // 低优先级或空闲状态
        expirationTime = Never;
        break;
      default:
        invariant(
          false,
          'Unknown priority level. This error is likely caused by a bug in ' +
            'React. Please file an issue.',
        );
    }

    // If we're in the middle of rendering a tree, do not update at the same
    // expiration time that is already rendering.
    // 下一个fiber存在,且当前的fiber的过期时间和下一个fiber的过期时间一致
    // 把当前的fiber的过期时间减1 
    // 避免在渲染树的时候同时去更新已经渲染的树
    if (nextRoot !== null && expirationTime === nextRenderExpirationTime) {
      expirationTime -= 1;
    }
  }

  // Keep track of the lowest pending interactive expiration time. This
  // allows us to synchronously flush all interactive updates
  // when needed.
  // TODO: Move this to renderer?
  // 记录下挂起的用户交互任务中expirationTime最短的一个,在需要时同步刷新所有交互式更新
  if (
    priorityLevel === UserBlockingPriority &&
    (lowestPriorityPendingInteractiveExpirationTime === NoWork ||
      expirationTime < lowestPriorityPendingInteractiveExpirationTime)
  ) {
    lowestPriorityPendingInteractiveExpirationTime = expirationTime;
  }

  return expirationTime;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77

这里 expirationTime 的计算有点东西。使用和 0 取余来进行取整。 在一定范围内的时间都会视为相同的。

(100 / 10) | 0   // 10
(103 / 10) | 0   // 10
(110 / 10) | 0   // 11
1
2
3

ReactFiberExpirationTime.js

const UNIT_SIZE = 10;
const MAGIC_NUMBER_OFFSET = MAX_SIGNED_31_BIT_INT - 1; // 到期时间偏移量

// 1 unit of expiration time represents 10ms.
export function msToExpirationTime(ms: number): ExpirationTime {
  // Always add an offset so that we don't clash with the magic number for NoWork.
  return MAGIC_NUMBER_OFFSET - ((ms / UNIT_SIZE) | 0);
}

export function expirationTimeToMs(expirationTime: ExpirationTime): number {
  return (MAGIC_NUMBER_OFFSET - expirationTime) * UNIT_SIZE;
}

// 向上取整,间隔在precision内的两个num最终得到的相同的值 如:(60, 25)  (74, 25)
function ceiling(num: number, precision: number): number {
  return (((num / precision) | 0) + 1) * precision;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

下面再回到 updateContainer 方法(调来调去的有点乱), 最后 return 了 updateContainerAtExpirationTime,updateContainerAtExpirationTime 又返回了 scheduleRootUpdate

updateContainer() => updateContainerAtExpirationTime() => scheduleRootUpdate()

scheduleRootUpdate

function scheduleRootUpdate(
  current: Fiber,
  element: ReactNodeList,
  expirationTime: ExpirationTime,
  callback: ?Function,
) {
  // 新建一个update  createUpdate() 下面是它的返回值  可以理解为一个更新单元
  //   expirationTime: expirationTime,
  //   tag: UpdateState,
  //   payload: null,
  //   callback: null,
  //   next: null,
  //   nextEffect: null,
  const update = createUpdate(expirationTime);
  // Caution: React DevTools currently depends on this property
  // being called "element".
  update.payload = {element};

  callback = callback === undefined ? null : callback;
  if (callback !== null) {
    warningWithoutStack(
      typeof callback === 'function',
      'render(...): Expected the last optional `callback` argument to be a ' +
        'function. Instead received: %s.',
      callback,
    );
    update.callback = callback;
  }
  // 调用schedule的回调
  flushPassiveEffects();
  // 延迟创建update quenes, 并把update 更新到update quenes中
  // update 添加到 current.updateQuene.firstUpdate|lastUpdate
  enqueueUpdate(current, update);

  scheduleWork(current, expirationTime);

  return expirationTime;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

# scheduleWork

scheduleWork

function scheduleWork
(fiber: Fiber, expirationTime: ExpirationTime) {
  // 设置expirationTime & 返回root节点的Fiber对象
  const root = scheduleWorkToRoot(fiber, expirationTime);
  if (root === null) {
    return;
  }
  // isWorking 在render和commit两个阶段都会为true
  // 新的render过期时间不是noWork
  // 之前的过期时间大于现在新的过期时间
  // 表达的含义:当前没有任务在执行,之前执行过任务,同时当前的任务比之前执行的任务过期时间要小
  if (
    !isWorking &&
    // nextRenderExpirationTime 在初始的时候是noWork, 被设置后不再是noWork
    nextRenderExpirationTime !== NoWork &&
    // ExpirationTime时间越小, 优先级更高
    expirationTime > nextRenderExpirationTime
  ) {
    // This is an interruption. (Used for performance tracking.)
    // 中断了执行 , 因为有优先级更高的任务进来了 
    // isWorking=false意味着,在上一个时间片执行完之后进这个判断,
    // 有更高优先级的任务,则中断了之前任务的执行
    interruptedBy = fiber;
    // 清空之前任务的的stack
    resetStack();
  }
  // 更新最近和最早的时间
  markPendingPriorityLevel(root, expirationTime);
  // 要么没有任何任务 要么有任务但处于commitRoot阶段
  if (
    // If we're in the render phase, we don't need to schedule this root
    // for an update, because we'll do it before we exit...
    !isWorking ||
    isCommitting ||
    // ...unless this is a different root than the one we're rendering.
    nextRoot !== root
  ) {
    const rootExpirationTime = root.expirationTime;
    requestWork(root, rootExpirationTime);
  }
  if (nestedUpdateCount > NESTED_UPDATE_LIMIT) {
    // Reset this back to zero so subsequent updates don't throw.
    nestedUpdateCount = 0;
    invariant(
      false,
      'Maximum update depth exceeded. This can happen when a ' +
        'component repeatedly calls setState inside ' +
        'componentWillUpdate or componentDidUpdate. React limits ' +
        'the number of nested updates to prevent infinite loops.',
    );
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

# 先总结一下

上面贴了很多源码,一个一个文件调来调去的,看起来很乱。

在这里在简单总结一下 react render 的总体流程:

判断是否存在ReactRoot

如果不存在,则通过 legacyCreateRootFromDOMContainer 创建 ReactRoot

接下来分两步

  • 在reactRoot 里创建FiberRoot
    • expirationTime 每一个 fiber 都有的失效时间 => 通过任务优先级计算失效时间
    • finishedWork 储存fiber 需要更新的队列
    • alternate 存储 Fiber的镜像,即workInProgress 节点,dom diff 的时间直接workInProgress 对比
  • 通过 unbatchedUpdates 直接更新,不用等待
    • 判断fiber 的优先级,从而得到每一个fiber 的过期时间 ((ms / UNT_SIZE) | 0)
    • scheduleWork
      • 有优先级更高的任务,中断当前任务,执行优先级更高的任务
      • 无优先级更高的任务,执行requestWork
更新时间: 11/8/2019, 4:51:43 PM