React-hook源码阅读(下) - 一文搞定useEffect及其他effect hook

从第一篇 React-hook源码阅读 - useState(上) 了解了React Hook的基本原理以及useState的具体逻辑

从第二篇 React-hook源码阅读(中) - 趁热打铁 我们根据第一篇的内容趁热打铁, 了解6个常见hook的原理

这一篇, 我们走入 Effect 相关的原理, 涉及的 hookuseEffect, useLayoutEffect, useInsertionEffect, useImperativeHandle

总体文章的思路为先通过useEffect较详细了解Effect相关处理, 然后比较其他Hook。 如果对源码不是很感兴趣可以直接看总结

一、useEffect

mountEffect

还是老规矩, 先找初次挂载的入口mountEffect。 这里通过判断了Mode判断他走入哪个分支, 本质上都是调用了mountEffectImpl。 只不过fiber flags传入的多了MountPassiveDev。 不过我们的关注重点一般都在hook flagsPassive

js 复制代码
function mountEffect(create, deps) {
  if ( (currentlyRenderingFiber$1.mode & StrictEffectsMode) !== NoMode) {
    return mountEffectImpl(MountPassiveDev | Passive | PassiveStatic, Passive$1, create, deps);
  } else {
    return mountEffectImpl(Passive | PassiveStatic, Passive$1, create, deps);
  }
}

接着看他调用的 mountEffectImpl。 这里一共做了三步, 具体可看注释

js 复制代码
function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;  // 这里对传入的deps做了处理
  // 这里给当前的Fiber打上了flags标签, 表示有副作用, 在commit阶段会使用到
  currentlyRenderingFiber.flags |= fiberFlags; 
  // 挂载上hook的链表
  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    undefined,  // 可以看到这里暂时是不收集destroy的
    nextDeps,
  );
}

接着继续看pushEffect。 这里主要就是根据useEffect hook对象形成完整的Effect链表。 放在hook链表上以及updateQueue

js 复制代码
function pushEffect(tag, create, destroy, deps) {
 // 生成了对应的Effect对象, 最后是要挂上hook链表的
  var effect = {
    tag: tag,
    create: create,
    destroy: destroy,
    deps: deps,
    // Circular
    next: null
  };
  // 拿到当前Fiber的updateQueue
  var componentUpdateQueue = currentlyRenderingFiber$1.updateQueue;
  // 这里就是生成链表的过程了, 主要是处理了updateQueue和effect链表, 具体效果可以看下面的图示
  if (componentUpdateQueue === null) {
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber$1.updateQueue = componentUpdateQueue;
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    var lastEffect = componentUpdateQueue.lastEffect;
    if (lastEffect === null) {
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
      var firstEffect = lastEffect.next;
      lastEffect.next = effect;
      effect.next = firstEffect;
      componentUpdateQueue.lastEffect = effect;
    }
  }
  return effect;
}

来个例子, 我们来看一下走完hook之后该函数Fiber变成啥样啦

js 复制代码
export default function MyApp() {
  const [count, setCount] =  useState(1);
  const [count1, setCount1] = useState(2);
  useEffect(() => { 
    console.log('1');
    return () => console.log('3')
  }, [])
  useEffect(() => {
    console.log('2');
  }, [count])
  return (
    <div onClick={() => setCount(count + 1)}>{count}</div>
  )
}

updateEffect

了解完mountEffect做了什么后我们继续看updateEffect。 可以看到也是传入Passive

js 复制代码
function updateEffect(create, deps) {
  return updateEffectImpl(Passive, Passive$1, create, deps);
}
  • 直接看updateEffectImpl。 这里的流程也很清晰, 就不赘述了
js 复制代码
function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;  // 拿到现在的依赖
  let destroy = undefined;
  if (currentHook !== null) {
    const prevEffect = currentHook.memoizedState;
    destroy = prevEffect.destroy;  
    if (nextDeps !== null) {
      const prevDeps = prevEffect.deps;
      // 这里比较了前后依赖, 如果一致的话处理hook之后就reuturn出去了
      if (areHookInputsEqual(nextDeps, prevDeps)) { 
        hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
        return;
      }
    }
  }
  // 如果依赖不一致的话则给Fiber打上Passive的标志
  currentlyRenderingFiber.flags |= fiberFlags;
  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,  // effect的tag由hookFlags决定
    create,
    destroy,
    nextDeps,
  );
}

