Vue 生命周期钩子 源码解析

Vue 生命周期钩子 源码解析

引入

笔者最近想给自己的一个开源库增加生命周期钩子,从而能够让开发者在特定阶段运行自己的代码,增加库的可扩展性。于是我学习了Vue3生命周期钩子的源码,对其产生了一些理解和思考。

生命周期钩子(以下简称钩子函数)的源码主要分为两部分:

  • 注入。注入过程不会调用钩子函数的回调,只是把用户传入的回调注册到组件实例上
  • 触发。这部分会根据钩子函数的类别,以同步或异步的方式调用钩子函数的回调

本文将主要介绍钩子函数的注入和触发过程,并回答以下几个主要问题

  1. 钩子函数是如何注入到组件实例当中的
  2. 在组件实例的生命周期当中,钩子函数是如何触发

本文最后写了我的一点思考与总结。

注入

这部分的代码都位于runtime-core/src/apiLifecycle.ts中。

export const createHook =
  <T extends Function = () => any>(lifecycle: LifecycleHooks) =>
  (hook: T, target: ComponentInternalInstance | null = currentInstance) =>
    // post-create lifecycle registrations are noops during SSR (except for serverPrefetch)
    (!isInSSRComponentSetup || lifecycle === LifecycleHooks.SERVER_PREFETCH) &&
    injectHook(lifecycle, (...args: unknown[]) => hook(...args), target)

export const onBeforeMount = createHook(LifecycleHooks.BEFORE_MOUNT)
export const onMounted = createHook(LifecycleHooks.MOUNTED)
export const onBeforeUpdate = createHook(LifecycleHooks.BEFORE_UPDATE)
export const onUpdated = createHook(LifecycleHooks.UPDATED)
export const onBeforeUnmount = createHook(LifecycleHooks.BEFORE_UNMOUNT)
export const onUnmounted = createHook(LifecycleHooks.UNMOUNTED)

通过查看源码我们可以发现,常用的钩子函数(如onBeforeMountonMounted等)都是通过createHook函数得到,当我们调用钩子函数时,实际上并没有执行我们传给钩子函数的回调,而是执行了injectHook函数,把回调注册到了组件实例上

export function injectHook(
  type: LifecycleHooks,
  hook: Function & { __weh?: Function },
  target: ComponentInternalInstance | null = currentInstance,
  prepend: boolean = false
): Function | undefined {
  if (target) {
    const hooks = target[type] || (target[type] = [])
    // cache the error handling wrapper for injected hooks so the same hook
    // can be properly deduped by the scheduler. "__weh" stands for "with error
    // handling".
    const wrappedHook =
      hook.__weh ||
      (hook.__weh = (...args: unknown[]) => {
        if (target.isUnmounted) {
          return
        }
        // disable tracking inside all lifecycle hooks
        // since they can potentially be called inside effects.
        pauseTracking()
        // Set currentInstance during hook invocation.
        // This assumes the hook does not synchronously trigger other hooks, which
        // can only be false when the user does something really funky.
        setCurrentInstance(target)
        const res = callWithAsyncErrorHandling(hook, target, type, args)
        unsetCurrentInstance()
        resetTracking()
        return res
      })
    if (prepend) {
      hooks.unshift(wrappedHook)
    } else {
      hooks.push(wrappedHook)
    }
    return wrappedHook
  }
  // 省略一些不重要的代码
}

injectHook的代码看上去多,实际上做的事情非常清晰:

  • 首先拿到target(也就是组件实例)的对应类型的钩子函数的数组,比如目前注入的是onMounted钩子,对应类型m,那么我们先拿到hooks = target['m'],对应目前已经注册的onMounted钩子
  • 然后把hook(也就是我们传给钩子函数的回调)和错误处理的内容包裹到了一起,从而得到wrappedHook,再把wrappedHook放到hooks当中

至此,我们就回答了“钩子函数是如何注入到组件实例当中的”这个问题:实际上就是把钩子函数的回调分类放到了实例的hooks数组中,从而可以在组件的生命周期对回调进行触发。

触发

按生命周期来分,不同的钩子函数回调的触发时机不同,例如:

  • onBeforeMountonMounted:触发时机在mountComponent阶段,具体代码位于renderer.tssetupRenderEffect函数中
  • onBeforeUpdateonUpdated:触发时机在updateComponent阶段,代码同样位于setupRenderEffect函数中
  • onBeforeUnmountonUnmounted:触发时机在unmountComponent阶段,代码位于unmountComponent函数中

按触发方式来分,可以分为同步和异步两类:

  • 同步:onBeforeMountonBeforeUpdateonBeforeUnmount
  • 异步:onMountedonUpdatedonUnmounted

由于钩子函数回调的触发具有相似性,以下仅介绍onBeforeMountonMounted,其他钩子函数的原理是一样的。

