组件化-生命周期
每个 Vue 实例在被创建之前都要经过一系列的初始化过程。例如需要设置数据监听、编译模板、挂载实例到 DOM、在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,给予用户机会在一些特定的场景下添加他们自己的代码。- 官网:实例生命周期钩子
相信使用 Vue
的我们,每天都要和生命周期打招呼,在各个生命周期处理不同的事情,比如我喜欢在 created
阶段调用接口,在 mounted
阶段操作 DOM 等。那么本文就来了解 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();
}
复制代码
callHook
主要做了几件事:
- 根据传入的字符串
hook
名称,拿到对应的回调函数数组 - 循环回调函数数组,然后执行,执行的时候把
vm
作为函数执行上下文
在上一节 组件化-合并配置 中,我们介绍过在合并 options
的过程中,各个阶段的生命周期也被作为数组合并到了 vm.$options
中,因此调用 callHook
时就可以把里面的回调全部执行完。
在了解执行/触发方式之后,继续看一下每个生命周期的调用时机。
beforeCreate & created
beforeCreate
& created
是在初始化实例 Vue
的阶段调用的,他定义在 _init
中:
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");
// ...
};
复制代码
可以看到在 initState
前后分别调用了 beforeCreate
和 created
。
initState
的作用是初始化 Props
、Methods
、Data
、Computed
、Watch
等,所以我们在 beforeCreate
时还拿不到 this.xxx
的值。
另外在这两个钩子执行的时候还没有 渲染 DOM
,所以不能 操作 DOM
。
beforeMount & mounted
beforeMount
钩子函数在 mount
执行时被调用,也就是 挂载 DOM
之前,它的调用时机是在 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) {
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;
}
复制代码
在执行 updateComponent
之前调用 beforeMount
,在执行完 updateComponent
把 VNode
patch
到 真实 DOM
后执行 mounted
。
注意在 mounted
时有一个判断,如果是组件的方式,则不会在这里触发钩子函数,只有手动调用:new Vue(options)
时才会调用。
组件的调用时机和代码在 invokeInsertHook
函数中,之前我们提到过,组件的 VNode
patch
到 DOM
后,会执行 invokeInsertHook
函数,
把 insertedVnodeQueue
里保存的钩子函数依次执行一遍,它的定义在 src/core/vdom/patch.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]);
}
}
}
复制代码
该函数会执行 insert
这个钩子函数,对于组件而言,insert
钩子函数的定义在 src/core/vdom/create-component.js
中的 componentVNodeHooks
中:
const componentVNodeHooks = {
// ...
insert(vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode;
if (!componentInstance._isMounted) {
componentInstance._isMounted = true;
callHook(componentInstance, "mounted");
}
// ...
},
};
复制代码
注意:insertedVnodeQueue
的添加顺序是先子后父,所以对于同步渲染的子组件来说,mounted
的执行顺序也是先子后父。
beforeUpdate & updated
beforeUpdate
& updated
的触发时机是在数据更新的时候,这个涉及到数据双向绑定、更新,后面细讲。
beforeUpdate
的执行时机是在渲染 Watcher
的 before
函数中:
export function mountComponent(
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
// ...
// 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) {
callHook(vm, "beforeUpdate");
}
},
},
true /* isRenderWatcher */
);
// ...
}
复制代码
这时候生成一个渲染 Watcher
,每次更新时触发 beforeUpdate
。
updated
的执行时机是在 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");
}
}
}
复制代码
在这里我们大概了解一下 flushSchedulerQueue
,后面会细讲。
updatedQueue
是更新了的 watcher
数组,然后调用 callUpdatedHooks
进行遍历,只有满足 vm._watcher === watcher && vm._isMounted
才会触发 updated
。
我们之前提到在 mount
过程(调用 mountComponent
)中会实例化一个 渲染Watcher
:
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 = true
,则把当前 watcher
赋值给 vm._watcher
:
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
钩子函数。
beforeDestroy & destroyed
beforeDestroy
& destroyed
是在组件销毁阶段触发的,组件最终会调用 $destroy
方法,定义在 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;
}
};
复制代码
首先调用 beforeDestroy
,然后从 parent
中移除自身,删除 watcher
,移除 observer
的引用,执行 vm.__patch__(vm._vnode, null)
触发子组件的销毁钩子函数,也是递归调用,和 mounted
一样,先子后父。
activated & deactivated
activated
& deactivated
是 KeepAlive
的专属生命周期钩子。
总结
这里主要介绍了各个生命周期的钩子是在什么时机被触发的,通过这些知道在 created
时可以访问 props
、data
、method
等。在 mounted
阶段能够进行 DOM 操作
。在 destroy
阶段可以清除定时器、DOM 引用
赋值为 null
等,了解这些可以让我们在不同的生命周期中调用方法和操作时游刃有余。
参考:Vue.js 技术揭秘