Vue实例从创建到销毁的过程,就是生命周期。详细来说也就是从开始创建、初始化数据、编译模板、挂载Dom、渲染→更新→渲染、卸载等一系列过程。
首先我们来看一下官网的生命周期图(我自己做了一点点注释):
也可以看我之前的博客 vue生命周期的理解
Vue提供给我们的钩子为上图的红色的文字。
生命周期钩子的函数,给予用户机会在一些特定的场景下添加他们自己的代码。
我们现在用源码来分析生命周期的钩子函数是如何被执行的。
源码中最终执行生命周期的函数都是调用 callHook 方法,它的定义在 src/core/instance/lifecycle 中:
export function callHook (vm: Component, hook: string) {
// #7573 disable dep collection when invoking lifecycle hooks
pushTarget()
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`)
}
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
popTarget()
}
参数:
1:Vue实例(组件类型的)
2:hook,String类型的, 比入我们可以传入 created mounted等生命周期
步骤1:
const handlers = vm.$options[hook]
Vue.js 初始化合并 options 的过程,各个阶段的生命周期的函数也被合并到 vm. options里,并且是一个数组。根据传入的字符串hook,去拿到vm. o p t i o n s 里 , 并 且 是 一 个 数 组 。 根 据 传 入 的 字 符 串 h o o k , 去 拿 到 v m . options[hook] 对应的回调函数数组
步骤2:
for (let i = 0, j = handlers.length; i < j; i++) {
try {
handlers[i].call(vm)
} catch (e) {
handleError(e, vm, `${hook} hook`)
}
}
遍历执传入的生命周期中所有的所对应的函数,执行的时候把 vm 作为函数执行的上下文。
总结
callhook 函数的功能就是在当前vue组件实例中,调用某个生命周期钩子注册的所有回调函数。
beforeCreate 和 created 函数都是在实例化 Vue 的阶段,在 _init 方法中执行的,它的定义在 src/core/instance/init.js 中:
Vue.prototype._init = function (options?: Object) {
.
.
.
//主要就是给vm对象添加了$parent、$root、$children属性,以及一些其它的生命周期相关的标识。
initLifecycle(vm)
// 初始化事件相关的属性
initEvents(vm)
// vm添加了一些虚拟dom、slot等相关的属性和方法
initRender(vm)
callHook(vm, 'beforeCreate')
//下面initInjections(vm)和 initProvide(vm) 两个配套使用,用于将父组件_provided中定义的值,通过inject注入到子组件,且这些属性不会被观察
initInjections(vm)
//主要就是操作数据了,props、methods、data、computed、watch,从这里开始就涉及到了Observer、Dep和Watcher
initState(vm)
initProvide(vm)
callHook(vm, 'created')
//
.
.
.
}
_init 函数分为3步
1:合并options
2. 调用初始化函数
3. 挂载到DOM — $mount
beforeCreate:
是拿不到数据的比如定义在,props、methods、data、computed、watch中的,因为他 initState(vm)
之前执行
created
是可以拿到数据的的,因为他在initState(vm)
之后执行
总结:
在这俩个钩子函数执行的时候,并没有渲染 DOM,所以我们也不能够访问 DOM,一般来说,如果组件在加载的时候需要和后端有交互,放在这俩个钩子函数执行都可以,如果是需要访问 props、data 等数据的话,就需要使用 created
钩子函数。
在我们初始化最后回去执行$mount
,也就是去执行挂载。
顾名思义,beforeMount
钩子函数发生在 mount
之前,也就是 DOM 挂载之前,它的调用时机是在 mountComponent
函数中,定义在 src/core/instance/lifecycle.js
中:
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// 执行beforeMount
callHook(vm, 'beforeMount')
let updateComponent
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
.
.
.
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
new Watcher(vm, updateComponent, noop, {
.
.
.
}, true /* isRenderWatcher */)
hydrating = false
if (vm.$vnode == null) {
vm._isMounted = true
// 执行mounted
callHook(vm, 'mounted')
}
return vm
}
过程是
watcher
监听updateComponent
方法,在数据初始化和变更的时候调用。updateComponent
也就是vm._update
(将虚拟DOM映射到真实DM)的函数。vm._update
之前会先调用 vm._render()
函数渲染 VNode在执行 vm._render()
函数渲染 VNode 之前,执行了 beforeMount
钩子函数
在执行完 vm._update()
把 VNode patch 到真实 DOM 后,执行 mouted
钩子。
这是通过外部 new Vue 初始化过程。
因为这里对 mouted
钩子函数执行有一个判断逻辑,vm.$vnode
为 null,因为vm.$vnode
的意思是父VNode,如果vue实力没有父VNode,说明他只有根VNode。则表明这不是一次组件的初始化过程,而是我们通过外部 new Vue 初始化过程。
那么对于组件,它的 mounted 时机在哪儿呢?
组件的 VNode patch 到 DOM ,patch的最后会执行 invokeInsertHook
函数,
它的定义在 src/core/vdom/patch.js
中:
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
invokeInsertHook也定义在该 js
function invokeInsertHook (vnode, queue, initial) {
// delay insert hooks for component root nodes, invoke them after the
// element is really inserted
if (isTrue(initial) && isDef(vnode.parent)) {
vnode.parent.data.pendingInsert = queue
} else {
for (let i = 0; i < queue.length; ++i) {
queue[i].data.hook.insert(queue[i])
}
}
}
可以看到把 queue
就是insertedVnodeQueue
,invokeInsertHook
是将保存在insertedVnodeQueue
的钩子函数依次执行一遍
insertedVnodeQueue
是一个数组,在patch的过程中,会将子组件的子VNode掺入到子组件,这是也会将子组件push
到insertedVnodeQueue
数组,子组件插入到父组件的时候也会将父组件push
到insertedVnodeQueue
数组,所以是子组件先插入,父组件后插入
我们可以看到遍历insertedVnodeQueue
数组(里面放着组件)的时候,会调用组件的data中的insert 这个钩子函数
对于组件而言,insert
钩子函数的定义在 src/core/vdom/create-component.js
中的 componentVNodeHooks
中:
const componentVNodeHooks = {
// insert函数
insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
// 组件调用mounted方法
callHook(componentInstance, 'mounted')
}
//
},
}
我们可以看到,每个子组件都是在这个钩子函数中执行 mouted
钩子函数,并且我们之前分析过,insertedVnodeQueue
的添加顺序是先子后父,所以对于同步渲染的子组件而言,mounted 钩子函数的执行顺序也是先子后父。
beforeMount
钩子函数,也就是说beforeMount
函数中是拿不到DOM的beforeMount
钩子,是先父后子。 beforeMount
钩子,再往后发现父组件有子组件就会再执行一遍patch,子组件会再执行beforeMount
钩子,依次类推,最后mounted后分别插入父辈组件)顾名思义,beforeUpdate
和 updated
的钩子函数执行时机都应该是在数据更新的时候。
beforeUpdate
的执行时机是在渲染 Watcher
的 before
函数中,我们刚才提到过:
在mouted
的时候调用的mountComponent
函数中, src/core/instance/lifecycle.js
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
// ...
new Watcher(vm, updateComponent, noop, {
before () {
// 先判断是否mouted完成
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
//
}
我们看到有一个before
函数,这个函数是在数据变化的时候调用flushSchedulerQueue
函数中执行, 它的定义在 src/core/observer/scheduler.js
中:
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()
}
}
queue是一个包含所有watcher的数组,遍历queue,如果watcher有before函数就会先执行before函数
注意在调用before
这里有个判断,也就是在组件已经 mounted
之后,才会去调用这个钩子函数。也就是说初始化的时候不会调用该方法。
我们知道watcher
监听update
方法,在变更的时候调用。
也就是说会在数据变化前调用beforeUpdate
这个钩子
数据变化的时候调用flushSchedulerQueue
函数,
它的定义在 src/core/observer/scheduler.js 中:
function flushSchedulerQueue () {
// ...
// 获取到 updatedQueue
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) {
callHook(vm, 'updated')
}
}
}
updatedQueue
是 更新了的 wathcer
数组,那么在 callUpdatedHooks
函数中,它对这些数组做遍历,只有满足当前 watcher
为 vm._watcher
(也就是当前的渲染watcher) 以及组件已经 mounted
这两个条件,才会执行 updated
钩子函数。
我们之前提过,在组件 mount
的过程中,会实例化一个渲染的 Watcher
去监听 vm 上的数据变化重新渲染,这断逻辑发生在 mountComponent
函数执行的时候:
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
// ...
// 这里是简写
let updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
// ...
}
那么在实例化 Watcher
的过程中,在它的构造函数里会判断 isRenderWatcher
,接着把当前 watcher
的实例赋值给 vm._watcher
,定义在 src/core/observer/watcher.js
中:
export default class Watcher {
// ...
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// ...
}
}
同时,还把当前 wathcer
实例 push
到 vm._watchers
中,vm._watcher
是专门用来监听 vm 上数据变化然后重新渲染的,所以它是一个渲染相关的 watcher
,因此在 callUpdatedHooks
函数中,只有 vm._watcher
的回调执行完毕后,才会执行 updated 钩子函数。
过程:
Watcher
去监听 vm 上的数据变化Watcher
的过程中,在它的构造函数里会判断 isRenderWatcher
,接着把当前 watcher
的实例赋值给 vm._watcher
,vm._watcher
是专门用来监听 vm 上数据变化然后重新渲染的,所以它是一个渲染相关的 watcher
wathcer
实例 push
到 vm._watchers
中flushSchedulerQueue
函数在数据变化的时候调用,再去调用callUpdatedHooks
函数,并在其中遍历更新了的 ,只有满足当前 watcher
为 vm._watcher
(也就是当前的渲染watcher) 以及组件已经 mounted
这两个条件,才会执行 updated
钩子函数。vm._watcher
的回调执行完毕后,才会执行 updated 钩子函数。)beforeUpdate
发生在数据变化的前,初始化数据并不会触发update
发生在数据变化的后,初始化数据并不会触发beforeUpdate
和 update
都只会在mounted
以后调用顾名思义,beforeDestroy
和 destroyed
钩子函数的执行时机在组件销毁的阶段,最终会调用 $destroy
方法,它的定义在 src/core/instance/lifecycle.js
中:
Vue.prototype.$destroy = function () {
const vm: Component = this
if (vm._isBeingDestroyed) {
return
}
// 调用beforeDestroy钩子
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)
// 执行destroyed钩子
callHook(vm, 'destroyed')
vm.$off(
if (vm.$el) {
vm.$el.__vue__ = null
}
if (vm.$vnode) {
vm.$vnode.parent = null
}
}
beforeDestroy
钩子函数的执行时机是在 $destroy
函数执行最开始的地方parent
的 $children
中删掉自身,删除 watcher
,当前渲染的VNode 执行销毁钩子函数等,vm.__patch__(vm._vnode, null)
触发它子组件的销毁钩子函数,这样一层层的递归调用destroyed
钩子函数。在 $destroy
的执行过程中,它会执行 vm.__patch__(vm._vnode, null)
触发它子组件的销毁钩子函数,这样一层层的递归调用,所以 destroy
钩子函数执行顺序是先子后父,和 mounted
过程一样。
总结
beforeDestroy
钩子函数的执行时机在组件销毁的阶段前调用
destroyed
钩子函数的执行时机在组件销毁的阶段后调用
created
钩子函数可以访问数据mounted
钩子函数可以访问DOMdestroyed
钩子函数可以做一些定时器销毁工作