再谈Vue的生命周期----结合Vue源码

简介

关于Vue的生命周期函数,目前网上有许多介绍文章,但也都只是分析了表象。这篇文档,将结合Vue源码分析,为什么会有这样的表象。

Vue中的生命周期函数也可以称之为生命周期钩子(hook)函数,在特定的时期,调用特定的函数。

随着项目需求的不断扩大,生命周期函数被广泛使用在数据初始化、回收、改变Loading状态、发起异步请求等各个方面。

而Vue实例的生命周期函数有beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、beforeDestpry、destroyed,8个。

本文假设读者使用过Vue.js,但对相应的开发经验不做要求。如果你对Vue很感兴趣,却不知如何下手,建议你先阅读官方文本

资源

以下是这篇文章所需的资源,欢迎下载。

  1. 项目仓库
  2. Vue源码笔记

表象

我们在该页面来研究,Vue的生命周期函数究竟会在何时调用,又会有什么不同的特性。强烈建议你直接将项目仓库克隆至本地,并在真机环境中,跑一跑。Vue.js已经添加在版本库中,因此你不需要添加任何依赖,直接在浏览器中打开lifeCycle.html即可。

$ git clone https://github.com/AmberAAA/vue-guide

编写实现组件

定义templatedata,使其在可以在屏幕实时渲染出来。

{
  //...
  template: `
    <div>
      <p v-html="info"></p>
    </div>
  `,
  data () {
    return {
      info: ''
    }
  }
  //...
}

beforeCreate为例,定义全部的证明周期函数。

beforeCreate() {
    console.group('------beforeCreate------');
    console.log('beforeCreate called')
    console.log(this)
    console.log(this.$data)
    console.log(this.$el)
    this.info += 'beforeCreate called <br>'
    console.groupEnd();
  }

屏幕输出

在浏览器中打开lifeCycle.html,点击挂载组件后,屏幕依次输出created called beforeMount called mounted called 。表现出,在挂载组件后,infocreated, beforeMount, mounted赋值,并渲染至屏幕上。但是本应在最开始就执行的beforeCreate却并没有给info赋值。
卸载组件时,因为程序运行太快,为了方便观察,特意为beforeDestroybeforeDestroy函数在最后添加了断点。发现点击卸载组价后,Vue在v-if=true时会直接从文档模型中卸载组件(此时组件已经不在document)。

控制台输出

控制台输出的内容非常具有代表性。

再谈Vue的生命周期----结合Vue源码

我们可以发现,控制台按照创建、挂载、更新、销毁依次打印出对应的钩子函数。展开来看

再谈Vue的生命周期----结合Vue源码

在触发beforeCreate函数时,vue实例还尚未初始化$data,因此也就无法给$data赋值,也就很好的解释了为什么在屏幕上,没有渲染出beforeCreate called。同时,因为尚未被挂载,也就无法获取到$el

在触发created函数时,其实就表明,该组件已经被创建了。因此给info赋值后,待组件挂载后,视图也会渲染出created called

在触发beforeMount函数时,其实就表明,该组件即将被挂载。此时组建表现出的特性与created保持一致。

在触发mounted函数时,其实就表明,该组件已经被挂载。因此给info赋值后,待组件挂载后,视图也会渲染出mounted called,并且控制台可以获取到$el

触发beforeUpdateupdated,分别表示视图更新前后,更新前$data领先视图,更新后,保持一致。在这两个回调函数中,更改data时注意避免循环回调。

触发beforeDestroydestroyed,表示实例在销毁前后,改变$data,会触发一次updated,因在同一个函数中(下文会介绍)回调,故捏合在一起说明。

名称触发阶段$data$el
beforeCreate组件创建前
created组件创建后
beforeMount组件挂载前
mounted组件挂载后
beforeUpdate组件更新前
updated组件更新后
beforeDestroy组件创建前
destroyed组件创建前

原理

Vue生命周期函数在源码文件/src/core/instance/init.js中定义,并在/src/core/instance/init.jssrc/core/instance/lifecycle.js/src/core/observer/scheduler.js三个文件中调用了所有的生命周期函数

callHooK

当在特定的使其,需要调用生命周期钩子时,源码只需调用callHook函数,并传入两个参数,第一个为vue实例,第二个为钩子名称。如下

