——最困难的事情就是认识自己
前言
在Vue
中,每个Vue
实例从被创建出来到最终被销毁都会经历一个过程,就像人一样,从出生到死亡。在这一过程里会发生许许多多的事,例如设置数据监听,编译模板,组件挂载等。在Vue
中,把Vue
实例从被创建出来到最终被销毁的这一过程称为Vue
实例的生命周期,同时,在Vue
实例生命周期的不同阶段Vue
还提供了不同的钩子函数,以方便用户在不同的生命周期阶段做一些额外的事情。
正文
先用官方的生命周期图解镇楼,接下来我们一一来看:
beforeCreate
在创建Vue
实例的时候,内部会执行this._init(options)
:
vm.$options = mergeOptions( // 合并配置
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
initLifecycle(vm) // 初始化生命周期
initEvents(vm) // 初始化事件中心
initRender(vm) // 初始化 render
callHook(vm, 'beforeCreate')
复制代码
mergeOptions | 合并配置 | 将Vue、用户自定义、实例上的属性进行合并,钩子函数会合并为数组 |
initLifecycle(vm) | 初始化生命周期 | 给Vue实例挂载了一些属性并设置默认值,$parent 、$root 较为重要 |
initEvents(vm) | 初始化事件中心 | 父组件给子组件的注册事件中,把自定义事件传给子组件,在子组件实例化的时候进行初始化;而浏览器原生事件是在父组件中处理。 |
initRender(vm) | 初始化 render | 给Vue实例挂载$slots、$scopedSlots、_c、$createElement 等属性,将$attrs、$listeners 变为响应式 |
此阶段可以加一些loading
效果,在created
时进行移除
created
initInjections(vm) // 初始化 inject
initState(vm) // 初始化 state
initProvide(vm) // 初始化 provide
callHook(vm, 'created')
复制代码
initProvide(vm)
在当前组件注入属性,initInjections(vm)
原理:自底向上查找上游父级组件所注入的对应的属性。initState(vm)
按顺序初始化了5个选项:props、methods、data、computed、watch
。
initProps(vm, opts.props) | 初始化 props | 对 props 赋予响应式并代理到 Vue 实例上 |
initMethods(vm, opts.methods) | 初始化 methods | 将 methods 代理到 Vue 实例上 |
initData(vm) | 初始化 data | 对 data 赋予响应式并代理到 Vue 实例上 |
initComputed(vm, opts.computed) | 初始化 computed | 循环创建 computed watcher并收集至 handler 所依赖的响应式属性上,最后将 computed 代理到 Vue 实例上 |
initWatch(vm, opts.watch) | 初始化 watch | 循环创建 user watcher并收集至所监听的响应式属性上 |
此阶段可以执行异步请求数据的方法,完成数据的初始化
beforeMount
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
// src\platforms\web\entry-runtime-with-compiler.js
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (el, hydrating) {
el = el && query(el)
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
if (!options.render) {
if (template) {
// 处理 # 开始的选择符、dom对象的情况
} else if (el) { // 获取 el 的 outerHTML 作为模板
template = getOuterHTML(el)
}
if (template) { // 编译生成 render 函数
compileToFunctions(template, {...}
}
}
return mount.call(this, el, hydrating)
}
// src\core\instance\lifecycle.js
export function mountComponent (vm, el, hydrating) {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
}
callHook(vm, 'beforeMount')
// ......
}
复制代码
此阶段主要做了这么几件事:
- 判断
el
是否存在,存在时才会去调用$mount
去执行后续的挂载操作 query(el)
拿到el
的dom
元素引用- 对
el
进行判断,不能为body、html
元素 - 判断
render
函数是否定义,未定义时针对template
进行处理并进行模板编译生成render
函数 - 在
Vue
实例上挂载$el
属性 - 判断
render
函数是否定义,未定义时设值为一个空的VNode
(此处判断针对的是子组件挂载时的情况)
mounted
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
复制代码
此阶段创建render watcher
,vm._render()
生成VNode
,vm._update()
进行patch
渲染视图。
该阶段DOM
加载完成 ,完成挂载,可以进行DOM
的相关操作。
beforeUpdate
// 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()
}
// ......
callUpdatedHooks(updatedQueue)
}
复制代码
上面mounted
阶段创建render watcher
时,第4参数给到了before
的定义,当数据更新时,会执行至此处异步队列刷空的操作,会在每一个watcher
执行run
方法前执行beforeUpdate
钩子函数。
updated
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')
}
}
}
复制代码
上面beforeUpdate
阶段所有的watcher
执行完run
方法后会调用callUpdatedHooks
,进行所有Vue
实例updated
钩子函数的循环执行。
此阶段数据更新完成,DOM
也重新渲染,业务的统一处理可以在这里进行。
beforeDestroy
// src\core\instance\lifecycle.js
Vue.prototype.$destroy = function () {
const vm: Component = 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
}
}
}
复制代码
我们可以看到beforeDestroy
钩子函数调用的时机是在this.$destroy()
执行的一开始,this.$destroy()
除我们手动调用外,会在什么时候执行?
?♂️:在组件销毁的时候执行
?♀️:组件什么时候会被销毁?
?♂️:在patch
阶段,VNode
渲染更新的时候,对比出oldVNode
需要被移除时,组件被销毁
// src\core\vdom\create-component.js
var componentVNodeHooks = {
destroy: function destroy (vnode) {
var componentInstance = vnode.componentInstance;
if (!componentInstance._isDestroyed) {
if (!vnode.data.keepAlive) {
componentInstance.$destroy();
} else {
deactivateChildComponent(componentInstance, true /* direct */);
}
}
}
}
复制代码
在Vnode
渲染更新的时候,它在执行相关操作的同时,还会在每个阶段触发相应的钩子函数。组件销毁时会触发destroy
这个钩子函数,从而执行this.$destroy()
。
destroyed
destroyed
阶段主要做了这么几件事:
- 将当前的
Vue
实例从其父级实例中移除 - 卸载当前实例上的
watcher
vm.__patch__(vm._vnode, null)
重新patch
,这里针对手动调用this.$destroy()
的情况
注意:所有自定义事件监听器的移除操作vm.$off()
是在destroyed
钩子函数执行之后的。
最后,再移除一些相关属性的引用,至此,组件销毁完毕。
结尾
没啥可说的,奥力给!!!