作为React
开发者,你能答上如下两个问题么:
function Child() {
useEffect(() => {
console.log('child');
}, [])
return <p>hello</p>;
}
function Parent() {
useEffect(() => {
console.log('parent');
}, [])
return <Child/>;
}
function App() {
useEffect(() => {
console.log('app');
}, [])
return <Parent/>;
}
渲染<App/>
时控制台的打印顺序是?
// componentDidMount生命周期钩子
class App extends React.Component {
componentDidMount() {
console.log('hello');
}
}
// 依赖为[]的useEffect
useEffect(() => {
console.log('hello');
}, [])
答案:
?向右滑动翻看答案 1. child -> parent -> app
2. 不同
其实,这两个问题分别考察的是:
useEffect
的执行顺序useEffect
如何介入React
工作流程本文接下来将深入源码,带你了解这些知识。
这,就是关于useEffect
的一切。
React
的源码可以拆分为三块:
其中,只有渲染器
会执行渲染视图操作。
对于浏览器环境来说,只有渲染器
会执行类似appendChild
、insertBefore
这样的DOM
操作。
协调器
如何决定更新的内容呢?
答案是:他会为需要更新的内容对应的fiber
(可以理解为虚拟DOM
)打上标记。
这些被打标记的fiber
会形成一条链表effectList
。
渲染器
会遍历effectList
,执行标记对应的操作。
Placement
标记对应插入DOM
Update
标记对应更新DOM
属性useEffect
也遵循同样的工作原理:
FunctionComponent
被执行,执行到useEffect
时会判断他的第二个参数deps
是否有变化。deps
变化,则useEffect
对应FunctionComponent
的fiber
会被打上Passive
(即:需要执行useEffect)的标记。渲染器
中,遍历effectList
过程中遍历到该fiber
时,发现Passive
标记,则依次执行该useEffect
的destroy
(即useEffect
回调函数的返回值函数)与create
(即useEffect
回调函数)。其中,前两步发生在协调器
中。
所以,effectList
构建的顺序就是useEffect
的执行顺序。
协调器
的工作流程是使用遍历
实现的递归
。所以可以分为递
与归
两个阶段。
我们知道,递
是从根节点向下一直到叶子节点,归
是从叶子节点一路向上到根节点。
effectList
的构建发生在归
阶段。所以,effectList
的顺序也是从叶子节点一路向上。
useEffect
对应fiber
作为effectList
中的一个节点,他的调用逻辑也遵循归
的流程。
现在,我们有充足的知识回答第一个问题:
由于归
阶段是从Child
到Parent
到App
,所以相应effectList
也是同样的顺序。
所以useEffect
回调函数执行也是同样的顺序。
我们在初学hook
时,会用ClassComponent
的生命周期钩子类比hook
的执行时机。
即使官网也是这样教学的。
但是,从上文我们已经知道,React
的执行遵循:
调度 -- 协调 -- 渲染
渲染相关工作原理是按照:
构建effectList -- 遍历effectList执行对应操作
整个过程都和生命周期钩子
没有关系。
事实上生命周期钩子
只是附着在这一流程上的钩子函数。
所以,更好的方式是从React
运行流程来理解useEffect
的执行时机。
按照流程,effectList
会在渲染器
中被处理。
对于useEffect
来说,遍历effectList
时,会找到的所有包含Passive
标记的fiber
。
依次执行对应useEffect
的destroy
。
所有destroy
执行完后,再依次执行所有create
。
整个过程是在页面渲染后异步执行的。
回答第二个问题:
如果useEffect
的deps
为[]
,由于deps
不会改变,对应fiber
只会在mount
时被标记Passive
。
这点是类似componentDidMount
的。
但是,处理Passive
effect
是在渲染完成后异步执行,而componentDidMount
是在渲染完成后同步执行,所以他们是不同的。
与componentDidMount
更类似的是useLayoutEffect
,他会在渲染完成后同步执行。
这里提供个在线Demo[1],你可以将Demo
中的useLayoutEffect
替换为useEffect
,看看他们的区别。
通过本文,我们了解了useEffect
的完整执行过程。
本系列文章接下来会继续以实例 + 源码的方式,解读业务中经常使用的React
特性。
[1]
在线Demo: https://code.h5jun.com/haxufe/edit?js,output