mountEffectudpateEffect都有一个关键的操作就是给当前的Fiber flags打上Passive的标志。 该标志在commit阶段的时候对Effect进行处理。

从这里你就能知道为什么

  • useEffect deps传入空数组的时候只会在初次渲染调用一次: 因为两个空数组中没得比较,故每次都认为l两个数组中的每一项都是一致的, 是没有变化的, 会直接return出去, 而不会打上flags标签
  • useEffect为什么无论如何都最少会调用一次: 因为mountEffect的时候没有其他条件可以return出去, 一定会打上flags标签

那么打上flags标签之后, React是如何去处理useEffect的回调函数呢。 我们可以走到commit阶段,React源码阅读(四)- commit 这篇文章有更具体的commit流程, 这篇主要以Effect为主。

如何处理useEffect(重点)

异步原理

先来一张宏观图(录制的是初次渲染阶段), 可以看到图的左边是进入了commit段,最后通过DOM API appendChild将处理好的DOM树挂载到页面上,然后交出线程, 浏览器进行渲染。 接着再执行了图的右边部分, 这一部分实际上就是在处理触发的useEffect的回调。

总的流程就是 commit阶段(宏任务) - 浏览器渲染 - useEffect相关回调执行(宏任务)

那么是如何达到useEffect做到在渲染之后执行的目的呢: React是在commit的准备阶段中进行处理的, 通过以 schduleCallback 为入口的方法进行调度(这里本质上是通过MessageChannel的方式达到让一些操作能够在推入宏任务队列等待执行, 建议学习路线:flushPassiveEffects -> schduleCallback -> requestHostCallback -> schedulePerformWorkUntilDeadline),让 flushPassiveEffects 能够在下一个宏任务进行处理

  • 从链接点进去都有蛮详细的解释了, 这里就不多说了
  • 以下具体的源码流程建议先看一篇commit的基础源码之后再看这个, 会舒服流畅很多, 因为都是同一个套路。 如果看不下去的话可以跳过源码直接看总结

那么我们继续看flushPassiveEffects这个函数究竟做了什么?

这里初始化了挺多东西, 我省略了,我们直接关注主流程。 可以看到主要就是调用了flushPassiveEffectsImpl

js 复制代码
export function flushPassiveEffects(): boolean {
  if (rootWithPendingPassiveEffects !== null) {
    .....
    try {
      ......
      return flushPassiveEffectsImpl();
    } finally {
      ......
    }
  }
  return false;
}
  • 那么flushPassiveEffectsImpl又做了什么。 最重要的就是commitPassiveUnmountEffectscommitPassiveMountEffects。 接下去会重点介绍这两个函数
js 复制代码
function flushPassiveEffectsImpl() {
  ..... 
  // 这里就是主流程了
  commitPassiveUnmountEffects(root.current);
  commitPassiveMountEffects(root, root.current, lanes, transitions);
 ....
  flushSyncCallbacks();
  return true;
}

处理destory

commitPassiveUnmountEffects作为入口函数, 这里的写法很像commit阶段的beforMutationlayout阶段。 都是以一个函数xxxEffects为入口, 然后使用xx_beignxx_complete函数去搜索Fiber树从而能够在目标节点通过xxxOnFiber执行一些操作的过程

我称之为React常见四件套

该函数就是一个入口函数

js 复制代码
export function commitPassiveUnmountEffects(firstChild: Fiber): void {
  nextEffect = firstChild;
  commitPassiveUnmountEffects_begin();
}

如果想了解xxbeiginxxxcomplete的流程可以看 beforeMutation总结 有图解。

这里的commitPassiveUnmountEffects_begin做了更多的事情, 为了整体性(而且涉及后面需要具体讲的逻辑), 故这部分放在后面讲

那么我们就只需要看两点

  • 什么节点是目标节点
  • 目标节点做了什么事情

对于第一点我们看xxcomplete, 作为commitPassiveUnmountOnFiber的入口函数, 这里限制了flags上有Passive, 可以回顾之前的mountEffectupdateEffect, 我们给节点打上的flags就有Passvive

js 复制代码
  if ((fiber.flags & Passive) !== NoFlags) {
      commitPassiveUnmountOnFiber(fiber);
  }

对于第二点, 我们回归关注commitPassiveUnmountOnFiber。 这里通过判断了组件函数, 走入commitHookEffectListUnmount

