生命周期
# 生命周期
https://vue-js.com/learn-vue/lifecycle/#_1-%E5%89%8D%E8%A8%80
# 前言
在Vue一个相对比较大的框架中,在不同的阶段需要做出不同的事情,包括初始化、编译、挂载、销毁等等
Vue的生命周期究竟是怎么样的?
在Vue中,把Vue实例从被创建出来到最终被销毁的这一过程称为Vue实例的生命周期,同时,在Vue实例生命周期的不同阶段Vue还提供了不同的钩子函数,在不同的钩子中可以做不同的事情
# 生命周期流程图
大致分为几个阶段:
- 初始化阶段:为
Vue
实例上初始化一些属性,事件以及响应式数据; - 模板编译阶段:将模板编译成渲染函数;
- 挂载阶段:将实例挂载到指定的
DOM
上,即将模板渲染到真实DOM
中; - 销毁阶段:将实例自身从父组件中删除,并取消依赖追踪及事件监听器;
那么每个阶段都干了些什么呢?
# 初始化阶段
为Vue的实例话初始化一些属性、事件以及响应式数据
从上述的图中可以看出,初始化阶段所做的工作也可大致分为两部分:
- 第一部分是
new Vue()
,也就是创建一个Vue
实例 - 第二部分是为创建好的
Vue
实例初始化一些事件、属性、响应式数据等
# new Vue
Vue实例上就是一个类,通过new关键词实例化出来的一个对象,也就是说 new Vue() 实际上是执行了Vue类的构造函数
源码:src/core/instance/index.js
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)
}
将用户所写的options传到 _init 函数中,接着往上走,找到 initMixin(Vue) 函数
源码:src/core/instance/init.js
// 给Vue的原型上绑定_init方法
export function initMixin (Vue) {
Vue.prototype._init = function (options) {
const vm = this
// 合并属性
// 把用户传递的options选项与当前构造函数的options属性及其父级构造函数的options属性进行合并,得到一个新的options选项赋值给 $options属性,并将$options属性挂载到Vue实例上
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
// 调用一些初始化函数来为Vue实例初始化一些属性,事件,响应式数据等
vm._self = vm
initLifecycle(vm) // 初始化生命周期
initEvents(vm) // 初始化事件
initRender(vm) // 初始化渲染
callHook(vm, 'beforeCreate') // 调用生命周期钩子函数
initInjections(vm) //初始化injections
initState(vm) // 初始化props,methods,data,computed,watch
initProvide(vm) // 初始化 provide
callHook(vm, 'created') // 调用生命周期钩子函数
// 判断用户是否传入了el选项,如果传入了则调用$mount函数进入模板编译与挂载阶段,如果没有传入el选项,则不进入下一个生命周期阶段,需要用户手动执行vm.$mount方法才进入下一个生命周期阶段
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
**合并策略:**mergeOptions
如果 childVal
不存在,就返回 parentVal
;否则再判断是否存在 parentVal
,如果存在就把 childVal
添加到 parentVal
后返回新数组;否则返回 childVal
的数组。所以回到 mergeOptions
函数,一旦 parent
和 child
都定义了相同的钩子函数,那么它们会把 2 个钩子函数合并成一个数组
为什么要把相同的钩子函数转换成数组呢?
因为Vue
允许用户使用Vue.mixin
方法(关于该方法会在后面章节中介绍)向实例混入自定义行为,Vue
的一些插件通常都是这么做的。所以当Vue.mixin
和用户在实例化Vue
时,如果设置了同一个钩子函数,那么在触发钩子函数时,就需要同时触发这个两个函数,所以转换成数组就是为了能在同一个生命周期钩子列表中保存多个钩子函数
callHook函数如何触发钩子函数?
源码:src/core/instance/lifecycle.js
export function callHook (vm: Component, hook: string) {
// 从实例的$options中获取到需要触发的钩子名称所对应的钩子函数数组handlers
const handlers = vm.$options[hook]
if (handlers) {
// 每个生命周期钩子名称都对应了一个钩子函数数组。然后遍历该数组,将数组中的每个钩子函数都执行一遍
for (let i = 0, j = handlers.length; i < j; i++) {
try {
handlers[i].call(vm)
} catch (e) {
handleError(e, vm, `${hook} hook`)
}
}
}
}
# initLifecycle
源码:src/core/instance/lifecycle.js
export function initLifecycle (vm: Component) {
const options = vm.$options
// 给实例上挂载$parent属性
/*
如果当前组件不是抽象组件并且存在父级,那么就通过while循环来向上循环,如果当前组件的父级是抽象组件并且也存在父级,那就继续向上查找当前组件父级的父级,直到找到第一个不是抽象类型的父级时,将其赋值vm.$parent,同时把该实例自身添加进找到的父级的$children属性中。这样就确保了在子组件的$parent属性上能访问到父组件实例,在父组件的$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
初始化事件函数,在Vue实例生成的时候,需要对Vue实例注册事件,也就是通过initEvents实现的
解析事件
源码:src/compiler/parser/index.js
export const onRE = /^@|^v-on:/
export const dirRE = /^v-|^@|^:/
function processAttrs (el) {
const list = el.attrsList
let i, l, name, value, modifiers
for (i = 0, l = list.length; i < l; i++) {
name = list[i].name
value = list[i].value
// 在对标签属性进行解析时,判断如果属性是指令,首先通过 parseModifiers 解析出属性的修饰符,然后判断如果是事件的指令,则执行 addHandler(el, name, value, modifiers, false, warn) 方法
if (dirRE.test(name)) {
// 解析修饰符
modifiers = parseModifiers(name)
if (modifiers) {
name = name.replace(modifierRE, '')
}
if (onRE.test(name)) { // v-on
name = name.replace(onRE, '')
// 源码:src/compiler/helpers.js
addHandler(el, name, value, modifiers, false, warn)
}
}
}
}
父组件给子组件的注册事件中,把自定义事件传给子组件,在子组件实例化的时候进行初始化;而浏览器原生事件是在父组件中处理
实例初始化阶段调用的初始化事件函数initEvents
实际上初始化的是父组件在模板中使用v-on或@注册的监听子组件内触发的事件。
initEvent函数分析
export function initEvents (vm: Component) {
// 在vm上新增_events属性并将其赋值为空对象,用来存储事件
vm._events = Object.create(null)
// init parent attached events
const listeners = vm.$options._parentListeners
// 获取父组件注册的事件赋给listeners,如果listeners不为空,则调用updateComponentListeners函数,将父组件向子组件注册的事件注册到子组件的实例中
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
初始化事件函数initEvents实际上初始化的是父组件在模板中使用v-on或@注册的监听子组件内触发的事件
# initInjections
允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。并且
provide
选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的属性
inject
选项应该是:
- 一个字符串数组,或
- 一个对象,对象的 key 是本地的绑定名,value 是:
- 在可用的注入内容中搜索用的 key (字符串或 Symbol),或
- 一个对象,该对象的:
from
属性是在可用的注入内容中搜索用的 key (字符串或 Symbol)default
属性是降级情况下使用的 value
# initState
用于初始化实例的状态,主要包括:props、data、methods、computed、watch 选项
源码:src/core/instance/state.js
export function initState (vm: Component) {
// 给实例上新增了一个属性_watchers,用来存储当前实例中所有的watcher实例,无论是使用vm.$watch注册的watcher实例还是使用watch选项注册的watcher实例,都会被保存到该属性中
vm._watchers = []
const opts = vm.$options
// 有什么选项就调用对应的选项初始化子函数去初始化什么选项
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
初始化props
props选项通常是由当前组件的父级组件传入的,当父组件在调用子组件的时候,通常会把props属性值作为标签属性添加在子组件的标签上
初始化methods
判断method有没有?
method的命名符不符合命名规范?
如果method既有又符合规范那就把它挂载到vm实例上
初始化data
通过一些条件判断用户传入的data选项是否合法
最后将data转换成响应式并绑定到实例vm上
初始化computed
计算属性的结果会被缓存,除非依赖的响应式属性变化才会重新计算
初始化watch
用来侦听某个已有的数据,当该数据发生变化时执行对应的回调函数
# 模版编译阶段
模版编译阶段所做的主要工作是获取到用户传入的模板内容并将其编译成渲染函数
vue
基于源码构建的有两个版本,一个是runtime only
(一个只包含运行时的版本)
另一个是runtime + compiler
(一个同时包含编译器和运行时的完整版本)
后者相当于多了一个编译器,也就是当写templete的时候使用runtime+compiler版本,或者使用runtime版本加上vue-loader进行编译,达到可以直接运行的状态。当时用render的时候,这时候直接用runtime版本就好了,也就是当前已经存在了已经编译好了的代码,可以直接运行。
在vue的运行版和完整版之间大致相差了30%的体积大小,通过vue-loader的方式有效的将包的体积变小了,性能上优化了不少
# 模版编译阶段分析
完整版和只包含运行时版之间的差异主要在于是否有模板编译阶段,而是否有模板编译阶段主要表现在vm.$mount
方法的实现上
运行版
Vue.prototype.$mount = function (el,hydrating) {
el = el && inBrowser ? query(el) : undefined;
return mountComponent(this, el, hydrating)
};
完整版
var mount = Vue.prototype.$mount;
Vue.prototype.$mount = function (el,hydrating) {
// 省略获取模板及编译代码
return mount.call(this, el, hydrating)
}
# 挂载阶段
挂载阶段所做的主要工作是创建Vue
实例并用其替换el
选项对应的DOM
元素,同时还要开启对模板中数据(状态)的监控,当数据(状态)发生变化时通知其依赖进行视图更新
在模版编译阶段,无论是运行版直接$mount然后进入挂载,还是完整版在实现编译模版之后回到只包含运行版中的$mount上进行挂载,最终都是走的mountComponent函数
export function mountComponent (vm,el,hydrating) {
vm.$el = el
// 首先会判断实例上是否存在渲染函数,如果不存在,则设置一个默认的渲染函数createEmptyVNode,该渲染函数会创建一个注释类型的VNode节点
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
}
// 调用callHook函数来触发beforeMount生命周期钩子函数
callHook(vm, 'beforeMount')
let updateComponent
// 定义了一个updateComponent函数
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// 挂载的上半段和下半段,上半段实现的是完成挂在,下半段就是开启模版中的数据监控,当数据发生变化时,通知依赖进行试图更新
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
# 总结
在该阶段中所做的主要工作是创建Vue实例并用其替换el选项对应的DOM元素,同时还要开启对模板中数据(状态)的监控,当数据(状态)发生变化时通知其依赖进行视图更新。
我们将挂载阶段所做的工作分成两部分进行了分析
第一部分是将模板渲染到视图上
第二部分是开启对模板中数据(状态)的监控
两部分工作都完成以后挂载阶段才算真正的完成了。
# 销毁阶段
将当前的Vue
实例从其父级实例中删除,取消当前实例上的所有依赖追踪并且移除实例上的所有事件监听器
源码:src/core/instance.lifecycle.js
Vue.prototype.$destroy = function () { const vm: Component = this // _isBeingDestroyed属性标志着当前实例是否处于正在被销毁的状态 if (vm._isBeingDestroyed) { return } // 触发生命周期钩子函数beforeDestroy,该钩子函数的调用标志着当前实例正式开始销毁 callHook(vm, 'beforeDestroy') vm._isBeingDestroyed = true // remove self from parent // 将当前的Vue实例从其父级实例中删除 const parent = vm.$parent if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) { remove(parent.$children, vm) } // teardown watchers // 实例身上的依赖包含两部分:一部分是实例自身依赖其他数据,需要将实例自身从其他数据的依赖列表中删除;另一部分是实例内的数据对其他数据的依赖(如用户使用$watch创建的依赖),也需要从其他数据的依赖列表中删除实例内数据。所以删除依赖的时候需要将这两部分依赖都删除掉 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 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 }}
当调用了实例上的 vm.$destory 方法后,实例就进入了销毁阶段,在该阶段所做的主要工作是将当前的 Vue 实例从其父级实例中删除,取消当前实例上的所有依赖追踪并且移除实例上的所有事件监听器