Vue instance lifecycle and lifecycle hooks

Each Vue instance goes through a series of initialization procedures when it is created. For example, you need to set up data listening, compile templates, mount instances to the DOM, and update the DOM as the data changes.

To give developers the opportunity to insert their own code logic at different stages of the Vue instance lifecycle, Vue provides functions called lifecycle hooks. The main lifecycle hooks are as follows:

  • beforeCreate
  • created
  • beforeMount
  • mounted
  • beforeUpdate
  • updated
  • activated
  • deactivated
  • beforeDestroy
  • destroyed

The official vUE instance lifecycle diagram provides a good illustration of the process. You can refer to this picture to read the article.

Initialize the

After new Vue() is executed, the instance initialization phase is entered. This phase fires two hooks: beforeCreate and Created. Take a look at the main code:

 // Vue constructor
function Vue (options) {
  // Instance initialization
  this._init(options)
}

 Vue.prototype._init = function (options? :Object) {
    const vm: Component = this
    // a uid Assigns a unique ID to each VUE instance
    vm._uid = uid++
    vm._isVue = true
    
    / /... Omit some code
    
    if (options && options._isComponent) {
      // Merge component option objects
      initInternalComponent(vm, options)
    } else {
      // Merge vUE option objects
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    // After initialization, the created hook function is called
    callHook(vm, 'created')
    
    / /... Omit some code

    // If mount elements are specified, mount logic is performed
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
Copy the code

beforeCreate

As you can see from the _init method, VUE merges the user-defined option object with the pre-defined static option object on the VUE constructor.

The initLifecycle function is called to initialize lifecycle properties for the instance. For example, initialize vue instance _isMounted, _isDestroyed, and _inactive.

The initEvents function is called to initialize the event-related properties for the instance. For example, initialize the values of the _events, _hasHookEvent, and other attributes of the Vue instance.

Call the initRender function to initialize render related properties for the instance. For example, initialize the vue instance’s _vnode, _c(), $slots, and so on.

Lifecycle, Render, and event will be initialized when the beforeCreate hook is called. However, data, props, methods, provide/ Inject, Watch, computed, and so on have not been processed.

created

As you can see from the code, the Created hook is invoked with initInjections, initState, and initProvide executed successively. These functions mainly delegate properties defined in Data, props, Inject, methods, computed, and Watch to vUE instances using defineProperty. All properties in Data, props, Inject, computed, and Watch are converted to responsive. After this process, when we change the property value in the instance, the page will be automatically redrawn.

Thus, when a Created hook is called, the instance is initialized and the instance property is responsive. But you haven’t started mounting DOM elements yet. A common operation in a hook is to asynchronously fetch data from the backend.

DOM element mount

Once the instance is initialized, $mount is called to start mounting the DOM elements. This phase fires two hook functions: beforeMount and Mounted.

// src/platforms/web/entry-runtime-with-compiler.js

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
) :Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
Copy the code
// src/platforms/web/runtime/index.js
// Override the $mount method
const mount = Vue.prototype.$mount

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
) :Component {
  el = el && query(el)

  const options = this.$options
  if(! options.render) {let template = options.template
    // The template option is specified
    if (template) {
      if (typeof template === 'string') {
        // If the value starts with #, it is used as a selector and uses the innerHTML of the matching element as the template
        if (template.charAt(0) = = =The '#') {
          template = idToTemplate(template)
          if(process.env.NODE_ENV ! = ='production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`.this)}}}else if (template.nodeType) {
        Although not documented, template can also specify a DOM element as a template
        template = template.innerHTML
      } else {
        if(process.env.NODE_ENV ! = ='production') {
          warn('invalid template option:' + template, this)}return this}}else if (el) {
      // El is specified in the option
      template = getOuterHTML(el)
    }

    // Parse the template into the render function
    if (template) {
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV ! = ='production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns
    }
  }
  
  / /... Omit some code
  
  return mount.call(this, el, hydrating)
}
Copy the code
// src/core/instance/lifecycle.js
export function mountComponent (vm: Component, el: ? Element, hydrating? : boolean) :Component {
  // Save references to DOM elements to $el
  vm.$el = el
  
  / /... Omit some code

  // The template compilation logic needs to be executed before calling beforeMount
  callHook(vm, 'beforeMount')

  let updateComponent = () = > {
    vm._update(vm._render(), hydrating)
  }
  
  new Watcher(vm, updateComponent, noop, {
    before () {
      if(vm._isMounted && ! vm._isDestroyed) { callHook(vm,'beforeUpdate')}}},true /* isRenderWatcher */)
  
  hydrating = false

  if (vm.$vnode == null) {
    // mark as mounted
    vm._isMounted = true
    
    // The Mounted event is triggered
    callHook(vm, 'mounted')}return vm
}
Copy the code

beforeMount

We know that Vue needs the render function to generate vNodes. In practice, however, templates and el are usually used to specify templates, rarely providing a render function directly. So one of the most important things vue does before triggering beforeMount is compile the HTML template into the render function. BeforeMount hook function is called when we cannot access DOM elements yet.

mounted

Each Vue instance corresponds to a Render Watcher. Render Watcher creates a Vnode (using the _render method), diff the VNode, and then creates or updates the DOM element (using the _update method). For the first rendering, after the DOM element is created, insert the root element of the DOM tree into the body and trigger the Mounted hook function. At this point, you can manipulate DOM elements in the hook function.

update

After the instance is initialized and mounted, it enters the update phase if its state changes due to user interaction. For example, to execute this. MSG = ‘update MSG ‘in code, the Vue instance needs to update the DOM element.

Instances are updated asynchronously. As mentioned earlier, render Watcher is responsible for the scheduler creating vNodes and creating updated DOM elements. When data changes, Vue does not immediately initiate DOM updates, but adds the instance’s corresponding Render Watcher to a queue. Then in the next event loop, DOM updates are uniformly performed to clear the queue. That is, call the flushSchedulerQueue function in the code below.

The hooks that trigger in this phase are: beforeUpdate and updated.

/** * Clear all queues and execute Watcher's update logic */
function flushSchedulerQueue () {
  flushing = true
  let watcher, id

  // The queue is sorted by watcher's ID in ascending order to ensure that:
  // 1. Components are always updated from parent to child
  // 2. The user-created Watcher is updated before the rendered Watcher
  // 3. If the component is destroyed while the parent component's Watcher is running, the component's Watcher can skip processing
  queue.sort((a, b) = > a.id - b.id)

  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    // Call watcher.before to trigger beforeUpdate hook
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    / / update the dom
    watcher.run()
  }

  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()

  resetSchedulerState()

  // Triggers the activated hook
  callActivatedHooks(activatedQueue)
  // Triggers the updated hook
  callUpdatedHooks(updatedQueue)
}

// Triggers the updated hook
function callUpdatedHooks (queue) {
  let i = queue.length
  while (i--) {
    const watcher = queue[i]
    const vm = watcher.vm
    if(vm._watcher === watcher && vm._isMounted && ! vm._isDestroyed) { callHook(vm,'updated')}}}Copy the code

beforeUpdate

In the initialization phase, vue instance data (data/props/computed/watch) have been treated to response type. Any getter for the data is added by Watcher as a dependency, and any change to the data triggers an update to the watcher that depends on the data. Watcher calls the watcher. Before method before the update, which is defined when the Watcher instance is created during the mount phase. The beforeUpdate hook is triggered in watcher. Before. At this point, the VUE instance just identifies the data that will eventually need to be updated, and has not yet actually started updating.

updated

As you can see from the code, the Vue instance needs to update the DOM element before the updated hook is triggered. The update process is asynchronous. Execute the Run method using the Render Watcher of the instance. This method calls the updateComponent function we introduced in the mount phase. The VNode is recreated and the DOM element is updated after the VNode diff operation.

We also notice that the code calls the callActivatedHooks function that triggers the Activated hook. We’ll explain it later, but we won’t expand it here.

The destruction

When the $destroy method of a Vue instance is used, the instance enters the destruction phase. The hooks that trigger are: beforeDestory and destroyed.

  // Destroy the Vue instance
  Vue.prototype.$destroy = function () {
    const vm: Component = this
    // Avoid repeated destruction operations
    if (vm._isBeingDestroyed) {
      return
    }
    // Triggers the beforeDestroy hook of the instance
    callHook(vm, 'beforeDestroy')
    
    vm._isBeingDestroyed = true
    // Remove the instance from its parent's $chilren (disconnect from the parent)
    const parent = vm.$parent
    if(parent && ! parent._isBeingDestroyed && ! vm.$options.abstract) { remove(parent.$children, vm) }// Destroy instance Watchers
    if (vm._watcher) {
      vm._watcher.teardown()
    }
    let i = vm._watchers.length
    while (i--) {
      vm._watchers[i].teardown()
    }

    if (vm._data.__ob__) {
      vm._data.__ob__.vmCount--
    }
    
    vm._isDestroyed = true
    // Destroy instructions, ref, etc
    vm.__patch__(vm._vnode, null)
    // Trigger the Destroyed event
    callHook(vm, 'destroyed')
    // Remove all event listeners for the instance
    vm.$off()
    if (vm.$el) {
      vm.$el.__vue__ = null
    }
    // Release circular references (#6759)
    if (vm.$vnode) {
      vm.$vnode.parent = null}}}Copy the code

beforeDestroy

As you can see from the code, when $destroy is called, trigger beforeDestroy before the actual destroy action is performed. At this point, the instance and DOM elements are still accessible because the actual destruction code has not started.

destroyed

As you can see from the code, destruction consists of clearing all watcher, deleting all instructions, deleting DOM elements and closing all events on the DOM, disconnecting the parent instance, and marking the instance as destroyed. At this point, the instance has been destroyed and its attributes and DOM elements are no longer accessible.

A lifecycle hook for a component wrapped under keep-alive

The two hooks below fire only if the component is wrapped in keep-alive.

activated

Both HTML tags and component tags have corresponding VNodes in the vUE internal implementation. Component VNodes are different in design from normal HTML tag VNodes. For example, the component VNode contains hooks such as init, prepatch, INSERT, and destroy. These hooks are called at various stages of component instance initialization, update, and destruction.

// Component vNode hooks
const componentVNodeHooks = {
    / /... Omit other hooks
  insert (vnode: MountedComponentVNode) {
    const { context, componentInstance } = vnode
    if(! componentInstance._isMounted) { componentInstance._isMounted =true
      // Triggers the mounted hook of the component
      callHook(componentInstance, 'mounted')}if (vnode.data.keepAlive) {
      if (context._isMounted) {
        queueActivatedComponent(componentInstance)
      } else {
        activateChildComponent(componentInstance, true /* direct */)}}}}// Triggers the activated hook