js 复制代码
function commitPassiveUnmountOnFiber(finishedWork: Fiber): void {
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent: {
        .... 
        commitHookEffectListUnmount(
          HookPassive | HookHasEffect,
          finishedWork,
          finishedWork.return,
        );
        ...
      break;
    }
  }
}

这个函数就是最终真正处理Unmount的核心函数了。 可以看到这里就是拿到当前Fiber上的updateQueue, 然后从头开始, 进行遍历, 对于每一个effect, 拿出来effectdestroy,然后清空了, 再调用safelyCallDestroy(其实就是执行destroy函数, 之所以命名为safelyxx就是因为里面多了try catch去捕获错误)去处理

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
        const destroy = effect.destroy;
        effect.destroy = undefined;
        if (destroy !== undefined) {
          safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

无奖竞猜: 初次执行调用flushPassiveEffects走到这里会执行destory

答案是不会的。 你可以回看mountEffect, 我们在挂载到hook链表上的时候, 此时存入的destroyundefined

小总结

commitPassiveUnmountEffects 函数主要流程就是找到Fiber树上带有Passive的节点, 取出它的destroy函数, 然后执行。 并且清空destroy函数

执行完commitPassiveUnmountEffects的下一步就是执行commitPassiveMountEffects, 我们继续看

处理create

commitPassiveMountEffects作为入口函数, 可以看到这里的流程也是经典四件套

我们还是关注两个点

  • 什么节点是目标节点
  • 目标节点做了什么事情

对于第一点, 看commitPassiveMountEffects_complete函数。 可以看到这里依旧是判断了flags上带有Passive

js 复制代码
if ((fiber.flags & Passive) !== NoFlags) {
     commitPassiveMountOnFiber(
       root,
       fiber,
       committedLanes,
       committedTransitions,
     );
}

对于第二个点, 看commitPassiveMountOnFiber函数。 这里的逻辑还是相当多的, 对Fiber tag进行了判断, 其中对函数组件, 根组件等都有不同的处理。 这里关注函数组件。 可以看到调用了函数commitHookEffectListMount

js 复制代码
function commitPassiveMountOnFiber(
  finishedRoot: FiberRoot,
  finishedWork: Fiber,
  committedLanes: Lanes,
  committedTransitions: Array<Transition> | null,
): void {
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent: {
     ...
      commitHookEffectListMount(HookPassive | HookHasEffect, finishedWork);
      break;
    }
    case HostRoot: ....
    case LegacyHiddenComponent:
    case OffscreenComponent:....
    case CacheComponent: ....
      break;
    }
  }
}

继续看commitHookEffectListMount。 可以看到这里的逻辑有点类似于commitHookEffectListUnmount。 也是去找到Fiber节点的updateQueue, 然后遍历他, 拿到每一个Effect上的create()函数, 这也就是我们传入给useEffect的回调函数, 拿到之后对他进行调用, 调用的结果再赋值给Effectdestory

js 复制代码
function commitHookEffectListMount(flags: HookFlags, finishedWork: Fiber) {
  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) {
        // Mount
        const create = effect.create;
        effect.destroy = create();
      }
      .....
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}
小总结

这个函数的作用就是拿到Fiber上的updateQueue, 然后执行每一个Effect上的create()函数, 并且把执行结果赋值给Effect上的destory

到这里就能解答为什么我们在mountEffect的时候不直接给effectdestory进行赋值 如果一开始就直接赋值的话(而且你要赋值的话也得调用吧, 这调用时间也不合理hhh), 那么我们在初次处理useEffect的时候就会去调用destory函数了。 这显然不符合destory的定义

那我们对destory的定义是什么: 在每次依赖项变更重新渲染后,React 将首先使用旧值运行cleanup函数,然后使用新值运行setup函数

那怎么做到旧值执行destory, 新值执行create函数的呢

OK, 你以为走到这里就结束了? 别忘记我们上面跳过的commitPassiveUnmountEffects_begin

处理卸载的destory

commitPassiveUnmountEffects_begin作为入口函数

这里主要是对删除的节点进行额外的处理。 为什么呢,要考虑到当我们一个组件要被卸载的时候, 其useEffectcleanup函数应该都被执行一次。 这一块的逻辑目的就是这个

