preface

Version: 3.0.2

Vue3 life cycle through the source code is described, including usage, use instructions, source code introduction, etc.

1. Opening a picture

What is the life cycle?

I think it’s quite understandable.

You call different hooks at different times, just like people do different things at different ages; But some hooks need to trigger under special circumstances, just like people do, and what happens when they do something.

Ii. Usage of each life cycle

  • 1, page initialization, direct trigger

Related hooks are beforeCreate, created, beforeMount, renderTracked, mounted

Usage:

Vue.createApp({
  // Data is not available at this time
  beforeCreate() {
    console.log("beforeCreate");
  },
  
  // Data is available, DOM is not available
  created() {
    console.log("created");
  },
  
  // After this hook and before the mounted lifecycle hook, the render function is first called
  beforeMount() {
    console.log("beforeMount");
  },
  
  // triggered when the page has value operations (e.g. binding data, e.g. interpolation syntax {{count}})
  renderTracked({ type, key, target, effect }) {
    console.log("renderTracked ----", { type, key, target, effect });
  },
  
  // Triggered after the page is mounted
  mounted() {
    console.log("mounted");
  }
}).mount("#app");
Copy the code

Output:

beforeCreate
created
beforeMount
renderTracked ---- {type: "get", key: "count", target: {... }, effect: f} mountedCopy the code

Vue3.x added a life cycle renderTracked description

Official explanation: Called when tracing the virtual DOM to re-render (also called when initializing the render). The hook receives a Debugger event as an argument. This event tells you which action tracks the component and the target object and key for that action.

In simple terms, this operation is triggered when reactive data (values) are bound to the page.

Take a chestnut

<div id="app">
	<div>{{ count }}</div>
  <button @click="addCount">Add 1</button>
</div>
Copy the code
Vue.createApp({
  methods: {
    addCount() {
      this.count += 1; }},// Each time you render, the 'renderTracked' hook is fired.
  renderTracked(e) {
    console.log("renderTracked ----", e);
  }
}).mount("#app");
Copy the code

Output:

renderTracked ---- {type: "get", key: "count", target: {... }, effect: f}Copy the code

debugger eventinstructions

Type: Operation type, including get, has, iterate.

Key: a key used to manipulate data.

Target: indicates a responsive object, such as data, ref, and computed.

Effect: The data type is Function, and the English word means to evoke and execute. The effect method rerenders the view.

  • 2. Triggered after data changes

Related hooks: renderTriggered, beforeUpdate, renderTracked, updated

Usage:

Vue.createApp({
  // change data (e.g. set)
  renderTriggered(e) {
    console.log("renderTriggered ----", e);
  },
  
  /*--------- called after the data has changed and before the DOM is updated. -- -- -- -- -- -- -- -- -- - * /
  beforeUpdate() {
    console.log("beforeUpdate");
  },
  
  // read data (e.g.
  renderTracked(e) {
    console.log("renderTracked ----", e);
  },
  
  /*--------- is called after DOM is updated. Note: Updated does not guarantee that all child components have also been rerendered ---------*/
  updated() {
    console.log("updated");
  }
}).mount("#app");
Copy the code

Output:

renderTriggered ---- {target: {... }, key:"count", type: "set", newValue: 2, effect: f, oldTarget: undefined, oldValue: 1} beforeUpdate renderTracked ---- {target: {... }, type:"get", key: "count", effect: f}
update
Copy the code

Vue3.x added the life cycle renderTriggered description

Official explanation: called when the virtual DOM rerendering is triggered. Receive a Debugger event as a parameter. This event tells you what action triggered the rerender, as well as the target object and key for that action.

Simple understanding: Something was done that caused the page to be rerendered.

Take a chestnut

<div id="app">
	<div>{{ count }}</div>
  <button @click="addCount">Add 1</button>
</div>
Copy the code
Vue.createApp({
  methods: {
    addCount() {
      this.count += 1; }},// Every time reactive data is modified, the 'renderTriggered' hook is triggered
  renderTriggered(e) {
    console.log("renderTriggered ----", e);
  }
}).mount("#app");
Copy the code

Output:

renderTriggered ---- {target: {... }, key:"count", type: "set", newValue: 2, effect: f, oldTarget: undefined, oldValue: 1}
Copy the code

debugger eventinstructions

Type: Indicates the operation type, including set, Add, clear, and delete.

Key: a key used to manipulate data. E.g. the count used above.

Target: indicates a responsive object, such as data, ref, and computed.

Effect: The data type is Function. The English word means to evoke, perform. The effect method rerenders the view.

NewValue: indicates the newValue.

OldValue: indicates the oldValue.

