# React 源码解析(二)FiberRoot 构建

Fiber 是对 React 核心算法的重构,也是 React 最重要的概念,这一章从 ReactDOM.render 开始,逐步分析 FiberRoot 的创建过程。

  • 创建 FiberRoot
  • BatchUpdate 和 setState

# 创建 FiberRoot

我们顺着流程继续分析,jsx 转换后的代码,会在 ReactDOM.render 中进行执行。会依次调用以下几个方法,最终会构建一颗 fiber 树。

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

ReactDOM.render 执行时,首先在 legacyRenderSubtreeIntoContainer 方法中执行初始化,然后在 legacyCreateRootFromDOMContainer 方法里给 root 元素打上标识 _reactRootContainer,并调用 ReactSyncRoot 创建 fiberRoot,具体的创建过程在 react-reconciler 中。

首次渲染时采用 ReactSyncRoot 进行同步渲染,不会进入异步调度过程,因为组件需要尽快的完成渲染。最终渲染完成后生成一颗完整的 fiber 树。

cmd-react-children

# 创建 FiberRoot 具体过程

在 ReactDOM 中可以看到,具体的创建 fiber,更新 fiber,批量更新等过程都是在 react-reconciler 这个库里进行管理的。

import {
  updateContainerAtExpirationTime,
  createContainer,
  updateContainer,
  batchedUpdates,
  unbatchedUpdates
} from "react-reconciler/inline.dom";
1
2
3
4
5
6
7

react-reconciler 负责具体 FiberRoot 的构建,它承载了整个 React 更新调度全部的数据结构。

# 创建 fiber createContainer

在创建 fiberRoot 的同时,还会渲染 children。

// 初始化渲染
let fiberRoot;
// 初次渲染
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
  container,
  forceHydrate
);
fiberRoot = root._internalRoot;
// 不使用批量更新
unbatchedUpdates(() => {
  updateContainer(children, fiberRoot, parentComponent, callback);
});
1
2
3
4
5
6
7
8
9
10
11
12

代码中可以看到,第一次渲染时,会调用 unbatchedUpdates 下的 updateContainer 批量更新子节点。

# 更新 fiber updateContainer

updateContainer 方法用来更新 fiber,顺着第一次渲染的情况,调用情况如下:

  • updateContainerAtExpirationTime,进入该方法更新 container。
  • scheduleRootUpdate 调度根组件渲染。
    • 创建 update。
    • 调用 enqueueUpdate(current, update) 更新 update 队列。
  • scheduleWork(current, expiration Time) 进入异步调度流程。
    • renderRoot(root, Sync, true) 渲染 root 组件。
    • workLoopSync 执行循环渲染。
    • performUnitOfWork(workInProgress)
    • beginWork(current, unitOfWork, renderExpirationTime) 创建子节点 fiber。

创建完 Fiber Root 在 unbatchedUpdates 中执行 updateContainer 对容器内容进行更新,更新前会先通过 expirationTime 计算节点过期时间,具体是通过在 ReactFiberWorkLoop 中 computeExpirationForFiber 进行计算。

do {
  try {
    if (isSync) {
      workLoopSync();
    } else {
      workLoop();
    }
    break;
  } catch (thrownValue) {
    // Reset module-level state that was set during the render phase.
    resetContextDependencies();
    resetHooks();

    const sourceFiber = workInProgress;
    // 如果跟节点没有设置 componentDidCatch 生命周期,即没有捕获作物,就会抛出错误。
    if (sourceFiber === null || sourceFiber.return === null) {
      // Expected to be working on a non-root fiber. This is a fatal error
      // because there's no ancestor that can handle it; the root is
      // supposed to capture all errors that weren't caught by an error
      // boundary.
      prepareFreshStack(root, expirationTime);
      executionContext = prevExecutionContext;
      throw thrownValue;
    }

    const returnFiber = sourceFiber.return;
    throwException(
      root,
      returnFiber,
      sourceFiber,
      thrownValue,
      renderExpirationTime
    );
    workInProgress = completeUnitOfWork(sourceFiber);
  }
} while (true);

function workLoop() {
  // Perform work until Scheduler asks us to yield
  while (workInProgress !== null && !shouldYield()) {
    workInProgress = performUnitOfWork(workInProgress);
  }
}

function performUnitOfWork(unitOfWork: Fiber): Fiber | null {
  const current = unitOfWork.alternate;
  let next;
  next = beginWork(current, unitOfWork, renderExpirationTime);

  if (next === null) {
    // 如果没有任务,调用执行完成的方法。
    next = completeUnitOfWork(unitOfWork);
  }

  return next;
}
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

reactFiberWorkLoop 其实是一个循环监控的过程,会在执行完一定时间的 requestAnimationFrame 后(具体下一章会讲到),进行其他任务的调度。如果有多个任务 expirationTime 都在 25ms 之内,则会进行批量更新,对应一次 batchedUpdates。

这个超时时间实现的非常精妙,我们拿 computeAsyncExpiration 举例子,在 computeExpirationBucket 中接收的就是 currentTime、5000 和 250 ,最终的公式:((((currentTime - 2 + 5000 / 10) / 25) | 0) + 1) * 25

