前面的话
Vue实例在创建时有一系列的初始化步骤,例如建立数据观察,编译模板,创建数据绑定等。这篇文章介绍Vue实例的生命周期。
图解
vue生命周期总共分为8个阶段: 创建前/后,载入前/后,更新前/后, 销毁前/后。
创建前/后
new Vue实例之后会调用init 方法,init方法代码如下:
Vue.prototype._init = function (options) {
const vm = this
/*合并option配置*/
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
...
// 一系列初始化
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')//调用beforeCreate钩子
initInjections(vm)
initState(vm)
initProvide(vm)
callHook(vm, 'created') // 调用created钩子
// ...
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
在调用beforeCreate钩子函数前,调用的三个初始化函数的作用:
- initLifecycle : 确认组件(Vue实例)的父子关系
- initEvents: 将父组件的自定义事件传递给子组件
- initRender: 提供将render函数转为vnode的方法
可见:在调用beforeCreate钩子函数时,不能使用props、methods、data、computed和watch等数据。
在调用created钩子函数前,调用的三个初始化函数的作用:
-
initInjections: 主要作用是初始化inject,可以访问到对应的依赖
-
initState:初始化会使用到的状态,状态包括props、methods、data、computed、watch五个选项。
export function initState(vm) { ... const opts = vm.$options if(opts.props) initProps(vm, opts.props) if(opts.methods) initMethods(vm, opts.methods) if(opts.data) initData(vm) ... if(opts.computed) initComputed(vm, opts.computed) if(opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch) } }
在initState函数中顺序调用这些函数: ininProps、initMethods、initData、initComputed、initWatch。这样初始化的结果是在data选项中可以使用props;在computed选项中可以使用data、props中的数据;wacth选项可以监听data、props、computed数据的变化。methods选项的组成是函数,在这些函数被调用时,初始哈工作已经完成,可以使用全部的数据。
-
initProvide:初始化父组件提供的provide依赖。
可见:在调用created钩子函数时,可以访问data里面的数据。
提两个问题:
-
可以在beforeCreate钩子内通过this访问到data中定义的的变量么?
答案:是不可以访问的,在调用beforeCreate这个钩子函数时,data中的变量还没有挂载到this上,这个时候访问时undefined。
-
methods选项内的方法可以使用箭头函数么,会造成什么样的结果?
答案:是不可以使用箭头函数的,箭头函数中的this,是继承父级作用域中的this。在vue内部,methods内每个方法的上下文是当前的组件实例。如果使用的箭头函数,this就指向当前实例的父级作用域上的this,也就是undefined。
载入前/后
-
在init函数的最后检查
vm.$options
上面有没有el属性
,如果有的话使用vm.$mount
方法挂载vm,形成数据层与视图层的联系。如果没有el属性,就自己手动vm.$mount("#app")
挂载。 -
而
vm.$mount()
在很多地方都会定义,是根据不同打包方式和平台有关的,比如这几个文件都定义了src/platform/web/entry-runtime-with-compiler.js、src/platform/web/runtime/index.js、src/platform/weex/runtime/index.js,我们的关注点在第一个文件,但在 entry-runtime-with-compiler.js 文件中会首先把 runtime/index.js 中的$mount 方法保存下来
,并在最后用 call 运行:// src/platform/web/entry-runtime-with-compiler.js const mount = Vue.prototype.$mount // 把原来的$mount保存下来,位于 src/platform/web/runtime/index.js Vue.prototype.$mount = function( el?: string | Element, // 挂载的元素 hydrating?: boolean // 服务端渲染相关参数 ): Component { el = el && query(el) const options = this.$options if (!options.render) { // 如果没有定义render方法 let template = options.template // 把获取到的template通过编译的手段转化为render函数 if (template) { const { render, staticRenderFns } = compileToFunctions(template, {...}, this) options.render = render } } return mount.call(this, el, hydrating) // 执行原来的$mount }
在Vue 2.0中我们要将
template、el
转化为渲染render()函数
,上面代码中的compileToFunctions
函数的作用就是将template通过编译的手段转化为render函数。 -
转化为render函数之后,完成挂载的代码如下:
// src/platform/weex/runtime/index.js Vue.prototype.$mount = function ( el?: string | Element, // 挂载的元素 hydrating?: boolean // 服务端渲染相关参数 ): Component { el = el && inBrowser ? query(el) : undefined // query就是document.querySelector方法 return mountComponent(this, el, hydrating) // 位于core/instance/lifecycle.js }
看一下
mountComponent
这个方法:// src/core/instance/lifecycle.js export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { vm.$el = el if (!vm.$options.render) { vm.$options.render = createEmptyVNode } callHook(vm, 'beforeMount') // 调用beforeMount钩子 // 渲染watcher,当数据更改,updateComponent作为Watcher对象的getter函数,用来依赖收集,并渲染视图 let updateComponent updateComponent = () => { vm._update(vm._render(), hydrating) } // 渲染watcher, Watcher 在这里起到两个作用,一个是初始化的时候会执行回调函数 // ,另一个是当 vm 实例中的监测的数据发生变化的时候执行回调函数 new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted) { callHook(vm, 'beforeUpdate') // 调用beforeUpdate钩子 } } }, true /* isRenderWatcher */) // 这里注意 vm.$vnode 表示 Vue 实例的父虚拟 Node,所以它为 Null 则表示当前是根 Vue 的实例 if (vm.$vnode == null) { vm._isMounted = true // 表示这个实例已经挂载 callHook(vm, 'mounted') // 调用mounted钩子 } return vm }
这里可以看到,
beforeMount
钩子函数在render
函数生成之后、开始执行挂载之前调用的。 beforeMount钩子函数执行之后,会实例化一个Watcher
观察者对象。这个观察者在响应式原理中发挥巨大的作用。 -
在完成挂载之后,会调用mounted钩子函数,此时可以对DOM进行操作。
更新前/后
这两个钩子函数跟数据更新有关系。
其相关代码,在实例挂载过程中有如下代码:
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true)
Watcher构造函数代码:
class Watcher {
constructor (vm,expOrFn,cb,options,isRenderWatcher) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
if (options) {
/* 省略... */
this.before = options.before
}
}
/* 省略... */
// 数据更新时调用,没有添加强制要求时,默认使用queueWatcher函数完成数据更新
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
/* 省略... */
}
在实例化Watcher观察者时,会将传入的before函数添加到观察者对象上。在数据更新时会执行update方法,在没有添加强制要求时,默认使用queueWatcher函数完成数据更新。
export function queueWatcher (watcher: Watcher) {
/* 省略... */
flushSchedulerQueue()
/* 省略... */
}
function flushSchedulerQueue () {
/* 省略... */
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
// 数据更新调用run方法
watcher.run()
/* 省略... */
}
callUpdatedHooks(updatedQueue)
/* 省略... */
}
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')
}
}
}
从上面代码看出,数据更新时通过调用观察者对象的实例方法run
完成的,在调用run之前,会调用实例对象上的 before
方法,从而执行beforeUpdata钩子函数
;在调用run之后(数据更新完成后),通过调用callUpdatedHooks函数执行updated钩子函数
。
总结:在生成渲染函数之后,调用 beforeMount 钩子,接着根据渲染函数生成真实DOM并挂载,然后调用 mounted 钩子。
销毁前/后
调用实例方法$destroy()完全销毁一个实例。代码如下:
Vue.prototype.$destroy = function () {
const vm = this
if (vm._isBeingDestroyed) { return }
callHook(vm, 'beforeDestroy')
vm._isBeingDestroyed = true
const parent = vm.$parent
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract){
remove(parent.$children, vm)
}
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
vm.__patch__(vm._vnode, null)
callHook(vm, 'destroyed')
vm.$off()
if (vm.$el) {
vm.$el.__vue__ = null
}
if (vm.$vnode) {
vm.$vnode.parent = null
}
}
- 先判断实例上的_isBeingDestroyed是否为true,这是实例正在被销毁的标识,为了防止重复销毁组件。在正式开始执行销毁之前,调用
beforeDestroy钩子函数
。 - 之后开始销毁组件:
- 将实例从其父级实例中删除
- 移除实例的依赖
- 移除实例内响应式数据的引用
- 删除子组件实例
- 完成上述操作之后,调用
destroyed钩子函数
。
总结
-
beforeCreate (创建前): 数据对象 data都是undefined, 还未初始化
-
created (创建后) : data数据初始化完成, el还未初始化
-
beforeMount (载入前): vue实例的$el和data都初始化了, 相关的render函数首次被调用。实例已完成以下的配置:编译模板,把data里面的数据和模板生成html。注意此时还没有挂载html到页面上。
-
mounted (载入后) :在el 被新创建的 vm.$el替换,并挂载到实例上去之后调用。实例已完成以下的配置:用上面编译好的html内容替换el属性指向的DOM对象。完成模板中的html渲染到html页面中。此过程中进行ajax交互。
-
beforeUpdate (更新前): 在数据更新之前调用,发生在虚拟DOM重新渲染和打补丁之前。
-
updated (更新后): 在由于数据更改导致的虚拟DOM重新渲染和打补丁之后调用。调用时,组件DOM已经更新,所以可以执行依赖于DOM的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用。
-
beforeDestroy (销毁前): 在实例销毁之前调用。实例仍然完全可用。
-
destroyed (销毁后): 在实例销毁之后调用。调用后,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务器端渲染期间不被调用。
参考文章: