在 Vue 中,除函数式组件外,全部组件都是 Vue 实例。每一个 Vue 实例在被建立时都要通过一系列的初始化过程:数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。
在生成 Vue 实例的过程当中会运行一些叫作生命周期钩子的函数,这给了用户在不一样阶段添加本身的代码的机会。本文从源码的角度来详细阐述组件生命周期的相关内容。
前端
生命周期钩子函数调用是经过 callHook 函数完成的,callHook 函数主要包含三个方面的内容。
vue
function callHook (vm, hook) {
pushTarget()
const handlers = vm.$options[hook]
const info = `${hook} hook`
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
invokeWithErrorHandling(handlers[i], vm, null, vm, info)
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
popTarget()
}
复制代码
在生成 Vue 实例的过程当中会调用 mergeOptions 函数对选项进行处理,生命周期钩子函数通过合并处理后会添加到实例对象的 $options 属性上,合并后各生命周期函数存储在对应的数组中。具体细节可参看文章《选项合并》。
callHook 函数调用的形式以下所示:
node
// 调用 created 生命周期钩子函数
callHook(vm, 'created')
复制代码
此时 callHook 函数会循环遍历执行 vm.$options.created 数组中的函数,以完成 created 生命周期钩子函数的调用。
vue-router
在函数首尾有以下代码:
后端
function callHook (vm, hook) {
pushTarget()
/* 省略... */
popTarget()
}
复制代码
这两个函数的源码以下所示:
数组
Dep.target = null
const targetStack = []
function pushTarget (target) {
targetStack.push(target)
Dep.target = target
}
function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
复制代码
Vue 实例的当前观察者对象是惟一的,所谓当前观察者对象是指即将要收集依赖的目标,pushTarget 函数将观察者对象入栈而不是简单的赋值,是为了在当前观察者对象操做完成后恢复成以前的观察者对象。
在函数的首尾调用 pushTarget() 和 popTarget() 函数,是为了防止在生命周期钩子函数中使用 props 数据时收集冗余的依赖。具体详情可参看《响应式原理》。
缓存
在 callHook 函数中还有一部分代码:
ide
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
复制代码
这行代码比较有意思,也就是说在执行生命周期钩子函数时,若是 vm._hasHookEvent 的值为 true,则会额外触发一个形如 hook:created 的事件。
那么何时实例的 _hasHookEvent 属性值为真呢?还记得在上篇文章讲解 $on 方式时有提过这点:
函数
const hookRE = /^hook:/
Vue.prototype.$on = function(event, fn){
/* 省略... */
if (hookRE.test(event)) {
vm._hasHookEvent = true
}
/* 省略... */
}
复制代码
上篇文章同时讲到,在组件上使用自定义指令最终会转化成调用 $on 的形式,也就是说按照如下使用就能命中这种状况:
oop
<Child @hook:created = "doSomething"></Child>
复制代码
这种形式的事件称为 hookEvent,在官方文档上没有找到 hookEvent 的说明,可是在 Vue 源码中有实现。所谓 hookEvent 就是特殊命名的事件—— hook: + 生命周期名称。这种事件会在子组件对应生命周期钩子函数调用时被调用。
那 hookEvent 有什么用呢?其实在使用第三方组件的时候可以用到,使用 hookEvent 能够在不破坏第三方组件代码的前提下,向其注入生命周期函数。
关于组件实例的生命周期,官网上面有一张很经典的图片:
function Vue (options) {
/* 省略警告信息 */
this._init(options)
}
复制代码
_init 方法首先进行合并选项,而后初始化生命周期、事件等,最后挂载 DOM 元素。代码以下所示:
Vue.prototype._init = function (options) {
const vm = this
/*...*/
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')
initInjections(vm)
initState(vm)
initProvide(vm)
callHook(vm, 'created')
/*...*/
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
复制代码
这里函数调用的顺序很重要,数据的处理都是在 beforeCreate 生命周期函数调用以后初始化的,也就是说在 beforeCreate 生命周期函数中,不能使用 props、methods、data、computed 和 watch 等数据,也不能使用 provide/inject 中的数据。通常从后端加载数据不用赋值给data中时,能够放在这个生命周期中。
在 beforeCreate 与 created 生命周期函数调用中间,调用初始化各个数据的函数。initState 函数代码以下所示:
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)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
复制代码
注意 initState 函数中函数的调用顺序:initProps——initData——initComputed——initWatch。这样初始化顺序的结果是在 data 选项中可使用 props;在 computed 选项中可使用 data、props 中的数据;watch 选项能够监听 data、props、computed 数据的变化。methods 选项的组成是函数,在函数调用时这些初始化工做已经完成,因此可使用所有的数据。
初始化 inject 的 initInjections 函数在 initState 以前调用,最后调用初始化 provide 的 initProvide 函数。这样就决定了在 data、props、computed 等选项中可使用 inject 中的数据,provide 选项中可使用 data、props、computed、inject 等的数据。
调用 created 生命周期函数以前,数据初始化已经完成,在函数中能够操做这些数据。向后端请求的数据须要赋值给 data 时,能够放在 created 生命周期函数中。
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (el,hydrating){
el = el && query(el)
/* 省略... */
if (!options.render) {
let template = options.template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template)
/* 省略... */
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
/* 省略... */
return this
}
} else if (el) {
template = getOuterHTML(el)
}
if (template) {
/* 省略... */
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
/* 省略... */
}
}
return mount.call(this, el, hydrating)
}
复制代码
该函数的做用是将 template/el 转化成渲染函数,具体的转化过程可参看《模板编译》一文。
根据渲染函数完成挂载的代码以下所示:
Vue.prototype.$mount = function (el,hydrating){
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
function mountComponent (vm,el,hydrating){
vm.$el = el
/* 省略渲染函数不存在的警告信息 */
callHook(vm, 'beforeMount')
let updateComponent
/* 删除性能埋点相关 */
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
复制代码
能够看到 beforeMount 生命周期是在渲染函数生成以后、开始执行挂载以前调用的。beforeMount 钩子函数执行后,实例化一个渲染函数观察者对象,关于 Watcher 相关内容能够参看《响应式原理》。
从渲染函数到生成真实DOM的过程由 updateComponent 函数来完成,其中 _render 函数的做用是根据渲染函数生成 VNode,_update 函数的做用是根据 VNode 生成真实DOM并插入到对应位置中。
在挂载完成后,会调用 mounted 生命周期钩子函数,在该生命周期内能够对DOM进行操做。
这里有个判断条件:vm.$vnode == null,组件初始化的时候 $vnode 不为空,当条件成立时,说明是经过 new Vue() 来进行初始化的。换而言之,组件初始化时,不会在此处执行 mounted 生命周期钩子函数,那么组件 mounted 生命周期函数在何处调用呢?
_update 函数本质上是经过调用 patch 函数来完成真实DOM元素的生成与插入,在 patch 函数的最后有以下代码:
function patch (oldVnode,vnode,hydrating,removeOnly){
/* 省略... */
invokeInsertHook(vnode,insertedVnodeQueue,isInitialPatch)
return vnode.elm
}
function invokeInsertHook (vnode, queue, initial) {
/* 省略... */
for (let i = 0; i < queue.length; ++i) {
queue[i].data.hook.insert(queue[i])
}
}
function insert (vnode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
/* 省略 keep-alive 相关...*/
}
复制代码
能够看到组件 mounted 生命周期钩子函数的调用是在 patch 的最后阶段进行的,另外 insertedVnodeQueue 是一个 VNode 数组,数组中 VNode 的顺序是子 VNode 在前,父 VNode 在后,所以 mounted 钩子函数的执行顺序也是子组件先执行,父组件后执行。
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
}
}
/* 省略... */
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
/* 省略... */
}
复制代码
能够看到在实例化渲染函数观察者对象时,会将传入的 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
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 完成的,从上代码能够看到:在数据更新前会调用实例对象上的 before 方法,从而执行 beforeUpdate 生命周期钩子函数;在数据更新完成后,经过执行 callUpdatedHooks 函数完成 updated 生命周期函数的调用。
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 生命周期钩子函数,而后移除实例上的所有事件监听器。
当组件被 keep-alive 内置组件包裹时,组件实例会被缓存起来。这些组件在首次渲染时各生命周期与普通组件同样,再次渲染时 created、mounted 等钩子函数就再也不生效。
被 keep-alive 包裹的组件被缓存以后有两个独有的生命周期: activated 和 deactivated。activated 生命周期在组件激活时调用、deactivated 生命周期在组件停用时调用。
上一节讲 mounted 生命周期时说过,组件的 mounted 的生命周期钩子函数是在 insert 方法中调用的。当时将函数中对 keep-alive 的处理省略了,这里重点阐述。
insert (vnode) {
/* 省略... */
if (vnode.data.keepAlive) {
if (context._isMounted) {
queueActivatedComponent(componentInstance)
} else {
activateChildComponent(componentInstance, true)
}
}
}
复制代码
queueActivatedComponent 函数的调用是为了修复 vue-router 中的一个问题:在更新过程当中 keep-alive 的子组件可能会发生改变,直接遍历树结构可能会调用错误子组件实例的 activated 生命周期钩子函数,所以这里不作处理而是将组件实例放入队列中,等 patch 过程结束后再作处理。
queueActivatedComponent 最终也是调用 activateChildComponent 函数来执行 activated 生命周期钩子函数。
function activateChildComponent(vm,direct){
/* 省略... */
if (vm._inactive || vm._inactive === null) {
vm._inactive = false
for (let i = 0; i < vm.$children.length; i++) {
activateChildComponent(vm.$children[i])
}
callHook(vm, 'activated')
}
}
复制代码
能够看到就是在这里调用的 activated 生命周期钩子函数,而且会递归调用所有子组件的 activated 生命周期钩子函数。
deactivated 生命周期是在 destroy 钩子函数中调用的:
destroy (vnode) {
const { componentInstance } = vnode
if (!componentInstance._isDestroyed) {
if (!vnode.data.keepAlive) {
componentInstance.$destroy()
} else {
deactivateChildComponent(componentInstance, true)
}
}
}
复制代码
keep-alive 的子组件的子组件会走 else 分支,直接调用 deactivateChildComponent 函数。
function deactivateChildComponent(vm, direct){
/* 省略... */
if (!vm._inactive) {
vm._inactive = true
for (let i = 0; i < vm.$children.length; i++) {
deactivateChildComponent(vm.$children[i])
}
callHook(vm, 'deactivated')
}
}
复制代码
在该函数中,会调用的 deactivated 生命周期钩子函数,而且会递归调用所有子组件的 deactivated 生命周期钩子函数。
生命周期钩子函数的是经过 callHook 来调用的,该函数不只遍历执行对应的生命周期函数,还能防止收集冗余依赖和触发 hookEvent 事件。hookEvent 可以非侵入的向一个组件注入生命周期函数。
经过 new Vue() 实例化 Vue 对象会调用 _init 方法完成一系列初始化操做,在初始化数据以前会调用 beforeCreate 钩子,在数据初始化后调用 created 钩子。在生成渲染函数以后,调用 beforeMount 钩子,接着根据渲染函数生成真实DOM并挂载,而后调用 mounted 钩子。数据更新时,在从新渲染以前调用 beforeUpdate 钩子,在完成渲染后调用 updated 钩子。在调用实例方法 $destroy() 销毁实例时首先调用 beforeDestroy 钩子,而后执行销毁操做,最后调用 destroyed 钩子。
被 keep-alive 缓存起来的组件被激活时会调用 activated 钩子,在 patch 最后阶段的 insert 钩子函数中执行。组件停用时调用 deactivated 钩子,在 patch 的 destroy 钩子函数中执行。
欢迎关注公众号:前端桃花源,互相交流学习!