export function callHook (vm: Component, hook: string) {
  // #7573 disable dep collection when invoking lifecycle hooks
  pushTarget()
  const handlers = vm.$options[hook]
  if (handlers) {
    //? 这里为什么是数组?在什么情况下,数组的索引会大于1?
    for (let i = 0, j = handlers.length; i < j; i++) {
      try {
        handlers[i].call(vm)
      } catch (e) {
        handleError(e, vm, `${hook} hook`)
      }
    }
  }
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
  popTarget()
}

耍个流氓

callHook在打包时,并没有暴露在全局作用域。但我们可以根据Vue实例来手动调用生命周期函数。试着在挂在组件后在控制台输入vue.$children[0].$options['beforeCreate'][0].call(vue.$children[0]),可以发现组件的beforeCreate钩子已经被触发了。并且表示出了与本意相驳的特性。此时因为组件已经初始化,并且已经挂载,所以成功在控制台打印出$el$data,并在修改info后成功触发了beforeUpdatebeforeUpdate

beforeCreatecreated

Vue会在/src/core/instance/init.js中通过initMixin函数对Vue实例进行进一步初始化操作。

export function initMixin (Vue: Class<Component>) {
    Vue.prototype._init = function (options?: Object) {
      /*
        ....
      */
      vm._self = vm
      initLifecycle(vm) 
      initEvents(vm)
      initRender(vm)
      callHook(vm, 'beforeCreate')
      initInjections(vm) // resolve injections before data/props
      initState(vm)   // 定义$data
      initProvide(vm) // resolve provide after data/props
      callHook(vm, 'created')

      /*
        ...
      */
    }
  }

可以看出在执行callHook(vm, 'beforeCreate')之前,Vue还尚未初始化data,这也就解释了,为什么在控制台beforeCreate获取到的$dataundefined,而callHook(vm, 'created')却可以,以及屏幕上为什么没有打印出beforeCreate called

beforeMountmounted

Vue在/src/core/instance/lifecycle.js中定义了mountComponent函数,并在该函数内,调用了beforeMountmounted

export function mountComponent (
    vm: Component,
    el: ?Element,
    hydrating?: boolean
  ): Component {
    vm.$el = el    // 组件挂载时 `el` 为`undefined`

    callHook(vm, 'beforeMount') // 所以获取到的`$el`为`undefined`

    /*
      ...
    */
    // we set this to vm._watcher inside the watcher's constructor
    // since the watcher's initial patch may call $forceUpdate (e.g. inside child
    // component's mounted hook), which relies on vm._watcher being already defined

    //! 挖个新坑 下节分享渲染watch。 经过渲染后,即可获取`$el`
    new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */)
    hydrating = false

    // manually mounted instance, call mounted on self
    // mounted is called for render-created child components in its inserted hook
    if (vm.$vnode == null) {
      vm._isMounted = true
      // 因为已经渲染,`$el`此时已经可以成功获取
      callHook(vm, 'mounted')
    }
    return vm
  }

beforeUpdateupdated

beforeUpdateupdated涉及到watcher,因此将会在以后的章节进行详解。

beforeDestroydestroyed

Vue将卸载组件的方法直接定义在原型链上,因此可以通过直接调用vm.$destroy()方法来卸载组件。

Vue.prototype.$destroy = function () {
    const vm: Component = this
    if (vm._isBeingDestroyed) {
      return
    }
    // 吊起`beforeDestroy`钩子函数
    callHook(vm, 'beforeDestroy')
    vm._isBeingDestroyed = true
    // remove self from parent
    const parent = vm.$parent
    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
      remove(parent.$children, vm)
    }
    // teardown watchers
    if (vm._watcher) {
      vm._watcher.teardown()
    }
    let i = vm._watchers.length
    while (i--) {
      vm._watchers[i].teardown()
    }
    // remove reference from data ob
    // frozen object may not have observer.
    if (vm._data.__ob__) {
      vm._data.__ob__.vmCount--
    }
    // call the last hook...
    vm._isDestroyed = true
    // invoke destroy hooks on current rendered tree
    vm.__patch__(vm._vnode, null)
    // fire destroyed hook
    // 调起`destroyed`钩子函数
    callHook(vm, 'destroyed')
    // turn off all instance listeners.
    vm.$off()
    // remove __vue__ reference
    if (vm.$el) {
      vm.$el.__vue__ = null
    }
    // release circular reference (#6759)
    if (vm.$vnode) {
      vm.$vnode.parent = null
    }
  }
}

问题

  1. vue.$children[0].$options['beforeCreate']为什么是一个数组?
  2. 卸载组件,会触发一个updated called?
  3. new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */)这行代码之后发生了什么?
  4. beforeUpdate背后实现原理。

相关推荐