前言
建议没有看过前面系列文章从前面看起,这是本系列的第五篇文章,本篇文章主要介绍的是react
中hooks
的useEffect
, 这应该是除了useState
以外最常用的hook
了,话不多说,一起开始今天的学习!
准备工作
接下来我们看一道关于useEffect
的例子
const WatchCount = () => {
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(function log() {
console.log(`Count: ${count}`);
}, 2000);
}, []);
const handleClick = () => setCount(count => count + 1);
return (
<>
<button onClick={handleClick}>+</button>
<div>Count: {count}</div>
</>
);
}
在这个例子中,useEffect
每隔两秒就会打印Count
,但是当我们点击button
,将count
变为1的时候,那打印的会是2么,但是结果可能要让我们失望了,打印的依然是0。
让我们来分析一下,在第一次打印的时候应该没有什么问题,打印的是0,但是我们通过点击事件将count
增加到1的时候,setInterval
仍然调用的是从初次渲染中捕获的count
为0的旧的log闭包,解决方法就是每次count
变化,我们就重置定时器。
const WatchCount = () => {
const [count, setCount] = useState(0);
useEffect(function() {
const id = setInterval(function log() {
console.log(`Count: ${count}`);
}, 2000);
return () => clearInterval(id);
}, [count]);
const handleClick = () => setCount(count => count + 1);
return (
<>
<button onClick={handleClick}>+</button>
<div>Count: {count}</div>
</>
);
}
这样,当状态变量count发生变化时,就会更新闭包。为了防止闭包捕获到旧值,就要确保在提供给hook的回调中使用的prop或者state都被指定为依赖性。
初始化mount
mountEffectImpl
- fiberFlags:有副作用的更新标记,用来标记hook所在的
fiber
; - hookFlags:副作用标记;
- create:使用者传入的回调函数;
- deps:使用者传入的数组依赖;
function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
// 1. 创建hook
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 2. 设置workInProgress的副作用标记
currentlyRenderingFiber.flags |= fiberFlags; // fiberFlags 被标记到workInProgress
// 2. 创建Effect, 挂载到hook.memoizedState上
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags, // hookFlags用于创建effect
create,
undefined,
nextDeps,
);
}
pushEffect
function pushEffect(tag, create, destroy, deps) {
// 1. 创建effect对象
const effect: Effect = {
tag,
create, // 回调函数
destroy, // 回调函数里的return(mount时是undefined)
deps, // 依赖数组
next: (null: any),
};
// 2. 把effect对象添加到环形链表末尾
let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
if (componentUpdateQueue === null) {
// 新建 workInProgress.updateQueue 用于挂载effect对象
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
// updateQueue.lastEffect是一个环形链表
componentUpdateQueue.lastEffect = effect.next = 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;
}
}
// 3. 返回effect
return effect;
}
上面这段代码除了初始化副作用的结构代码外,都是我们前面讲过的操作闭环链表,向链表末尾添加新的effect
,该effect.next
指向fisrtEffect
,并且链表当前的指针指向最新添加的effect
。
useEffect
的初始化就这么简单,简单总结一下:给hook
所在的fiber
打上副作用更新标记,并且fiber.memoizedState.hook.memoizedState
和fiber.updateQueue
存储了相关的副作用,这些副作用通过闭环链表的结构存储。
更新update
updateEffectImpl
function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
// 1. 获取当前hook
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
let destroy = undefined;
// 2. 分析依赖
if (currentHook !== null) {
const prevEffect = currentHook.memoizedState;
// 继续使用先前effect.destroy
destroy = prevEffect.destroy;
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
// 比较依赖是否变化
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 2.1 如果依赖不变, 新建effect(tag不含HookHasEffect)
pushEffect(hookFlags, create, destroy, nextDeps);
return;
}
}
}
// 2.2 如果依赖改变, 更改fiber.flag, 新建effect
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
destroy,
nextDeps,
);
}
我们仔细看会发现在上面出现两次pushEffect
,但我们发现只有一个pushEffect
进行了赋值,原因在于这个areHookInputsEqual
,我们来看下这个函数做了什么
areHookInputsEqual
function areHookInputsEqual(nextDeps, prevDeps) {
// 没有传deps的情况返回false
if (prevDeps === null) {
return false;
}
// deps不是[],且其中的值有变动才会返回false
for (var i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (objectIs(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
// deps = [],或者deps里面的值没有变化会返回true
return true;
}
它会判断两次依赖数组中的值是否有变化以及deps
是否是空数组来决定返回true
和false
,返回true
表明这次不需要调用回调函数。
执行副作用
上面我们讲了在mount
和update
时的如何创建副作用,那么这些副作用会在什么时候执行呢?
在React源码解读(3):commit阶段也已经讲过了,在commit
阶段有一系列处理副作用的操作
大概流程如上大图显示,首先在mutation
之前阶段,基于副作用创建任务并放到taskQueue
中,同时会执行requestHostCallback
,这个方法就涉及到了异步了,它首先考虑使用MessageChannel
实现异步,其次会考虑使用setTimeout
实现。使用MessageChannel
时,requestHostCallback
会马上执行port.postMessage(null);
,这样就可以在异步的第一时间执行workLoop
,workLoop
会遍历taskQueue
,执行任务,如果是useEffect
的effect
任务,会调用flusnPassiveEffects
。
总结
useEffect
和useLayoutEffect
的区别是执行时机不同,前者是异步执行,后者是在layout
后台同步执行,会阻塞渲染。这一部分和以前文章commit
阶段紧密相连,建议先复习一遍commit
阶段,在来读此篇文章会更加明白些。