• 【React源码】(十四)Hook 原理(副作用 Hook)


    Hook 原理(副作用 Hook)

    本节建立在前文Hook 原理(概览)Hook 原理(状态 Hook)的基础之上, 重点讨论useEffect, useLayoutEffect等标准的副作用Hook.

    创建 Hook

    fiber初次构造阶段, useEffect对应源码mountEffectuseLayoutEffect对应源码mountLayoutEffect

    mountEffect:

     
    
    1. function mountEffect(
    2. create: () => (() => void) | void,
    3. deps: Array | void | null,
    4. ): void {
    5. return mountEffectImpl(
    6. UpdateEffect | PassiveEffect, // fiberFlags
    7. HookPassive, // hookFlags
    8. create,
    9. deps,
    10. );
    11. }

    mountLayoutEffect:

     
    
    1. function mountLayoutEffect(
    2. create: () => (() => void) | void,
    3. deps: Array | void | null,
    4. ): void {
    5. return mountEffectImpl(
    6. UpdateEffect, // fiberFlags
    7. HookLayout, // hookFlags
    8. create,
    9. deps,
    10. );
    11. }

    可见mountEffectmountLayoutEffect内部都直接调用mountEffectImpl, 只是参数不同.

    mountEffectImpl:

     
    
    1. function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
    2. // 1. 创建hook
    3. const hook = mountWorkInProgressHook();
    4. const nextDeps = deps === undefined ? null : deps;
    5. // 2. 设置workInProgress的副作用标记
    6. currentlyRenderingFiber.flags |= fiberFlags; // fiberFlags 被标记到workInProgress
    7. // 2. 创建Effect, 挂载到hook.memoizedState上
    8. hook.memoizedState = pushEffect(
    9. HookHasEffect | hookFlags, // hookFlags用于创建effect
    10. create,
    11. undefined,
    12. nextDeps,
    13. );
    14. }

    mountEffectImpl逻辑:

    1. 创建hook
    2. 设置workInProgress的副作用标记: flags |= fiberFlags
    3. 创建effect(在pushEffect中), 挂载到hook.memoizedState上, 即 hook.memoizedState = effect
      • 注意: 状态Hookhook.memoizedState = state

    创建 Effect

    pushEffect:

     
    
    1. function pushEffect(tag, create, destroy, deps) {
    2. // 1. 创建effect对象
    3. const effect: Effect = {
    4. tag,
    5. create,
    6. destroy,
    7. deps,
    8. next: (null: any),
    9. };
    10. // 2. 把effect对象添加到环形链表末尾
    11. let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
    12. if (componentUpdateQueue === null) {
    13. // 新建 workInProgress.updateQueue 用于挂载effect对象
    14. componentUpdateQueue = createFunctionComponentUpdateQueue();
    15. currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
    16. // updateQueue.lastEffect是一个环形链表
    17. componentUpdateQueue.lastEffect = effect.next = effect;
    18. } else {
    19. const lastEffect = componentUpdateQueue.lastEffect;
    20. if (lastEffect === null) {
    21. componentUpdateQueue.lastEffect = effect.next = effect;
    22. } else {
    23. const firstEffect = lastEffect.next;
    24. lastEffect.next = effect;
    25. effect.next = firstEffect;
    26. componentUpdateQueue.lastEffect = effect;
    27. }
    28. }
    29. // 3. 返回effect
    30. return effect;
    31. }

    pushEffect逻辑:

    1. 创建effect.
    2. effect对象添加到环形链表末尾.
    3. 返回effect.

    effect的数据结构:

     
    
    1. export type Effect = {|
    2. tag: HookFlags,
    3. create: () => (() => void) | void,
    4. destroy: (() => void) | void,
    5. deps: Array | null,
    6. next: Effect,
    7. |};
    • effect.tag: 二进制属性, 代表effect的类型(源码).

       
      1. export const NoFlags = /* */ 0b000;
      2. export const HasEffect = /* */ 0b001; // 有副作用, 可以被触发
      3. export const Layout = /* */ 0b010; // Layout, dom突变后同步触发
      4. export const Passive = /* */ 0b100; // Passive, dom突变前异步触发
      5. effect.create: 实际上就是通过useEffect()所传入的函数.
      6. effect.deps: 依赖项, 如果依赖项变动, 会创建新的effect.

    renderWithHooks执行完成后, 我们可以画出fiber,hook,effect三者的引用关系:

    现在workInProgress.flags被打上了标记, 最后会在fiber树渲染阶段的commitRoot函数中处理. (这期间的所有过程可以回顾前文fiber树构造/fiber树渲染系列, 此处不再赘述)

    useEffect & useLayoutEffect

    站在fiber,hook,effect的视角, 无需关心这个hook是通过useEffect还是useLayoutEffect创建的. 只需要关心内部fiber.flags,effect.tag的状态.

    所以useEffectuseLayoutEffect的区别如下:

    1. fiber.flags不同
    • 使用useEffect时: fiber.flags = UpdateEffect | PassiveEffect.
    • 使用useLayoutEffect时: fiber.flags = UpdateEffect.
    1. effect.tag不同
    • 使用useEffect时: effect.tag = HookHasEffect | HookPassive.
    • 使用useLayoutEffect时: effect.tag = HookHasEffect | HookLayout.

    处理 Effect 回调

    完成fiber树构造后, 逻辑会进入渲染阶段. 通过fiber 树渲染中的介绍, 在commitRootImpl函数中, 整个渲染过程被 3 个函数分布实现:

    1. commitBeforeMutationEffects
    2. commitMutationEffects
    3. commitLayoutEffects

    这 3 个函数会处理fiber.flags, 也会根据情况处理fiber.updateQueue.lastEffect

    commitBeforeMutationEffects

    第一阶段: dom 变更之前, 处理副作用队列中带有Passive标记的fiber节点.

     
    
    1. function commitBeforeMutationEffects() {
    2. while (nextEffect !== null) {
    3. // ...省略无关代码, 只保留Hook相关
    4. // 处理`Passive`标记
    5. const flags = nextEffect.flags;
    6. if ((flags & Passive) !== NoFlags) {
    7. if (!rootDoesHavePassiveEffects) {
    8. rootDoesHavePassiveEffects = true;
    9. scheduleCallback(NormalSchedulerPriority, () => {
    10. flushPassiveEffects();
    11. return null;
    12. });
    13. }
    14. }
    15. nextEffect = nextEffect.nextEffect;
    16. }
    17. }

    注意: 由于flushPassiveEffects被包裹在scheduleCallback回调中, 由调度中心来处理, 且参数是NormalSchedulerPriority, 故这是一个异步回调(具体原理可以回顾React 调度原理(scheduler)).

    由于scheduleCallback(NormalSchedulerPriority,callback)是异步的, flushPassiveEffects并不会立即执行. 此处先跳过flushPassiveEffects的分析, 继续跟进commitRoot.

    commitMutationEffects

    第二阶段: dom 变更, 界面得到更新.

     
    
    1. function commitMutationEffects(
    2. root: FiberRoot,
    3. renderPriorityLevel: ReactPriorityLevel,
    4. ) {
    5. // ...省略无关代码, 只保留Hook相关
    6. while (nextEffect !== null) {
    7. const flags = nextEffect.flags;
    8. const primaryFlags = flags & (Placement | Update | Deletion | Hydrating);
    9. switch (primaryFlags) {
    10. case Update: {
    11. // useEffect,useLayoutEffect都会设置Update标记
    12. // 更新节点
    13. const current = nextEffect.alternate;
    14. commitWork(current, nextEffect);
    15. break;
    16. }
    17. }
    18. nextEffect = nextEffect.nextEffect;
    19. }
    20. }
    21. function commitWork(current: Fiber | null, finishedWork: Fiber): void {
    22. // ...省略无关代码, 只保留Hook相关
    23. switch (finishedWork.tag) {
    24. case FunctionComponent:
    25. case ForwardRef:
    26. case MemoComponent:
    27. case SimpleMemoComponent:
    28. case Block: {
    29. // 在突变阶段调用销毁函数, 保证所有的effect.destroy函数都会在effect.create之前执行
    30. commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);
    31. return;
    32. }
    33. }
    34. }
    35. // 依次执行: effect.destroy
    36. function commitHookEffectListUnmount(tag: number, finishedWork: Fiber) {
    37. const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
    38. const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
    39. if (lastEffect !== null) {
    40. const firstEffect = lastEffect.next;
    41. let effect = firstEffect;
    42. do {
    43. if ((effect.tag & tag) === tag) {
    44. // 根据传入的tag过滤 effect链表.
    45. const destroy = effect.destroy;
    46. effect.destroy = undefined;
    47. if (destroy !== undefined) {
    48. destroy();
    49. }
    50. }
    51. effect = effect.next;
    52. } while (effect !== firstEffect);
    53. }
    54. }

    调用关系: commitMutationEffects->commitWork->commitHookEffectListUnmount.

    • 注意在调用commitMutationEffects(HookLayout | HookHasEffect, finishedWork)时, 参数是HookLayout | HookHasEffect, 所以只处理由useLayoutEffect()创建的effect.
    • 根据上文的分析HookLayout | HookHasEffect是通过useLayoutEffect创建的effect. 所以commitMutationEffects函数只能处理由useLayoutEffect()创建的effect.
    • 同步调用effect.destroy().

    commitLayoutEffects

    第三阶段: dom 变更后

     
    
    1. function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) {
    2. // ...省略无关代码, 只保留Hook相关
    3. while (nextEffect !== null) {
    4. const flags = nextEffect.flags;
    5. if (flags & (Update | Callback)) {
    6. // useEffect,useLayoutEffect都会设置Update标记
    7. const current = nextEffect.alternate;
    8. commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes);
    9. }
    10. nextEffect = nextEffect.nextEffect;
    11. }
    12. }
    13. function commitLifeCycles(
    14. finishedRoot: FiberRoot,
    15. current: Fiber | null,
    16. finishedWork: Fiber,
    17. committedLanes: Lanes,
    18. ): void {
    19. // ...省略无关代码, 只保留Hook相关
    20. switch (finishedWork.tag) {
    21. case FunctionComponent:
    22. case ForwardRef:
    23. case SimpleMemoComponent:
    24. case Block: {
    25. // 在此之前commitMutationEffects函数中, effect.destroy已经被调用, 所以effect.destroy永远不会影响到effect.create
    26. commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
    27. schedulePassiveEffects(finishedWork);
    28. return;
    29. }
    30. }
    31. }
    32. function commitHookEffectListMount(tag: number, finishedWork: Fiber) {
    33. const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
    34. const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
    35. if (lastEffect !== null) {
    36. const firstEffect = lastEffect.next;
    37. let effect = firstEffect;
    38. do {
    39. if ((effect.tag & tag) === tag) {
    40. const create = effect.create;
    41. effect.destroy = create();
    42. }
    43. effect = effect.next;
    44. } while (effect !== firstEffect);
    45. }
    46. }
    1. 调用关系: commitLayoutEffects->commitLayoutEffectOnFiber(commitLifeCycles)->commitHookEffectListMount.
    • 注意在调用commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork)时, 参数是HookLayout | HookHasEffect,所以只处理由useLayoutEffect()创建的effect.
    • 调用effect.create()之后, 将返回值赋值到effect.destroy.
    1. flushPassiveEffects做准备

      • commitLifeCycles中的schedulePassiveEffects(finishedWork), 其形参finishedWork实际上指代当前正在被遍历的有副作用的fiber

      • schedulePassiveEffects比较简单, 就是把带有Passive标记的effect筛选出来(由useEffect创建), 添加到一个全局数组(pendingPassiveHookEffectsUnmountpendingPassiveHookEffectsMount).

         
        1. function schedulePassiveEffects(finishedWork: Fiber) {
        2. // 1. 获取 fiber.updateQueue
        3. const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
        4. // 2. 获取 effect环形队列
        5. const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
        6. if (lastEffect !== null) {
        7. const firstEffect = lastEffect.next;
        8. let effect = firstEffect;
        9. do {
        10. const { next, tag } = effect;
        11. // 3. 筛选出由useEffect()创建的`effect`
        12. if (
        13. (tag & HookPassive) !== NoHookEffect &&
        14. (tag & HookHasEffect) !== NoHookEffect
        15. ) {
        16. // 把effect添加到全局数组, 等待`flushPassiveEffects`处理
        17. enqueuePendingPassiveHookEffectUnmount(finishedWork, effect);
        18. enqueuePendingPassiveHookEffectMount(finishedWork, effect);
        19. }
        20. effect = next;
        21. } while (effect !== firstEffect);
        22. }
        23. }
        24. export function enqueuePendingPassiveHookEffectUnmount(
        25. fiber: Fiber,
        26. effect: HookEffect,
        27. ): void {
        28. // unmount effects 数组
        29. pendingPassiveHookEffectsUnmount.push(effect, fiber);
        30. }
        31. export function enqueuePendingPassiveHookEffectMount(
        32. fiber: Fiber,
        33. effect: HookEffect,
        34. ): void {
        35. // mount effects 数组
        36. pendingPassiveHookEffectsMount.push(effect, fiber);
        37. }

    综上commitMutationEffectscommitLayoutEffects2 个函数, 带有Layout标记的effect(由useLayoutEffect创建), 已经得到了完整的回调处理(destroycreate已经被调用).

    如下图: 其中第一个effect拥有Layout标记, 所以有effect.destroy(); effect.destroy = effect.create()

    flushPassiveEffects

    在上文commitBeforeMutationEffects阶段, 异步调用flushPassiveEffects. 在这期间带有Passive标记的effect已经被添加到pendingPassiveHookEffectsUnmountpendingPassiveHookEffectsMount全局数组中.

    接下来flushPassiveEffects就可以脱离fiber节点, 直接访问effects

     
    
    1. export function flushPassiveEffects(): boolean {
    2. // Returns whether passive effects were flushed.
    3. if (pendingPassiveEffectsRenderPriority !== NoSchedulerPriority) {
    4. const priorityLevel =
    5. pendingPassiveEffectsRenderPriority > NormalSchedulerPriority
    6. ? NormalSchedulerPriority
    7. : pendingPassiveEffectsRenderPriority;
    8. pendingPassiveEffectsRenderPriority = NoSchedulerPriority;
    9. // `runWithPriority`设置Schedule中的调度优先级, 如果在flushPassiveEffectsImpl中处理effect时又发起了新的更新, 那么新的update.lane将会受到这个priorityLevel影响.
    10. return runWithPriority(priorityLevel, flushPassiveEffectsImpl);
    11. }
    12. return false;
    13. }
    14. // ...省略无关代码, 只保留Hook相关
    15. function flushPassiveEffectsImpl() {
    16. if (rootWithPendingPassiveEffects === null) {
    17. return false;
    18. }
    19. rootWithPendingPassiveEffects = null;
    20. pendingPassiveEffectsLanes = NoLanes;
    21. // 1. 执行 effect.destroy()
    22. const unmountEffects = pendingPassiveHookEffectsUnmount;
    23. pendingPassiveHookEffectsUnmount = [];
    24. for (let i = 0; i < unmountEffects.length; i += 2) {
    25. const effect = ((unmountEffects[i]: any): HookEffect);
    26. const fiber = ((unmountEffects[i + 1]: any): Fiber);
    27. const destroy = effect.destroy;
    28. effect.destroy = undefined;
    29. if (typeof destroy === 'function') {
    30. destroy();
    31. }
    32. }
    33. // 2. 执行新 effect.create(), 重新赋值到 effect.destroy
    34. const mountEffects = pendingPassiveHookEffectsMount;
    35. pendingPassiveHookEffectsMount = [];
    36. for (let i = 0; i < mountEffects.length; i += 2) {
    37. const effect = ((mountEffects[i]: any): HookEffect);
    38. const fiber = ((mountEffects[i + 1]: any): Fiber);
    39. effect.destroy = create();
    40. }
    41. }

    其核心逻辑:

    1. 遍历pendingPassiveHookEffectsUnmount中的所有effect, 调用effect.destroy().
      • 同时清空pendingPassiveHookEffectsUnmount
    2. 遍历pendingPassiveHookEffectsMount中的所有effect, 调用effect.create(), 并更新effect.destroy.
      • 同时清空pendingPassiveHookEffectsMount

    所以, 带有Passive标记的effect, 在flushPassiveEffects函数中得到了完整的回调处理.

    如下图: 其中拥有Passive标记的effect, 都会执行effect.destroy(); effect.destroy = effect.create()

    更新 Hook

    假设在初次调用之后, 发起更新, 会再次执行function, 这时function只使用的useEffectuseLayoutEffectapi也会再次执行.

    在更新过程中useEffect对应源码updateEffectuseLayoutEffect对应源码updateLayoutEffect.它们内部都会调用updateEffectImpl, 与初次创建时一样, 只是参数不同.

    更新 Effect

    updateEffectImpl:

     
    
    1. function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
    2. // 1. 获取当前hook
    3. const hook = updateWorkInProgressHook();
    4. const nextDeps = deps === undefined ? null : deps;
    5. let destroy = undefined;
    6. // 2. 分析依赖
    7. if (currentHook !== null) {
    8. const prevEffect = currentHook.memoizedState;
    9. // 继续使用先前effect.destroy
    10. destroy = prevEffect.destroy;
    11. if (nextDeps !== null) {
    12. const prevDeps = prevEffect.deps;
    13. // 比较依赖是否变化
    14. if (areHookInputsEqual(nextDeps, prevDeps)) {
    15. // 2.1 如果依赖不变, 新建effect(tag不含HookHasEffect)
    16. pushEffect(hookFlags, create, destroy, nextDeps);
    17. return;
    18. }
    19. }
    20. }
    21. // 2.2 如果依赖改变, 更改fiber.flag, 新建effect
    22. currentlyRenderingFiber.flags |= fiberFlags;
    23. hook.memoizedState = pushEffect(
    24. HookHasEffect | hookFlags,
    25. create,
    26. destroy,
    27. nextDeps,
    28. );
    29. }

    updateEffectImplmountEffectImpl逻辑有所不同: - 如果useEffect/useLayoutEffect的依赖不变, 新建的effect对象不带HasEffect标记.

    注意: 无论依赖是否变化, 都复用之前的effect.destroy. 等待commitRoot阶段的调用(上文已经说明).

    如下图:

    • 图中第 1,2 个hookdeps没变, 故effect.tag中不会包含HookHasEffect.
    • 图中第 3 个hookdeps改变, 故effect.tag中继续含有HookHasEffect.

    处理 Effect 回调

    新的hook以及新的effect创建完成之后, 余下逻辑与初次渲染完全一致. 处理 Effect 回调时也会根据effect.tag进行判断: 只有effect.tag包含HookHasEffect时才会调用effect.destroyeffect.create()

    组件销毁

    function组件被销毁时, fiber节点必然会被打上Deletion标记, 即fiber.flags |= Deletion. 带有Deletion标记的fibercommitMutationEffects被处理:

     
    
    1. // ...省略无关代码
    2. function commitMutationEffects(
    3. root: FiberRoot,
    4. renderPriorityLevel: ReactPriorityLevel,
    5. ) {
    6. while (nextEffect !== null) {
    7. const primaryFlags = flags & (Placement | Update | Deletion | Hydrating);
    8. switch (primaryFlags) {
    9. case Deletion: {
    10. commitDeletion(root, nextEffect, renderPriorityLevel);
    11. break;
    12. }
    13. }
    14. }
    15. }

    commitDeletion函数之后, 继续调用unmountHostComponents->commitUnmount, 在commitUnmount中, 执行effect.destroy(), 结束整个闭环.

    总结

    本节分析了副作用Hook从创建到销毁的全部过程, 在react内部, 依靠fiber.flagseffect.tag实现了对effect的精准识别. 在commitRoot阶段, 对不同类型的effect进行处理, 先后调用effect.destroy()effect.create()

  • 相关阅读:
    SqlBoy:间隔连续问题
    小程序开发平台源码系统+万能门店小程序功能+完整的搭建教程
    Python爬虫(十八)_多线程糗事百科案例
    解决报错:fatal: in unpopulated submodule *
    腾讯云2023年双十一活动时间、活动入口、活动内容详细解读
    redis缓存一致性以及解决方案
    K8S配置管理---secret与configmap
    GBase 8c 创建和管理Schema (二)
    Unity InputField宽度自适应内容
    Echarts设置饼状图保证你看的明明白白
  • 原文地址:https://blog.csdn.net/weixin_44828588/article/details/126545752