Vue3生命周期详解
前言
版本:3.0.2
说明:通过源码对Vue3的生命周期进行阐述,包含用法、使用说明、源码介绍等。
一、开局一张图
什么是生命周期?
关于这个问题,我觉得很好理解。
就是在不同的时期调用不同的钩子,就像人一样,在不同的年龄段,会做不同的事;但有些钩子需要在特殊的条件下才能触发,就像人一样,做了某件事,而这件事会引发怎样的后果。
二、各个生命周期的用法
1、页面初始化时,直接触发
涉及钩子:beforeCreate
、created
、beforeMount
、renderTracked
、mounted
使用方式:
Vue.createApp({ // 此时data还不可用 beforeCreate() { console.log("beforeCreate"); }, // data可用,DOM不可用 created() { console.log("created"); }, // 在这个钩子后,mounted生命周期钩子之前,render函数(渲染页面函数)首次被调用 beforeMount() { console.log("beforeMount"); }, // 页面有取值操作时(如:绑定数据,e.g. 插值语法{{ count }})触发 renderTracked({ type, key, target, effect }) { console.log("renderTracked ----", { type, key, target, effect }); }, // 页面挂载完毕后触发 mounted() { console.log("mounted"); } }).mount("#app"); 复制代码
输出:
beforeCreate created beforeMount renderTracked ---- {type: "get", key: "count", target: {...}, effect: f} mounted 复制代码
Tip:Vue3.x新增生命周期
renderTracked
说明
官方解释:跟踪虚拟DOM重新渲染时调用(初始化渲染时也会调用)。钩子接收debugger event
作为参数。此事件告诉你哪个操作跟踪了组件以及该操作的目标对象和键。
简单理解来说就是:页面上绑定了响应式数据(取值),就会触发该操作。
举个栗子
<div id="app"> <div>{{ count }}</div> <button @click="addCount">加1</button> </div> 复制代码
Vue.createApp({ methods: { addCount() { this.count += 1; } }, // 每次渲染时,都会触发`renderTracked`钩子。 renderTracked(e) { console.log("renderTracked ----", e); } }).mount("#app"); 复制代码
输出:
renderTracked ---- {type: "get", key: "count", target: {...}, effect: f} 复制代码
debugger event
说明
type
:操作类型,有get
,has
,iterate
,也就是取值操作。
key
:键,简单理解就是操作数据的key
,e.g.
上文使用的count
。
target
:响应式对象,如:data
、ref
、computed
。
effect
:数据类型为Function
,英文单词意思为唤起
、执行
的意思。effect
方法的作用是重新render视图。
2、数据发生改变后触发
涉及钩子:renderTriggered
、beforeUpdate
、renderTracked
、updated
使用方式:
Vue.createApp({ // 改变数据(e.g. set) renderTriggered(e) { console.log("renderTriggered ----", e); }, /*--------- 在数据发生改变后,DOM被更新之前调用。 ----------*/ beforeUpdate() { console.log("beforeUpdate"); }, // 读取数据(e.g. get) renderTracked(e) { console.log("renderTracked ----", e); }, /*--------- DOM更新完毕之后调用。 注意事项:updated不会保证所有子组件也都被重新渲染完毕 ---------*/ updated() { console.log("updated"); } }).mount("#app"); 复制代码
输出:
renderTriggered ---- {target: {...}, key: "count", type: "set", newValue: 2, effect: f, oldTarget: undefined, oldValue: 1} beforeUpdate renderTracked ---- {target: {...}, type: "get", key: "count", effect: f} update 复制代码
Tip:Vue3.x新增生命周期
renderTriggered
说明
官方解释:当虚拟DOM重新渲染被触发时调用。接收debugger event
作为参数。此事件告诉你是什么操作触发了重新渲染,以及该操作的目标对象和键。
简单理解:做了某件事,从而引发了页面的重新渲染。
举个栗子
<div id="app"> <div>{{ count }}</div> <button @click="addCount">加1</button> </div> 复制代码
Vue.createApp({ methods: { addCount() { this.count += 1; } }, // 每次修改响应式数据时,都会触发`renderTriggered`钩子 renderTriggered(e) { console.log("renderTriggered ----", e); } }).mount("#app"); 复制代码
输出:
renderTriggered ---- {target: {...}, key: "count", type: "set", newValue: 2, effect: f, oldTarget: undefined, oldValue: 1} 复制代码
debugger event
说明
type
:操作类型,有set
、add
、clear
、delete
,也就是修改操作。
key
:键,简单理解就是操作数据的key。e.g.
上文使用的count
。
target
:响应式对象,如:data
、ref
、computed
。
effect
:数据类型为Function
。英文单词意思为唤起
、执行
的意思。effect
方法的作用是重新render视图。
newValue
:新值。
oldValue
:旧值。
oldTarget
:旧的响应式对象。
3、组件被卸载时触发
涉及钩子:beforeUnmount
、unmounted
模拟一下
通过v-if
来模拟子组件的销毁。
<div id="app"> <!-- 子组件 --> <child v-if="flag"></child> <button @click="unload">卸载子组件</button> </div> 复制代码
首先定义一个子组件,然后在页面中引用它,通过点击卸载子组件
按钮来销毁子组件。
const { defineComponent } = Vue; const child = defineComponent({ data() { return { title: "我是子组件" } }, template: `<div>{{ title }}</div>`, // 在卸载组件实例之前调用,在这个阶段,实例仍然是可用的。 beforeUnmount() { console.log("beforeUnmount"); }, // 卸载组件实例后调用。 unmounted() { console.log("unmounted"); } }); Vue.createApp({ components: { child }, data: () => { return { flag: true } }, methods: { unload() { this.flag = false; } } }).mount("#app"); 复制代码
点击按钮,卸载子组件,重新渲染页面。打开控制台,将会输出:
beforeUnmount unmounted 复制代码
4、捕获错误的钩子
涉及钩子:errorCaptured
Tip:Vue3.x新增生命周期
errorCaptured
说明
官方解释:在捕获一个来自后代组件的错误时被调用。此钩子会收到三个参数:错误对象、发送错误的组件实例以及一个包含错误来源信息的字符串。此钩子可以返回false
以阻止该错误继续向上传播。
模拟一下
在页面中引用。
<div id="app"> <!-- 父组件 --> <parent v-if="flag"></parent> </div> 复制代码
首先定义一个子组件和一个父组件,在页面中引用父组件,然后在父组件里引用子组件。
const { defineComponent } = Vue; const child = definedComponent({ data() { return { title: "我是子组件" } }, template: `<div>{{ title }}</div>`, setup() { // 这个方法未定义,直接调用将会引发报错 childSetupError(); return {}; } }); const parent = defineComponent({ components: { child }, data() { return { title: "渲染子组件:" } }, setup() { // 这个方法也没有定义,直接调用会引发报错 parentSetupError(); return {}; }, template: ` <div>{{ title }}</div> <child></child> `, errorCaputed(err, instance, info) { console.log("errorCaptured", err, instance, info); return false; } }); const app = Vue.createApp({ components: { parent }, errorCaptured(err, instance, info) { console.log("errorCaptured", err, instance, info); } }); app.config.errorHandler = (err, vm, info) => { console.log("configErrorHandler", err, vm, info); }; app.mount("#app"); 复制代码
errorCaptured
参数说明
err
:Error
错误对象;e.g.
ReferenceError: parentSetupError is not defined。
instance
:组件实例。
info
:捕获的错误信息。e.g.
setup function
errorCaptured
返回一个false
的理解
错误的传播规则是自上而下的,默认情况下,如果全局的config.errorHandler
被定义,所有错误最终都会传递到它那里,可以通过显式的使用return false
来阻止错误向上传递。
以上面的例子来解释:
在parent
组件的errorCaptured
钩子中return false
,child
组件的childSetupError
错误只会传递到parent
组件中,而parent
组件的parentSetupError
错误既传递到了Vue.createApp
初始化Vue实例中,也传递到了app.config.errorHandler
中。
所以,输出的顺序为:
errorCaptured ReferenceError: parentSetupError is not defined configErrorHandler ReferenceError: parentSetupError is not defined parentCaptured ReferenceError: childSetupError is not defined 复制代码
三、源码介绍
生命周期的钩子在Vue3源码中是怎样实现的呢?通过对下面的各个函数的介绍,一步步的理解生命周期的执行过程。因为有些函数涉及的代码比较多,在这里只截取有关生命周期的部分重要的代码进行介绍。
首先介绍applyOptions
函数。
applyOptions
函数的作用是应用程序员传递的options
,即createApp(options)
创建一个Vue
实例传递的options
function applyOptions(instance, options, deferredData = [], deferredWatch = [], deferredProvide = [], asMixin = false) { // ... // -------------------beforeCreate生命周期 callSyncHook('beforeCreate', "bc" /* BEFORE_CREATE */, options, instance, globalMixins); // ... // dataOptions => data if (dataOptions) { // 解析data对象,并将其转成响应式的对象 resolveData(instance, dataOptions, publicThis); } // ... // -------------------created生命周期 // 到了这一步,data可以使用了 callSyncHook('created', "c" /* CREATED */, options, instance, globalMixins); // ------------------注册hooks if (beforeMount) { // deforeMount onBeforeMount(beforeMount.bind(publicThis)); } if (mounted) { // mounted onMounted(mounted.bind(publicThis)); } if (beforeUpdate) { // beforeUpdate onBeforeUpdate(beforeUpdate.bind(publicThis)); } if (updated) { // updated onUpdated(updated.bind(publicThis)); } if (errorCaptured) { // errorCaptured onErrorCaptured(errorCaptured.bind(publicThis)); } if (renderTracked) { // renderTracked onRenderTracked(renderTracked.bind(publicThis)); } if (renderTriggered) { // renderTriggered onRenderTriggered(renderTriggered.bind(publicThis)); } // 这个钩子已被移除 if (beforeDestroy) { // beforeDestory被重命名为beforeUnmount了 warn(`\`beforeDestroy\` has been renamed to \`beforeUnmount\`.`); } if (beforeUnmount) { // beforeUnmount onBeforeUnmount(beforeUnmount.bind(publicThis)); } // 这个钩子已被移除 if (destroyed) { // destoryed被重命名为unmounted了 warn(`\`destroyed\` has been renamed to \`unmounted\`.`); } if (unmounted) { // unmounted onUnmounted(unmounted.bind(publicThis)); } } 复制代码
在applyOptions
函数中,可以看到,Vue3
使用callSyncHook
来执行我们定义的生命周期钩子,如beforeCreate
,下面我们来看看callSyncHook
函数。
// 同步执行hook function callSyncHook(name, type, options, instance, globalMixins) { // 触发全局定义的mixins callHookFromMixins(name, type, globalMixins, instance); const { extends: base, mixins } = options; if (base) { // 触发extends里面的hook callHookFromExtends(name, type, base, instance); } if (mixins) { // 触发我们自定义的mixins callHookFromMixins(name, type, mixins, instance); } // 自定义的hook // e.g. beforeCreate、created const selfHook = options[name]; if (selfHook) { callWithAsyncErrorHandling(selfHook.bind(instance.proxy), instance, type); } } 复制代码
通过callSyncHook
函数,我们可以知道,触发一个钩子函数,首先会触发Vue3
全局定义的mixins
中的钩子,如果extends存在
,或者mixins
存在,就会先触发这两个里面的生命周期钩子,最后才会查找我们在组件中定义的生命周期钩子函数。
下面我们来看一下callWithAsyncErrorHandling
函数。
// 用try catch包裹执行回调函数,以便处理错误信息 function callWithErrorHandling(fn, instance, type, args) { let res; try { res = args ? fn(...args) : fn(); } catch (err) { handleError(err, instance, type); } return res; } function callWithAsyncErrorHandling(fn, instance, type, args) { // 如果传入的fn是一个Function类型 if (isFunction(fn)) { // 执行fn const res = callWithErrorHandling(fn, instance, type, args); // 如果有返回值,且返回值为promise类型,则为这个返回值添加一个catch,以便捕捉错误信息 if (res && isPromise(res)) { res.catch(err => { handleError(err, instance, type); }); } return res; } // 如果传入的钩子为数组类型,则循环执行数组中的每一项,并返回执行的数组结果 const values = []; for (let i = 0; i < fn.length; i++) { values.push(callWithAsyncErrorHandling(fn[i], instance, type, args)); } return values; } 复制代码
在applyOptions
函数中,有两个钩子是直接触发的(beforeCreate
、created
),剩下的钩子都是通过先注入,然后触发。
注入
// 创建hook const createHook = (lifecycle) => (hook, target = currentInstance) => !isInSSRComponentSetup && injectHook(lifecycle, hook, target); const onBeforeMount = createHook("bm" /* BEFORE_MOUNT */); const onMounted = createHook("m" /* MOUNTED */); const onBeforeUpdate = createHook("bu" /* BEFORE_UPDATE */); const onUpdated = createHook("u" /* UPDATED */); const onBeforeUnmount = createHook("bum" /* BEFORE_UNMOUNT */); const onUnmounted = createHook("um" /* UNMOUNTED */); const onRenderTriggered = createHook("rtg" /* RENDER_TRIGGERED */); const onRenderTracked = createHook("rtc" /* RENDER_TRACKED */); const onErrorCaptured = (hook, target = currentInstance) => { injectHook("ec" /* ERROR_CAPTURED */, hook, target); }; // 注入hook,target为instance function injectHook(type, hook, target = currentInstance, prepend = false) { if (target) { // Vue实例上的钩子,e.g. instance.bm = [fn]; const hooks = target[type] || (target[type] = []); const wrappedHook = hook.__weh || (hook.__weh = (...args) => { if (target.isUnmounted) { return; } pauseTracking(); setCurrentInstance(target); // 触发已注入的钩子 const res = callWithAsyncErrorHandling(hook, target, type, args); setCurrentInstance(null); resetTracking(); return res; }); if (prepend) { // 前置注入 hooks.unshift(wrappedHook); } else { hooks.push(wrappedHook); } return wrappedHook; } else { const apiName = toHandlerKey(ErrorTypeStrings[type].replace(/ hook$/, '')); warn(`${apiName} is called when there is no active component instance to be ` + `associated with. ` + `Lifecycle injection APIs can only be used during execution of setup().` + (` If you are using async setup(), make sure to register lifecycle ` + `hooks before the first await statement.` )); } } 复制代码
触发
同步触发钩子,通过invokeArrayFns
方法来调用。
const invokeArrayFns = (fns, arg) => { // 遍历数组,执行每一项 for (let i = 0; i < fns.length; i++) { fns[i](arg); } }; 复制代码
举个栗子
在渲染组件时,先触发beforeMount
钩子。
// 在injectHook时,已经把bm、m添加到instance中了,且bm、m为数组 const { bm, m, parent } = instance; if (bm) { // ---------------beforeMount生命周期 invokeArrayFns(bm); } 复制代码
使用同步触发的钩子有:beforeMount
、beforeUpdate
、beforeUnmount
、beforeUnmount
、renderTracked
、renderTriggered
、errorCaptured
异步触发钩子,通过使用queuePostRenderEffect
方法来清除队列中的钩子函数。
// 刷新任务队列,支持suspense组件 function queueEffectWithSuspense(fn, suspense) { if (suspense && suspense.pendingBranch) { if (isArray(fn)) { suspense.effects.push(...fn); } else { suspense.effects.push(fn); } } else { queuePostFlushCb(fn); } } // 刷新后置任务队列的回调函数 function queuePostFlushCb(cb) { queueCb(cb, activePostFlushCbs, pendingPostFlushCbs, postFlushIndex); } // 添加回调函数到等待队列中,并刷新回调队列 function queueCb(cb, activeQueue, pendingQueue, index) { if (!isArray(cb)) { if (!activeQueue || !activeQueue.includes(cb, cb.allowRecurse ? index + 1 : index)) { pendingQueue.push(cb); } } else { pendingQueue.push(...cb); } // 刷新任务 queueFlush(); } const queuePostRenderEffect = queueEffectWithSuspense; 复制代码
使用:
const { m } = instance; if (m) { // -----------------mounted生命周期 queuePostRenderEffect(m, parentSuspense); } 复制代码
使用异步触发的钩子有:mounted
、updated
其中,queueFlush
函数为刷新任务队列,即遍历队列中的所有hook并执行。关于异步钩子的触发,涉及的代码比较多,在这里不做过多解释。如果想了解更多,可以点击文末的附录,是我写的Vue3源码注释。
作者:别像我一样
链接:https://juejin.cn/post/7020017329815683085