render阶段生成Fiber树的时候就对删除的节点进行了处理, 具体可以看recursivelyTraverseMutationEffects(看截图就够了, 链接中写的是commit阶段中的mutation阶段对打上删除flags节点的进行删除真实DOM节点的处理)

那我们根据上面的思路来说: 推测这里的思路应该就是找到被删除的Fiber节点, 然后拿到他的updateQueue, 如果updateQueue不为空的话, 就遍历每一个effect对象, 拿到destory函数进行执行

接下去看源码和我们推测的一不一样

js 复制代码
function commitPassiveUnmountEffects_begin() {
  while (nextEffect !== null) {
    const fiber = nextEffect;
    const child = fiber.child;
    // 子节点有删除节点, ChildDeletion就是当子节点被删除时会打上的flags
    // 这一块就是挨个拿到删除的节点,然后调用commitPassiveUnmountEffectsInsideOfDeletedTree_begin
    if ((nextEffect.flags & ChildDeletion) !== NoFlags) {
      const deletions = fiber.deletions;
      if (deletions !== null) {
        for (let i = 0; i < deletions.length; i++) {
          const fiberToDelete = deletions[i];
          nextEffect = fiberToDelete;
          commitPassiveUnmountEffectsInsideOfDeletedTree_begin(
            fiberToDelete,
            fiber,
          );
        }
        // 断开Child链和Sibling链
         const previousFiber = fiber.alternate;
          if (previousFiber !== null) {
            let detachedChild = previousFiber.child;
            if (detachedChild !== null) {
              previousFiber.child = null;
              do {
                const detachedSibling = detachedChild.sibling;
                detachedChild.sibling = null;
                detachedChild = detachedSibling;
              } while (detachedChild !== null);
            }
          }
        nextEffect = fiber;
      }
    }
    // 这里删除了无关逻辑....
  }
}

进来了commitPassiveUnmountEffectsInsideOfDeletedTree_begin 之后会顺着他的Child链和Sibling链(逻辑位于commitPassiveUnmountEffectsInsideOfDeletedTree_complete)调用commitPassiveUnmountInsideDeletedTreeOnFiber。 这个函数内部其实本质上就是调用了commitHookEffectListUnmountcommitHookEffectListUnmount就是处理destory的核心函数. 也就是说和我们推测的流程是一致的。

js 复制代码
function commitPassiveUnmountEffectsInsideOfDeletedTree_begin(
  deletedSubtreeRoot: Fiber,
  nearestMountedAncestor: Fiber | null,
) {
  while (nextEffect !== null) {
    const fiber = nextEffect;
    commitPassiveUnmountInsideDeletedTreeOnFiber(fiber, nearestMountedAncestor);
    const child = fiber.child;
    if (child !== null) {
      child.return = fiber;
      nextEffect = child;
    } else {
      commitPassiveUnmountEffectsInsideOfDeletedTree_complete(
        deletedSubtreeRoot,
      );
    }
  }
}

总结

我们梳理一下useEffect的相关流程

首先是初次渲染阶段

  • render阶段的时候, 此时调用的是mountEffect, 会形成hook对象挂上hook链表。 并且形成Effect链表, udpateQueue存放对应的Effect链表。还会给Fiber打上Passiveflag
  • commit阶段的时候,在准备阶段利用MessageChannel, 让处理useEffect的流程异步执行, 也就是推到下一个宏任务处理
  • 异步处理useEffect
    • 首先先处理被删除的节点执行destory函数(这里一般没有的)
    • 然后找到打上PassiveFiber节点(所有使用到useEffect的节点都会有该标签)
    • 然后会先处理destory, 因为此时destoryundefined, 故不处理。
    • 然后再处理create, 此时是遍历updateQueue上存放的effect 链表, 然后挨个执行create函数。 其执行结果再赋值给distory

(这也就是为什么useEffect在初次渲染的时候会执行一次)

接着函数可能因其他时候产生了变动, 重新执行组件函数, 此时走入了update流程

  • render阶段的时候, 此时调用的是updateEffect, 对前后的deps数组进行浅比较的判断, 如果前后依赖一致的话则不处理, 不一致的话则给当前的Fiber打上Passiveflag
  • commit阶段和初次渲染一致
  • 异步处理useEffect
    • 首先先处理被删除的节点执行destory函数
    • 然后找到打上PassiveFiber节点(这里只有依赖改变了的节点才会打)
    • 然后对于目标节点会先处理destory, 此时因为初次渲染/上一次commit处理过, 所以如果代码中有的话这里一般都会有, 也是遍历updateQueue上的Effect链表, 拿到destory函数进行执行
    • create处理同初次渲染

