vue 生命周期
Vue 实例有一个完整的生命周期,也就是从开始创建、初始化数据、编译模板、挂载 Dom、渲染 → 更新 → 渲染、销毁等一系列过程,我们称这是 Vue 的生命周期。通俗说就是 Vue 实例从创建到销毁的过程,就是生命周期。
每个 Vue 实例在被创建之前都要经过一系列的初始化过程。例如需要设置数据监听、编译模板、挂载实例到 DOM、在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,给予用户机会在一些特定的场景下添加他们自己的代码。

在我们实际项目开发过程中,会非常频繁地和 Vue 组件的生命周期打交道,接下来我们就从源码的角度来看一下这些生命周期的钩子函数是如何被执行的。
源码中最终执行生命周期的函数都是调用 callHook
方法
1export function callHook(vm: Component, hook: string) {
2 // #7573 disable dep collection when invoking lifecycle hooks
3 pushTarget();
4 const handlers = vm.$options[hook];
5 if (handlers) {
6 for (let i = 0, j = handlers.length; i < j; i++) {
7 try {
8 handlers[i].call(vm);
9 } catch (e) {
10 handleError(e, vm, `${hook} hook`);
11 }
12 }
13 }
14 if (vm._hasHookEvent) {
15 vm.$emit('hook:' + hook);
16 }
17 popTarget();
18}
callHook
函数的逻辑很简单,根据传入的字符串 hook,去拿到 vm.$options[hook]
对应的回调函数数组,然后遍历执行,执行的时候把 vm
作为函数执行的上下文。
beforeCreate & created
beforeCreate
和 created
函数都是在实例化 Vue 的阶段,在_init
方法中执行的, 也就是初始化实例的时候
1Vue.prototype._init = function (options?: Object) {
2 // ...
3 initLifecycle(vm);
4 initEvents(vm);
5 initRender(vm);
6 callHook(vm, 'beforeCreate');
7 initInjections(vm); // resolve injections before data/props
8 initState(vm);
9 initProvide(vm); // resolve provide after data/props
10 callHook(vm, 'created');
11 // ...
12};
beforeCreate
和 created
的钩子调用是在 initState
的前后,initState
的作用是初始化 props
、data
、methods
、watch
、computed
等属性,之后我们会详细分析。那么显然 beforeCreate
的钩子函数中就不能获取到 props
、data
中定义的值,也不能调用 methods
中定义的函数。
beforeMount & mounted
顾名思义,beforeMount
钩子函数发生在 mount
,也就是 DOM 挂载之前,它的调用时机是在 mountComponent
函数中
1export function mountComponent(vm: Component, el: ?Element, hydrating?: boolean): Component {
2 vm.$el = el;
3 // ...
4 callHook(vm, 'beforeMount');
5 let updateComponent;
6 /* istanbul ignore if */
7 if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
8 updateComponent = () => {
9 const name = vm._name;
10 const id = vm._uid;
11 const startTag = `vue-perf-start:${id}`;
12 const endTag = `vue-perf-end:${id}`;
13 mark(startTag);
14 const vnode = vm._render();
15 mark(endTag);
16 measure(`vue ${name} render`, startTag, endTag);
17 mark(startTag);
18 vm._update(vnode, hydrating);
19 mark(endTag);
20 measure(`vue ${name} patch`, startTag, endTag);
21 };
22 } else {
23 updateComponent = () => {
24 vm._update(vm._render(), hydrating);
25 };
26 }
27 // we set this to vm._watcher inside the watcher's constructor
28 // since the watcher's initial patch may call $forceUpdate (e.g. inside child
29 // component's mounted hook), which relies on vm._watcher being already defined
30 new Watcher(
31 vm,
32 updateComponent,
33 noop,
34 {
35 before() {
36 if (vm._isMounted) {
37 callHook(vm, 'beforeUpdate');
38 }
39 },
40 },
41 true /* isRenderWatcher */
42 );
43 hydrating = false;
44 // manually mounted instance, call mounted on self
45 // mounted is called for render-created child components in its inserted hook
46 if (vm.$vnode == null) {
47 vm._isMounted = true;
48 callHook(vm, 'mounted');
49 }
50 return vm;
51}
在执行 vm._render()
函数渲染VNode
之前,执行了 beforeMount
钩子函数,在执行完 vm._update()
把 VNode patch
到真实 DOM 后,执行 mounted
钩子。注意,这里对 mounted
钩子函数执行有一个判断逻辑,vm.$vnode
如果为 null
,则表明这不是一次组件的初始化过程,而是我们通过外部new Vue
初始化过程。那么对于组件,它的 mounted
时机在哪儿呢?组件的 VNode
patch
到 DOM 后,会执行 invokeInsertHook
函数,把 insertedVnodeQueue
里保存的钩子函数依次执行一遍
1function invokeInsertHook(vnode, queue, initial) {
2 // delay insert hooks for component root nodes, invoke them after the
3 // element is really inserted
4 if (isTrue(initial) && isDef(vnode.parent)) {
5 vnode.parent.data.pendingInsert = queue
6 }
7 else {
8 for (let i = 0; i < queue.length; ++i)
9 queue[i].data.hook.insert(queue[i])
10 }
11}
该函数会执行 insert
这个钩子函数,对于组件而言,insert
钩子函数的定义在 src/core/vdom/create-component.js
中的 componentVNodeHooks 中:
1const componentVNodeHooks = {
2 // ...
3 insert(vnode: MountedComponentVNode) {
4 const { context, componentInstance } = vnode;
5 if (!componentInstance._isMounted) {
6 componentInstance._isMounted = true;
7 callHook(componentInstance, 'mounted');
8 }
9 // ...
10 },
11};
可以看到,每个子组件都是在这个钩子函数中执行 mounted
钩子函数,并且我们之前分析过,insertedVnodeQueue
的添加顺序是先子后父,所以对于同步渲染的子组件而言,mounted
钩子函数的执行顺序也是先子后父。
beforeUpdate & updated
顾名思义,beforeUpdate
和 updated
的钩子函数执行时机都应该是在数据更新的时候,到目前为止,还没有分析 Vue 的数据双向绑定、更新相关。 beforeUpdate
的执行时机是在渲染 Watcher
的 before
函数中:
1export function mountComponent(vm: Component, el: ?Element, hydrating?: boolean): Component {
2 // ...
3 // we set this to vm._watcher inside the watcher's constructor
4 // since the watcher's initial patch may call $forceUpdate (e.g. inside child
5 // component's mounted hook), which relies on vm._watcher being already defined
6 new Watcher(
7 vm,
8 updateComponent,
9 noop,
10 {
11 before() {
12 if (vm._isMounted) {
13 callHook(vm, 'beforeUpdate');
14 }
15 },
16 },
17 true /* isRenderWatcher */
18 );
19 // ...
20}
注意这里有个判断,也就是在组件已经 mounted
之后,才会去调用这个钩子函数。 update
的执行时机是在 flushSchedulerQueue
函数调用的时候,它的定义在 src/core/observer/scheduler.js 中:
1function flushSchedulerQueue() {
2 // ...
3 // 获取到 updatedQueue
4 callUpdatedHooks(updatedQueue)
5}
6function callUpdatedHooks(queue) {
7 let i = queue.length
8 while (i--) {
9 const watcher = queue[i]
10 const vm = watcher.vm
11 if (vm._watcher === watcher && vm._isMounted)
12 callHook(vm, 'updated')
13 }
14}
flushSchedulerQueue
updatedQueue
是更新了的 wathcer
数组,那么在 callUpdatedHooks
函数中,它对这些数组做遍历,只有满足当前 watcher
为 vm.\_watcher
以及组件已经 mounted
这两个条件,才会执行 updated
钩子函数。在组件 mount
的过程中,会实例化一个渲染的 Watche
r 去监听 vm
上的数据变化重新渲染,这段逻辑发生在 mountComponent 函数执行的时候:
1export function mountComponent(vm: Component, el: ?Element, hydrating?: boolean): Component {
2 // ...
3 // 这里是简写
4 let updateComponent = () => {
5 vm._update(vm._render(), hydrating);
6 };
7 new Watcher(
8 vm,
9 updateComponent,
10 noop,
11 {
12 before() {
13 if (vm._isMounted) {
14 callHook(vm, 'beforeUpdate');
15 }
16 },
17 },
18 true /* isRenderWatcher */
19 );
20 // ...
21}
那么在实例化 Watcher
的过程中,在它的构造函数里会判断 isRenderWatcher
,接着把当前 watcher
的实例赋值给 vm.\_watcher
,定义在 src/core/observer/watcher.js 中:
1export default class Watcher {
2 // ...
3 constructor(vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean) {
4 this.vm = vm;
5 if (isRenderWatcher) {
6 vm._watcher = this;
7 }
8 vm._watchers.push(this);
9 // ...
10 }
11}
同时,还把当前 wathcer
实例 push
到 vm.\_watchers
中,vm.\_watcher
是专门用来监听 vm
上数据变化然后重新渲染的,所以它是一个渲染相关的 watcher
,因此在 callUpdatedHooks
函数中,只有 vm.\_watcher
的回调执行完毕后,才会执行 updated
钩子函数。
beforeDestroy & destroyed
顾名思义,beforeDestroy
和 destroyed
钩子函数的执行时机在组件销毁的阶段,组件的销毁过程之后会详细介绍,最终会调用 $destroy
方法,它的定义在 src/core/instance/lifecycle.js中:
1Vue.prototype.$destroy = function () {
2 const vm: Component = this;
3 if (vm._isBeingDestroyed) {
4 return;
5 }
6 callHook(vm, 'beforeDestroy');
7 vm._isBeingDestroyed = true;
8 // remove self from parent
9 const parent = vm.$parent;
10 if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
11 remove(parent.$children, vm);
12 }
13 // teardown watchers
14 if (vm._watcher) {
15 vm._watcher.teardown();
16 }
17 let i = vm._watchers.length;
18 while (i--) {
19 vm._watchers[i].teardown();
20 }
21 // remove reference from data ob
22 // frozen object may not have observer.
23 if (vm._data.__ob__) {
24 vm._data.__ob__.vmCount--;
25 }
26 // call the last hook...
27 vm._isDestroyed = true;
28 // invoke destroy hooks on current rendered tree
29 vm.__patch__(vm._vnode, null);
30 // fire destroyed hook
31 callHook(vm, 'destroyed');
32 // turn off all instance listeners.
33 vm.$off();
34 // remove __vue__ reference
35 if (vm.$el) {
36 vm.$el.__vue__ = null;
37 }
38 // release circular reference (#6759)
39 if (vm.$vnode) {
40 vm.$vnode.parent = null;
41 }
42};
beforeDestroy
钩子函数的执行时机是在 $destroy
函数执行最开始的地方,接着执行了一系列的销毁动作,包括从 parent
的 $children
中删掉自身,删除 watcher
,当前渲染的 VNode
执行销毁钩子函数等,执行完毕后再调用 destroy
钩子函数。在 $destroy
的执行过程中,它又会执行 vm.**patch**(vm.\_vnode, null)
触发它子组件的销毁钩子函数,这样一层层的递归调用,所以 destroy
钩子函数执行顺序是先子后父,和 mounted
过程一样。
activated & deactivated
activated
和 deactivated
钩子函数是专门为keep-alive
组件定制的钩子.
总结
Vue 生命周期中各个钩子函数的执行时机以及顺序,在 created
钩子函数中可以访问到数据,在 mounted
钩子函数中可以访问到 DOM,在 destroy
钩子函数中可以做一些定时器销毁工作,了解它们有利于我们在合适的生命周期去做不同的事情。