likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 17

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

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

一、useEffect

mountEffect

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

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。 这里一共做了三步, 具体可看注释

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

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变成啥样啦

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>
  )
}

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

updateEffect

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

function updateEffect(create, deps) {
  return updateEffectImpl(Passive, Passive$1, create, deps);
}
  • 直接看updateEffectImpl。 这里的流程也很清晰, 就不赘述了
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标签

如何处理useEffect(重点)

异步原理

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

总的流程就是 commit阶段(宏任务) - 浏览器渲染 - useEffect相关回调执行(宏任务) React-hook源码阅读(下) - 一文搞定useEffect及其他effect hook

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

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

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

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

处理destory

我称之为React常见四件套

React-hook源码阅读(下) - 一文搞定useEffect及其他effect hook 该函数就是一个入口函数

export function commitPassiveUnmountEffects(firstChild: Fiber): void {
  nextEffect = firstChild;
  commitPassiveUnmountEffects_begin();
}

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

那么我们就只需要看两点

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

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

  if ((fiber.flags & Passive) !== NoFlags) {
      commitPassiveUnmountOnFiber(fiber);
  }

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

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去捕获错误)去处理

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作为入口函数, 可以看到这里的流程也是经典四件套

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

我们还是关注两个点

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

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

if ((fiber.flags & Passive) !== NoFlags) {
     commitPassiveMountOnFiber(
       root,
       fiber,
       committedLanes,
       committedTransitions,
     );
}

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

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

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函数的呢

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

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

处理卸载的destory

commitPassiveUnmountEffects_begin作为入口函数

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

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

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

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的核心函数. 也就是说和我们推测的流程是一致的。

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

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

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函数)

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

卸载导致

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

commitMutationEffects(进入mutation阶段)->

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

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

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

safelyCallDestroy

处理create

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

总结

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

这里再总结一波加深印象

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

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

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

三、useInsertionEffect

mountInsertionEffect

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

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

updateInsertionEffect

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

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

处理destory

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

卸载导致

一样在commitDeletionEffectsOnFiber中,如图所示 React-hook源码阅读(下) - 一文搞定useEffect及其他effect hook

deps变更导致

一样在commitMutationEffectsOnFiber中如图所示

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

处理create

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

总结

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

  • 对于useInsertionEffect来说flagInsertion
  • 对于useLayoutEffect来说flagLayout
  • 对于useEffect来说flagPassive React-hook源码阅读(下) - 一文搞定useEffect及其他effect hook

我们把图再更新一下

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

四、useImperativeHandle

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

React-hook源码阅读(下) - 一文搞定useEffect及其他effect hook 诶你会发现这不就类似于Effect的逻辑, 在依赖变更之后, 去执行一些操作。 让我们探究一下是不是这回事

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

mountImperativeHandle

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

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值。

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差不多

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也有了一定的了解。

转载自:https://juejin.cn/post/7304540179183370255
评论
请登录