前言
在面试中,关于Vue
的知识点环节里一般都会问到生命周期,整体的生命周期不复杂,如下所示:
一般靠使用Vue
的经验和背网上的答案都可以基本答出个大概,但如果被细问其中就可能答不出来,这里就从源码的角度开始分析Vue
的生命周期。
关于callHook
这里首先要介绍一下callHook
函数,以方便一下阅读源码,在Vue
的源码中,当要执行到生命周期钩子的函数时,都会通过该函数去调用执行用户注册在该钩子下的代码。例如,当执行到created
时,就会调用callHook(vm, 'created')
。
在介绍callHook
函数的源码之前,先说一下Vue
在初始化的时候对用户注册在钩子函数下的代码是怎么处理的:
初始化时通过mergeOption
合并Vue
构造函数中的options
以及用户传入的options
,然后放到vm.$options
// src\core\instance\init.js
// 合并options,这里的options指new Vue(option)中的option
// _isComponent=true代表该Vue实例是组件
// 我们在这里把Vue实例分两种:
// 1. 通过new Vue()生成的普通Vue,
// 2. 写在单文件里,会被外部引用注册到component里的组件Vue
if (options && options._isComponent) {
// 针对是组件的Vue实例,用initInternalComponent以优化内部组件的实例化,
// 因为动态选项合并非常缓慢,而且没有一个内部组件选项需要特殊处理。
initInternalComponent(vm, options)
} else {
// 普通Vue实例则用mergeOptions,
// 把vm和Vue构造函数自身的option
vm.$options = mergeOptions(
// resolveConstructorOptions在普通的new Vue中返回的是Vue.options
// 如果要初始化的类是通过Vue.extend生成的构造函数,则返回mergeOption(Vue.option,该构造函数.extendOptions)
// 该构造函数.extendOptions为传入Vue.extend(extendOptions)中的形参,即组件
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
mergeOptions
函数执行过程中,针对options
中每一个生命周期钩子的合并处理会通过mergeHook
实现,我们看一下mergeHook
的源码:
// src\core\util\options.js
function mergeHook (
parentVal: ?Array<Function>,
childVal: ?Function | ?Array<Function>
): ?Array<Function> {
const res = childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
return res
? dedupeHooks(res)
: res
}
里面有好几个嵌套的三目运算符,这里我拆开分析一下:
-
如果
childVal
为真值,判断parentVal
:-
如果
parentVal
为真值,则通过parentVal.concat(childVal)
合并数组 -
如果
parentVal
为假值,则判断childVal
是否为数组:- 如果
childVal
是数组,则返回childVal
- 如果
childVal
不是数组,则返回[childVal]
- 如果
-
-
如果
childVal
为假值,返回parentVal
可见,钩子函数最后会以数组的形式记录在vm.$options
中,例如:
当我们在Vue
中的开发代码如下:
new Vue({
created: function () {
console.log('hello world')
}
})
在Vue
初始化时,vm.$options
为:
vm.$options = {
// ...其他属性
created: [
function created() {
console.log('hello world')
}
],
}
以数组的形式保存是为了存放Vue.mixin
中混入的生命周期钩子函数。现在再看一下callHook
的源码:
// src\core\instance\lifecycle.js
export function callHook (vm: Component, hook: string) {
// #7573 disable dep collection when invoking lifecycle hooks
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()
}
// 执行传入的handler钩子函数且对错误进行处理
export function invokeWithErrorHandling (
handler: Function,
context: any,
args: null | any[],
vm: any,
info: string
) {
let res
try {
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)`))
// issue #9511
// avoid catch triggering multiple times when nested calls
res._handled = true
}
} catch (e) {
handleError(e, vm, info)
}
return res
}
callHooks
就是根据传入的hook
(生命周期钩子名称)获取对应的钩子函数列表,然后遍历传入invokeWithErrorHandling
函数中执行。
这一节主要介绍callHook
函数,其作用是用来执行用户注册的钩子函数。
从new Vue() 分析生命周期
入口分析
Vue.js
是一个跨平台的 MVVM
框架,它可以跑在 web
上,也可以配合 weex
跑在 native
客户端上。不同运行平台的入口文件放在src\platforms
目录下,而与平台无关的核心代码(包括内置组件、全局 API 封装,Vue 实例化、观察者、虚拟 DOM、工具函数)文件都放在src\core
目录下,这里我们从web环境下的运行时版本进行分析:
src\platforms\web\runtime\index.js
//这里只显示涉及到分析的代码
import Vue from 'core/index'
// $mount放在这里声明是因为“运行时版本”和“运行加编译器版本”的$mount函数不一样,“运行加编译器版本”的$mount函数会添加处理template属性的逻辑
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
// 用于通过diff算法对比VNode且异步更新到页面上,由于不同平台(weex和web)的DOM修改方式不同,因此__patch__需要根据不同的平台初始化
Vue.prototype.__patch__ = inBrowser ? patch : noop
export default Vue
src\core\index.js
// 这里去掉关于`SSR`的代码
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
// 初始化Vue.options以存放全局注册的component、filter、directive
// 定义挂载到Vue类上的静态方法:set,delete,nextTick,observable,use,mixin,extend,component,filter,directive
initGlobalAPI(Vue)
Vue.version = '__VERSION__'
export default Vue
接下来看看定义Vue
构造函数的文件代码
src\core\instance\index.js
function Vue (options) {
this._init(options)
}
// 定义Vue.prototype._init方法,供构造函数的内部调用
initMixin()
// 在Vue.prototype中定义$data(指向实例的_data),$props(指向实例的_props),$set,$delete,$watch
stateMixin(Vue)
// 在Vue.prototype中定义$on,$once,$off,$emit
eventsMixin(Vue)
// 在Vue.prototype中定义_update(实例初次渲染和更新视图时会调用),$forceUpdate,$destroy
lifecycleMixin(Vue)
// 在Vue.prototype中定义$nextTick,_render(实例初次渲染和更新视图时会调用)
renderMixin(Vue)
可以看出很多静态方法方法其实定义在Vue.prototype中。
我们下面从new Vue
的执行过程触发,从源码上分析何时执行钩子函数。
beforeCreate && created
new Vue()
时,开始调用Vue
的构造函数,构造函数中会调用在initMixin
中声明的Vue.prototype.init
方法,然后我们往下分析:
src\core\instance\init.js
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// 初始化uid
vm._uid = uid++
// 被标记_isVue=true的对象在传入obverse方法时不会被处理,即不会被响应式处理
vm._isVue = true
// 合并option,_isComponent=true代表该Vue实例是组件
// 我们在这里把Vue实例分两种:
// 1. 通过new Vue()生成的普通Vue,
// 2. 写在单文件里,会被外部引用注册到component里的组件Vue
if (options && options._isComponent) {
// 针对是组件的Vue实例,用initInternalComponent以优化内部组件的实例化,
// 因为动态选项合并非常缓慢,而且没有一个内部组件选项需要特殊处理。
initInternalComponent(vm, options)
} else {
// 普通Vue实例则用mergeOptions,
// 把vm和Vue构造函数自身的option
vm.$options = mergeOptions(
// resolveConstructorOptions在普通的new Vue中返回的是Vue.options
// 如果要初始化的类是通过Vue.extend生成的构造函数,则返回mergeOption(Vue.option,该构造函数.extendOptions)
// 该构造函数.extendOptions为传入Vue.extend(extendOptions)中的形参,即组件
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
vm._renderProxy = vm
vm._self = vm
// 初始化与生命周期相关的属性,如$parent,$children,$refs(初始化为空对象),$root
initLifecycle(vm)
// 处理父组件通过v-on绑定在该Vue实例上的方法,通常是组件Vue才会详细执行里面的内容
initEvents(vm)
// 初始化与渲染相关的属性,如:$createElement,$slots,$scopedSlots,$attrs,$listeners
initRender(vm)
// 此时初始化走到'beforeCreate'生命周期,调用注册在beforeCreate下的钩子函数
callHook(vm, 'beforeCreate')
// 初始化Injections
initInjections(vm)
// 依次初始化实例上的props,methods,data,computed,watch属性,其中包括把data和props转为响应式
initState(vm)
// 初始化provide属性
initProvide(vm)
// 此时初始化走到'created'生命周期,调用注册在created下的钩子函数
callHook(vm, 'created')
// 调用开头声明的$mount方法
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
总结一下,Vue
到beforeCreate
之前,初始化了以下属性:
- 与生命周期相关的属性,如
$parent
,$children
,$refs
(初始化为空对象),$root
- 父组件通过
v-on
绑定在该Vue实例上的方法 - 与渲染相关的属性,如:
$createElement
,$slots
,$scopedSlots
,$attrs
,$listeners
Vue
从beforeCreate
到created
做了以下操作:
- 初始化
Injections
属性 - 初始化实例上的
props
,methods
,data
,watch
,computed
属性 - 初始化
provide
属性
beforeMount && mounted
从开头的Vue.prototype.$mount
函数中看出他其实调用了mountComponent
函数去执行挂载逻辑,接下来看看mountComponent
函数的源码:
export const createEmptyVNode = (text: string = '') => {
const node = new VNode()
node.text = text
node.isComment = true
return node
}
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// 如果渲染函数不存在,则初始化为空的VNode
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
}
// 此时初始化走到'beforeMount'生命周期,调用注册在beforeMount下的钩子函数
callHook(vm, 'beforeMount')
// 初始化updateComponent用于更新DOM,其先调用 vm._render 生成VNode,
// 然后调用 vm._update 对比新旧VNode节点且更新到DOM视图上。
let updateComponent
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
// 实例化一个渲染Watcher,
// 作为参数路径的形参 updateComponent 函数会在Watcher实例化过程中立即被执行
// 因此,当渲染Watcher实例化完成时,视图已被更新且挂在到页面上
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// vm.$node == null 代表此实例是普通Vue,并非组件Vue
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
从beforeMount
到mounted
过程中,做了以下步骤:
- 实例化渲染Watcher,且执行传入的更新视图的函数(
updateComponent
) - 在
updateComponent
执行过程中,所调用到的data
里的数据里会把渲染Watcher放在Dep实例(订阅器)上,形成观察者模式的监听触发逻辑,在之后数据再次发生变化时,会通知Dep实例执行记录在Dep中的Wacther实例,从而完成视图更新
以下内容参考:Vue.js 技术揭秘
拓展: 当上述vm.$vnode !== null
时代表改实例为组件Vue
,组件Vue
是在调用vm._update
,执行其中的Vue.prototype.__patch__
函数(即patch
函数)中过程,在视图被更新之后,执行invokeInsertHook
函数:
function invokeInsertHook (vnode, queue, initial) {
if (isTrue(initial) && isDef(vnode.parent)) {
vnode.parent.data.pendingInsert = queue
} else {
// 这里的queue为insertedVnodeQueue,用于
for (let i = 0; i < queue.length; ++i) {
queue[i].data.hook.insert(queue[i])
}
}
}
其中,insert
函数的定义为:
const componentVNodeHooks = {
// ...
insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
// ...
},
}
在完成组件的整个 patch
过程后,最后执行 insert(parentElm, vnode.elm, refElm)
完成组件的 DOM
插入,如果组件 patch
过程中又创建了子组件,那么DOM 的插入顺序是先子后父。因此保证了父子组件的执行顺序是:
父beforeCreate
> 父created
> 父beforeMount
> 子beforeCreate
> 子created
> 子beforeMount
> 子mounted
> 父mounted
beforeUpdate && updated
当数据更新时,会调用该数据关联的订阅器实例dep
的notify
方法,notify
方法源码如下所示:
src\core\observer\dep.js
export default class Dep {
// ...
notify () {
// subs中存放与该数据关联的watcher
const subs = this.subs.slice()
// 遍历调用watcher中的update方法
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
src\core\observer\watcher.js
let uid = 0
export default class Watcher {
// .....
update () {
// 这里先以渲染watcher为例做分析,渲染watcher的lazy和sync为false
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
}
src\core\observer\scheduler.js
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
// 查询has中是否已记录该watcher.id从而达到去重的目的,防止更新队列queue出现重复的watcher
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
// index代表当前处理到queue更新队列的第几个元素
// 这里说一下watcher.id,每一个Watcher类被实例化时,都会执行this.id = uid++,
// uid是在watcher类所在的文件开头就被定义的,并非在class Watcher{}里面,
// 故每一个watcher的uid都不一样且随着初始化顺序而递增
// 这里的while里面的语句比较巧妙:
// 1. 如果更新队列正在执行,则在会放在大于queue[index]后面的watcher.id的位置上,此举为了保证:
// (1) 父组件的watcher先于子组件的watcher执行,因此父组件的beforeUpdate钩子函数先于子组件的执行
// (2) 保证了同一组件中watcher的执行顺序:计算watcher(即定义在computed里)>用户watcher(定义在watch里)>渲染watcher
// 2. 如果目前更新队列执行的watcher的id比要插入的watcher的id大,则直接插入到目前执行的watcher的下一个位置,即queue[index+1],此举可以保证立即执行
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// 判断waiting是否执行,保证nextTick(flushSchedulerQueue)在执行期间不会被调用
// flushSchedulerQueue用于遍历执行记录在queue的watcher中的run方法
// flushSchedulerQueue执行完成后会调用resetSchedulerState去清空has以及把flushing和waiting设置为false
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
这里不展示nextTick
方法的源码,他的用法和vm.$nextTick
一样,就是把传入函数放到异步队列中。接下来直接看flushSchedulerQueue
的源码:
src\core\observer\scheduler.js
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
// 表示当前在用该方法处理queue队列
flushing = true
let watcher, id
// 根据watcher中的id以正序进行排列,此举为了保证:
// 1. 父组件的watcher先于子组件的watcher执行
// 2. 保证了同一组件中watcher的执行顺序:计算watcher(即定义在computed里)>用户watcher(定义在watch里)>渲染watcher。因为计算watcher先于用户watcher创建,用户watcher先于渲染watcher创建
// 3. 如果一个组件在父组件的 watcher 执行期间被销毁,那么它对应的 watcher 执行都可以被跳过,所以父组件的 watcher 应该先执行。当子组件销毁的时候,子组件的渲染watcher会执行teardown方法把渲染watcher自身的active置为false。watcher.run()方法里会先根据this.active是否为true从而判断是否执行
queue.sort((a, b) => a.id - b.id)
// 遍历取出queue更新队列中的watcher然后执行其before和run方法
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
/**
* 这里再放以下渲染watcher的初始化语句:
* new Watcher(vm, updateComponent, noop, {
* before () {
* if (vm._isMounted && !vm._isDestroyed) {
* callHook(vm, 'beforeUpdate')
* }
* }
* }, true)
* 此处的watcher.before会在渲染watcher初始化时
* 设置为传入构造函数的第四个参数里的before,
* 此时执行watcher.before相当于执行beforeUpdate钩子函数
* 注意: watcher.before仅在渲染watcher才存在
*/
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
// watcher.run是记录更新逻辑的函数
watcher.run()
}
// 保留queue的副本,因为resetSchedulerState会清空queue
const updatedQueue = queue.slice()
// 把waiting和flushing置为false,清空queue和has
resetSchedulerState()
// 执行updated钩子函数
callUpdatedHooks(updatedQueue)
}
function callUpdatedHooks (queue) {
let i = queue.length
// 倒序遍历更新队列,保证子组件的updated钩子函数先于父组件的执行
while (i--) {
const watcher = queue[i]
const vm = watcher.vm
// vm._watcher用于记录vm的渲染watcher,因此这里逻辑是,如果当前遍历到的watcher是渲染watcher,
// 且本次不是首次渲染(vm._isMounted为真),且vm没被销毁
// 则对该watcher对应的vm执行'updated'钩子函数
if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'updated')
}
}
}
以上的逻辑比较多,总结就是:
beforeUpdate
执行之前,变更数据对应订阅器已被通知,然后所有需要更新的wathcer
已放在更新队列queue
,更新列表通过nextTick
放到异步队列然后开始遍历执行。
updated
执行之前,对应的更新函数已执行完毕。
主要需要注意的是,父子组件的执行顺序是:父beforeUpdate
> 子beforeUpdate
> 子beforeUpdate
> 父beforeUpdate
beforeDestroy && destroyed
当Vue实例将要被销毁时,会调用Vue.prototype.$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
// 把自身从vm.$parent.$children中移除
const parent = vm.$parent
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
remove(parent.$children, vm)
}
// 销毁实例的渲染watcher
if (vm._watcher) {
vm._watcher.teardown()
}
// 遍历销毁实例的所有watcher,包括计算watcher,用户watcher,渲染watcher
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--
}
vm._isDestroyed = true
// 更新页面,把该组件的UI从页面中移除
vm.__patch__(vm._vnode, null)
// 调用该实例的destroyed钩子函数
callHook(vm, 'destroyed')
// 移除所有注册在该实例上的事件
vm.$off()
// remove __vue__ reference
if (vm.$el) {
vm.$el.__vue__ = null
}
// release circular reference (#6759)
if (vm.$vnode) {
vm.$vnode.parent = null
}
}
总结一下,在beforeDestroy
到destroyed
中,把自身从vm.$parent.$children
中移除,遍历销毁实例的所有watcher
。移除其他私有属性的引用等。但注册绑定的事件是在destroyed
之后才移除的。
拓展: 在vm.__patch__(vm._vnode, null)
执行时,相当于执行patch
方法,而patch
开头的源码如下:
vue2-study\src\core\vdom\patch.js
return function patch (oldVnode, vnode, hydrating, removeOnly) {
// 此时传入的oldVnode有定义且vnode为null
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
// ...
}
function invokeDestroyHook (vnode) {
let i, j
const data = vnode.data
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode) //此处i为data.hook.destroy
for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
}
// 如果vnode有子节点,则遍历子节点递归调用invokeDestroyHook
if (isDef(i = vnode.children)) {
for (j = 0; j < vnode.children.length; ++j) {
invokeDestroyHook(vnode.children[j])
}
}
}
上面的vnode.data.hook.destroy
在componentVNodeHooks
中被定义,其源码如下:
vue2-study\src\core\vdom\create-component.js
destroy (vnode: MountedComponentVNode) {
const { componentInstance } = vnode
// 此时componentInstance._isDestroyed已被设置为true,故以下逻辑不执行
// 如果是父节点被销毁从而递归销毁到子节点,此时其_isDestroyed为false,进而调用子节点对应的子组件的$destroy方法,
// 从而实现从父节点到子节点的销毁,而且保证了此过程的执行顺序是:
// 父beforeDestroy>子beforeDestroy>子destroyed>父destroyed
if (!componentInstance._isDestroyed) {
if (!vnode.data.keepAlive) {
componentInstance.$destroy()
} else {
deactivateChildComponent(componentInstance, true /* direct */)
}
}
}
总结
总结一下:
Vue
在初始阶段,会通过mergeOption
合并option
配置,此函数会把实例中的钩子函数转化为数组。当调用callHook
时,会遍历执行对应的钩子函数。
new Vue
- 初始化与生命周期相关的属性,如
$parent
,$children
,$refs
(初始化为空对象),$root
- 处理父组件通过
v-on
绑定在该Vue
实例上的方法,通常是组件Vue
才会详细执行里面的内容 - 初始化与渲染相关的属性,如:
$createElement
,$slots
,$scopedSlots
,$attrs
,$listeners
beforeCreate
- 初始化
Injections
- 初始化实例上的
props
,methods
,data
,watch
,computed
属性,其中包括把data和props转为响应式 - 初始化
provide
属性created
1.初始化渲染函数,如果不存在渲染函数则解析template
beforeMount
- 生成渲染
watcher
并执行传入其中的updateComponent
函数,调用render
函数生成VNode
后更新挂载到视图上 - 在
updateComponent
执行过程中,所调用到的data
里的数据里会把渲染Watcher
放在Dep
实例(订阅器)上mounted
数据变化时
- 触发记录绑定该数据的订阅器,从而触发异步队列执行更新队列任务
beforeUpdate
- 实例的页面已被更新
updated
销毁时
beforeDestroy
- 销毁属性,包括
data
,computed
,watcher
等 - 销毁实例对应的挂载元素的内容
destroy
- 移除所有注册在该实例上的事件
后记
之后会找个时间写一下Vue
的响应式原理,因为里面叶有一堆知识点需要学习。