Vue2生命周期详解

3,194 阅读5分钟

认识Vue2中的生命周期

  • beforeCreate 在实例初始化之后,进行数据侦听和事件/侦听器的配置之前同步调用。
  • created 在实例创建完成后被立即同步调用。
  • beforeMount 在挂载开始之前被调用
  • mounted 实例被挂载后调用
  • beforeUpdate 在数据发生改变后,DOM 被更新之前被调用。
  • updated 在数据更改导致的虚拟 DOM 重新渲染和更新完毕之后被调用。
  • activated 被 keep-alive 缓存的组件激活时调用。
  • deactivated 被 keep-alive 缓存的组件失活时调用。
  • beforeDestroy 实例销毁之前调用。在这一步,实例仍然完全可用。
  • destroyed 实例销毁后调用。

如上是官方文档对生命周期的简要描述,那接下来我们就来详细介绍一下每个生命周期调用前实例是怎样的,此时实例上究竟挂载了哪些东西,在该阶段我们能用的属性有哪些

beforeCreate

这是我们组件进入时最先调用的生命周期,在这里我们先介绍一下如何走到这一步调用的,后面就不在赘述了 我们通常会在main.js中引入vue,然后使用如下代码;

	import vue from "vue"new Vue({
	  options//传入的参数
	}).$mount("#app");

首先,我们来看看导入的Vue是什么

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)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

如上代码,我们导入的是一个构造函数,在我们使用import导入的时候,已经执行了initMixin这几个函数;我们使用new Vue()的时候创建了vue的一个实例;

接下来我们来介绍一下initMixin这几个函数分别都干了什么事

  • 第一个initMixin,这个函数只干了一件事,就是在vue原型对象上加了_init方法,就是Vue.prototype._init=function(vue){},后面用到我们在说这个方法具体干了什么;
  • 第二个stateMixin,看名字大概就能猜到是初始化状态之类的,我们来看看吧,这个方法初始化了我们常用的props,props,data,watch,还有不是很常用的watch,还有不是很常用的set,delete;dataprops添加了监听,Object.defineProperty(Vue.prototype,delete;给data和props添加了监听,Object.defineProperty(Vue.prototype, 'data',{get:function(){return this._data}}) Object.defineProperty(Vue.prototype, '$props', {get:function(){return this._props}}) 这也是为什么我们在模版中使用data和props中的数据时可以直接用而不用写上data.xxx;
  • 第三个eventsMixin,初始化了事件扩展,我们常用的总线时间on,on,once,off,off,emit就是在这里添加的;
  • lifecycleMixin就是生命周期的扩展了,这里给实例添加了_update,forceUpdate,forceUpdate,destroy
  • renderMixin就初始化了渲染事件,将$nextTick,_render挂载到实例上

回到我们上面,当我们创建vue实例时,在构造函数里面调用了_init函数,现在我们来看看这个函数干了什么;

  • 首先给组件加一个_uid属性,然后合并我们传入的属性信息和构造函数中的属性信息放到$options属性上,接着执行下面几个函数
    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
    callHook(vm, 'created')

我们看到在执行beforeCreate钩子函数前,执行了3个函数,这3个函数分别干了什么;

  • initLifecycle,这个函数主要给组件添加了一些属性,并且找到组件的根结点并将组件实例放入根节点的$children数组中; 添加的属性如下所示:
 let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent //循环向上查找根节点
    }
    parent.$children.push(vm)
  }
  vm.$parent = parent
  vm.$root = parent ? parent.$root : vm
  vm.$children = []
  vm.$refs = {}
  vm._watcher = null
  vm._inactive = null
  vm._directInactive = false
  vm._isMounted = false
  vm._isDestroyed = false
  vm._isBeingDestroyed = false
  • initEvents和initRender,前者是初始化父组件传递的事件,后者是将创建虚拟节点的方法绑定到实例上

综上,我们可以知道,在beforeCreate中,我们仅仅能拿到组件的实例,这个阶段可以给组件实例添加属性什么的,但是我们没法操作数据以及dom

created

从上文我们可以看到在beforeCreate和created之间也执行了3个函数,我们来看看这三个函数;

  • initInjections(vm),initProvide(vm)这个从名字就可以看到是初始化inject和provide了, inject是注入组件的内容,provide是组件提供给外部使用的内容,这个通常开发框架的时候才会用到,我们这里就不展开讲了;

  • initState(vm),我们重点来看看这里做了什么,如下代码,watcher我们先不管,这个是响应式相关的,接下来我们看到在这里先初始化了props,然后是methods,再是data,computed,watch

vm._watchers = []//给组件添加watcher
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)//添加一些警告,规则之类的
  if (opts.methods) initMethods(vm, opts.methods)//将方法挂载到实例上,并给每个方法绑定this
  if (opts.data) {
    initData(vm)//会在这里判断key有没有在props和methods中重复定义,有重复定义非生产环境会报警告
  } else {
    observe(vm._data = {}, true /* asRootData */)//如果没有data,则侦听一个空对象{}
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)//对watch中的函数添加监听
  }

通过上面的解读及代码注释我们很明确知道在调用created时可以使用data,props,methods,computed中定义的数据,包括通过inject注入的数据也是可以用的

beforeMount

在上面的this._init()函数中最后执行了

if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }

在我们的main.js中我们通常会这样写

new Vue({el:"#app",...});
//或者
new Vue().$mount("#app");

可以看到这两种写法最终执行的都是vue.$mount("#app")方法,所以是等效的 那接下来我们就来看看mount方法吧

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
export function query (el: string | Element): Element {
  if (typeof el === 'string') {
    const selected = document.querySelector(el)
    if (!selected) {
      process.env.NODE_ENV !== 'production' && warn(
        'Cannot find element: ' + el
      )
      return document.createElement('div')
    }
    return selected
  } else {
    return el
  }
}
  • 上面代码主要是先找到el对应的dom对象,如果没有则报警告;我们主要来看mountComponent,这个方法我放个图吧,主要太长了,把不重要的折叠起来了,折叠的直接跳过去,不影响我们理解代码

如上图,我们在这里调用beforeMount,可以看到这边对比created时期仅仅只是获取了一下dom对象,在这里其实是可以拿到dom对象的

mounted

接下来我们来仔细看上张图中的折叠部分

我们主要看vm._render()和vm._update()方法,还记得我们前面说到的在初始化的时候给实例挂载的这两个方法吗,忘了的往前翻一下哈; 现在我们来看看这两个方法具体的实现逻辑:

  • 先来看_render方法,这个方法的主要作用就是将真实节点转化为虚拟节点,具体怎么转换我们这边就先不讲了,简单来说就是将文本解析成对象;

  • 接下来我们来看_update方法,这里判断有没有prevVnode,如果没有执行挂载,有则执行更新;也就是说这个方法执行完后,我们的挂载就完成了,所以接下来我们就可以调用mounted钩子函数了;

这边我们可以看到在调用mounted之前有个判断if(vm.$vnode==null),也就是说我们的mounted钩子函数只有在第一次挂载的时候才会调用,之后的更新就不会再执行这个方法了;

beforeUpdate

在上面讲mountComponent方法时,我们看到截图中有一个new Watcher,我们在这里创建了一个watcher实例,这个watcher实例就是用来收集我们的数据依赖的,怎么个收集法呢,简单来说就是创建一个deps对象,我们数据在使用的时候就会自动调用数据的get方法,我们在这个get方法里面把使用这个数据的方法添加到这个deps里面,这样的话,当数据更改了,调用数据的set方法时我们就吧deps中添加的所有方法都拿出来执行一遍,这样就实现了数据的双向绑定了;beforeCreate具体调用在下面截图中

在我们将deps中的对象拿出来执行前,这个时候就调用了beforeCreate钩子函数,所以这个时候我们的数据是已经是新的了,但是所有的依赖项还没有执行更新

updated

上文说到当数据更新了我们会取出所有的依赖方法执行更新,vue在这里做了一个优化,我们在执行update()方法时,会调用queueWatcher(this)方法,这边会判断watcher.id在不在队列中,如果已经在的话就不添加到更新队列,如果是生产环境我们会调用nextTick(flushSchedulerQueue),flushSchedulerQueue这个方法是真正执行更新的方法,使用nextTick包装是为了优化在一个事件循环中只执行一次,不重复执行;

这边我们可以看到将watcher一一取出的时候调用了watcher.before(),这个其实就是beforeCreate钩子函数,在我们前面创建Watcher实例的时候传入的;然后我们才执行的watcher.run()方法执行更新,当更新完后我们要重置更新队列,之后调用callUpdatedHooks(updatedQueue)执行updated钩子函数

从代码中我们可以看到,在执行updated钩子函数时所有依赖数据的依赖项已经执行完更新,当然run方法也会调用patch方法更新vnode渲染到界面上

activated和deactivated我们用得比较少,碍于篇幅已经比较长,我们就先不展开讲了

beforeDestroy和destroyed

最开始初始化的时候我们就在实例上挂载了destroy方法;从代码中可以看到在我们执行beforeDestroy时,组件的实例依然存在,完全可以用;然后我们组件实例isBeingDeatroyedtrue,然后我们一一卸载组件的属性,然后执行patch更新组件的vnodenull,卸载组件;最后调用destroyed钩子函数,再调用vm.destroy方法;从代码中可以看到在我们执行beforeDestroy时,组件的实例依然存在,完全可以用;然后我们组件实例的_isBeingDeatroyed为true,然后我们一一卸载组件的属性,然后执行patch更新组件的vnode为null,卸载组件;最后调用destroyed钩子函数,再调用vm.off移除组件所有监听;