Vue组件的生命周期
Vue的组件从创建、到数据更新最后到组件销毁,在这个过程中Vue框架都会对组件都会经过一系列固定的操作,例如初始化监听器、更新DOM等等。
在这个过程中,Vue也提供了不同阶段的回调函数给开发者,来执行自己的业务逻辑。 有关Vue组件生命周期的知识在官方文档中已经介绍的比较详细了,这里进行一些总结和梳理,中间将相关知识点串联起来。
组件的创建与初始化
创建阶段在Vue实例的生命周期中只会执行一次,主要进行Vue组件实例的初始化的相关工作。
...
// 初始化Vue实例
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
和created
两个钩子函数提供给开发者执行相关操作,
我们也按钩子函数划分阶段进行讨论。
I. ——> beforeCreate
在beforeCreate
方法回调之前,Vue组件将进行一些初始化操作,
主要包括以下几个部分:
- 收集并初始化组件options
// 初始化options
// 缓存实例对象,便于操作
const vm: Component = this
// a uid for vue instance
vm._uid = uid++
// 标志位,避免Vue实例对象本身被声明为响应式数据
vm._isVue = true
// merge options
...
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),// 解析出对象中定义的options
options || {},
vm
)
// dev模式下,hook掉vue实例上属性的handler操作,便于开发check
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
options
对象承载了开发者对于组件的输入和定义,其中又各式各样的语法糖格式,
mergeOptions
不仅对options进行了合并,同时也包括属性等option的泛化处理,保证格式统一。
- 初始化Vue实例属性
function initLifecycle (vm: Component) {
const options = vm.$options
// locate first non-abstract parent
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
}
这里在初始化属性值时,也建立了Vue父组件与子组件的联系,构建整个组件实例的树结构。
- 初始化Vue实例的事件
我们知道,子组件通过向父组件emit
事件来完成通信,这里先找到父组件中所监听的事件:
// 找到父组件中所监听的事件
function initEvents (vm: Component) {
vm._events = Object.create(null)
vm._hasHookEvent = false
// init parent attached events
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
然后告诉子组件,父组件都监听了哪些事件:
function updateComponentListeners (
vm: Component,
listeners: Object,
oldListeners: ?Object
) {
target = vm
updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
target = undefined
}
updateListeners方法中遍历了父组件中的事件监听器, 并通过调用vm.$on方法注册到子组件的监听器中去。
Vue组件维护了一个监听器对象,来存储组件所监听的事件
至此,Vue组件的实例对象完成了一些内部属性相关的初始化工作,但并不包括组件内部的响应式数据部分,因此我们在beforeCreate
方法中是无法访问到组件状态和方法相关数据。
II. beforeCreate ——> created
而在beforeCreate
回调之后,则会对数据进行响应式化的处理,
主要包括以下两个方面:
- 初始化provide / inject
- 初始化组件的数据状态
组件状态的相关数据主要包括父组件传递过来的props,以及自身的data,method,watcher等等。 这部分工作主要包括两个部分,一个是相关数据的校验以及初始化,另一部分是Vue的响应式系统会对组件状态数据的收集,从而监听数据变化,进行响应式更新操作。 具体内容见「Vue 响应式系统的原理」
function initState (vm: Component) {
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)
}
}
这里进行了组件内部数据(data,props)的观测以及侦听器和计算属性的配置操作。 有关组件属性的使用和相关分析的具体内容见「Vue 属性」
至此,Vue组件实例对象的创建和初始化工作已经基本上完成了, 但此时组件还没有被挂载到DOM上。
组件的挂载
在Vue组件初始化工作基本完成后,则会进行组件的挂载工作,这里会有两种挂载方式:
- 手动挂载:指定挂载节点,并调用
$mount
函数 - 自动挂载:根据
vm.$options.el
所指定的挂载节点执行挂载
初始化末尾会判断该Vue组件是否指定了挂载节点:
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
如果Vue组件的options.el
中指定了该组件的挂载节点,则主动调用$mount
方法执行挂载相关逻辑;
否则该组件的生命周期暂停下来,处于一个“未挂载”的状态,直到vm.$mount
方法被调用。
模板的编译( created ——> beforeMount )
我们知道,Vue提供了基于 HTML 的模板语法,允许开发者声明式地将 DOM 绑定至底层 Vue 实例的数据。
Vue的编译器会对通过模板文件来定义的组件进行编译,生成一个可供调用的render
函数。
如果我们没有为Vue实例指定模板(options.template
),Vue则会选择以挂载节点el
外层的html作为模板。
编译模板为render函数这部分工作,会根据使用的Vue包不同而有所区别($mount函数不同): 如果使用了运行时版本,则需要在代码构建过程中完成模板的编译;而如果采用了完整版,则模板的编译则放在组件的挂载之前。
另外,根据指定的el
,查找到组件的挂载点:
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
......
callHook(vm, 'beforeMount')
}
可以看到在beforeMount
方法被回调执行之前,Vue组件此时已经完成了模板编译以及挂载点查找。
beforeMount ——> mounted
我们都知道,为了避免直接操作原始DOM树,Vue采用了Virtual Dom机制来完成相关的计算,
而前面经过编辑的模板所生成了render
函数,它作用就是返回了对应组件的VNode
节点。
在前面的mountComponent
方法中创建了一个监听器来追踪数据的变化,并执行渲染和更新操作:
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
当数据发生变化则会回调updateComponent
方法,触发渲染和更新操作,并将真实DOM渲染在页面上。
至此, 组件的渲染和挂载执行完毕,并展示在页面上。
组件销毁
通常情况下,路由跳转等操作都会触发组件的销毁操作。
beforeDestroy ——> destroyed
beforeDestroy
方法会在组件刚刚开始销毁时回调,可以进行定时器销毁等操作。
随后的工作包括以下几个部分:
- 从父组件中移除
callHook(vm, 'beforeDestroy')
vm._isBeingDestroyed = true
// remove self from parent
const parent = vm.$parent
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
remove(parent.$children, vm)
}
- 移除监听器
// teardown watchers
if (vm._watcher) {
vm._watcher.teardown()
}
vm.$off()
- 销毁实例相关属性和引用
// remove reference from data ob
// frozen object may not have observer.
if (vm._data.__ob__) {
vm._data.__ob__.vmCount--
}