Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React fiber 浅窥 #1

Open
RaoHai opened this issue Oct 27, 2017 · 0 comments
Open

React fiber 浅窥 #1

RaoHai opened this issue Oct 27, 2017 · 0 comments

Comments

@RaoHai
Copy link
Owner

RaoHai commented Oct 27, 2017

本文内容参考 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant