高频面试题:
vue中的生命周期是怎么实现的?
首先我们知道生命周期钩子函数共11个,分别为beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、activated、deactivated、beforeDestroy、destroyed和errorCaptured。
通过例子来分别分析生命周期的底层实现。
// 当前代码在main.js文件中
import Vue from "vue";
import VueRouter from "vue-router";
Vue.use(VueRouter);
// 组件定义,用于演示beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、activated和deactivated
const comA = {
data() {
return {
count: 1
};
},
template: `<div>
<button @click='changeCount'>递增</button>
<span>{{count}}</span>
</div>`,
methods: {
changeCount() {
this.count++;
}
},
beforeCreate() {
console.log("----beforeCreate----");
},
created() {
console.log("----created----");
},
beforeMount() {
console.log("----beforeMount----");
},
mounted() {
console.log("----mounted----");
},
beforeUpdate() {
console.log("----beforeUpdate----");
},
updated() {
console.log("----updated----");
},
activated() {
console.log("----activated----");
},
deactivated() {
console.log("----deactivated----");
}
};
const comB = {
template: `<div>
<router-link to="/routerA">Go to routerA</router-link>
<router-link to="/routerB">Go to routerB</router-link>
<router-view></router-view>
</div>`
};
Vue.component("comA", comA);
Vue.component("comB", comB);
// 路由定义,用于演示声明周期beforeDestroy和destroyed
const routerA = {
template: "<div>routerA</div>",
beforeDestroy() {
console.log("----beforeDestroy----");
},
destroyed() {
console.log("----destroyed----");
}
};
const routerB = { template: "<div>routerB</div>" };
const routes = [
{ path: "/routerA", component: routerA },
{ path: "/routerB", component: routerB }
];
const router = new VueRouter({
routes
});
// 错误组件定义,演示errorCaptured
const comErr = {
template: `<div></div>`,
render: {},
}
Vue.component("comErr", comErr);
// 演示入口:
new Vue({
el: "#app",
router,
data() {
return {
currentComponet: "comA"
};
},
methods: {
changeComponent() {
this.currentComponet = this.currentComponet === "comA" ? "comB" : "comA";
}
},
errorCaptured(err, vm, info) {
console.log("----errorCaptured----");
console.log(err, vm, info);
},
template: `<div>
<button @click='changeComponent'>当前组件为:{{currentComponet}}</button>
<keep-alive>
<component v-bind:is="currentComponet"></component>
</keep-alive>
<comErr></comErr>
</div>`,
});
1、beforeCreate和created
在实例化Vue的过程中会执行初始化方法this._init:
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// a flag to avoid this being observed
vm._isVue = true
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
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')
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
在执行callHook(vm, 'beforeCreate')之前完成了Vue的实例化、生命周期、事件和渲染函数的初始化。
在执行callHook(vm, 'created')之前完成了provide和inject的处理,并且对methods、data、props、computed和watch等进行配置化处理。
2、beforeMount和mounted
在初始化this._init最后执行vm.$mount(vm.$options.el),通过挂载节点的处理以及render的获取以后,会执行到mountComponent函数:
// 在文件src/core/instance/lifecycle.js中
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// ...
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
updateComponent = () => {
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
new Watcher(vm, updateComponent, noop, {
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
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
在执行callHook(vm, 'beforeMount')时,已完成了vm.$el的赋值和render函数的获取。
在执行callHook(vm, 'mounted')时,已通过vm._update(vm._render(), hydrating)的方式完成了render函数的渲染,视图进行展示。如果需要等到所有的子组件都完成挂载后再执行业务逻辑,可以借助this.$nextTick。
3、beforeUpdate和updated
当数据发生变化时会执行发布者dep的notify,最后会执行到flushSchedulerQueue:
function flushSchedulerQueue () {
currentFlushTimestamp = getNow();
flushing = true;
var watcher, id;
queue.sort(function (a, b) { return a.id - b.id; });
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
if (watcher.before) {
watcher.before();
}
id = watcher.id;
has[id] = null;
watcher.run();
// ...
// keep copies of post queues before resetting state
var activatedQueue = activatedChildren.slice();
var updatedQueue = queue.slice();
resetSchedulerState();
// call component updated and activated hooks
callActivatedHooks(activatedQueue);
callUpdatedHooks(updatedQueue);
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush');
}
}
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.before()时,就执行了渲染Watcher中的before:
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
beforeUpdate钩子函数在数据发生改变后,DOM 被更新之前被调用。这里适合在现有 DOM 将要被更新之前访问它,比如移除手动添加的事件监听器。
当视图进行更新后,会执行到callUpdatedHooks(updatedQueue),即通过while循环的方式,依次执行其中的updated函数。当这个钩子被调用时,组件 DOM 已经更新,可以执行依赖于DOM的操作,如果需要等到所有的子组件都完成挂载后再执行业务逻辑,可以借助this.$nextTick。
4、activated和deactivated
当前生命周期主要是针对动态组件在keepAlive组件中激活或者失活的场景。
(1)activated过程:
组件在patch过程中都会执行到invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch):
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])
}
}
}
其中的hook.insert指的是:
// inline hooks to be invoked on component VNodes during patch
const componentVNodeHooks = {
insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
if (vnode.data.keepAlive) {
if (context._isMounted) {
// vue-router#1212
// During updates, a kept-alive component's child components may
// change, so directly walking the tree here may call activated hooks
// on incorrect children. Instead we push them into a queue which will
// be processed after the whole patch process ended.
queueActivatedComponent(componentInstance)
} else {
activateChildComponent(componentInstance, true /* direct */)
}
}
},
}
激活函数activateChildComponent:
export function activateChildComponent (vm: Component, direct?: boolean) {
if (direct) {
vm._directInactive = false
if (isInInactiveTree(vm)) {
return
}
} else if (vm._directInactive) {
return
}
if (vm._inactive || vm._inactive === null) {
vm._inactive = false
for (let i = 0; i < vm.$children.length; i++) {
activateChildComponent(vm.$children[i])
}
callHook(vm, 'activated')
}
}
执行到callHook(vm, 'activated')时,指的被keep-alive缓存的组件激活。
(2)deactivated过程:
当前例子中,当切换this.currentComponet的时候,会让组件comA失活,当执行到removeVnodes时会执行invokeDestroyHook方法:
function invokeDestroyHook (vnode) {
var i, j;
var data = vnode.data;
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.destroy)) { i(vnode); }
for (i = 0; i < cbs.destroy.length; ++i) { cbs.destroy[i](vnode); }
}
if (isDef(i = vnode.children)) {
for (j = 0; j < vnode.children.length; ++j) {
invokeDestroyHook(vnode.children[j]);
}
}
}
其中的i = data.hook && i = i.destroy将componentVNodeHooks中的destroy赋值给i:
// inline hooks to be invoked on component VNodes during patch
const componentVNodeHooks = {
destroy (vnode: MountedComponentVNode) {
const { componentInstance } = vnode
if (!componentInstance._isDestroyed) {
if (!vnode.data.keepAlive) {
componentInstance.$destroy()
} else {
deactivateChildComponent(componentInstance, true /* direct */)
}
}
}
}
失活函数deactivateChildComponent:
export function deactivateChildComponent (vm: Component, direct?: boolean) {
if (direct) {
vm._directInactive = true
if (isInInactiveTree(vm)) {
return
}
}
if (!vm._inactive) {
vm._inactive = true
for (let i = 0; i < vm.$children.length; i++) {
deactivateChildComponent(vm.$children[i])
}
callHook(vm, 'deactivated')
}
}
执行到callHook(vm, 'deactivated')时,指的被keep-alive缓存的组件失活。
5、beforeDestroy和destroyed
// 在文件src/core/instance/lifecycle.js中
Vue.prototype.$destroy = function () {
const vm: Component = this
if (vm._isBeingDestroyed) {
return
}
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
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
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
}
}
}
在执行callHook(vm, 'beforeDestroy')时,实例仍然完全可用。
在执行callHook(vm, 'destroyed')时,对应 Vue 实例的所有指令都被解绑,所有的事件监听器被移除,所有的子实例也都被销毁。
6、errorCaptured
在执行_render函数获取虚拟DOM的过程中,会对可能遇到的错误进行捕捉:
try {
currentRenderingInstance = vm;
vnode = render.call(vm._renderProxy, vm.$createElement);
} catch (e) {
handleError(e, vm, "render");
// return error render result,
// or previous vnode to prevent render error causing blank component
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
try {
vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e);
} catch (e) {
handleError(e, vm, "renderError");
vnode = vm._vnode;
}
} else {
vnode = vm._vnode;
}
} finally {
currentRenderingInstance = null;
}
这里因为定义的错误组件中的render不是函数,所以会执行到handleError(e, vm, "render"):
export function handleError (err: Error, vm: any, info: string) {
pushTarget()
try {
if (vm) {
let cur = vm
while ((cur = cur.$parent)) {
const hooks = cur.$options.errorCaptured
if (hooks) {
for (let i = 0; i < hooks.length; i++) {
try {
const capture = hooks[i].call(cur, err, vm, info) === false
if (capture) return
} catch (e) {
globalHandleError(e, cur, 'errorCaptured hook')
}
}
}
}
}
globalHandleError(err, vm, info)
} finally {
popTarget()
}
}
这里通过const hooks = cur.$options.errorCaptured的方式获取到errorCaptured函数,即例子中定义的errorCaptured函数,达到错误捕捉的目的。
总结
生命周期的目的是在实例化
Vue的过程中,函数的不同阶段可以调用不同的钩子函数,根据不同阶段的不同特点进行业务逻辑的处理。
后记
如有纰漏,请贵手留言~