前言
版本: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
官方解释:在捕获一个来自后代组件的错误时被调用。此钩子会收到三个参数:错误对象、发送错误的组件实例以及一个包含错误来源信息的字符串。此钩子可以返回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源码注释。
看完3件事
1、如果文章对你有帮助,可以给博主点个赞。
2、如果你觉得文章还不错,可以动动你的小手,收藏一下。
3、如果想看更多的源码详解,可以添加关注博主。
附录:
1、Vue3.x完整版源码解析:github.com/fanqiewa/vu…
2、其它源码解析:www.fanqiewa.xyz/