阅读 859

Vue3生命周期详解

前言

版本:3.0.2

说明:通过源码对Vue3的生命周期进行阐述,包含用法、使用说明、源码介绍等。

一、开局一张图

lifecycle.png

什么是生命周期?

关于这个问题,我觉得很好理解。

就是在不同的时期调用不同的钩子,就像人一样,在不同的年龄段,会做不同的事;但有些钩子需要在特殊的条件下才能触发,就像人一样,做了某件事,而这件事会引发怎样的后果。

二、各个生命周期的用法

  • 1、页面初始化时,直接触发

涉及钩子:beforeCreatecreatedbeforeMountrenderTrackedmounted

使用方式:

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:操作类型,有gethasiterate,也就是取值操作。

key:键,简单理解就是操作数据的keye.g.上文使用的count

target:响应式对象,如:datarefcomputed

effect:数据类型为Function,英文单词意思为唤起执行的意思。effect方法的作用是重新render视图。

  • 2、数据发生改变后触发

涉及钩子:renderTriggeredbeforeUpdaterenderTrackedupdated

使用方式:

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:操作类型,有setaddcleardelete,也就是修改操作。

key:键,简单理解就是操作数据的key。e.g.上文使用的count

target:响应式对象,如:datarefcomputed

effect:数据类型为Function。英文单词意思为唤起执行的意思。effect方法的作用是重新render视图。

newValue:新值。

oldValue:旧值。

oldTarget:旧的响应式对象。

  • 3、组件被卸载时触发

涉及钩子:beforeUnmountunmounted

模拟一下

通过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参数说明

errError错误对象;e.g.ReferenceError: parentSetupError is not defined。

instance:组件实例。

info:捕获的错误信息。e.g.setup function

errorCaptured返回一个false的理解

错误的传播规则是自上而下的,默认情况下,如果全局的config.errorHandler被定义,所有错误最终都会传递到它那里,可以通过显式的使用return false来阻止错误向上传递。

以上面的例子来解释:

parent组件的errorCaptured钩子中return falsechild组件的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函数中,有两个钩子是直接触发的(beforeCreatecreated),剩下的钩子都是通过先注入,然后触发。

注入

// 创建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); } 复制代码

使用同步触发的钩子有:beforeMountbeforeUpdatebeforeUnmountbeforeUnmountrenderTrackedrenderTriggerederrorCaptured

异步触发钩子,通过使用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); } 复制代码

使用异步触发的钩子有:mountedupdated

其中,queueFlush函数为刷新任务队列,即遍历队列中的所有hook并执行。关于异步钩子的触发,涉及的代码比较多,在这里不做过多解释。如果想了解更多,可以点击文末的附录,是我写的Vue3源码注释。


作者:别像我一样
链接:https://juejin.cn/post/7020017329815683085


文章分类
后端
文章标签
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