从VUE源码看生命周期过程

487 阅读2分钟

vue生命周期主要是在实例的初始化阶段做的一些事情,比如初始化data、prop等,还会对数据进行响应式处理,模版编译,数据渲染等操作。

vue的初始化阶段,根据生命周期我们可以知道,可以总结为四个阶段:

  • 初始化阶段
  • 模版编译阶段
  • 节点挂载阶段
  • 卸载阶段

初始化阶段

从我们new一个vue实例开始,到created之间,这个阶段就是初始化。结合源码我们可以看一下里面做了什么事情。

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

vue的真面目其实就是一个函数,里面执行_init()方法。接下来我们进去这个方法看一下,我简化了一下:

Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    ...
    // merge options
    if (options && options._isComponent) {
      initInternalComponent(vm, options)
    } else {
      /**
       * 1、合并配置
       */
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    ...
    /**
     * 2、初始化生命周期
     */
    initLifecycle(vm)
    /**
     * 3、初始化事件中心
     */
    initEvents(vm)
    /**
     * 4、初始化渲染
     */
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    /**
     * 初始化 data
     */
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')
    ...

    /**
     * 在初始化的最后,检测到如果有 el 属性,则调用 vm.$mount 方法挂载 vm ,挂载的目标就 是把模板渲染成最终的 DOM
     */
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }

粗略的看一下主流程,其实就是做了一些配置的合并,生命周期、事件、渲染函数、data数据的初始化。最后检测到有模版的话就挂载模版。

模版编译阶段

在这个阶段,首先会判断一下是否有el选项,如果没有其实就是走了只包含运行时的版本,它是没有模版编译这一说的,因为它默认实例上已经有了渲染函数,我们需要手动开启模版编译与挂载。

如果没有渲染函数就会创建一个并返回空节点,避免报错。最后使用mountComponent将实例挂载DOM节点上。

我们在只包含运行时的vue代码里面看到了相关的函数【vue.runtime.esm.js】:

Vue.prototype.$mount = function (
  el,
  hydrating
) {
  el = el && inBrowser ? query(el) : undefined;
  return mountComponent(this, el, hydrating)
};

我们在mountComponent里面发现了这段代码,判断没有render函数的话就会调用方法创建一个。完了之后调用beforeMount钩子函数,之后将执行真正的挂载操作。

if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode;
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {
        warn(
          'You are using the runtime-only build of Vue where the template ' +
          'compiler is not available. Either pre-compile the templates into ' +
          'render functions, or use the compiler-included build.',
          vm
        );
      } else {
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        );
      }
    }
  }
  callHook(vm, 'beforeMount');
var createEmptyVNode = function (text) {
  if ( text === void 0 ) text = '';
  var node = new VNode();
  node.text = text;
  node.isComment = true;
  return node
};

这是首次渲染的时候执行的代码,其实就是调用vm._render()获取到虚拟dom,然后调用update方法把虚拟dom渲染

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

这个update方法是watcher订阅者的方法

update () {
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

首次渲染会执行run方法,这是同步操作。queueWatcher方法是后面数据发生变化的时候推到异步队列里面去用的。

run方法里面最重要的其实就是this.get()方法,这个get方法很重要。里面记录了一个非常重要的参数getter, 这个参数实际上就是vm.update(vm.render()), 然后执行getter触发全局的依赖收集,这是我们后面能够获取到响应式数据的基础

依赖收集后才能派发通知,才能知道是谁依赖了我,我该通知给谁

run () {
    if (this.active) {
      /**
       * 那么对于渲染 watcher 而言,它在执行 this.get() 方法求值的时候,会执行 getter 方法
       */
      const value = this.get()
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        /**
         *  注意回调函数执行的时候会把第一个和第二个参数传入新值 value 和旧值 oldValue
         *  这就是当我们添加自定义 watcher 的时候能在回调函数的参数中拿到新旧值的原因
         */
        const oldValue = this.value
        this.value = value
        if (this.user) {
          const info = `callback for watcher "${this.expression}"`
          invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

模版编译原理

模版编译阶段包含了三个阶段,它的主要作用就是将模版编译成渲染函数,其中包含了将模版解析成抽象语法树(AST),然后语法数生成渲染函数。

  • 解析器 : 将模版编译成抽象语法树(AST)
  • 优化器 : 遍历AST,检测静态节点并打标签,除了首次渲染后面就不用任何渲染操作,优化性能。
  • 代码生成器: 将AST转换成渲染函数中的内容,内容就是“代码字符串”。虚拟DOM有很多类型,不同类型对应不同的创建方法。

节点挂载阶段

在上面的图中,beforeMount到mounted就是把上一步获取到的模版render到指定的DOM元素中,完成挂载。

在模版渲染完了之后,vue会持续去追踪依赖的变化,然后通知虚拟DOM完成patch,看哪些节点变化了,如果有数据更新了就会执行beforeUpdate钩子,更新完了渲染完了之后就调用updated函数。

这一部分就是vue的核心,关于数据的处理部分都在这里,值得我们好好阅读。

节点卸载阶段

节点的卸载阶段非常简单,无非就是把上面初始化的一堆东西给还原。这个阶段vue会从自身的父组件上面删除,然后移除实例上面所有的依赖,取消所有的数据追踪,移除所有的事件监听器。

Vue.prototype.$destroy = function () {
    const vm: Component = this
    // 判断是不是在销毁阶段,防止组件重复销毁
    if (vm._isBeingDestroyed) {
      return
    }
    // 触发钩子函数beforeDestroy
    callHook(vm, 'beforeDestroy')
    vm._isBeingDestroyed = true
    // 如果当前自组建有父组件,并且父组件没有销毁且不是抽象组件
    // 那么就把当前组件从父组件的$children移除
    const parent = vm.$parent
    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
      remove(parent.$children, vm)
    }
    // 实例自身从其他数据依赖表中删除、
    if (vm._watcher) {
      vm._watcher.teardown()
    }
    // 所有实例内的数据对其他的数据依赖都在_watchers里面
    // 将其中的每一个watcher都调用teardown方法,从而实现移除实例内数据对其他数据的依赖
    let i = vm._watchers.length
    while (i--) {
      vm._watchers[i].teardown()
    }
    // 移除响应式数据
    if (vm._data.__ob__) {
      vm._data.__ob__.vmCount--
    }
    // 标记当前实例已经销毁
    vm._isDestroyed = true
    // 实例的虚拟DOM设置为null
    vm.__patch__(vm._vnode, null)
    // 触发钩子函数
    callHook(vm, 'destroyed')
    // 取消事件监听
    vm.$off()
    if (vm.$el) {
      vm.$el.__vue__ = null
    }
    if (vm.$vnode) {
      vm.$vnode.parent = null
    }
  }