[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 会重新创建(或更新)
workInProgress
Fiber Tree
具体过程如下,
-
commit 阶段,React 会将
workInProgress
Fiber 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. 函数组件更新