[FE] React 初窥门径(六):React 组件的更新过程
1. 回顾
前两篇文章,我们介绍了 React 函数组件的加载过程,总共分成了两个阶段,
在函数组件的加载和更新的过程中,React 会在内存维护两个 Fiber Tree。
-
current:实际已经写入到 DOM 中的 Fiber Tree -
workInProgress:在内存中还在 render 的 Fiber Tree
函数组件在首次加载的时候,
(1)render 阶段:React 只会创建一棵完整的 Fiber Tree(标记为 workInProgress),
另一颗 Fiber Tree 只创建一个根节点(Fiber Node tag 为 HostRoot)(标记为 current)。
(2)commit 阶段:将标记为 workInProgress 的 Fiber Tree 实际写入到 DOM 中
函数组件在更新的时候,
(1)render 阶段:React 设置当前 workInProgress 的 Fiber Tree 为之前 current 的那个,
然后开始创建(或更新)这个 Fiber Tree 的 Fiber Node。
(从上到下,一层一层的创建 Fiber Node,改变 child 和 return 和 alternate 的指向)(下文详细介绍)
(2)commit 阶段:将标记为 workInProgress Fiber Tree(连同它的更新) 实际写入到 DOM 中
本文重点介绍一下 React 函数组件的更新全流程。
2. 示例项目的一些调整
为了能展示组件的更新过程,我们的示例项目需要做一些调整,
github: thzt/react-tour
(1)example-project/src/AppState.js
import { useState } from 'react';
const App = () => {
debugger;
const [state, setState] = useState(0);
debugger;
const onDivClick = () => {
debugger;
setState(1);
debugger;
};
return <div onClick={onDivClick}>
{state}
</div>
};
export default App;
我们使用了 useState 为 App 组件增加了状态。
组件首次加载的时候,state 的值为 0,鼠标点击后触发 onDivClick 事件,更新状态为 1。
并且为了便于调试,增加了一些 debugger 语句。
(2)example-project/src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
// import App from './App';
import App from './AppState';
ReactDOM.render(
<App />,
document.getElementById('root')
);
改变了 ReactDOM.render 的组件,为带状态的这个 App。
3. Fiber Tree 的一些变化
由于 App 组件发生了变化,
const App = () => {
debugger;
return 'hello world'
};
export default App;
变成了,
const App = () => {
debugger;
const [state, setState] = useState(0);
debugger;
const onDivClick = () => {
debugger;
setState(1);
debugger;
};
return <div onClick={onDivClick}>
{state}
</div>
};
所以 Fiber Tree 结构也有了一些变化。
在组件首次加载的时候,
(1)render 阶段 结束后,内存中两棵 Fiber Tree 的关系如下,
与之前(上一篇) Fiber Tree 结构的不同点在于,
最底下那个 Fiber Node,
-
stateNode由原来的#text('hello world'),变成了div(就是绑定事件的那个 div) -
tag由原来的HostText变成了Host Component
(2)commit 阶段 在写入 DOM 之后,React 会更改 FiberRootNode 的 current 指向,
现在我们有了一棵 3 节点的 Fiber Tree 了(current Fiber Tree)。
4. 组件更新全流程
借用了 VSCode 插件 CodeTour,我们记录了以上函数组件的 首次加载 和 更新 过程。
可以查看这里 6. 函数组件更新,它包含了两个阶段,
- 首次加载:ReactDOM.render 过程
- 用户点击 div 导致的组件更新:user click + update
下文重点介绍从用户点击 div 到 React 组件更新完毕的整个过程。
CodeTour 的数据保存在了这里,
github: thzt/react-tour。
VSCode 插件 CodeTour 的安装可以参考 第四篇 文章。
值得一提的是,本篇文章介绍的代码逻辑会在 ExampleProject(示例项目)和 ReactProject(React 源码目录)之间来回跳转,
因此为了让 code tour 更加连贯,我们在 ReactProject 根目录又增加了一个软连接,指向 ExampleProject
$ ln -s $ExampleProject $ReactProject/example-project
这样就可以在 ReactProject 的 code tour 中定位到 ExampleProject 中的文件了。
相关的修改可参考这里,github: thzt/react-tour/tool/link.bash
(1)setState
用户点击 div 之后,会触发 React 在 DOM 中绑定的事件 listener,最后调用 App 中编写的 onClick 回调。
上文还原了 React 的整个 dispatchEvent 调用栈。(并未包含 listener 的挂载过程,事件绑定是在组件载入时做的,后续文章会介绍)
onDivClick 就是 App 中编写的 onClick 回调。
const App = () => {
debugger;
const [state, setState] = useState(0);
debugger;
const onDivClick = () => {
debugger;
setState(1);
debugger;
};
return <div onClick={onDivClick}>
{state}
</div>
};
我们看到 onDivClick 在执行的过程中调用了 setState,它做的事情是把一个 callback(performSyncWorkOnRoot) 放到了 syncQueue 中,然后就返回了。
(参考 react-dom.development.js#L11501)
然后 React 在后续 flushSyncCallbackQueue 中,从 syncQueue 中拿到这个 callback(performSyncWorkOnRoot)进行操作。
(2)performSyncWorkOnRoot
performSyncWorkOnRoot 这个函数名字很熟悉啊,正是 React 源码阅读(四):React 组件的加载过程(render 阶段) 介绍过的执行 render 和 commit 两个阶段事情的函数。
performSyncWorkOnRoot
renderRootSync <- render 阶段
commitRoot <- commit 阶段
可以看到 React 在组件更新的过程中,又重新执行了一遍 render 和 commit。
-
在 render 执行之前,React 会设置
workInProgress指向另一棵 Fiber Tree。
(如果是组件的第一次更新,另一棵 Fiber Tree 只包含了一个节点,是一棵空树,见上文的介绍)
-
在 render 阶段,React 会重新创建(或更新)
workInProgressFiber Tree
具体过程如下,
-
commit 阶段,React 会将
workInProgressFiber Tree 实际写入到 DOM 中,最后设置current指向这棵 Fiber Tree。
如此这般,React 函数组件的第一次更新过程就讲解完了。第二次更新的过程会有所不同(主要体现在 createWorkInProgress),因为现在内存中已经有完整的两棵 Fiber Tree 了(不会再新建 Fiber Node 了)。
这时候 React 会复用 Fiber Tree 的节点,让同一层 Fiber Tree 最多只有 current 和 alternate 两个重复节点。createWorkInProgress 函数位于 react-dom.development.js#L25780
function createWorkInProgress(current, pendingProps) {
var workInProgress = current.alternate;
if (workInProgress === null) { // <- 会尽量复用 workInProgress 节点,不会重复创建 Fiber Node
workInProgress = createFiber(current.tag, pendingProps, current.key, current.mode);
...
workInProgress.alternate = current;
current.alternate = workInProgress;
} else {
workInProgress.pendingProps = pendingProps; // Needed because Blocks store data on type.
...
}
...
workInProgress.child = current.child;
...
return workInProgress;
} // Used to reuse a Fiber for a second pass.
参考
React 初窥门径(四):React 组件的加载过程(render 阶段)
React 初窥门径(五):React 组件的加载过程(commit 阶段)
VSCode: CodeTour
github: thzt/react-tour
6. 函数组件更新