React中useEffect的源码解读
前⾔
对源码的解读有利于搞清楚Hooks到底做了什么,如果您觉得useEffect很“魔法”,这篇⽂章也许对您有些帮助。
本篇博客篇幅有限,只看useEffect,⼒求简单明了,带您到React Hooks的深处看看
按图索骥到Hook相关源码(可以直接跳)
⾸先我们从Github上得到react的源码,然后可以在packages中到react⽂件夹,其中的index.js就是我们的⼊⼝。代码很简单,就两⾏:
const React = require('./src/React');
所以接下来我们去看看 'react/src/React',代码有点多,我们简化⼀下:
import ReactVersion from 'shared/ReactVersion';
// ...
import {
useEffect,
} from './ReactHooks';
const React = {
useEffect
};
//...
export default React;
很好,现在我们⾄少知道为什么Hooks的引⽤⽅式是:
import {useEffect} from 'react'
接下来我们继续看看 'react/src/ReactHooks'。
ReactHooks⽂件(可以直接跳)
之前说好了只看useEffect,所以同样需要简化⼀下代码。
并且考虑到有⼈对TypeScript语法不熟悉,还去掉了TypeScript语法,之后的简化代码也会如此。
现在我们看下简化后的代码:
import invariant from 'shared/invariant';
import ReactCurrentDispatcher from './ReactCurrentDispatcher';
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
// React版本不对或者Hook使⽤有误什么的就报错
// ...
return dispatcher;
}
export function useEffect(create,inputs) {
const dispatcher = resolveDispatcher();
return dispatcher.useEffect(create, inputs);
}
这⾥可以看到,我们的useEffect实际上是ReactCurrentDispatcher.current.useEffect。ReactCurrentDispatcher⽂件(可以直接跳)
看⼀下ReactCurrentDispatcher⽂件,这⾥没有简化:
import type {Dispatcher} from 'react-reconciler/src/ReactFiberHooks';
const ReactCurrentDispatcher = {
current: (null: null | Dispatcher),
};
export default ReactCurrentDispatcher;
发现他的current的类型是null或者Dispatcher,所以这⾥我们很简单就能猜到,这个东西的源码在 'react-reconciler/src/ReactFiberHooks'。ReactFiberHooks⽂件
⼏千⾏代码,头⼤。但是莫慌,咱们⼜不是来写react的,看看原理⽽已。
我们之前已经知道useEffect实际上是ReactCurrentDispatcher.current.useEffect。
很明显ReactCurrentDispatcher.current不管是什么东西单独列出来,我们只需要知道谁赋值给他就⾏了。
精简代码,去掉⽤__DEV__区分的开发代码之后,我们发现整个⽂件给ReactCurrentDispatcher.current赋值的没⼏个。
⽽唯⼀⼀个与异常判断⽆关的是renderWithHooks函数中的这⼀块代码:
export function renderWithHooks(
current,
workInProgress,
Component,
props,
secondArg,
nextRenderExpirationTime
){
ReactCurrentDispatcher.current =
current === null || izedState === null
HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
let children = Component(props, secondArg);
return children;
}
我们不知道这段代码是⼲嘛的,但是他肯定是渲染组件时⽤的。
⽽这⾥很显然ReactCurrentDispatcher.current的值就只能是HooksDispatcherOnMount和HooksDispatcherOnUpdate。
很明显这两个⼀个⽤于加载时,⼀个⽤于更新时。
然后我们们搜⼀下相关代码:
const HooksDispatcherOnMount = {
useEffect: mountEffect
};
const HooksDispatcherOnUpdate = {
useEffect: updateEffect
};
也就是说,组件加载时,useEffect会调⽤mountEffect,组件更新时会调⽤updateEffect。
让我们继续看看这两个函数:
function mountEffect(create, deps) {
return mountEffectImpl(
UpdateEffect | PassiveEffect,
UnmountPassive | MountPassive,
create,
deps,
);
}
function updateEffect(create, deps) {
return updateEffectImpl(
UpdateEffect | PassiveEffect,
UnmountPassive | MountPassive,
create,
deps,
);
}
这⾥的UpdateEffect和PassiveEffect是⼆进制常数,⽤位运算的⽅式操作。
先不⽤知道具体意义,知道是个常量即可。
接下来我们看看具体的mountEffectImpl:
reacthooks理解
function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps){
const hook = mountWorkInProgressHook();
// useEffect不传依赖,那么就为null
const nextDeps = deps === undefined ? null : deps;
currentlyRenderingFiber.effectTag |= fiberEffectTag;
// 链表尾部hook对象的memoizedState为pushEffect的返回值
}
我们看到第⼀⾏代码调⽤mountWorkInProgressHook新建了⼀个hook对象,让我们看看mountWorkInProgressHook:
function mountWorkInProgressHook() {
const hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) {
// This is the first hook in the list
} else {
/
/ Append to the end of the list
workInProgressHook = = hook;
}
return workInProgressHook;
}
很明显这⾥有个链表结构workInProgressHook,如果workInProgressHook链表为null就将新建的hook对象赋值给它,如果不为null,那么就加在链表尾部。
这⾥有必要讲解⼀下:
Hooks作为⼀个链表存储在fiber的memoizedState中。
currentHook 是当前fiber的链表。
workInProgressHook 是即将被加⼊到 work-in-progress fiber的链表。
然后我们再看看pushEffect:
function pushEffect(tag, create, destroy, deps) {
// 新建⼀个effect,很明显⼜是个链表结构
const effect = {
tag,
create,
destroy,
deps,
// Circular
next: null,
};
// 从currentlyRenderingFiber.updateQueue获取组件更新队列
let componentUpdateQueue= currentlyRenderingFiber.updateQueue;
// 判断组件更新队列是否为空,每次在调⽤renderWithHooks都会将这个componentUpdateQueue置为null
// 这样的话每次update这个组件时,就会创建⼀个新的effect链表
if (componentUpdateQueue === null) {
// 为空就创建⼀个组件更新队列
componentUpdateQueue = createFunctionComponentUpdateQueue();
// 并赋值给currentlyRenderingFiber.updateQueue
currentlyRenderingFiber.updateQueue = componentUpdateQueue;
// 组件更新队列最新的effect为我们新建的effect
componentUpdateQueue.lastEffect = = effect;
} else {
// 如果组件更新队列已经存在,获取它最新的Effect
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
// 如果最新的Effect为null,那么组件更新队列最新的Effect为我们新建的effect
componentUpdateQueue.lastEffect = = effect;
} else {
// 否则将我们的effect加⼊到链表结构中最末尾,然后他的next为链表结构的第⼀个effect
// 这⾥的effect链表是个闭环
const firstEffect = ;
< = effect;
< = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
return effect;
}
我们再看看更新时调⽤的updateEffectImpl:
function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {
//  这⾥ updateWorkInProgressHook
//  workInProgressHook = ;
//  currentHook = ;
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
let destroy = undefined;
if (currentHook !== null) {
const prevEffect = izedState;
destroy = prevEffect.destroy;
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
// 对⽐两个依赖数组的各个值之间是否有变动,如果没变动,那么就设置标志位为NoHookEffect
if (areHookInputsEqual(nextDeps, prevDeps)) {
pushEffect(NoHookEffect, create, destroy, nextDeps);
return;
}
}
}
currentlyRenderingFiber.effectTag |= fiberEffectTag;
}
我们可以看到updateEffectImpl和mountEffectImpl很像,最重要的是我们得两个函数串起来,看看他们到底实现了⼀个什么。
Hook相关数据结构简图
这⾥我⾃⼰画了⼀张图,利于理解:
这张图的结构是⼀个组件某⼀时刻的结构。
图中黄⾊为Fiber节点,绿⾊为Hook节点,蓝⾊为Effect节点。
Fiber节点,其实就是我们的虚DOM节点,react会⽣成⼀个Fiber节点树,每个组件在Fiber树上都有对应的Fiber节点。
其中currentlyRenderingFiber表⽰我们正在进⾏渲染的节点,它来⾃于workInProgress,current表⽰已经渲染的节点。
组件加载时,会执⾏各个useEffect,然后就会建⽴⼀个Hook链表,⽽workInProgress的memoizedState字段就指向了Hook链表的尾部Hook 节点。
⽽构建每个Hook节点时,会同时构造⼀个Effect节点,同样,Hook节点的memoizedState字段就指向了对应的Effect节点。
⽽每个Effect节点⼜会连接起来形成⼀个链表,然后workInProgress的updateQueue字段指向了Effect链表的尾部Effect节点。
组件更新时,会依次对⽐currentHook指向的Effect的依赖数组与新的依赖数组的不同,如果⼀样,就设置Effect节点的effectTag为NoHookEffect。
但是⽆论依赖数组中的值是否改变,都会新构造⼀个Effect节点,作为Hook节点的memoizedState字段的值。
然后在准备渲染时,会去直接到Fiber节点的updateQueue的lastEffect,也就是直接指向Effect链表的尾部Effect节点。
因为effect链表是闭环的,这⾥通过lastEffect的next到第⼀个Effect。
然后循环遍历effect链表,当effectTag为NoHookEffect则不做操作,否则会去先执⾏effect的destroy操作,然后再执⾏create操作。
对,你没看错,总结起来就是每次更新后,只要依赖项改变,那么就会执⾏useEffect的卸载函数,再执⾏第⼀个参数create函数。
这⼀部分代码⽐较远:
function commitHookEffectList(
unmountTag,
mountTag,
finishedWork,
)
{
const updateQueue = finishedWork.updateQueue;
let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = ;
let effect = firstEffect;
do {
if ((effect.tag & unmountTag) !== NoHookEffect) {
// Unmount
const destroy = effect.destroy;
effect.destroy = undefined;
if (destroy !== undefined) {
destroy();
}
}
if ((effect.tag & mountTag) !== NoHookEffect) {
// Mount
const create = ate;
effect.destroy = create();
}
effect = ;
} while (effect !== firstEffect);
}
}
这⾥的位运算⼤家可能有点看不懂,因为NoHookEffect的值是0,所以只要effect.tag被设置为NoHookEffect,那么
effect.tag & unmountTag
就必然为NoHookEffect。
我们还记得,我们之前的玩法,依赖数组各个值不变时,就设置Effect节点的effectTag为NoHookEffect。
此时是绝对不会执⾏先destroy Effect节点,再执⾏Effect函数create的操作。
⽽如果effect.tag的值不为NoHookEffect,那也得需要effect.tag与unmountTag⾄少有⼀个位相同才能执⾏destroy。
让我们看看之前⽆论是mountEffectImpl还是updateEffectImpl都默认传的是:UnmountPassive | MountPassive,也就是说effect.tag为UnmountPassive | MountPassive。
⽽很明显这个设计的⽬的在于,当mountTag为MountPassive时执⾏create函数,⽽unmountTag为UnmountPassive时创建执⾏destroy函数。
⽽只有下⾯这个地⽅会做这个Passive操作:
export function commitPassiveHookEffects(finishedWork: Fiber): void {
if ((finishedWork.effectTag & Passive) !== NoEffect) {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Chunk: {
commitHookEffectList(UnmountPassive, NoHookEffect, finishedWork);
commitHookEffectList(NoHookEffect, MountPassive, finishedWork);
break;
}
default:
break;
}
}
}
这⾥的意思很明显,先遍历⼀遍effect链表,每个依赖项变了的hook都destroy⼀下,然后再遍历⼀遍effect链表,每个依赖项变了的,都执⾏create函数⼀下。
也就是说每次都会按照useEffect的调⽤顺序,先执⾏所有useEffect的卸载函数,再执⾏所有useEffect的create函数。
⽽commitPassiveHookEffects⼜是只有flushPassiveEffects这个函数最终能调⽤到。
⽽每次 React 在检测到数据变化时,flushPassiveEffects就会执⾏。
不论是props还是state的变化都会如此。
所以如果您真的有需要去模拟⼀个像之前的componentDidMount和componentWillUnmount的⽣命周期,那么最好⽤上⼀个单独的Effect:
useEffect(()=>{
// 加载时的逻辑
return ()=>{
// 卸载时的逻辑
}
},[])
这⾥⽤[]作为依赖数组,是因为这样依赖就不会变动,也就是只在加载时执⾏⼀次加载逻辑,卸载时执⾏⼀次卸载逻辑。
不加依赖数组时,那么每次渲染都会执⾏⼀次加载和卸载。
总结
希望我的这篇⽂章让您有⼀些收获。