这篇文章会先从最基础的vue组件的生命周期开始阐述,后续结合keep-alive
与vue-router
来梳理一下平常用到的生命周期hook,加强印象。
vue 组件
这是一个老生常谈的问题,有时候回顾一下,会有另外的收获;先引用官方的图:
图片引用地址: https://cn.vuejs.org
vue的生命周期分几类:
- create
- mount
- update
- destroy
- ...
整体初始化过程
图中简单描述了生命周期过程,我们从代码上面看一下初始化的过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// https://github.com/vuejs/vue/blob/dev/src/core/instance/init.js
// 截一段相对关键的代码,加上简单的注释
// @function initMixin
vm._self = vm
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')
// ...
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
|
beforeCreate 之前
在beforeCreate
之前,主要做了三个动作:initLifeCycle
, initEvents
, initRender
;这三个动作完成之后再执行beforeCreate
的hook函数,这三个函数分别做的事情:
TL;DR
- initLifecycle: 建立组件的父子关系,赋值部分标识数据到组件实例,
vm.$parent
,vm.$refs
,vm.$root
等 - initEvents: 初始化组件监听的事件
- initRender:初始化
$slot
、$attr
、$listener
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
// 1. initLifecycle (src/core/instance/lifecycle.js)
// @function initLifecycle
export function initLifecycle (vm: Component) {
const options = vm.$options
// 建立父子组件的关系
let parent = options.parent
if (parent && !options.abstract) {
// 对于抽象的组件,不断往上找父组件,找到不是抽象的父组件为止
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
// balabala 初始化很多数据
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
}
// 2. initEvents (src/core/instance/events.js)
// @function initEvents
export 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)
}
}
// 3. initRender (src/core/instance/render.js)
// @function initRender
// 中间去掉一些声明变量,主要保留一些赋值到vm的数据
export function initRender (vm: Component) {
vm._vnode = null // the root of the child tree
vm._staticTrees = null // v-once cached trees
//...
// 赋值 slot 的值与对应的 slot 对应的数据
vm.$slots = resolveSlots(options._renderChildren, renderContext)
vm.$scopedSlots = emptyObject
//...
// 赋值从组件传过来的属性值与没有显式被组件监听的事件,分别赋值到$attr与$listener
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
!isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
}, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
!isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
}, true)
} else {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}
} |
beforeCreate 到 created
执行完这三个初始化函数,就可以触发beforeCreate
的hook函数,可以看到还没有初始化$data
的相关数据;在beforeCreate
与created
之间,执行的函数有:initInjections
, initState
, initProvide
;
TL;DR
- initInjections: 把组件注入的数据,挂载到当前组件实例
- initState:初始化
data
,computed
,methods
,watcher
- initProvide:将provide的数据挂载到组件实例的
_provided
字段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
// 1. initInjections (src/core/instance/inject.js)
// @function initInjections
export function initInjections (vm: Component) {
// 拿到注入的数据
const result = resolveInject(vm.$options.inject, vm)
if (result) {
// 标识inject的属性与方法在当前组件不需要成为 observer,不用监听变化进行响应
toggleObserving(false)
Object.keys(result).forEach(key => {
// ...
// 绑定注入的数据到当前组件
defineReactive(vm, key, result[key])
})
// 把 observer 的标识位置为 true
toggleObserving(true)
}
}
// 2. initState (src/core/instance/state.js)
// @function initState
export function initState (vm: Component) {
// 初始化依赖的props,methods
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
// 初始化 data
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
// 初始化计算属性
if (opts.computed) initComputed(vm, opts.computed)
// 初始化 watch 的数据
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
// 3. initProvide (src/core/instance/inject.js)
// @function initProvide
export function initProvide (vm: Component) {
const provide = vm.$options.provide
if (provide) {
// 对 provide 是函数的情况,执行函数赋值到 _provided;否则直接赋值
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
} |
执行完这个三个函数之后,就会触发created
的hook函数,这个时候就可以拿到data
与methods
等数据;现在再回去initMixin
函数:
mount 与 update
1 2 3 4 5 6 |
// 忽略已分析代码
callHook(vm, 'created')
// ...
if (vm.$options.el) {
vm.$mount(vm.$options.el)
} |
当有el
的元素的时候,就触发$mount
方法,否则到后面主动调用方法再触发;这个$mount
方法在:src/core/instance/lifecycle.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
// 定义挂载组件的方法
// mountComponent (src/core/instance/lifecycle.js)
// @function mountComponent
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// 没有 render 函数
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
// ...
}
// 触发 beforeMount hook 函数
callHook(vm, 'beforeMount')
// ...
// 定义数据发生变化的回调方法
updateComponent = () => {
// 调用该方法更新当前的组件,执行完毕之后,需要通过 scheduler 来触发 updated 的 hook,为什么不是马上触发hook,是因为需要保证子组件都更新了,才调用当前组件的 updated,详细可以看一下源码,位置如下
// src/core/instance/lifecycle.js
// @function Vue.prototype._update
vm._update(vm._render(), hydrating)
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
// 新建一个 watcher,用来监听数据发生变化
// 注意 beforeUpdate 的hook也是在这里进行监听调用
new Watcher(vm, updateComponent, noop, {
// 在执行 updateComponent 之前先执行 before 函数,也就是触发 beforeUpdate
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
// 挂载的对象如果不是为空,则触发 mounted 回调方法
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
} |
destroy
TL;DR
- 触发 beforeDestroy hook
- 移除父组件与该组件的引用关系
- 实例标识
_isBeingDestroyed
更改 - 移除实例watcher
- 移除 vnode 节点
- 触发 destroy hook
- 移除
$el
与$vnode
引用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
// src/core/instance/lifecycle.js
// @function Vue.prototype.$destroy
Vue.prototype.$destroy = function () {
const vm: Component = this
if (vm._isBeingDestroyed) {
return
}
// 触发 beforeDestroy 的 hook
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
// 移除所有watcher
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
// 移除 vnode 内容
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
}
} |
至此,基本的声明周期就差不多了,后续的内容主要是对hook的触发顺序进行一个巩固记忆
基础切换组件
当从一个组件a切换到组件b的时候,执行的顺序是:
b-component
beforeCreateb-component
createdb-component
beforeMounta-component
beforeDestroya-component
destroyedb-component
mounted
注意从a切换到b的时候,并不是a的destroy
的相关方法马上执行,而是等到b组件的beforeMount
函数执行后再调用之前的destroy
的相关方法;当旧的组件被销毁之后,再执行新的组件的mounted
的挂载方法,因为挂载完毕之后就会显示组件对应的内容
keep-alive
当使用 keep-alive 来缓存组件的时候,keep-alive里面的生命周期会有点不一样;
1 2 3 4 |
<keep-alive>
<component-a v-if="componentName === 'component-a'"></component-a>
<component-b v-if="componentName === 'component-b'"></component-b>
</keep-alive> |
当切换不同的componentName
变量的时候,在没有使用keep-alive
的时候,触发周期如前面所说的;但使用keep-alive
之后,一个组件进入的周期就变成了:
- beforeCreate
- created
- beforeMount
- mounted
- activated
注意这里多了一个activated
的hook调用,这一个hook是仅在keep-alive
中使用的,表示当前组件被激活;对应这另外一个hook就是,deactivated
,表示当前组件被停用,那么从component-a
切换到component-b
的过程中,生命周期hook调用顺序就变成了:
component-a
初始化:
- beforeCreate
- created
- beforeMount
- mounted
- activated
从component-a
切换到:component-b
:
component-b
beforeCreatecomponent-b
createdcomponent-b
beforeMountcomponent-a
deactivatedcomponent-b
mountedcomponent-b
activated
可以注意到,这个时候没有了之前看到的destroy
类的触发,而是deactivated
;mounted
之后,也是跟之前类似,也会调用activated
方法
然后从component-b
再切换到: component-a
:
component-b
deactivatedcomponent-a
activated
因为这个时候component-a
已经初始化了,所以没有触发create
与mount
类的hook,而是先component-b
停用,再component-a
激活;后续不断切换也是只反复调用这两个hook...
keep-alive的最大缓存数量 max
keep-alive
可以设置一个最大缓存的数量,当超出设置的最大缓存的数量,则最久没有被访问到的实例会被销毁:
1 2 3 4 5 |
<keep-alive :max="2">
<component-a v-if="componentName === 'component-a'"></component-a>
<component-b v-if="componentName === 'component-b'"></component-b>
<component-c v-if="componentName === 'component-c'"></component-c>
</keep-alive> |
从component-a
切换到component-b
的hook调用顺序与没有设置max
类似;再从component-b
切换到component-c
的时候,调用顺序为:
component-a
beforeDestroycomponent-a
destroyedcomponent-c
beforeCreatecomponent-c
createdcomponent-c
beforeMountcomponent-b
deactivatedcomponent-c
mountedcomponent-c
activated
由于设置了最大的缓存数量为2,当切换到component-c
的时候,首先触发的是component-a
的destroy
的相关方法;再执行初始化component-c
,然后component-b
失活
component-a
=>component-b
: 与没有max一致component-b
=>component-c
:首先component-a
的destroy相关hook被调用,后续的调用顺序是先初始化component-c
,再让component-b失活
component-c
=>component-b
: 仅执行deactivated
与activated
的方法component-b
=>component-a
;首先componet-c
的destroy相关hook被调用,后续hook调用顺序是先初始化component-a
,再让component-b
失活
router
在vue-router当中,定义了好多hook,称之为导航守卫,现在简单结合一下组件的生命周期梳理一下:
实验例子:
1 2 3 4 5 6 7 8 9 |
<ul>
<li>
<router-link :to="{name: 'foo'}">jump to foo</router-link>
</li>
<li>
<router-link :to="{name: 'bar'}">jump to bar</router-link>
</li>
</ul>
<router-view></router-view> |
当点击跳转到/foo
的时候,foo
组件的生命周期与路由钩子触发顺序为:
- beforeRouteEnter 优先触发路由的导航守卫 hook
- beforeCreate
- created
- beforeMount
- mounted
需要注意的是,有时候我们在beforeRouteEnter
的钩子做一些处理,例如判断用户是否有权限进入该组件,没有权限就跳转去别的页面,有权限则进入页面,伪代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import router from 'router' // vue-router object
export default {
beforeRouteEnter (to, from, next) {
console.log('beforeRouteEnter')
requestPermission().then(allowAccessed => {
if (allowAccessed) {
next(vm => {
console.log('beforeRouteEnter next')
vm.allow = true
})
} else {
router.push({ name: 'homepage' })
}
})
}
} |
那么这个时候触发的顺序为:
- beforeRouteEnter
- beforeCreate
- created
- beforeMount
- mounted
- beforeRouteEnter next
next
回调函数是最后才执行;因为在next
所传的函数里面,已经可以拿到当前组件的实例
ok,回到之前的例子,然后点击从foo
跳转到/bar
,foo
与bar
组件的生命周期与路由钩子触发顺序为:
- foo component beforeRouteLeave
- bar component beforeRouteEnter
- bar component beforeCreate
- bar component created
- bar component beforeMount
- foo component beforeDestroy
- foo component destroyed
- bar component mounted
可以看到先触发foo beforeRouteLeave
再到bar beforeRouteEnter
;而后续3-8点,与之前组件切换类似
keep-alive 包含 router-view
实验代码更改为:
1 2 3 4 5 6 7 8 9 10 11 |
<ul>
<li>
<router-link :to="{name: 'foo'}">jump to foo</router-link>
</li>
<li>
<router-link :to="{name: 'bar'}">jump to bar</router-link>
</li>
</ul>
<keep-alive>
<router-view></router-view>
</keep-alive> |
首次进入/foo
路由
- beforeRouteEnter
- beforeCreate
- created
- beforeMount
- mounted
- activated
进入的顺序没有特别,最后多了一个activated
的调用,与之前使用keep-alive类似
然后从/foo
进入/bar
:
- foo component beforeRouteLeave
- bar component beforeRouteEnter
- bar component beforeCreate
- bar component created
- bar component beforeMount
- foo component deactivated
- bar component mounted
- bar component activated
再从/bar
进入/foo
:
- bar component beforeRouteLeave
- foo component beforeRouteEnter
- bar component deactivated
- foo component activated
路由的优先级始终是在最高级别,然后再到组件的初始化过程;若组件已经初始化且在缓存当中,则到keep-alive的activated
的相关hook