ceiling(
  MAGIC_NUMBER_OFFSET - currentTime + expirationInMs / UNIT_SIZE,
  bucketSizeMs / UNIT_SIZE
);

function ceiling(num: number, precision: number): number {
  return (((num / precision) | 0) + 1) * precision;
}
1
2
3
4
5
6
7
8

翻译一下就是:最终结果是以 25 为单位向上增加的,比如说我们输入 10002 - 10026 之间,最终得到的结果都是 10525,但是到了 10027 的结果就是 10550,这就是除以 25 取整的效果。

其实可以这样理解:

(100 / 25) | 0 = 4 | 0 = 4;

(101 / 25) | 0 = 4.04 | 0 = 4;
1
2
3

React 这么设计抹相当于抹平了 25ms 内计算过期时间的误差,那它为什么要这么做呢?

细想了一下,这么做也许是为了让非常相近的两次更新得到相同的 expirationTime,然后在一次更新中完成,相当于一个自动的 batchedUpdates。

# 渲染更新流程分析

在了解了创建 fiber 以及更新 fiber 的流程之后,这里总结了一张渲染更新的过程图,方便理解 renderRoot 时的更新流程。

scheduler-render-root

# BatchUpdate 和 setState

在 react 中,setState 一直是比较重要的概念,接下来我们深入了解一下吧。

# BatchUpdate

首先,我们已经多次探讨过 batchedUpdates 了,那么它到底是如何工作的呢?

我们运行以下代码进行测试。

import React from "react";
import { unstable_batchedUpdates as batchedUpdates } from "react-dom";

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }
  test() {
    this.setState({
      count: this.state.count + 1
    });
    this.setState({
      count: this.state.count + 1
    });
  }
  componentDidMount() {
    let me = this;
    me.setState({
      count: me.state.count + 1
    });
    console.log("第一次 componentDidMount:", me.state.count);
    me.setState({
      count: me.state.count + 1
    });
    console.log("第二次 componentDidMount:", me.state.count);
    setTimeout(function() {
      me.setState({
        count: me.state.count + 1
      });
      console.log("第一次 setTimeout:", me.state.count);
      me.setState({
        count: me.state.count + 1
      });
      console.log("第二次 setTimeout:", me.state.count);
    }, 0);
    setTimeout(function() {
      batchedUpdates(() => {
        me.setState({
          count: me.state.count + 1
        });
        console.log("第一次 batchedUpdates:", me.state.count);
        me.setState({
          count: me.state.count + 1
        });
        console.log("第二次 batchedUpdates:", me.state.count);
      });
    }, 0);
    console.log("第三次 componentDidMount:", me.state.count);
  }
  render() {
    return (
      <div>
        <h1>{this.state.count}</h1>
        <button onClick={this.test.bind(this)}>增加count</button>
      </div>
    );
  }
}

export default App;

// 第一次 componentDidMount: 0
// 第二次 componentDidMount: 0
// 第三次 componentDidMount: 0
// 第一次 setTimeout: 2
// 第二次 setTimeout: 3
// 第一次 batchedUpdates: 3
// 第二次 batchedUpdates: 3
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

如果你对最后的打印感到疑惑?我们再顺着 batchedUpdates 的流程图,仔细分析一下。

scheduler-render-root

首先我们可以在 react.development.js 文件中给 requestWork 方法打一个 debugger,因为整个 react 的更新调度都会进入到这个函数。接着,批量更新分为两种情况,一种是渲染中的时候并不会进入到 BatchingUpdates。另一种是调用 performWork 通过寻找 findHighestPriorityRoot 最高优先级任务,并执行任务的调度。

图中 Transaction 事务类似一个面向切面编程的概念,将主流程放入 perform 方法中执行,其他的流程必须首先在 wrapper 中的 initialize 方法中进行注册,等到主流程 perform 方法执行完毕之后,才执行对应的 close 方法。

# setState 到底是怎么运行的

在组件已经渲染完成中,setState 是同步执行的,但是不会立马更新,因为他在批量处理中会等待组件 render 才真正触发。不在批处理中的任务可能会立马更新。到底更新不更新要取决于 setState 是否在 Async 的渲染过程中,因为他会进入到异步调度过程。如果 setState 处于我们某个生命周期中,暂时不会 BatchUpdate 参与,因为组件要尽早的提前渲染。

案例

setState 在 componentDidMount 生命周期中,isBatchUpdate 是 true,所以不会进入更新阶段,会被加入到 diryComponent 中,在 Transaction 事务完成之后,isBatchUpdate 会被重置为 false,在下一个循环中就会更新 state。

如果是在 setTimeout 中执行,不会进入到事务流程,直接进行批处理更新,但如果是异步更新,也要遵守 fiber 的调度过程。

# 总结

这一章分析了 ReactDOM 创建 fiberRoot 的过程,以及 BatchUpdate 和 setState 的流程。

这一章结束后还有个疑问,React 具体的异步调度过程是什么呢?我们下一章继续探究~

# 相关链接