举个例子来讲解下React.useEffect()
:
import React, {useEffect} from 'react';
import React from 'react';
export default function App() {
useEffect(()=>{
console.log('classComponent:componentDidMount')
return ()=>{
console.log('classComponent:componentWillUnmount')
}
},[])
return (
<div>
a
</div>
);
}
当执行App()
时,会调用useEffect(xxx)
,因为是useEffect()
的第一次调用,所以此时会执行源码里的mountEffect()
(1) 在 dev 下调试
(2) 执行mountEffectImpl()
//首次调用 React.useEffect 走这里
function mountEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
if (__DEV__) {
//删除了 dev 代码
}
return mountEffectImpl(
//逻辑或,即 是 UpdateEffect+PassiveEffect
UpdateEffect | PassiveEffect,
UnmountPassive | MountPassive,
//create 也就是 useEffect的第一个参数 callback
create,
//useEffect 的第二个可选参数 []
deps,
);
}
可以看到,如果不用 dev 调试的话,直接调mountEffectImpl()
就可以了
(1) 将当前hook
加入workInProgressHook
链表中
(2) 初始化effect
链并赋值给hook.memoizedState
function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
//将当前 hook 加入 workInProgressHook 链表中,并返回最新的 hook 链表
//关于mountWorkInProgressHook()的讲解,请看:
//[ReactHooks源码解析之useState及为什么useState要按顺序执行](https://juejin.im/post/5eb7c96ff265da7b90055137)中的「一、mountState()解析(1)」
const hook = mountWorkInProgressHook();
//初始化 deps 参数
const nextDeps = deps === undefined ? null : deps;
//将 sideEffectTag 置为 fiberEffectTag(因为sideEffectTag=0)
sideEffectTag |= fiberEffectTag;
//初始化 effect 链并返回
//useEffect hook 的 memoizedState 并不是一个具体的值,而是一个 effect 对象
hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps);
}
(1) 注意下传入的参数:
① fiberEffectTag:UpdateEffect | PassiveEffect
② hookEffectTag:UnmountPassive | MountPassive
③ create:useEffect 的第一个参数 callback,在「前言」的例子中,也就是:
()=>{
console.log('classComponent:componentDidMount')
return ()=>{
console.log('classComponent:componentWillUnmount')
}
}
④ deps:useEffect 的第二个参数 依赖数组,在例子中是:[ ]
(2) 调用mountWorkInProgressHook()
,将当前hook
加入workInProgressHook
链表中,并返回最新的hook
链表
const hook = mountWorkInProgressHook();
关于mountWorkInProgressHook()
的讲解,请看:
ReactHooks源码解析之useState及为什么useState要按顺序执行中的 「 一、mountState()解析(1)
」
(3) 初始化deps
参数
const nextDeps = deps === undefined ? null : deps;
(4) 将 sideEffectTag 置为 fiberEffectTag
sideEffectTag |= fiberEffectTag;
(5) 初始化 effect 对象并返回
hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps);
useState的hook.memoizedState
是设置的值,而 useEffect 的hook.memoizedState
是一个对象,也就是 effect 对象
接下来我们看下pushEffect
里做了什么
(1) 初始化effect
对象并返回
(2) 将effect
对象添加至更新队列componentUpdateQueue
末尾
function pushEffect(tag, create, destroy, deps) {
const effect: Effect = {
tag,
create,
destroy,
deps,
// Circular
next: (null: any),
};
//如果 FunctionComponent 的更新队列不存在的话,则初始化它
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
componentUpdateQueue.lastEffect = effect.next = effect;
}
//否则将此 effect 添加至更新队列末尾
else {
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
return effect;
}
(1) 因为 ReactHooks 是给 FunctionComponent 提供副作用的函数,也就是说一定是有一个地方来存放 FunctionComponent 的副作用的,那么在源码里就是componentUpdateQueue
链表来存放副作用的
(2) 如果FunctionComponent
的更新队列不存在,则调用createFunctionComponentUpdateQueue()
来创建一个更新队列,并将该useEffect
的effect
对象放至更新队列队尾
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
componentUpdateQueue.lastEffect = effect.next = effect;
}
补充:
createFunctionComponentUpdateQueue()
的源码:
//创建 FunctionComponent 的更新队列
function createFunctionComponentUpdateQueue(): FunctionComponentUpdateQueue {
return {
lastEffect: null,
};
}
(3) 如果FunctionComponent
的更新队列存在的话,则将此effect
添加至更新队列末尾
else {
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
综上,可以看到当第一次调用useEffect
时,React 做了 3 件事:
① 将当前hook
加入workInProgressHook
链表中
② 初始化effect
对象
③ 将effect
对象加入componentUpdateQueue
更新队列(FunctionComponent存放副作用的链表)队尾
④ 将effect
对象赋值给hook.memoizedState
以上是在render
阶段完成的,接下来会在commit
执行该effect
如果你看过中的React源码解析之Commit第一子阶段「before mutation」中的「三、commitHookEffectList()」的话,那么就会明白上文的effect.create
:
effect.create=()=>{
console.log('classComponent:componentDidMount')
return ()=>{
console.log('classComponent:componentWillUnmount')
}
}
会在commitHookEffectList()
中执行:
function commitHookEffectList(
unmountTag: number,
mountTag: number,
finishedWork: Fiber,
) {
//...
if ((effect.tag & mountTag) !== NoHookEffect) {
// Mount
const create = effect.create;
effect.destroy = create();
}
}
此时的effect.tag
=UnmountPassive | MountPassive
:
export const MountPassive = /* */ 0b01000000; //64
export const UnmountPassive = /* */ 0b10000000; //128
也就是effect.tag
=192
只有当传进commitHookEffectList()
的mountTag
为MountPassive
或者是UnmountPassive
,才会执行effect.create()
那么React
是在什么时候调用commitHookEffectList()
时传入MountPassive|UnmountPassive
呢?
调用顺序如下(均在 commit 阶段):
commitRootImpl()
——>flushPassiveEffects()
——>commitPassiveHookEffects(effect)
——>commitHookEffectList(UnmountPassive, NoHookEffect, finishedWork)
&commitHookEffectList(NoHookEffect, MountPassive, finishedWork)
补充:
关于commitRootImpl()
的讲解,请看:
React源码解析之commitRoot整体流程概览
接下来我们看下flushPassiveEffects()
主要做了什么
清除effect
上的副作用
export function flushPassiveEffects() {
//effect 链表上第一个有副作用的 fiber
////比如在 app() 中调用了 useEffect()
let effect = root.current.firstEffect;
while (effect !== null) {
if (__DEV__) {
//删除了 dev 代码
} else {
try {
//执行 fiber 上的副作用
commitPassiveHookEffects(effect);
} catch (error) {
invariant(effect !== null, 'Should be working on an effect.');
captureCommitPhaseError(effect, error);
}
}
effect = effect.nextEffect;
}
}
循环执行 effect 链上的副作用(side-effect
)
执行 fiber 上的副作用
//执行 fiber 上的副作用
export function commitPassiveHookEffects(finishedWork: Fiber): void {
commitHookEffectList(UnmountPassive, NoHookEffect, finishedWork);
commitHookEffectList(NoHookEffect, MountPassive, finishedWork);
}
主要是调用commitHookEffectList()
函数,但要注意下传的参数:
① 第一次调用先传的是UnmountPassive
,那么就会执行effect.destory()
方法,对应到开发层面,就是当多次更新调用useEffect
时,会先执行上个useEffect
的return
回调函数:
useEffect(()=>{
console.log('classComponent:componentDidMount')
//执行这个 return 的 callback
return ()=>{
console.log('classComponent:componentWillUnmount')
}
},[])
② 第二次调用传的是MountPassive
,那么就会执行effect.create()
方法,对应到开发层面, 就是执行useEffect
的第一个参数callback
:
useEffect(
//执行这个 callback
()=>{
console.log('classComponent:componentDidMount')
return ()=>{
console.log('classComponent:componentWillUnmount')
}
},[])
这也就解释了调用useEffect为什么会先执行上个useEffect的return回调函数? 这个问题
循环FunctionComponent
上的effect
链,根据hooks
上每个effect
上的 effectTag
,执行destroy/create
操作(类似于componentDidMount
/componentWillUnmount
)
function commitHookEffectList(
unmountTag: number,
mountTag: number,
finishedWork: Fiber,
) {
//FunctionComponent 的更新队列
//补充:FunctionComponent的 side-effect 是放在 updateQueue.lastEffect 上的
//ReactFiberHooks.js中的pushEffect()里有说明:componentUpdateQueue.lastEffect = effect.next = effect;
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
//如果有副作用 side-effect的话,循环effect 链,根据 effectTag,执行每个 effect
if (lastEffect !== null) {
//第一个副作用
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
//如果包含 unmountTag 这个 effectTag的话,执行destroy(),并将effect.destroy置为 undefined
//NoHookEffect即NoEffect
if ((effect.tag & unmountTag) !== NoHookEffect) {
// Unmount
const destroy = effect.destroy;
effect.destroy = undefined;
if (destroy !== undefined) {
destroy();
}
}
//如果包含 mountTag 这个 effectTag 的话,执行 create()
if ((effect.tag & mountTag) !== NoHookEffect) {
// Mount
const create = effect.create;
effect.destroy = create();
if (__DEV__) {
//删除了 dev 代码
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
① 主要参考之前写的文章——React源码解析之Commit第一子阶段「before mutation」 「 三、commitHookEffectList()
」
② 注意下lastEffect
是怎么取到的:
root.current.firstEffect.updateQueue.lastEffect
当前 fiber 节点上的 effect 链上第一个有 side-effect 的 effect 的更新队列上的最新的 lastEffect
③ 对应到开发层面上,当 App() 第一次调用useEffect
时,React 创建 App() 的 effect 链,并将lastEffect.destory
赋为undefined
,那么就不会执行destory()
了
但是会执行lastEffect.create()
,打印出'classComponent:componentDidMount'
那么,App()
第一次调用useEffect
的源码解析流程就结束了,接下来看下多次调用useEffect
的流程
多次调用 useEffect 时,调用的函数
//多次更新时,走这里
function updateEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
if (__DEV__) {
//删除了 dev 代码
}
return updateEffectImpl(
UpdateEffect | PassiveEffect,
UnmountPassive | MountPassive,
create,
deps,
);
}
注意下updateEffectImpl()
传的参数,跟二、mountEffectImpl()
中传的参数一样
比较 deps 判断是否需要重新执行 useEffect 的 callback
function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
// 当前正在 update 的 fiber 上的 hook
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
let destroy = undefined;
//currentHook:当前 fiber 对象上的 hook 对象
//当currentHook不为空时
if (currentHook !== null) {
//获取旧 effect 状态
const prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;
//如果 deps 参数存在的话
if (nextDeps !== null) {
//获取旧 deps 参数
const prevDeps = prevEffect.deps;
//比较前后 deps 是否相同
if (areHookInputsEqual(nextDeps, prevDeps)) {
//如果相同的话,则表示没有 update,那么就传入 NoHookEffect tag
pushEffect(NoHookEffect, create, destroy, nextDeps);
//return 代表下面的代码都不执行了
return;
}
}
}
//能执行到这里,说明currentHook=null 或者 deps 有 update
//那么就添加 UpdateEffectTag
sideEffectTag |= fiberEffectTag;
//初始化 effect 链并返回
hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps);
}
(1) 执行updateWorkInProgressHook()
,获取当前正在 update 的 fiber 上的 hook
const hook = updateWorkInProgressHook();
(2) 获取 deps,方便与prevDeps
比较,来决定是否更新
const nextDeps = deps === undefined ? null : deps;
(3) 然后就是调用核心函数areHookInputsEqual()
,比较前后 deps 是否相同
if (currentHook !== null) {
//获取旧 effect 状态
const prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;
//如果 deps 参数存在的话
if (nextDeps !== null) {
//获取旧 deps 参数
const prevDeps = prevEffect.deps;
//比较前后 deps 是否相同
if (areHookInputsEqual(nextDeps, prevDeps)) {
//如果相同的话,则表示没有 update,那么就传入 NoHookEffect tag
pushEffect(NoHookEffect, create, destroy, nextDeps);
//return 代表下面的代码都不执行了
return;
}
}
}
如果areHookInputsEqual()
返回的结果为 true 的话,说明该 effect 没有产生副作用,则为该 effect 添加NoHookEffect
的 effectTag 表示不更新执行useEffect
的 callback,并返回
(4) areHookInputsEqual()
源码
function areHookInputsEqual(
nextDeps: Array<mixed>,
prevDeps: Array<mixed> | null,
) {
//删除了 dev 代码
if (prevDeps === null) {
//删除了 dev 代码
return false;
}
//删除了 dev 代码
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
因为 deps 是一个 Array,所以会循环去比较 array 中的每个 item
注意: 这里是用 Object.is() 去进行浅比较的,也就是说深比较是一定会更新的
(5) Object.is()
源码
function is(x,y){
return (
(x===y&&(x!==0||1/x===1/y)) || (x!==x&&y!==y)
)
}
(6) 如果areHookInputsEqual()
返回为false
的话,就会执行下面的语句
//能执行到这里,说明currentHook=null 或者 deps 有 update
//那么就添加 UpdateEffectTag
sideEffectTag |= fiberEffectTag;
//初始化 effect 链并返回
hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps);
(7) 那么就会再次调用commitPassiveHookEffects()
——>commitHookEffectList()
注意:
多次调用同一个 useEffect 的时候,会先去执行上一次的 destory()
,再执行本次的create()
useEffect流程图.jpg
mountEffect()/mountEffectImpl()/pushEffect()/updateEffect()/updateEffectImpl()
:
https://github.com/AttackXiaoJinJin/reactExplain/blob/master/react16.8.6/packages/react-reconciler/src/ReactFiberHooks.js
flushPassiveEffects()
:
https://github.com/AttackXiaoJinJin/reactExplain/blob/master/react16.8.6/packages/react-reconciler/src/ReactFiberWorkLoop.js
commitPassiveHookEffects()/commitHookEffectList()
:
https://github.com/AttackXiaoJinJin/reactExplain/blob/master/react16.8.6/packages/react-reconciler/src/ReactFiberCommitWork.js