建议PC端观看,移动端代码高亮错乱
关于生命周期的概念就不罗嗦了,直奔主题
1. callHook
源码中最终执行生命周期的函数都是调用 callHook 方法,它的定义在 src/core/instance/lifecycle 中:
// src/core/instance/lifecycle
export function callHook (vm: Component, hook: string) {
// #7573 disable dep collection when invoking lifecycle hooks
pushTarget() // 为了避免在某些生命周期钩子中使用 props 数据导致收集冗余的依赖
const handlers = vm.$options[hook]
const info = `${hook} hook`
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
// 1. 核心调用逻辑
invokeWithErrorHandling(handlers[i], vm, null, vm, info)
}
}
// 2. 判断是否存在生命周期钩子的事件侦听器
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
popTarget() // 为了避免在某些生命周期钩子中使用 props 数据导致收集冗余的依赖
}
- 选项合并时会把生命周期钩子选项合并成一个数组,这在上一节介绍过了
- 遍历对应
hook的数组,执行invokeWithErrorHandling - 判断是否
vm._hasHookEvent触发相应的事件侦听器
1.1 invokeWithErrorHandling
这个函数定义在:src/core/util/error.js
// src/core/util/error.js
export function invokeWithErrorHandling (
handler: Function,
context: any,
args: null | any[],
vm: any,
info: string
) {
let res
try {
// 调用handler
res = args ? handler.apply(context, args) : handler.call(context)
if (res && !res._isVue && isPromise(res) && !res._handled) {
res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
// 对不同的钩子返回相同的promise时只绑定一次catch函数
res._handled = true
}
} catch (e) {
handleError(e, vm, info)
}
return res
}
- 调用
handler,同时绑定this,这样我们在钩子回调就能通过this访问到vm实例了 - 钩子如果返回一个
promise,那么给这个promise绑定一个catch函数 - 同时
_handled保证了只绑定一次catch
1.2 _hasHookEvent
vm._hasHookEvent 是在 initEvents 函数中定义的,它的作用是判断是否存在生命周期钩子的事件侦听器,初始化值为 false 代表没有,当组件检测到存在生命周期钩子的事件侦听器时,会将 vm._hasHookEvent 设置为 true 介绍下生命周期钩子事件帧听器:
<child
@hook:beforeCreate="handleChildBeforeCreate"
@hook:created="handleChildCreated"
@hook:mounted="handleChildMounted"
@hook:生命周期钩子
/>
到这里就把 callHook 的逻辑给介绍完了,下面来看看 callHook 都在什么时候调用
2. beforeCreate & created
这两个钩子是在 _init 方法中执行的
// src/core/instance/init.js
Vue.prototype._init = function (options?: Object) {
// ...
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的钩子调用是在initState的前后执行的initState的作用是初始化props、data、methods、watch、computed等属性。- 所以
beforeCreate的钩子函数中就不能获取到props、data中定义的值,也不能调用methods中定义的函数。 - 在这俩个钩子函数执行的时候,并没有渲染 DOM,所以我们也不能够访问
DOM - 之后我们会介绍
vue-router和vuex的时候会发现它们都混合了beforeCreate钩子函数。
3. beforeMount & mounted
beforeMount 钩子函数发生在 mount,也就是 DOM 挂载之前,它的调用时机是在 mountComponent 函数中,定义在 src/core/instance/lifecycle.js 中:
// src/core/instance/lifecycle.js
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// ...
callHook(vm, 'beforeMount')
let updateComponent
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
// ...
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// 手动调用根实例的 mounted 钩子
// 子组件的 mounted 钩子在 占位符vnode的insert 钩子中调用
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
beforeMount调用时机:在执行vm._render()函数渲染VNode之前mounted调用时机:在执行完vm._update()把VNodepatch到真实DOM后。
注意,这里对 mounted 钩子函数执行有一个判断逻辑,vm.$vnode 如果为 null,则表明这不是一次组件的初始化过程,而是我们通过外部 new Vue 初始化过程。那么对于组件,它的 mounted 时机在哪儿呢?
3.1 组件的mounted分析
稍后会结合例子和流程图分析这个过程,现在我们先来看看一些关键函数的定义:
先看看 patch 函数的一些关键逻辑:
// src/core/vdom/patch.js
export function createPatchFunction (backend) {
return function patch (oldVnode, vnode, hydrating, removeOnly) {
// ...
let isInitialPatch = false // 区分是否组件patch
const insertedVnodeQueue = [] // 存放占位符vnode,调用子组件的mounted
// oldVnode为空表示这是一个组件的patch
if (isUndef(oldVnode)) {
isInitialPatch = true
// ...
} else {
// ...
}
// 传入 insertedVnodeQueue,isInitialPatch
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
}
isInitialPatch表示这是组件的patch上下文还是根实例的patch上下文- 调用
invokeInsertHook,传入渲染vnode,insertedVnodeQueue,isInitialPatch三个参数
看看 invokeInsertHook 这个关键函数:
// src/core/vdom/patch.js
function invokeInsertHook (vnode, queue, initial) {
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])
}
}
}
- 当是组件调用此方法时,会往当前渲染
vnode的占位符vnode上的data对象挂载pendingInsert属性,用来存放队列,至于为什么这么做我们稍后结合例子分析 - 当是根实例调用此方法时,会遍历队列中的占位符
vnode,并执行insert钩子,这在之前就有介绍
下面看看 insert 钩子是怎么调用子组件的 mounted 钩子
// src/core/vdom/create-component.js
const componentVNodeHooks = {
insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted') // 调用 mounted 钩子
}
// keepAlive相关...
}
}
3.2 结合例子和流程图分析子组件的mounted
组件 mounted 有点绕,我们结合例子和流程图一步步分析:
假设现在我们有以下例子:
const App = {
name: 'app',
render(h) {
return h('div', {}, 'hi vue')
},
}
var root = new Vue({
el: '#app',
render(h) {
return h(App)
},
})
结合这个例子,画了下面的流程图,红色的标号表示步骤
步骤1-3就不重复罗嗦了,这个是在之前的章节就应该掌握的知识,直接从步骤4开始分析:
步骤4:invokeInsertHook
- 此时的
vnode是App组件的渲染vnode - 因为
App组件已经是最深的那个组件了,所以此时的queue是一个空数组 - 通过
vnode.parent拿到App组件的占位符vnode - 将
queue临时保存到占位符vnode上
步骤5:initComponent
上下文回到了根实例,执行 initComponent 方法
// src/core/vdom/patch.js
function initComponent (vnode, insertedVnodeQueue) {
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
vnode.data.pendingInsert = null
}
vnode.elm = vnode.componentInstance.$el
if (isPatchable(vnode)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
// ...
} else {
// ...
}
}
- 此时的
vnode就是App组件的占位符vnode - 将占位符
vnode上的临时数组push到队列中 isPatchable返回true,执行invokeCreateHooks方法
步骤6:invokeCreateHooks
// src/core/vdom/patch.js
function invokeCreateHooks (vnode, insertedVnodeQueue) {
// ...
let i = vnode.data.hook
if (isDef(i)) {
// ...
if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
}
}
- 判断
vnode是否定义了hook,如果是的话则表明这是一个占位符vnode - 将占位符
vnode推进队列中 此时的insertedVnodeQueue状态:
步骤7:invokeInsertHook
- 由于此时的上下文已经是根实例了,所以走的是
else逻辑 - 遍历
insertedVnodeQueue队列,执行insert钩子,在这个钩子又会执行mounted钩子
到这里我们组件的 mounted 也已经结合具体的例子和流程图介绍完了,总结两点:
beforeMount先父后子mounted先子后父
4. beforeUpdate & updated
顾名思义,beforeUpdate 和 updated 的钩子函数执行时机都应该是在数据更新的时候,到目前为止,我们还没有分析 Vue 的数据双向绑定、更新相关,下一章会详细介绍这个过程,现在大致了解一下:
当在执行 mountComponent 时,会实例化渲染 watcher:
// src/core/instance/lifecycle.js
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
// ...
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
// ...
}
- 在
Watcher的参数中有一个对象,对象中有一个before函数 - 这个函数判断如果组件已经
mounted并且还没有destroyed,就调用beforeUpdate钩子。
那么什么时候调用这个 before 函数呢?
执行时机是在 flushSchedulerQueue 函数调用的时候,此函数我们之后会详细介绍,可以先大概了解一下
// src/core/observer/scheduler.js
function flushSchedulerQueue () {
// ...
// 只需要知道 queue 存放的是一个个 watcher
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before() // 执行 before函数,会调用 beforeUpdate 钩子
}
// ...
}
// updatedQueue 是更新了的 wathcer 数组
callUpdatedHooks(updatedQueue)
}
- 遍历
queue,执行了before函数,从而执行了beforeUpdate函数 - 调用
callUpdatedHooks函数,参数updatedQueue是更新了的wathcer数组
看看 callUpdatedHooks 做了啥:
// src/core/observer/scheduler.js
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')
}
}
}
对这些数组做遍历,只有满足当前 watcher 为渲染 watcher 以及组件已经 mounted 这两个条件,才会执行 updated 钩子函数。
总结一下:
beforeUpdate先父后子updated先子后父
5. beforeDestroy & destroyed
顾名思义,beforeDestroy 和 destroyed 钩子函数的执行时机在组件销毁的阶段,组件的销毁过程之后会详细介绍,最终会调用 $destroy 方法,它的定义在 src/core/instance/lifecycle.js 中:
// src/core/observer/scheduler.js
Vue.prototype.$destroy = function () {
// ...
callHook(vm, 'beforeDestroy')
// 递归销毁逻辑
callHook(vm, 'destroyed')
// ...
}
beforeDestroy钩子函数的执行时机是在$destroy函数执行最开始的地方- 接着执行了一系列的销毁动作
- 包括从
parent的$children中删掉自身 - 删除
watcher - 当前的
VNode执行销毁钩子函数等
- 包括从
- 执行
vm.__patch__(vm._vnode, null)触发它子组件的销毁钩子函数,这样一层层的递归调用 - 执行完毕后再调用
destroy钩子函数。
总结一下:
beforeDestroy先父后子destroyed先子后父
6. activated & deactivated
activated 和 deactivated 钩子函数是专门为 keep-alive 组件定制的钩子,我们会在介绍 keep-alive 组件的时候详细介绍,这里先留个悬念。
总结
这一节主要介绍了 Vue 生命周期中各个钩子函数的执行时机以及顺序,通过分析,我们知道了如
- 在
created钩子函数中可以访问到数据 - 在
mounted钩子函数中可以访问到DOM - 在
destroy钩子函数中可以做一些定时器销毁工作
了解它们有利于我们在合适的生命周期去做不同的事情。