React深入useEffect
共 11545字,需浏览 24分钟
·
2021-06-01 12:34
本文适合熟悉React、以及在用useEffect遇到难题的小伙伴进行阅读。
欢迎关注前端早茶,与广东靓仔携手共同进阶~
作者:广东靓仔
一、前言
本文基于开源项目:
https://github.com/facebook/react/blob/master/packages/react/src/ReactHooks.js
// 弹框显示触发定时器
useEffect(() => {
timer = setInterval(() => {
if (showModal) {
requestFun()
}
}, 1000)
}, [showModal])
// 关闭弹框,清除定时器
const closeModal = () => {
clearInterval(timer)
}
useEffect(() => {
let intervalId = setInterval(() => {
fetchData();
}, 1000 * 60);
return () => {
clearInterval(intervalId);
intervalId = null;
}
}, [])
const fetchData = () => {
request({params}).then(ret => {
if (ret.code === OK) {
applyResult(ret.data);
}
})
}
二、useEffect介绍
useEffect
完成副作用操作,赋值给useEffect的函数会在组件渲染到屏幕之后执行。useEffect一般是在每轮渲染结束后执行,当然我们也可以让它在只有某些值改变的时候才执行。useEffect(() => {
const subscription = props.source.subscribe();
return () => {
// 清除订阅
subscription.unsubscribe();
};
});
useEffect(
() => {
const subscription = props.source.subscribe();
return () => {
subscription.unsubscribe();
};
},
[props.source],
);
三、useEffect原理
useEffect实际上是ReactCurrentDispatcher.current.useEffect(源码解析会讲到)
useEffect原理可以简单理解为:
函数组件在挂载阶段会执行MountEffect,维护hook的链表,同时专门维护一个effect的链表。 在组件更新阶段,会执行UpdateEffect,判断deps有没有更新,如果依赖项更新了,就执行useEffect里操作,没有就给这个effect标记一下NoHookEffect,跳过执行,去下一个useEffect。
我们都知道useEffect 在依赖变化时,执行回调函数。这个变化是指本次 render 和上次 render 时的依赖之间的比较。
默认情况下,effect 会在每轮组件渲染完成后执行,而且effect 触发后会把清除函数暂存起来,等下一次 effect 触发时执行,大概过程如下:
温馨提示:使用 hooks 要避免 if、for 等的嵌套使用
四、useEffrct源码解析
在react源码中,我们找到react.js中如下代码,篇幅有限,广东靓仔进行了简化,方便小伙伴阅读:
4.1 useEffect引入与导出
import {
...
useEffect,
...
} from './ReactHooks';
// ReactHooks.js
export function useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
const dispatcher = resolveDispatcher();
return dispatcher.useEffect(create, deps);
}
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
if (__DEV__) {
if (dispatcher === null) {
// React版本不对或者Hook使用有误什么的就报错...
}
}
return ((dispatcher: any): Dispatcher);
}
上面的代码就是引入与导出过程,不难看出useEffect实际上是ReactCurrentDispatcher.current.useEffect橙色的代码。
import type {Dispatcher} from 'react-reconciler/src/ReactInternalTypes';
const ReactCurrentDispatcher = {
current: (null: null | Dispatcher),
};
export default ReactCurrentDispatcher;
current的类型是null或者Dispatcher,不难看出接下来我们要找类型定义
// ReactInternalTypes.js
export type Dispatcher = {|
useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void,
|};
4.2 组件加载调用mountEffect
函数组件加载时,useEffect会调用mountEffect,接下来我们来看看mountEffect
// ReactFiberHooks.new.js
function mountEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return mountEffectImpl(
PassiveEffect | PassiveStaticEffect,
HookPassive,
create,
deps,
);
}
PassiveEffect和PassiveStaticEffect是二进制常数,用位运算的方式操作,用来标记是什么类型的副作用的。mountEffect走了mountEffectImpl方法
function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
undefined,
nextDeps,
);
}
上面代码中,往hook链表里追加一个hook,把hook存到链表中以后还把pushEffect的返回值存了下来。
function pushEffect(tag, create, destroy, deps) {
const effect: Effect = {
tag,
create,
destroy, // mountEffectImpl传过来的是undefined
deps,
next: (null: any),
};
// 一个全局变量,在renderWithHooks里初始化一下,存储全局最新的副作用
let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
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;
}
最后返回了一个effect对象。
Tips: mountEffect就是把useEffect加入了hook链表中,并且单独维护了一个useEffect的链表。
4.3 组件更新时调用updateEffect
函数组件加载时,useEffect会调用updateEffect,接下来我们来看看updateEffect
function updateEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
}
function updateEffectImpl(fiberFlags, hookFlags, 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,
hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
return;
}
}
}
// 两次依赖项不同,componentUpdateQueue上增加一个effect,并且更新当前hook的memoizedState值
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
destroy,
nextDeps,
);
}
从上面代码中我们看到areHookInputsEqual用来比较依赖项是否发生变化。下面我们看看这个areHookInputsEqual函数
function areHookInputsEqual(
nextDeps: Array<mixed>,
prevDeps: Array<mixed> | null,
) {
if (prevDeps === null) {
...
return false;
}
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
上面代码中,广东靓仔删掉了一些dev处理的代码,不影响阅读。
其实就是遍历deps数组,对每一项执行Object.is()方法,判断两个值是否为同一个值。
以上内容是源码中的一部分,如果感兴趣的小伙伴可以到react仓库进行阅读~
五、总结
在看源码前,我们先去官方文档复习下框架设计理念、源码分层设计 阅读下框架官方开发人员写的相关文章 借助框架的调用栈来进行源码的阅读,通过这个执行流程,我们就完整的对源码进行了一个初步的了解 接下来再对源码执行过程中涉及的所有函数逻辑梳理一遍
关注我,一起携手进阶
如果这篇文章有触动到你,欢迎关注前端早茶,与广东靓仔携手共同进阶~