在 Vue.js 3 中,有一部分组合式 API 是用来注册生命周期钩子函数 的,例如 onMounted、onUpdated 等,如下面的代码所示:
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 以外的生命周期钩子函数,其原理同上。