作用
副作用
前端开发中的副作用一般有:dom操作、浏览器事件的绑定和取消绑定、http请求、打印日志、访问系统状态、执行IO更新等。
在class类组件中,副作用一般写在componentDidMount,componentDidUpdate, componentWillUnMount, componentWillUpdate里,但是函数组件没有生命周期,这个时候就可以用useEffect来解决了,这一个hook可以替代以上四个生命周期;
useEffect的使用
useEffect是组件第一次渲染和每次更新,即componentDidMount,componentDidUpdate时都会执行的;
useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void,
create方法是每次执行useEffect时都会执行一次
create方法可以返回一个清除方法(只能是一个普通的方法),该方法会在componentWillUnMount,componentWillUpdate时执行;
一般在这个时候清除副作用;
如果不需要清除的话,不写返回清除方法即可。
deps:依赖项组成的数组,这个参数可以控制useEffect的方法不要每次执行,只有数组里的依赖的值发生改变时再执行。
如果useEffect只需要在第一次渲染时执行一次,deps传入空数组即可;
如果deps不传,则组件每次更新时都会执行useEffect里的方法;
源码分析
由上一篇的useState和useReducer可知,useEffect也是根据组件第一次渲染和更新分别调用的不同的方法,他们分别是:mountEffect和updateEffect。
- 先来看一下mountEffect,第一次渲染组件时做了什么
// ReactFiberHooks.js
function mountEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return mountEffectImpl(
UpdateEffect | PassiveEffect,
UnmountPassive | MountPassive,
create,
deps,
);
}
由源码可知,mountEffect
走了mountEffectImpl
方法,并传入了个奇奇怪怪得东西UpdateEffect | PassiveEffect
和UnmountPassive | MountPassive
-
UpdateEffect | PassiveEffect
和UnmountPassive | MountPassive
是什么?
答: 是一个二进制数字,用来标记是什么类型的副作用的 -
继续看mountEffectImpl方法
function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
//往hook链表里追加一个hook
const hook = mountWorkInProgressHook();
// 没有传deps时,会被处理成null
const nextDeps = deps === undefined ? null : deps;
sideEffectTag |= fiberEffectTag;
hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps);
}
useEffect的hook的memoizedState对象好像不太一样,把hook存到链表中以后还把pushEffect的返回值存了下来。
pushEffect
做了什么呢,很重要的亚子!
// 返回了一个effect对象
function pushEffect(tag, create, destroy, deps) {
const effect: Effect = {
tag,
create,
destroy, // mountEffectImpl传过来的是undefined
deps,
// Circular
next: (null: any),
};
// 一个全局变量,在renderWithHooks里初始化一下,存储全局最新的副作用
if (componentUpdateQueue === null) {
// { lastEffect: null }
componentUpdateQueue = createFunctionComponentUpdateQueue();
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
// 维护了一个副作用的链表,还是环形链表
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
// 最后一个副作用的next指针指向了自身
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
return effect;
}
所以mountEffect就是把useEffect加入了hook链表中,并且单独维护了一个useEffect的链表。
- 再来看看组件更新时调用updateEffect做了什么
// 跟mountEffect的执行类似,只不过走的是updateEffectImpl方法
// 直接看updateEffectImpl
function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
// 获取当前正在工作的hook
const hook = updateWorkInProgressHook();
// 最新的依赖项
const nextDeps = deps === undefined ? null : deps;
let destroy = undefined;
if (currentHook !== null) {
// 上一次的hook的effect
const prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
// 比较依赖项是否发生变化
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 如果两次依赖项相同,componentUpdateQueue增加一个tag为NoHookEffect = 0 的effect,
pushEffect(NoHookEffect, create, destroy, nextDeps);
return;
}
}
}
// 两次依赖项不同,componentUpdateQueue上增加一个effect,并且更新当前hook的memoizedState值
sideEffectTag |= fiberEffectTag;
hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps);
}
这一段有个重点: useEffect的依赖项没变化的时候,componentUpdateQueue增加一个tag为NoHookEffect
= 0 的effect
- 那么他是怎么比较deps的依赖项有没有更新的呢?
unction areHookInputsEqual(
nextDeps: Array<mixed>,
prevDeps: Array<mixed> | null,
) {
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数组,对每一项执行Object.is()方法
- 最后,我们来看一下useEffect什么时候执行,这得从react的函数组件的生命周期相关的调度开始。
在fiber的调度过程中,最终追溯到commitBeforeMutationLifeCycles
方法,在这里会根据组件类型,去执行对应的生命周期,FunctionComponent
组件执行commitHookEffectList
方法
function commitHookEffectList(
unmountTag: number,
mountTag: number,
finishedWork: Fiber,
) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
// 上面说到 NoHookEffect = 0
// 当effect.tag是0的时候,跟谁做与运算都会得到0, 即不执行任何操作
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 = effect.create;
effect.destroy = create();
// _Dev_中的一些警告处理省略掉
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
由源码可知,此时effect链表里的hook会被依次执行;
在执行的时候,会把effect.tag跟一个变量做与运算;
然后判断跟NoHookEffect(值为0)是否相等。
所以当effect.tag的值是0,不管跟谁做与运算,结果都是0 ,这个时候不会执行更新。(前面也说到,当useEffect的依赖项没更新时候,会声明一个tag是0的effect,所以这个时候,组件不会发生更新。)
到此,useEffect的源码就粗略的看过一遍了。
大概过程是函数组件在挂载阶段会执行MountEffect,维护hook的链表,同时专门维护一个effect的链表。
在组件更新阶段,会执行UpdateEffect,判断deps有没有更新,如果依赖项更新了,就执行useEffect里操作,没有就给这个effect标记一下NoHookEffect,跳过执行,去下一个useEffect