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

简介

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

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

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

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

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

资源

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

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

表象

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

$ 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)。异步

控制台输出

控制台输出的内容很是具备表明性。

1539056706.png

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

图片描述

在触发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背后实现原理。
相关文章
相关标签/搜索