export function activateChildComponent (vm: Component, direct? : boolean) {

  / /... Omit some code
  
  if (vm._inactive || vm._inactive === null) {
    vm._inactive = false
    for (let i = 0; i < vm.$children.length; i++) {
      activateChildComponent(vm.$children[i])
    }
    // Call the activated hook of the instance
    callHook(vm, 'activated')}}Copy the code

Like the root instance, a component instance wrapped under keep-Alive goes through the initialization and mount phases. However, after the mount phase, the component vNode’s INSERT hook is called, which triggers the activated hook of the component instance. Because INSERT is invoked after the component instance is mounted, Mounted triggers earlier than Activated.

When a component switches back and its data changes, the component enters the update phase, meaning beforeUpdate and updated hooks are fired in sequence.

The question is, which triggers the Activated, beforeUpdate, or updated hooks first?

Trigger beforeUpdate, activated, and updated.

Let’s review the code from the previous update phase:

// Triggers the activated hook
callActivatedHooks(activatedQueue)
// Triggers the updated hook
callUpdatedHooks(updatedQueue)

/ /... Omit some code

// Triggers the activated hook
function callActivatedHooks (queue) {
  for (let i = 0; i < queue.length; i++) {
    queue[i]._inactive = true
    activateChildComponent(queue[i], true /* true */)}}Copy the code

You can see that in the flushSchedulerQueue function, the callActivatedHooks function is used to trigger activated. The Order of callActivatedHooks precedes callUpdatedHooks, so the Activated hook fires before the updated hook.

deactivated

const componentVNodeHooks = {
  / /... Omit other hooks
  destroy (vnode: MountedComponentVNode) {
    const { componentInstance } = vnode
    if(! componentInstance._isDestroyed) {// The component is not destroyed by the keep-alive package
      if(! vnode.data.keepAlive) { componentInstance.$destroy() }else {
        // wrapped by keep-alive
        deactivateChildComponent(componentInstance, true /* direct */)}}}}export function deactivateChildComponent (vm: Component, direct? : boolean) {
  if (direct) {
    vm._directInactive = true
    if (isInInactiveTree(vm)) {
      return}}if(! vm._inactive) { vm._inactive =true
    for (let i = 0; i < vm.$children.length; i++) {
      deactivateChildComponent(vm.$children[i])
    }
    // Call the deactivated hook of the instance
    callHook(vm, 'deactivated')}}Copy the code

When a component is switched to another component, the component vNode’s destroy hook is called. In the case of a keep-alive wrapped component, only the DOM element corresponding to the component is removed from the DOM tree, but the component instance is not destroyed. The DeActivated Hook is triggered by calling the deactivateChildComponent. $destroy destroys the component instance only if the component is not wrapped by keep-AlVIE, triggering beforeDestroy and Destroyed hooks.

conclusion

About the vUE instance lifecycle, the official website explains is relatively easy to understand. The main purpose of this article is to further understand what is done in each lifecycle from a source perspective. Better understand when and in what order hooks are triggered. Welcome to comment ~

Pay attention to our