OldTarget: Old reactive object.

  • 3. Triggered when the component is uninstalled

Hooks involved: beforeUnmount and unmounted

A simulation

V-if is used to simulate the destruction of child components.

<div id="app">
  <! -- Subcomponent -->
  <child v-if="flag"></child>
  
  <button @click="unload">Unload child components</button>
</div>
Copy the code

First define a child component, then reference it in the page and destroy the child component by clicking the Uninstall child component button.

const { defineComponent } = Vue;
const child = defineComponent({
  data() {
    return {
      title: "I am a child component"}},template: `<div>{{ title }}</div>`.// Called before the component instance is unloaded, at which point the instance is still available.
  beforeUnmount() {
    console.log("beforeUnmount");
  },
  
  // called after the component instance is unloaded.
  unmounted() {
    console.log("unmounted"); }}); Vue.createApp({components: {
    child
  },
  data: () = > {
    return {
      flag: true}},methods: {
    unload() {
      this.flag = false;
    }
  }
}).mount("#app");
Copy the code

Click the button to unload the child component and re-render the page. When you open the console, it will print:

beforeUnmount
unmounted
Copy the code
  • Catch the wrong hook

Hooks involved: errorCaptured

Official explanation: Called when catching an error from a descendant component. The hook receives three parameters: the error object, the component instance that sent the error, and a string containing information about the source of the error. This hook can return false to prevent further propagation of the error.

A simulation

Reference in the page.

<div id="app">
  <! -- Parent component -->
  <parent v-if="flag"></parent>
</div>
Copy the code

You define a child component and a parent component, reference the parent component in the page, and then reference the child component in the parent.

const { defineComponent } = Vue;
const child = definedComponent({
  data() {
    return {
      title: "I am a child component"}},template: `<div>{{ title }}</div>`.setup() {
    // This method is undefined. Calling it directly will raise an error
    childSetupError();
    return{}; }});const parent = defineComponent({
  components: {
    child
  },
  data() {
    return {
      title: "Render subcomponent:"}},setup() {
    // This method is also undefined. Calling it directly will raise an error
    parentSetupError();
    return {};
  },
  template: ` 
      
{{ title }}
`
.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"); Copy the code

errorCapturedParameters that

Err: Error Error object; E.g.ferenceerror: parentSetupError is not defined.

Instance: indicates a component instance.

Info: error information captured. e.g.setup function

errorCapturedReturns afalseThe understanding of the

Error propagation rules are top-down. By default, if the global config.errorHandler is defined, all errors will eventually be passed to it. You can prevent errors from being passed up by explicitly using return false.

To illustrate:

ErrorCaptured hooks return false, and child childSetupError errors are only passed to the parent component. The parent component’s parentSetupError error is passed to both the Vue. CreateApp initialization Vue instance and app.config.errorHandler.

So, the order of output is:

errorCaptured ReferenceError: parentSetupError is not defined
configErrorHandler ReferenceError: parentSetupError is not defined
parentCaptured ReferenceError: childSetupError is not defined
Copy the code

Three, source code introduction

How are lifecycle hooks implemented in Vue3 source code? Understand the execution of the lifecycle step by step through the introduction of each of the following functions. Because some functions involve a lot of code, I will present only the important parts of the code related to the lifecycle.

Let’s start with the applyOptions function.

The applyOptions function creates the options passed by the application programmer, that is, createApp(Options) creates the options passed by a Vue instance

function applyOptions(instance, options, deferredData = [], deferredWatch = [], deferredProvide = [], asMixin = false) {
  // ...
  
  // -------------------beforeCreate Life cycle
  callSyncHook('beforeCreate'."bc" /* BEFORE_CREATE */, options, instance, globalMixins);
  
  // ...
  
  // dataOptions => data
  if (dataOptions) {
    // Parse the data object and turn it into a responsive object
    resolveData(instance, dataOptions, publicThis);
  }
  
  // ...
  
  // -------------------created lifecycle
  // At this point, data is ready to use
  callSyncHook('created'."c" /* CREATED */, options, instance, globalMixins);
  
  
  // ------------------ register 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));
  }
  // This hook has been removed
  if (beforeDestroy) {
    BeforeDestory was renamed to beforeUnmount
    warn(`\`beforeDestroy\` has been renamed to \`beforeUnmount\`.`);
  }
  if (beforeUnmount) {
    // beforeUnmount
    onBeforeUnmount(beforeUnmount.bind(publicThis));
  }
  // This hook has been removed
  if (destroyed) {
    // DeStoryed was renamed to unmounted
    warn(`\`destroyed\` has been renamed to \`unmounted\`.`);
  }
  if (unmounted) {
    // unmountedonUnmounted(unmounted.bind(publicThis)); }}Copy the code

In the applyOptions function, you can see that Vue3 uses callSyncHook to execute our defined lifecycle hooks, such as beforeCreate. Let’s look at the callSyncHook function.

// Execute hook synchronously
function callSyncHook(name, type, options, instance, globalMixins) {
  // Trigger globally defined mixins
  callHookFromMixins(name, type, globalMixins, instance);
  const { extends: base, mixins } = options;
  
  if (base) {
    // Triggers a hook in extends
    callHookFromExtends(name, type, base, instance);
  }
  if (mixins) {
    // Trigger our custom mixins
    callHookFromMixins(name, type, mixins, instance);
  }
  // Custom hook
  // e.g. beforeCreate, created
  const selfHook = options[name];
  if(selfHook) { callWithAsyncErrorHandling(selfHook.bind(instance.proxy), instance, type); }}Copy the code

If the extends extends exists, or if the mixins exist, then the lifecycle hooks in each of the two are triggered first. Finally, we look for the lifecycle hook functions we defined in the component.

Let’s take a look at callWithAsyncErrorHandling function.


// Execute the callback with a try catch wrap to handle the error message
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) {
  // If the fn passed in is a Function type
  if (isFunction(fn)) {
    / / execution fn
    const res = callWithErrorHandling(fn, instance, type, args);
    // If there is a return value of type PROMISE, add a catch to the return value to catch the error message
    if (res && isPromise(res)) {
      res.catch(err= > {
        handleError(err, instance, type);
      });
    }
    return res;
  }
  // If the hook is an array type, each item in the array is iterated and the array result is returned
  const values = [];
  for (let i = 0; i < fn.length; i++) {
    values.push(callWithAsyncErrorHandling(fn[i], instance, type, args));
  }
  return values;
}
Copy the code

In the applyOptions function, two hooks are created (beforeCreate, created) and the rest are injected and created.

injection

/ / create a 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);
};

