Vue3生命周期详解

11,275 阅读8分钟

前言

版本: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

官方解释:在捕获一个来自后代组件的错误时被调用。此钩子会收到三个参数:错误对象、发送错误的组件实例以及一个包含错误来源信息的字符串。此钩子可以返回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源码注释。

看完3件事

1、如果文章对你有帮助,可以给博主点个赞。

2、如果你觉得文章还不错,可以动动你的小手,收藏一下。

3、如果想看更多的源码详解,可以添加关注博主。

附录:

1、Vue3.x完整版源码解析:github.com/fanqiewa/vu…

2、其它源码解析:www.fanqiewa.xyz/