From 9de138917cc4e26fb05592edf27dc77269427520 Mon Sep 17 00:00:00 2001 From: xie jie <745007854@qq.com> Date: Wed, 25 Dec 2024 18:39:46 +0800 Subject: [PATCH] update hook answer --- .../2-21. effect相关hook/effect相关hook.md | 720 +++++++++--------- 1 file changed, 366 insertions(+), 354 deletions(-) diff --git a/就业篇/02. 第二章/2-21. effect相关hook/effect相关hook.md b/就业篇/02. 第二章/2-21. effect相关hook/effect相关hook.md index b35623c..f6e4dc2 100644 --- a/就业篇/02. 第二章/2-21. effect相关hook/effect相关hook.md +++ b/就业篇/02. 第二章/2-21. effect相关hook/effect相关hook.md @@ -1,354 +1,366 @@ -# effect相关hook - -> 面试题:说一说 useEffect 和 useLayoutEffect 的区别? - - - -在 React 中,用于定义有副作用的因变量的 hook 有三个: - -- useEffect:回调函数会在 commit 阶段完成后异步执行,所以它不会阻塞视图渲染 -- useLayoutEffect:回调函数会在 commit 阶段的 Layout 子阶段同步执行,一般用于执行 DOM 相关的操作 -- useInsertionEffect:回调函数会在 commit 阶段的 Mutation 子阶段同步执行,与 useLayoutEffect 的区别在于执行的时候无法访问对 DOM 的引用。这个 Hook 是专门为 CSS-in-JS 库插入全局的 style 元素而设计。 - - - -## 数据结构 - -对于这三个 effect 相关的 hook,hook.memoizedState 共同使用同一套数据结构: - -```js -const effect = { - // 用于区分 effect 类型 Passive | Layout | Insertion - tag, - // effect 回调函数 - create, - // effect 销毁函数 - destory, - // 依赖项 - deps, - // 与当前 FC 的其他 effect 形成环状链表 - next: null -} -``` - -tag 用来区分 effect 的类型: - -- Passive: useEffect -- Layout:useLayoutEffect -- Insertion:useInsertionEffect - - - -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
Hello
-} -``` - -结构如下图所示: - -![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 { - // 执行 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 的区别? -> -> 参考答案: -> -> +# effect相关hook + +> 面试题:说一说 useEffect 和 useLayoutEffect 的区别? + + + +在 React 中,用于定义有副作用的因变量的 hook 有三个: + +- useEffect:回调函数会在 commit 阶段完成后异步执行,所以它不会阻塞视图渲染 +- useLayoutEffect:回调函数会在 commit 阶段的 Layout 子阶段同步执行,一般用于执行 DOM 相关的操作 +- useInsertionEffect:回调函数会在 commit 阶段的 Mutation 子阶段同步执行,与 useLayoutEffect 的区别在于执行的时候无法访问对 DOM 的引用。这个 Hook 是专门为 CSS-in-JS 库插入全局的 style 元素而设计。 + +## 数据结构 + +对于这三个 effect 相关的 hook,hook.memoizedState 共同使用同一套数据结构: + +```js +const effect = { + // 用于区分 effect 类型 Passive | Layout | Insertion + tag, + // effect 回调函数 + create, + // effect 销毁函数 + destory, + // 依赖项 + deps, + // 与当前 FC 的其他 effect 形成环状链表 + next: null +} +``` + +tag 用来区分 effect 的类型: + +- Passive: useEffect +- Layout:useLayoutEffect +- Insertion:useInsertionEffect + + + +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
Hello
+} +``` + +结构如下图所示: + +![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 { + // 执行 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 阶段完成后异步执行,因此需要经历调度阶段。