1. 术语的一些解释
渲染 在维基百科中是这样解释的,
在电脑绘图中,是指以软件由模型生成图像的过程。
在前端领域,(页面)渲染指的是,代码中通过调用 DOM API 使得修改结果最终显示在 web 页面上。
在 React 源码中,渲染是由 performSyncWorkOnRoot
完成的,它包含了两个阶段
-
renderRootSync
(render 阶段):创建/修改 Fiber Tree -
commitRoot
(commit 阶段):将 Fiber Tree 的修改最终落实到 DOM 中
所以,拿电脑绘图领域的术语来讲,Fiber Tree 就是 模型,commitRoot
才叫 渲染。
而 renderRootSync
只是 修改 模型的过程。
因此,当我们提到 组件的渲染/组件的重新渲染 时,指的是 performSyncWorkOnRoot
而不是单独的 render 阶段。
只有单独提到 render 阶段 时,才指 renderRootSync
。
2. 回顾
上一篇 文章,我们介绍了 hook 相关的内容,
当组件里有多个 hook,一个 hook 被调用多次时,这些与组件相关的状态是如何创建或更新的。
(1)多个 hook 会被关联到 Fiber Node 上
(2)每个 hook 会维护一个 update quque 用来计算最终状态,每次调用 hook 会在队尾加一个 update 元素
(3)不管是多个 hook 还是一个 hook,只有第一次 hook 调用会立即计算结果。后续所有调用,都会在调用返回后,由 React 通过 flushSyncCallbackQueue
在 render 阶段 计算最终结果
熟悉 React 的朋友也许知道,早起 React 有两大核心概念:Vitual DOM 和 Diff 算法。
现在看来,Fiber Tree 就是 Virtual DOM 了。
那么 Diff 算法在哪里呢?什么情况下组件会重新渲染呢?Diff 是如何做的呢?
本文我们就来分析一下组件的重新渲染过程。
3. 示例项目的修改
示例项目 中,我们增加了一个文件 example-project/src/AppDiff.js
import { useState, memo } from 'react';
const App = () => {
debugger;
return <div>
<FnComp />
</div>
};
const FnComp = () => {
debugger;
const [state, setState] = useState(0);
debugger;
const onDivClick = () => {
debugger;
setState(state + 1);
debugger;
};
return <div onClick={onDivClick}>
<MemoFnComp />
</div>;
};
const MemoFnComp = memo(() => {
debugger;
return 'memo fn comp';
});
export default App;
组件结构如下:
[HostRoot] {tag: 3}
[FunctionComponent] (App) {tag: 0}
[HostComponent] (div) {tag: 5}
[FunctionComponent] (FnComp) {tag: 0}
[HostComponent] (div) {tag: 5}
[SimpleMemoComponent] (MemoFnComp) {tag: 15}
[HostText] ('memo fn comp') {tag: 6}
4. 第二棵 Fiber Tree 的创建过程
根据前面几篇文章的分析,我们知道页面载入时,React 只会创建一棵完整的 Fiber Tree(current
),
另一棵 Fiber Tree 只包含一个根节点,如下图所示,
我们来跟踪一下组件 FnComp
更新 state
时,第二棵 Fiber Tree 的变化,
8. 组件的重新渲染
图中按序号(顺序)标明了,第二棵 Fiber Tree 的创建过程,
可以看到几件事情:
(1)render 阶段 renderRootSync
对 Fiber Tree 的处理,总是从 root
(FiberRootNode)开始。
所以,即使是状态更新发生在了 FnComp
中,其祖先组件所对应的 Fiber Node 也被遍历了,
因此第二棵 Fiber Tree 中 FnComp
以上的 Fiber Node 都会被创建出来(下次更新会复用这些节点)。
(2)MemoFnComp
以下的 Fiber Node 没有被创建,仍然指向了第一棵 Fiber Tree(直到 MemoFnComp
有更新)。
(3)每一次调用 perfomrUnitWork
处理一层 Fiber Node。
从上到下依次是(与组件结构一致),
[HostRoot] {tag: 3}
[FunctionComponent] (App) {tag: 0}
[HostComponent] (div) {tag: 5}
[FunctionComponent] (FnComp) {tag: 0}
[HostComponent] (div) {tag: 5}
[SimpleMemoComponent] (MemoFnComp) {tag: 15}
[HostText] ('memo fn comp') {tag: 6}
处理到特定组件时(例如 FnComp
),会判断该组件是否需要更新。
如需更新就会通过 renderWithHooks
再次调用 FnComp
,此时 组件通过 hook 获取最新的值。
如果不需要更新,例如所有其他组件,都会调用 bailoutOnAlreadyFinishedWork
只处理 Fiber Node,不调用组件。
值得一提的是,React 似乎称 调用组件 FnComp
为 render(看函数名 renderWithHooks
可知)。
所以在这样的语境中,我们可以说,React 通过一套机制来决定 组件是否 re-render(“重新渲染”)。
看来这套机制就是所谓的 Diff 算法,与通常的理解不同的是,Diff 算法并不是一个独立的函数,而是耦合在 Fiber Node 的 re-render 过程中。
5. 组件的更新细节
组件是否需要 re-render(“重新渲染”)是 beginWork L19642 这个函数判断的(有 360 多行),
function beginWork(current, workInProgress, renderLanes) {
...
if (current !== null) {
...
if (oldProps !== newProps || hasContextChanged() || (
...
} else if (!includesSomeLane(renderLanes, updateLanes)) {
...
// 不更新
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
} else {
...
}
} else {
...
}
...
switch (workInProgress.tag) {
...
case FunctionComponent:
{
...
// 更新 函数组件(例如 FnComp)
return updateFunctionComponent(current, workInProgress, _Component, resolvedProps, renderLanes);
}
...
case HostComponent:
// 更新 html 标签组件(例如 div)
return updateHostComponent(current, workInProgress, renderLanes);
case SimpleMemoComponent:
{
// 更新 记忆函数组件(例如 MemoFnComp)
return updateSimpleMemoComponent(current, workInProgress, workInProgress.type, workInProgress.pendingProps, updateLanes, renderLanes);
}
...
}
...
}
以上示例项目中的组件更新,总共用到了 beginWork
4 个出口:
-
bailoutOnAlreadyFinishedWork
:不更新 -
updateFunctionComponent
:更新 函数组件 的业务逻辑 -
updateHostComponent
:更新 html 标签组件 的业务逻辑 -
updateSimpleMemoComponent
:更新 记忆函数组件 的业务逻辑
[0] performSyncWorkOnRoot
[1] renderRootSync
[2] workLoopSync
[3] performUnitOfWork {tag: 3}
[4] beginWork$1
[5] beginWork
[6] bailoutOnAlreadyFinishedWork <- 不更新 root
[3] performUnitOfWork {tag: 0}
[4] beginWork$1
[5] beginWork
[6] bailoutOnAlreadyFinishedWork <- 不更新 App
[3] performUnitOfWork {tag: 5}
[4] beginWork$1
[5] beginWork
[6] bailoutOnAlreadyFinishedWork <- 不更新 div
[3] performUnitOfWork {tag: 0}
[4] beginWork$1
[5] beginWork
[6] updateFunctionComponent <- 更新 FnComp
[7] renderWithHooks
[8] Component
[7] reconcileChildren
[3] performUnitOfWork {tag: 5}
[4] beginWork$1
[5] beginWork
[6] updateHostComponent <- 更新 div
[7] reconcileChildren
[3] performUnitOfWork {tag: 15}
[4] beginWork$1
[5] beginWork
[6] updateSimpleMemoComponent <- 更新 MemoFnComp
[7] bailoutOnAlreadyFinishedWork
[1] commitRoot
可以看到,只要父组件(FnComp
)有状态(state
)变化,它的子组件都会进行更新(update)操作。
[HostRoot] {tag: 3}
[FunctionComponent] (App) {tag: 0}
[HostComponent] (div) {tag: 5}
[FunctionComponent] (FnComp) {tag: 0} <- state 变更
[HostComponent] (div) {tag: 5} <- 更新
[SimpleMemoComponent] (MemoFnComp) {tag: 15} <- 更新(因为属性未变化,所以不更新子组件)
[HostText] ('memo fn comp') {tag: 6} <- 不更新
state
发生变化的组件,会经过 renderWithHooks
重新调用组件函数 FnComp
(不然无法拿到变更后的 state
),
然后这个函数(FnComp
)就返回了(返回的是 React Element)。
接着 React 通过 reconcileChildren
来处理返回的 React Element 创建/更新 child Fiber Node。
最后把这个 child Fiber Node 挂载到 Fiber Tree 中。
[5] beginWork
[6] updateFunctionComponent <- 更新 FnComp
[7] renderWithHooks
[8] Component <- 调用组件函数 FnComp
[7] reconcileChildren <- 将 返回的 React Element 转换成 Fiber Node
从这里可以看出 React 在 render 阶段 从上到下处理 Fiber Tree 时,是根据 React Element 来进行的。
只要 React Element 没变,Fiber Tree 就不会发生变化,
这一点在 React 处理 SimpleMemoComponent(MemoFnComp
)时,也能体现出来。
因为 MemoFnComp
的 React.memo
返回的带记忆功能的组件(属性未变时不会更新,普通函数组件属性未变也会更新),
MemoFnComp
函数并没有被再次调用(对比 FnComp
通过 renderWithHooks
调用了),
所以 React 在处理它(Fiber Node)的子节点时,只能沿用上次的结果(指向第一棵 Fiber Tree 中的节点)。
可参考 updateSimpleMemoComponent L17762 的业务逻辑,
6. 总结
- 处理页面渲染 React 用了两个步骤:
一个称为 render 阶段(更新 Fiber Tree【应该就是所谓的 Virtual DOM 了】)
另一个称为 commit 阶段(实际更新 DOM),而电脑绘图领域的 “渲染” 通常指的是后者。 -
组件的 重新渲染(re-render)指的是,组件函数 被重新调用(例如,
FnComp
)
React 用了一套机制来判定 组件函数 是否需要被调用,这套机制应该就是所谓的 “Diff 算法” 了,但是 React v17.0.2 中该算法并没有抽取到一个方法中,而是散布在 render 阶段 各处。
组件被重新渲染,就会返回新的 React Element,这些 React Element 所表示的组件都会被更新,不管其属性是否发生了变化。 - 函数组件 可以使用
React.memo
来实现属性未变时,不进行更新。
参考
维基百科:渲染
React 源码阅读(七):hook 状态创建/更新原理
github: thzt/react-tour/example-project
8. 组件的重新渲染
网友评论