本文内容参考 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 会做以下几件事情:
- 尝试在 ReactInstanceMap 中查找当前的组件实例
- 并获取该实例的优先级。
- 往调度队列中插入一条更新动作。
- 执行 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()
- 执行
nextUnitOfWork
的 beginWork
阶段。
beginWork
阶段将判断当前 fiber 的 tag,执行不同的生命周期函数。比如 ClassComponent 的 constructor, componentWillMount, componentWillUpdate 等就在这里执行。同时展开直接子节点,创建子节点的 fiber,回传给 nextUnitOfWork
- 如果没有子节点,则代表所有 fiber 都已执行,则
commitAllWork()
提交所有改动:
prepareForCommit();
commitAllHostEffects();
commitWork();
commitUpdate();
updateFiberProps();
updateProperties();
// 更新 DOM 属性
commitAllLifeCycles();
commitLifeCycles();
// componentDidMount, componentDidUpdate 在这执行。
- 否则,判断
nextPriorityLevel
:
- 如果是
SynchronousPriority
或 TaskPriority
, 则表示有剩余的同步任务需要执行,则继续循环。
- 如果是其他优先级的任务,则跳出。
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 文件夹下运行
将自动打开 localhost:3000,如下:
点击 “EDIT” 链接,在弹出的 textarea 中粘贴上面的那段 jsx,点击 “Run”,此时拖动 Slider,就可以一步一步调试 fiber 了。
组件展开为 fiber 的步骤分解
-
执行 root 的 beginWork, 并为子节点 <App />
创建 fiber, 添加入队列:
-
执行 的 beginWork,并为子节点 <div />
创建 fiber,添加入队列:
-
执行 <div />
的 beginWork, 并为子节点 "hello" 和 <World />
分别创建 fiber,添加入队列:
-
执行文字节点 "hello" 的 beginWork, 处理完毕,执行 "hello" 的 commitWork。(图中标记为紫色)
-
执行节点 <World />
的 beginWork,为子节点 "world" 创建 fiber,添加入队列。 !
-
处理文字节点 "world" 的beginWork,处理完毕,执行 "world" 的 commitWork。
-
从节点 "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>
}
}
此时的 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