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
}
}