React Fiber 浅窥

本文内容参考 https://github.com/facebook/react v16.0.0 版本源码。
如 React 实际行为与本文有出入,以 react repo 的 master 分支提交的最新改动为准。

ReactDOMFiberEntry.render() 干了些什么

由于 v16.0.0 已经使用了 ReactDOMFiberEntry 做渲染,所以在调用 ReactDOM.render 时,实际是在调用 ReactDOMFiberEntry.render()。调用流如下(点击跳转到 github 上相关源码):

一步一步看:

  • React 应用第一次 Render 时,由于当前的页面不存在 rootContainer,因此,React 会创建一个空的 fiber 实例作为 rootContainer,同时标记此次更新为 unbatchedUpdates (DOMRenderer.unbatchedUpdates()), 然后开始 updateContainer()
  • 为了确保首次 Render 尽快完成,此处会在当前的 fiberUpdateQuene 调度队列中插入一条高优先级 (HighPriority) 的更新动作,之后开始执行 fiber 调度。

在 setState() 发生的事情则简单一些:

在调用 setState() 后,fiberUpdater 会做以下几件事情:

  1. 尝试在 ReactInstanceMap 中查找当前的组件实例
  2. 并获取该实例的优先级。
  3. 往调度队列中插入一条更新动作。
  4. 执行 fiber 调度。

Fiber 更新队列 (fiberUpdateQueue)

Fiber 及 fiberUpdateQueue 的结构

Fiber 是一种最轻量化的线程(lightweight threads)。它是一种用户线程(user thread),让应用程序可以独立决定自己的线程要如何运作。
Fiber 是 React Fiber 的基本工作单元,简单来看,一个 Fiber 的数据结构关键字段如下:

	type Fiber = {
		tag: TypeOfWork, // fiber 类型,FunctionalComponent,ClassComponent 之类
		stateNode: any, // fiber 的局部状态
		return: Fiber, // process 结束后返回的结果,指向当前正在 processing 的 parent
		
		child, // fiber 的子节点
		sibling, // fiber 的兄弟节点
		index,	// 下标
	}

fiberUpdateQueue 由一个或多个 fiber 连接而成:


fiberUpdateQueue
-----------------------------------------------------------------
| index: 1      | index: 2      | index: 3      | index: 4      |
| tag: [root]   | tag: [root]   | tag: [class]  | tag: [host]   |
| type:null     | type: null    | type: <App>   | type: <div>   |
| child: 2      | child: 3      | child: null   | child: null   |
| sibling: null | sibling: null | sibling: 4    | sibling: null |
-----------------------------------------------------------------
    ^                                                    ^
    |                                                    |
  first                                                 last

scheduleUpdate() && performWork()

scheduleUpdate() 会从当前触发 scheduleUpdate 的节点开始,由 return 字段找父节点,直到找到根节点。如果正在执行工作,则不做任何事。否则,判断不同的优先级,从找到的根节点开始执行不同优先级的 performWork()

performWork() 主要执行 workLoop()。 workLoop 将循环执行以下逻辑:

  • 通过 createWorkInProgress() 获取当前正在执行的 fiber, 设置为 nextUnitOfWork
  • performUnitOfWork()
  • 执行 nextUnitOfWorkbeginWork 阶段。
    • beginWork 阶段将判断当前 fiber 的 tag,执行不同的生命周期函数。比如 ClassComponent 的 constructor, componentWillMount, componentWillUpdate 等就在这里执行。同时展开直接子节点,创建子节点的 fiber,回传给 nextUnitOfWork
    1. 如果没有子节点,则代表所有 fiber 都已执行,则 commitAllWork() 提交所有改动:
      • prepareForCommit();
      • commitAllHostEffects();
      • commitWork();
        • commitUpdate();
          • updateFiberProps();
          • updateProperties(); // 更新 DOM 属性
      • commitAllLifeCycles();
        • commitLifeCycles(); // componentDidMount, componentDidUpdate 在这执行。
    2. 否则,判断 nextPriorityLevel
      • 如果是 SynchronousPriorityTaskPriority, 则表示有剩余的同步任务需要执行,则继续循环。
      • 如果是其他优先级的任务,则跳出。

fiberUpdateQueue 调度过程分解

接下来结合官方提供的 react/fixtures/fiber-debugger 工具来一步一步观察 fiber 具体是如何展开、调度的。

React 官方提供了 react-noop-renderer 用于调试 React,在 react-noop-renderer 的 ReactFiberInstrumentation.debugTool 添加对应的回调,可以在 fiber 的 be
ginWork、completeWork、commitWork 阶段时得到通知并记录下来。

准备工作

准备好要测试的 jsx。

	class World extends React.Component {
		render() {
			return <div>world</div>;
		}
	}
	class App extends React.Component {
		render() {
			return <div> hello <World /> </div>;
		}
	}
	
	log('Render <App />');
	ReactNoop.render(<App />);

	ReactNoop.flush();

在 fixures/fiber-debugger 文件夹下运行

yarn install
yarn start

将自动打开 localhost:3000,如下:

image

点击 “EDIT” 链接,在弹出的 textarea 中粘贴上面的那段 jsx,点击 “Run”,此时拖动 Slider,就可以一步一步调试 fiber 了。