// 仅保留了钩子函数相关代码
const componentUpdateFn = () => {
  if (!instance.isMounted) {
    const { bm, m, parent } = instance
    // beforeMount hook
    if (bm) {
      invokeArrayFns(bm)
    }
    // mounted hook
    if (m) {
      queuePostRenderEffect(m, parentSuspense)
    }
  } else {
    // updateComponent
    let { next, bu, u, parent, vnode } = instance

    // beforeUpdate hook
    if (bu) {
      invokeArrayFns(bu)
    }
    // updated hook
    if (u) {
      queuePostRenderEffect(u, parentSuspense)
    }
  }
}

从代码可以看到,当实例还没有被挂载的情况下,组件实例上的bmm会被解构出来,这两个变量就是onBeforeMountedonMounted的钩子函数回调数组。

对于bminvokeArrayFns函数以同步的形式对bm中的回调进行调用

export const invokeArrayFns = (fns: Function[], arg?: any) => {
  for (let i = 0; i < fns.length; i++) {
    fns[i](arg)
  }
}

对于mqueuePostRenderEffect函数以异步的形式对m中的回调进行调用。如果想要了解queuePostRenderEffect的具体内容,建议去整体性地看一下scheduler.ts的源码,了解vue3的调度机制,我之前的文章也有介绍过该部分的原理和源码。简单总结的话就是:

  • vue中的异步任务在执行时存在顺序关系,从先到后分别是pre > DOM更新 > post > nextTick
  • 通过调用queuePostRenderEffectm会被添加到post这个时机的任务队列当中,从而异步地在post时机被执行

那么,为什么要如此复杂呢?直接同步调用m中的回调不可以吗,为什么要用异步的方式在post时机调用?

之所以这样做,是因为watch和计算属性的更新时机位于pre阶段,如果同步调用m的回调,就会导致在onMounted回调中无法访问到最新的值。并且onMounted本身也意味着DOM挂载后这个时点,因此理应在位于DOM更新时点之后的post节点,onUpdatedonUnmounted也是同理。

至此,我们回答了问题“在组件实例的生命周期当中,钩子函数是如何触发的”:实际上就是在组件实例生命周期的特定阶段,把实例上的钩子函数回调数组拿到,并且以同步/异步的方式进行调用。

思考与总结

1. 注入与调度的设计

生命周期钩子实际上从其自身原理上来说,并不是很复杂。但是这部分代码的逻辑层次很清晰:

  • 首先,关注点分离到了不同的文件当中。注入逻辑放在生命周期逻辑相关文件中,调用行为放在组件实例逻辑的文件中,具体的调度逻辑放在调度器的文件当中。
  • 其次,这种设计方式利用了已有的底层机制(调度器),只为组件实例添加了一个属性(就是hooks数组),对已有的代码结构完全没有破坏性,就实现了生命周期钩子。

这种使得代码具有可扩展性的代码组织方式和功能模块的设计方式是我值得学习的。

2. 变量名

vue的变量名和函数名的设计非常简洁而准确,很多概念的抽象也有助于理解,值得学习。

3. 闭包变量

vue使用了大量的由其他模块导入的变量,作为闭包变量来保存全局状态使用。

这部分主要使用了currentInstance用来保存当前活跃的实例,这样生命周期钩子就不用传入target来指定组件实例了,直接引用闭包变量即可。这一点之前的源码没注意到,这次学习到了。

全部评论

相关推荐

不愿透露姓名的神秘牛友
12-17 16:48
今天九点半到公司,我跟往常一样先扫了眼电脑,屁活儿没有。寻思着没事干,就去蹲了个厕所,回来摸出手机刷了会儿。结果老板刚好路过,拍了我一下说上班别玩手机,我吓得赶紧揣兜里。也就过了四十分钟吧,我的直属领导把我叫到小隔间,上来就给我一句:“你玩手机这事儿把老板惹毛了,说白了,你可以重新找工作了,等下&nbsp;HR&nbsp;会来跟你谈。”&nbsp;我当时脑子直接宕机,一句话都没憋出来。后面&nbsp;HR&nbsp;找我谈话,直属领导也在旁边。HR&nbsp;说我这毛病不是一次两次了,属于屡教不改,不光上班玩手机,还用公司电脑看论文、弄学校的事儿。我当时人都傻了,上班摸鱼是不对,可我都是闲得发慌的时候才摸啊!而且玩手机这事儿,从来没人跟我说过后果这么严重,更没人告诉我在公司学个习也算犯错!连一次口头提醒都没有,哪儿来的屡教不改啊?更让我膈应的是,昨天部门刚开了会,说四个实习生里留一个转正,让大家好好表现。结果今天我就因为玩手机被开了。但搞笑的是,开会前直属领导就把我叫去小会议室,明明白白告诉我:“转正这事儿你就别想了,你的学历达不到我们部门要求,当初招你进来也没打算给你这个机会。”合着我没入贵厂的眼是吧?可我都已经被排除在转正名单外了,摸个鱼至于直接把我开了吗?真的太离谱了!
rush$0522:转正名单没进,大概率本来就没打算留你
摸鱼被leader发现了...
点赞 评论 收藏
分享
程序员花海:实习和校招简历正确格式应该是教育背景+实习+项目经历+个人评价 其中项目经历注意要体现业务 实习经历里面的业务更是要自圆其说 简历模板尽可能保持干净整洁 不要太花哨的
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务