二、useLayoutEffect

mountLayoutEffect

还是先看mountLayoutEffect。诶你会发现, 这里和mountEffect的逻辑也基本一致, 都是调用了mountEffectImpl, 最大的区别就是这里打上的hook flagsHookLayout

js 复制代码
function mountLayoutEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  let fiberFlags: Flags = UpdateEffect;
  if (enableSuspenseLayoutEffectSemantics) {
    fiberFlags |= LayoutStaticEffect;
  }
  return mountEffectImpl(fiberFlags, HookLayout, create, deps);
}

udpateLayoutEffect

再看udpateLayoutEffect, 也是直接调用的updateEffectImpl。 只不过他传入的flags还是HookLayout

js 复制代码
function updateLayoutEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  return updateEffectImpl(UpdateEffect, HookLayout, create, deps);
}

既然是调用的一样的函数, 那么就说明他形成的hook对象, effect链都是一致的逻辑, 包括updateQueue。 体现不一样的就是每一个effect对象中的tag。 这个tag跟我们传入的flags有关

你会发现我们上面讲述的React如何处理useEffect的过程都是依赖着Passive的标签去寻找, 那么我们commit阶段处理useLayoutEffect的逻辑自然就不一样了

commit阶段是在mutation阶段去处理useLayoutEffect上的destory函数。 在layout阶段处理useLayoutEffect上的create函数

处理destory

对于destory函数, 其实本质上调用的还是commitPassiveUnmountOnFiber。 只不过和useEffect调用时机不一样

deps变更导致

对于deps产生变更需要执行的destory函数的执行路线是

commitMutationEffects(进入mutation阶段)->

commitMutationEffectsOnFiber(这里通过switch case tag进行筛选到FunctionComponent之后又通过筛选了fiber flags是否存在Update) ->

commitHookEffectListUnmount(这个在上面已经有讲解相关逻辑了, 注意这里传入的flagsLayout | HasEffect) ->

safelyCallDestroy(执行destory函数)

卸载导致

对于删除节点的话, 这里已经有详细讲解了。 调用逻辑就是

commitMutationEffects(进入mutation阶段)->

commitMutationEffectsOnFiber -> ( 这里无论Switch Case tag到哪个都一定会进入下一步, 上面deps的变更没有写是因为不重要hhh, 反正又会递归回来) ->

recursivelyTraverseMutationEffects (这个的第一步目标就是处理删除节点)->

commitDeletionEffects(删除节点流程的入口函数) ->

commitDeletionEffectsOnFiber (通过Switch Case Fiber tag定位到FunctionComponent然后筛选 effect tagLayout | HasEffectFunctionComponent 这里有详细讲解) ->

safelyCallDestroy

处理create

这个逻辑和useEffect的调用也是一致的, 不过调用它的地方在commitlayout阶段。 以 commitLayoutEffectOnFiber 为入口, 调用了commitHookEffectListMountuseEffect就是调用它处理的)。

不同的就是传入的参数不同, useEffect调用的时候传入的flagsHookPassive | HookHasEffectuseLayoutEffect调用的时候传入的flagsHookLayout | HookHasEffectcommitHookEffectListMount内部就是通过传入的flags进行识别调用的

总结

useLayoutEffectuseEffect的处理逻辑基本都是一样的。 唯一不同的就是执行的时间。 基于flags的区别的做到一样的逻辑达到两种效果。

这里再总结一波加深印象

useLayoutEffectdestory函数执行时机在commit阶段的mutation阶段, create函数执行时间在commit阶段的layout阶段

useEffectdestory函数和create函数都是利用MessageChannel达到在渲染之后的宏任务进行。 同步顺序执行就是先destorycreate

三、useInsertionEffect

mountInsertionEffect

可以看到也是调用了mountEffectImpl, 只不过传入的hook flagsHookInsertion

js 复制代码
function mountInsertionEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  return mountEffectImpl(UpdateEffect, HookInsertion, create, deps);
}

updateInsertionEffect

可以看到也是调用了updateEffectImpl,只不过传入的hook flagsHookInsertion

js 复制代码
function updateInsertionEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  return updateEffectImpl(UpdateEffect, HookInsertion, create, deps);
}

