2024-12-25 18:39:46 +08:00

367 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# effect相关hook
> 面试题:说一说 useEffect 和 useLayoutEffect 的区别?
在 React 中,用于定义有副作用的因变量的 hook 有三个:
- useEffect回调函数会在 commit 阶段完成后异步执行,所以它不会阻塞视图渲染
- useLayoutEffect回调函数会在 commit 阶段的 Layout 子阶段同步执行,一般用于执行 DOM 相关的操作
- useInsertionEffect回调函数会在 commit 阶段的 Mutation 子阶段同步执行,与 useLayoutEffect 的区别在于执行的时候无法访问对 DOM 的引用。这个 Hook 是专门为 CSS-in-JS 库插入全局的 style 元素而设计。
## 数据结构
对于这三个 effect 相关的 hookhook.memoizedState 共同使用同一套数据结构:
```js
const effect = {
// 用于区分 effect 类型 Passive | Layout | Insertion
tag,
// effect 回调函数
create,
// effect 销毁函数
destory,
// 依赖项
deps,
// 与当前 FC 的其他 effect 形成环状链表
next: null
}
```
tag 用来区分 effect 的类型:
- Passive useEffect
- LayoutuseLayoutEffect
- InsertionuseInsertionEffect
create 和 destory 分别指代 effect 的回调函数以及 effect 销毁函数:
```js
useEffect(()=>{
// create
return ()=>{
// destory
}
})
```
next 字段会与当前的函数组件的其他 effect 形成环状链表,连接的方式是一个单向环状链表。
```jsx
function App(){
useEffect(()=>{
console.log(1);
});
const [num1, setNum1] = useState(0);
const [num2, setNum2] = useState(0);
useEffect(()=>{
console.log(2);
});
useEffect(()=>{
console.log(3);
});
return <div>Hello</div>
}
```
结构如下图所示:
![image-20230307105834596](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2023-03-07-025835.png)
## 工作流程
整个工作流程可以分为三个阶段:
- 声明阶段
- 调度阶段useEffect 独有的)
- 执行阶段
### 声明阶段
声明阶段又可以分为 mount 和 update。
mount 的时候执行的是 mountEffectImpl相关代码如下
```js
function mountEffectImpl(fiberFlags, hookFlags, create, deps) {
// 生成 hook 对象
const hook = mountWorkInProgressHook();
// 保存依赖的数组
const nextDeps = deps === undefined ? null : deps;
// 修改当前 fiber 的 flag
currentlyRenderingFiber.flags |= fiberFlags;
// 将 pushEffect 返回的环形链表存储到 hook 对象的 memoizedState 中
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
undefined,
nextDeps
);
}
```
在上面的代码中,首先生成 hook 对象,拿到依赖,修改 fiber 的 flag之后将当前的 effect 推入到环状列表hook.memoizedState 指向该环状列表。
update 的时候执行的是 updateEffectImpl相关代码如下
```js
function updateEffectImpl(fiberFlags, hookFlags, create, deps) {
// 先拿到之前的 hook 对象
const hook = updateWorkInProgressHook();
// 拿到依赖项
const nextDeps = deps === undefined ? null : deps;
// 初始化清除 effect 函数
let destroy = undefined;
if (currentHook !== null) {
// 从 hook 对象上面的 memoizedState 上面拿到副作用的环形链表
const prevEffect = currentHook.memoizedState;
// 拿到销毁函数,也就是说副作用函数执行后返回的函数
destroy = prevEffect.destroy;
// 如果新的依赖项不为空
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
// 两个依赖项进行比较
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 如果依赖的值相同,即依赖没有变化,那么只会给这个 effect 打上一个 HookPassive 一个 tag
// 然后在组件渲染完以后会跳过这个 effect 的执行
hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
return;
}
}
}
// 如果deps依赖项发生改变赋予 effectTag 在commit节点就会再次执行我们的effect
currentlyRenderingFiber.flags |= fiberFlags;
// pushEffect 的作用是将当前 effect 添加到 FiberNode 的 updateQueue 中,然后返回这个当前 effcet
// 然后是把返回的当前 effect 保存到 Hook 节点的 memoizedState 属性中
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
destroy,
nextDeps
);
}
```
在上面的代码中,首先从 updateWorkInProgressHook 方法中拿到 hook 对象,之后会从 hook.memoizedState 拿到所存储的 effect 对象,之后会利用 areHookInputsEqual 方法进行前后依赖项的比较,如果依赖相同,那就会在 effect 上面打一个 tag在组件渲染完以后会跳过这个 effect 的执行。
如果依赖发生了变化,那么当前的 fiberNode 就会有一个 flags回头在 commit 阶段统一执行该 effect之后会推入新的 effect 到环状链表上面。
areHookInputsEqual 的作用是比较两个依赖项数组是否相同,采用的是浅比较,相关代码如下:
```js
function areHookInputsEqual(nextDeps, prevDeps){
// 省略代码
for(let i=0; i<prevDeps.length && i< nextDeps.length; i++){
// 使用 Object.is 进行比较
if (is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
```
pushEffect 方法的作用是生成一个 effect 对象,然后推入到当前的单向环状链表里面,相关代码如下:
```js
function pushEffect(tag, create, destroy, deps) {
// 创建副作用对象
const effect = {
tag,
create, // callback
destroy,
deps, // 依赖
// Circular
next: null,
};
let componentUpdateQueue = currentlyRenderingFiber.updateQueue;
// 创建单向环状链表
if (componentUpdateQueue === null) {
// 进入此 if说明是第一个 effect
// createFunctionComponentUpdateQueue 调用后会返回一个对象
// { lastEffect, events, stores, memoCache}
componentUpdateQueue = createFunctionComponentUpdateQueue();
// fiber 的 updateQueue 上面保存了该对象componentUpdateQueue
currentlyRenderingFiber.updateQueue = componentUpdateQueue;
// 该对象componentUpdateQueue上面 lastEffect 存储了副作用对象
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
// 存在多个 effect
// 拿到之前的副作用
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
// 如果没有,那就和上面的 if 处理一样
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
// 如果之前有副作用,先存储到 firstEffect
const firstEffect = lastEffect.next;
// lastEffect 指向新的副作用对象
lastEffect.next = effect;
// 新的副作用对象的 next 指向之前的副作用对象
// 最终形成一个环形链表
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
return effect;
}
```
update 的时候,即使 effect deps 没有变化,也会创建对应的 effect。因为这样才能后保证 effect 数量以及顺序是稳定的:
```js
// update 时 deps 没有变化情况
hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
// update 时 deps 有变化的情况
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
destroy,
nextDeps
);
```
### 调度阶段useEffect 独有的)
调度阶段是 useEffect 独有的,因为 useEffect 的回调函数会在 commit 阶段完成后异步执行,因此需要调度阶段。
在 commit 阶段的三个子阶段开始之前,会执行如下的代码:
```js
if (
(finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
(finishedWork.flags & PassiveMask) !== NoFlags
) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
pendingPassiveEffectsRemainingLanes = remainingLanes;
// ...
// scheduleCallback 来自于 Scheduler用于以某一优先级调度回调函数
scheduleCallback(NormalSchedulerPriority, () => {
// 执行 effect 回调函数的具体方法
flushPassiveEffects();
return null;
});
}
}
```
flushPassiveEffects 会去执行对应的 effects
```js
function flushPassiveEffects(){
if (rootWithPendingPassiveEffects !== null) {
// 执行 effects
}
return false;
}
```
另外,由于调度阶段的存在,为了保证下一次的 commit 阶段执行前,上一次 commit 所调度的 useEffect 都已经执行过了,因此会在 commit 阶段的入口处,也会执行 flushPassiveEffects而且是一个循环执行
```js
function commitRootImpl(root, renderPriorityLevel){
do {
flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);
}
```
之所以使用 do...while 循环,就是为了保证上一轮调度的 effect 都执行过了。
### 执行阶段
这三个 effect 相关的 hook 执行阶段,有两个相关的方法
- commitHookEffectListUnmount :用于遍历 effect 链表依次执行 effect.destory 方法
```js
function commitHookEffectListUnmount(
flags: HookFlags,
finishedWork: Fiber,
nearestMountedAncestor: Fiber | null,
) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & flags) === flags) {
// Unmount
// 从 effect 对象上面拿到 destory 函数
const destroy = effect.destroy;
effect.destroy = undefined;
// ...
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
```
- commitHookEffectListMount遍历 effect 链表依次执行 create 方法在声明阶段中update 时会根据 deps 是否变化打上不同的 tag之后在执行阶段就会根据是否有 tag 来决定是否要执行该 effect
```js
// 类型为 useInsertionEffect 并且存在 HasEffect tag 的 effect 会执行回调
commitHookEffectListMount(Insertion | HasEffect, fiber);
// 类型为 useEffect 并且存在 HasEffect tag 的 effect 会执行回调
commitHookEffectListMount(Passive | HasEffect, fiber);
// 类型为 useLayoutEffect 并且存在 HasEffect tag 的 effect 会执行回调
commitHookEffectListMount(Layout | HasEffect, fiber);
```
由于 commitHookEffectListUnmount 方法的执行时机会先于 commitHookEffectListMount 方法执行,因此每次都是先执行 effect.destory 后才会执行 effect.create。
## 真题解答
> 题目:说一说 useEffect 和 useLayoutEffect 的区别?
>
> 参考答案:
>
> 在 React 中,用于定义有副作用因变量的 Hook 有:
>
> - useEffect回调函数会在 commit 阶段完成后异步执行,所以不会阻塞视图渲染
> - useLayoutEffect回调函数会在 commit 阶段的 Layout 子阶段同步执行,一般用于执行 DOM 相关的操作
>
> 每一个 effect 会与当前 FC 其他的 effect 形成环状链表,连接方式为单向环状链表。
>
> 其中 useEffect 工作流程可以分为:
>
> - 声明阶段
> - 调度阶段
> - 执行阶段
>
> useLayoutEffect 的工作流程可以分为:
>
> - 声明阶段
> - 执行阶段
>
> 之所以 useEffect 会比 useLayoutEffect 多一个阶段,就是因为 useEffect 的回调函数会在 commit 阶段完成后异步执行,因此需要经历调度阶段。