组件展开为 fiber 的步骤分解

  1. 执行 root 的 beginWork, 并为子节点 <App /> 创建 fiber, 添加入队列:
    image

  2. 执行 的 beginWork,并为子节点 <div /> 创建 fiber,添加入队列:
    image

  3. 执行 <div /> 的 beginWork, 并为子节点 "hello" 和 <World /> 分别创建 fiber,添加入队列:
    image

  4. 执行文字节点 "hello" 的 beginWork, 处理完毕,执行 "hello" 的 commitWork。(图中标记为紫色)
    image

  5. 执行节点 <World /> 的 beginWork,为子节点 "world" 创建 fiber,添加入队列。 !
    image

  6. 处理文字节点 "world" 的beginWork,处理完毕,执行 "world" 的 commitWork。
    image

  7. 从节点 "world" 回溯执行 commitWork,直到跟节点。至此,<App /> 组件加载过程的 fiber 执行完毕。

直观感受 fiber

接下来将用一个例子,直观感受一下 fiber.

      const length = 30000;
      class Foo extends React.Component {
        constructor() {
          super();
          this.state = { text: 'foo' };
        }
        componentDidMount() {
          setInterval(() => {
            this.setState(state => ({ 
              text: state.text === 'foo' ? 'react' : 'foo'
            }))
          }, 500);
        }
        render() {
          return this.state.text;
        }
      }
	  
	 class App extends React.Component {
        constructor() {
          super();
          this.state = { offset: 0 };
        }
        add = () => {
           this.setState(state => ({ offset: state.offset + 1}))
        }
        render() {
          const result = [];
          for (let i = 0; i < length; i++) {
            result.push(<li key={i}>{i + this.state.offset}</li>)
          }
          return <ul>
            <Foo />
            <button onClick={this.add}> click me </button>
            {result}
          </ul>
        }
      }
	  
	 ReactDOM.render(
       <App />,
       document.getElementById('container')
     );

这里创建了一个 <App /> 组件,里边有一个 <Foo /> 组件, 一个按钮和一个长列表:

  • <Foo /> 组件每隔 500ms 就交换显示 “react” 和 "foo"
  • 每当点击 "click me" 按钮,视图将把 state 的 offset + 1, 同时重新渲染长列表。

运行这段代码,我们可以发现,由于列表非常之长,所以每次 diff 和重渲染都会耗费大量的时间。在点击 "click me " 按钮之后,整个页面陷入卡顿,等待 React 计算完全部的 diff 之后,<Foo /> 组件和列表才会更新。

在这个过程中,fiberUpdateQueue 大概长这样:

| [tag]type|

------------------------------------------------------------------------------------
| [root] | [class]<App> | ...30000个[class]<App>... | [class]<Foo> | -> null
-------------------------------------------------------------------------------------
    ^                     ^                                              |
    |                     |                                              v
  mount                 click                                          commit

接下来使用 fiber 的特性,把这个 <App /> 组件划分优先级,让 React 优先处理 <Foo /> 组件的更新

	class App extends React.Component {
        constructor() {
          super();
          this.state = { offset: 0 };
        }
        add = () => {
          ReactDOM.unstable_deferredUpdates(() => {
            this.setState(state => ({ offset: state.offset + 1}))
          });
        }
        render() {
          const result = [];
          for (let i = 0; i < length; i++) {
            result.push(<li key={i}>{i + this.state.offset}</li>)
          }
          return <ul>
            <Foo />
            <button onClick={this.add}> click me </button>
            {result}
          </ul>
        }
      }
  • 注意在 add 里,我们使用了 unstable_deferredUpdates() ,把当前上下文的优先级设置为低 :https://github.com/facebook/react/blob/v16.0.0/src/renderers/shared/fiber/ReactFiberScheduler.js#L1553
  • 随后,使用 setState() , 往 updateQueue 里添加了一条低优先级的更新 fiber(在上文 ReactDOM.render() 干了些什么 )里简述过。
  • <Foo /> 组件里定时 setState() 触发的更新动作则是正常优先级。
  • 所以,当点击 "click me" 之后,我们可以发现,<Foo /> 组件每隔 500ms 的更新并没有停止。React 在执行长列表的每个 Item 执行完毕后,会判断 nextPriorityLevel,此时优先执行 setInterval 插入的正常优先级的更新动作。保证优先响应高优先级的任务。

此时的 fiberUpdateQueue 长这样

| [tag]type|

------------------------------------------------------------------------------------
| [root] | [class]<App> | 数个[class]<App> | [class]<Foo> |  剩下的 [class]<App> =>
-------------------------------------------------------------------------------------
    ^                      ^                      ^                      ^
    |                     |                       |                      |
  mount                  click           500ms  后插入的高优先级        继续剩下的
                                                  |         
                                                  v
					      commit

------------------------------------------------------------------------------------
  => |..余下的 [class]<App> | -> null
-------------------------------------------------------------------------------------
                                 |
                                 v
		          commitAll	 

其他

  • React 目前只提供了 unstable_deferredUpdates() 来修改上下文的 fiber 优先级。
  • 之前声称的动画优先级以 '优势不大' 的理由被去掉了。
  • 更新的版本提供了更细粒度的基于 expirationTime 的 fiber 调度方法。等稳定了再看。

REFERENCES