// Inject hook, target is instance
function injectHook(type, hook, target = currentInstance, prepend = false) {
  if (target) {
		// hook on Vue instance, 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);
            // Triggers the injected hook
            const res = callWithAsyncErrorHandling(hook, target, type, args);
            setCurrentInstance(null);
            resetTracking();
            return res;
          });
    if (prepend) {
      // Pre-injection
      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.`)); }}Copy the code

The trigger

The synchronous trigger hook is invoked through the invokeArrayFns method.

const invokeArrayFns = (fns, arg) = > {
  // Iterate over the number group, executing each item
  for (let i = 0; i < fns.length; i++) { fns[i](arg); }};Copy the code

Take a chestnut

Trigger the beforeMount hook when rendering a component.

// In injectHook, bm and m have been added to instance, and bm and m are arrays
const { bm, m, parent } = instance;
if (bm) {
  // ---------------beforeMount Life cycle
  invokeArrayFns(bm);
}
Copy the code

The hooks that are triggered using synchronization are beforeMount, beforeUpdate, beforeUnmount, beforeUnmount, renderTracked, renderTriggered, errorCaptured

Triggers hooks asynchronously, using the queuePostRenderEffect method to clean up the hook function in the queue.

Update task queues to support suspense components
function queueEffectWithSuspense(fn, suspense) {
  if (suspense && suspense.pendingBranch) {
    if(isArray(fn)) { suspense.effects.push(... fn); }else{ suspense.effects.push(fn); }}else{ queuePostFlushCb(fn); }}// Refresh the callback function for the post-task queue
function queuePostFlushCb(cb) {
  queueCb(cb, activePostFlushCbs, pendingPostFlushCbs, postFlushIndex);
}

// Add callback function to wait queue and refresh callback queue
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); }// Refresh the task
  queueFlush();
}

const queuePostRenderEffect = queueEffectWithSuspense;
Copy the code

Use:

const { m } = instance;
if (m) {
  // ----------------- Mounted Life cycle
  queuePostRenderEffect(m, parentSuspense);
}
Copy the code

The following hooks that use asynchronous triggering are Mounted and updated

The queueFlush function flushes the task queue, that is, traverses all hooks in the queue and executes. There is a lot of code involved in asynchronous hook triggering that I won’t explain here. If you want to know more, you can click on the appendix at the end of this article, which is the Vue3 source code comment I wrote.

3 things to watch

1. Give the blogger a “like” if the post has helped you.

If you think the article is good, you can move your little hands and collect it.

3, if you want to see more source code details, you can add follow the blogger.

Appendix: github.com/fanqiewa/vu…