12.8 注册生命周期

81 阅读3分钟

在 Vue.js 3 中,有一部分组合式 API 是用来注册生命周期钩子函数 的,例如 onMountedonUpdated 等,如下面的代码所示:

import { onMounted } from "vue"

const MyComponent = {
  setup() {
    onMounted(() => {
      console.log("mounted 1")
    })
    // 可以注册多个
    onMounted(() => {
      console.log("mounted 2")
    })

    // ...
  },
}

setup 函数中调用 onMounted 函数即可注册 mounted 生命 周期钩子函数,并且可以通过多次调用 onMounted 函数来注册多个 钩子函数,这些函数会在组件被挂载之后再执行。这里的疑问在于, 在 A 组件的 setup 函数中调用 onMounted 函数会将该钩子函数注册 到 A 组件上;而在 B 组件的 setup 函数中调用 onMounted 函数会将 钩子函数注册到 B 组件上,这是如何实现的呢?实际上,我们需要维 护一个变量 currentInstance,用它来存储当前组件实例,每当初 始化组件并执行组件的 setup 函数之前,先将 currentInstance 设置为当前组件实例,再执行组件的 setup 函数,这样我们就可以通 过 currentInstance 来获取当前正在被初始化的组件实例,从而将 那些通过 onMounted 函数注册的钩子函数与组件实例进行关联。

接下来我们着手实现。首先需要设计一个当前实例的维护方法, 如下面的代码所示:

// 全局变量,存储当前正在被初始化的组件实例
let currentInstance = null
// 该方法接收组件实例作为参数,并将该实例设置为 currentInstance
function setCurrentInstance(instance) {
  currentInstance = instance
}

有了 currentInstance 变量,以及用来设置该变量的 setCurrentInstance 函数之后,我们就可以着手修改 mounteComponent 函数了,如下面的代码所示:

function mountComponent(vnode, container, anchor) {
  // 省略部分代码

  const instance = {
    state,
    props: shallowReactive(props),
    isMounted: false,
    subTree: null,
    slots,
    // 在组件实例中添加 mounted 数组,用来存储通过 onMounted 函数注册的生命周期钩子函数
    mounted: [],
  }

  // 省略部分代码

  // setup
  const setupContext = { attrs, emit, slots }

  // 在调用 setup 函数之前,设置当前组件实例
  setCurrentInstance(instance)
  // 执行 setup 函数
  const setupResult = setup(shallowReadonly(instance.props), setupContext)
  // 在 setup 函数执行完毕之后,重置当前组件实例
  setCurrentInstance(null)

  // 省略部分代码
}

上面这段代码以 onMounted 函数为例进行说明。为了存储由 onMounted 函数注册的生命周期钩子,我们需要在组件实例对象上添 加 instance.mounted 数组。之所以 instance.mounted 的数据 类型是数组,是因为在 setup 函数中,可以多次调用 onMounted 函 数来注册不同的生命周期函数,这些生命周期函数都会存储在 instance.mounted 数组中。

现在,组件实例的维护已经搞定了。接下来考虑 onMounted 函 数本身的实现,如下面的代码所示:

function onMounted(fn) {
  if (currentInstance) {
    // 将生命周期函数添加到 instance.mounted 数组中
    currentInstance.mounted.push(fn)
  } else {
    console.error("onMounted 函数只能在 setup 中调用")
  }
}

可以看到,整体实现非常简单直观。只需要通过 currentInstance 取得当前组件实例,并将生命周期钩子函数添加 到当前实例对象的 instance.mounted 数组中即可。另外,如果当 前实例不存在,则说明用户没有在 setup 函数内调用 onMounted 函 数,这是错误的用法,因此我们应该抛出错误及其原因。

最后一步需要做的是,在合适的时机调用这些注册到 instance.mounted 数组中的生命周期钩子函数,如下面的代码所 示:

function mountComponent(vnode, container, anchor) {
  // 省略部分代码

  effect(
    () => {
      const subTree = render.call(renderContext, renderContext)
      if (!instance.isMounted) {
        // 省略部分代码

        // 遍历 instance.mounted 数组并逐个执行即可
        instance.mounted &&
          instance.mounted.forEach((hook) => hook.call(renderContext))
      } else {
        // 省略部分代码
      }
      instance.subTree = subTree
    },
    {
      scheduler: queueJob,
    }
  )
}

可以看到,我们只需要在合适的时机遍历 instance.mounted 数组,并逐个执行该数组内的生命周期钩子函数即可。

对于除 mounted 以外的生命周期钩子函数,其原理同上。