处理destory

其实我们在了解useLayoutEffect的时候已经接触了, 跟useLayout的调用时间是一样的

卸载导致

一样在commitDeletionEffectsOnFiber中,如图所示

deps变更导致

一样在commitMutationEffectsOnFiber中如图所示

处理create

hhh你看上面那个图, 其实已经处理了, 调用了commtHookEffectListUnmount处理destory之后就调用了commitHookEffectListMount去处理create

总结

可以看到, 其实上面三种hook都是依赖着flags在实现在不同时机去执行逻辑。

  • 对于useInsertionEffect来说flagInsertion
  • 对于useLayoutEffect来说flagLayout
  • 对于useEffect来说flagPassive

我们把图再更新一下

四、useImperativeHandle

也许你会好奇, 诶我怎么把useImperativeHandle放在这里讲, 这一篇不是在将effect相关的吗。 别急, 你往下看看

我之前出过一篇文章讲过useImperativeHandle的用法, 不了解的可以看 useImperativeHandle

诶你会发现这不就类似于Effect的逻辑, 在依赖变更之后, 去执行一些操作。 让我们探究一下是不是这回事

Ok, 接下去按照我们的套路先看mountImperativeHandle

mountImperativeHandle

你会发现, 他依旧使用了mountEffectImpl。 且他的hook flags传入的是HookLayout。 故我们就了解了他的调用时机 。 这里传入的create函数是imperativeHandleEffect。 然后将我们传入的refcreate作为参数传入

js 复制代码
function mountImperativeHandle<T>(
  ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void,
  create: () => T,
  deps: Array<mixed> | void | null,
): void {
 // 这里对deps进行了处理, 将ref也放入deps中
  const effectDeps =
    deps !== null && deps !== undefined ? deps.concat([ref]) : null;

  let fiberFlags: Flags = UpdateEffect;
  if (enableSuspenseLayoutEffectSemantics) {
    fiberFlags |= LayoutStaticEffect;
  }
  return mountEffectImpl(
    fiberFlags,
    HookLayout,
    imperativeHandleEffect.bind(null, create, ref),
    effectDeps,
  );
}

imperativeHandleEffect又是什么呢。 可以看到基本逻辑是通过调用传入的create函数, 赋值给ref。 然后处理了destory函数, 在更新的时候能够重置ref值。

js 复制代码
function imperativeHandleEffect<T>(
  create: () => T,
  // 传入的ref可以是ref对象也可以是一个函数
  ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void,
) {
  if (typeof ref === 'function') {
    const refCallback = ref;
    const inst = create(); // 拿到暴露给父组件的ref对象
    refCallback(inst); // 作为参数传入ref函数
    return () => {
      refCallback(null);  // 这是destory函数,给他重置
    };
  } else if (ref !== null && ref !== undefined) {
    const refObject = ref;
    const inst = create(); // 拿到暴露给父组件的ref对象
    refObject.current = inst;  // 给传入的ref赋值
    return () => {
      refObject.current = null;  // destory函数, 给他重置
    };
  }
}

updateImperativeHandle

这里的逻辑没什么好讲了, 跟mountImperativeHandle差不多

js 复制代码
function updateImperativeHandle<T>(
  ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void,
  create: () => T,
  deps: Array<mixed> | void | null,
): void {
  const effectDeps =
    deps !== null && deps !== undefined ? deps.concat([ref]) : null;
  return updateEffectImpl(
    UpdateEffect,
    HookLayout,
    imperativeHandleEffect.bind(null, create, ref),
    effectDeps,
  );
}

useImperativeHandle的一整个调用逻辑和useLayoutEffect都很类似。 区别就在于,useLayoutEffectcreate函数和destory函数是我们传入的参数。 而useImperativeHandlecreate函数和destory函数都是固定的

结束语

React hook的相关逻辑就到这一篇结束, 三篇下来我们一共了解了11个hook原理, 基本覆盖了常见的hook。 相信你看完这三篇也能够有一定收获, 对于什么场景使用什么hook也有了一定的了解。

相关推荐
passerby60613 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了3 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅3 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅3 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅4 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment4 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅4 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊4 小时前
jwt介绍
前端
爱敲代码的小鱼4 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax
Cobyte5 小时前
AI全栈实战:使用 Python+LangChain+Vue3 构建一个 LLM 聊天应用
前端·后端·aigc