详解 Vue 生命周期实现

93 阅读6分钟
前言在我们平时使用各种框架的时候,都避免不了使用到一种特性,就是 生命周期 钩子,这些钩子,可以给我们提供很多便利,让我们在数据更新的每一个阶段,都可以捕捉到它的变化。
我们最主要讲的是 vue 的生命周期,先来一份大纲:
  • beforeCreate(初始化界面前)
  • created(初始化界面后)
  • beforeMount(渲染dom前)
  • mounted(渲染dom后)
  • beforeUpdate(更新数据前)
  • updated(更新数据后)
  • beforeDestroy(卸载组件前)
  • destroyed(卸载组件后)
今天,我就来分析一下,vue 在调用到每一个生命周期前,到底都在做了什么?
正文来看看官方的生命周期流程图:


这张图其实已经大概的告诉了我们,每个阶段做了什么,但是我觉得还有必要详细的去分析一下,这样在未来如果我们要实现类似于 vue 这种框架的时候,可以知道在什么时间,应该去做什么,怎么去实现。
beforeCreate(初始化界面前)
[JavaScript]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
function initInternalComponent (vm, options) {
var opts = vm.$options = Object.create(vm.constructor.options);
// doing this because it's faster than dynamic enumeration.
var parentVnode = options._parentVnode;
opts.parent = options.parent;
opts._parentVnode = parentVnode;
opts._parentElm = options._parentElm;
opts._refElm = options._refElm;
var vnodeComponentOptions = parentVnode.componentOptions;
opts.propsData = vnodeComponentOptions.propsData;
opts._parentListeners = vnodeComponentOptions.listeners;
opts._renderChildren = vnodeComponentOptions.children;
opts._componentTag = vnodeComponentOptions.tag;
if (options.render) {
opts.render = options.render;
opts.staticRenderFns = options.staticRenderFns;
}
}
function resolveConstructorOptions (Ctor) {
var options = Ctor.options;
if (Ctor.super) {
var superOptions = resolveConstructorOptions(Ctor.super);
var cachedSuperOptions = Ctor.superOptions;
if (superOptions !== cachedSuperOptions) {
// super 选项已更改,需要解决新选项。
Ctor.superOptions = superOptions;
// 检查是否有任何后期修改/附加选项
var modifiedOptions = resolveModifiedOptions(Ctor);
// 更新基本扩展选项
if (modifiedOptions) {
extend(Ctor.extendOptions, modifiedOptions);
}
options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions);
if (options.name) {
options.components[options.name] = Ctor;
}
}
}
return options
}
if (options && options._isComponent) {
initInternalComponent(vm, options);
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
}
if (process.env.NODE_ENV !== 'production') {
initProxy(vm);
} else {
vm._renderProxy = vm;
}
vm._self = vm;
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, 'beforeCreate');

在一开始,先做了一个属性的合并处理,如果 options 存在并且 _isComponent 为 true ,那么就调用 initInternalComponent 方法,这个方法最主要是优化内部组件实例化,因为动态选项合并非常缓慢,并且没有内部组件选项需要特殊处理;
如果不满足上述条件,就调用 mergeOptions 方法去做属性合并,最后的返回值赋值给 $options , mergeOptions 的实现原理,在 Vue 源码解析(实例化前) - 初始化全局API(一) 这里做过详细的讲解,有不了解的朋友,可以跳转这里去看;
做一个渲染拦截,这里的拦截,最主要是为了在调用 render 方法的时候,通过 vm.$createElement 方法进行 dom 的创建;
[JavaScript]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function initLifecycle (vm) {
var options = vm.$options;
// 找到第一个非抽象父级
var parent = options.parent;
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent;
}
parent.$children.push(vm);
}
vm.$parent = parent;
vm.$root = parent ? parent.$root : vm;
vm.$children = [];
vm.$refs = {};
vm._watcher = null;
vm._inactive = null;
vm._directInactive = false;
vm._isMounted = false;
vm._isDestroyed = false;
vm._isBeingDestroyed = false;
}

初始化了一些参数;
[JavaScript]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
function initEvents (vm) {
vm._events = Object.create(null);
vm._hasHookEvent = false;
// init父级附加事件
var listeners = vm.$options._parentListeners;
if (listeners) {
updateComponentListeners(vm, listeners);
}
}
function updateComponentListeners (
vm,
listeners,
oldListeners
) {
target = vm;
updateListeners(listeners, oldListeners || {}, add, remove$1, vm);
target = undefined;
}

初始化事件,如果 _parentListeners 存在的话,更新组件的事件监听;
[JavaScript]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function initRender (vm) {
vm._vnode = null; // 子树的根
vm._staticTrees = null; // v-once缓存的树
var options = vm.$options;
var parentVnode = vm.$vnode = options._parentVnode; // 父树中的占位符节点
var renderContext = parentVnode && parentVnode.context;
vm.$slots = resolveSlots(options._renderChildren, renderContext);
vm.$scopedSlots = emptyObject;
// 将createElement fn绑定到此实例,以便我们在其中获得适当的渲染上下文。
vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };
// 规范化始终应用于公共版本,在用户编写的渲染函数中使用。
vm.$createElement = function (a, b, c, d) { return createElement(vm, a, b, c, d, true); };
// 暴露了$ attrs和$ listeners以便更容易创建HOC。
// 他们需要被动反应,以便使用它们的HOC始终更新
var parentData = parentVnode && parentVnode.data;
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, function () {
!isUpdatingChildComponent && warn("$attrs is readonly.", vm);
}, true);
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, function () {
!isUpdatingChildComponent && warn("$listeners is readonly.", vm);
}, true);
} else {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true);
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true);
}
}

初始化渲染,defineReactive 的使用和作用,在 Vue 源码解析(实例化前) - 响应式数据的实现原理 这里有讲解,大家想了解可以看一下;
到了这里执行完毕后,就调用到了 beforeCreate 方法。
created(初始化界面后)
[JavaScript]
纯文本查看
复制代码
1
2
3
4
initInjections(vm); // 在数据/道具之前解决注入
initState(vm);
initProvide(vm); // 解决后提供的数据/道具
callHook(vm, 'created');

[JavaScript]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
function resolveInject (inject, vm) {
if (inject) {
// 因为流量不足以弄清楚缓存
var result = Object.create(null);
var keys = hasSymbol
? Reflect.ownKeys(inject).filter(function (key) {
return Object.getOwnPropertyDescriptor(inject, key).enumerable
})
: Object.keys(inject);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var provideKey = inject[key].from;
var source = vm;
while (source) {
if (source._provided && hasOwn(source._provided, provideKey)) {
result[key] = source._provided[provideKey];
break
}
source = source.$parent;
}
if (!source) {
if ('default' in inject[key]) {
var provideDefault = inject[key].default;
result[key] = typeof provideDefault === 'function'
? provideDefault.call(vm)
: provideDefault;
} else if (process.env.NODE_ENV !== 'production') {
warn(("Injection \"" + key + "\" not found"), vm);
}
}
}
return result
}
}
var shouldObserve = true;
function toggleObserving (value) {
shouldObserve = value;
}
function initInjections (vm) {
var result = resolveInject(vm.$options.inject, vm);
if (result) {
toggleObserving(false);
Object.keys(result).forEach(function (key) {
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, key, result[key], function () {
warn(
"Avoid mutating an injected value directly since the changes will be " +
"overwritten whenever the provided component re-renders. " +
"injection being mutated: \"" + key + "\"",
vm
);
});
} else {
defineReactive(vm, key, result[key]);
}
});
toggleObserving(true);
}
}

在这里,其实最主要就是用来做不需要响应式的数据,官方文档:provide / inject ;
[JavaScript]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
function initState (vm) {
vm._watchers = [];
var opts = vm.$options;
if (opts.props) { initProps(vm, opts.props); }
if (opts.methods) { initMethods(vm, opts.methods); }
if (opts.data) {
initData(vm);
} else {
observe(vm._data = {}, true /* asRootData */);
}
if (opts.computed) { initComputed(vm, opts.computed); }
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}

在处理完 inject 后,紧接着就做了 props 、methods 、data 、computed 和 watch 的初始化处理;
[JavaScript]
纯文本查看
复制代码
1
2
3
4
5
6
7
8
function initProvide (vm) {
var provide = vm.$options.provide;
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide;
}
}

Provide 和 Inject 作用其实是一样的,只是处理的方式不一样,具体区别请看官方文档:provide / inject ;
到这里执行完毕后,就要走到 created 钩子了。
beforeMount(渲染dom前)
[JavaScript]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
复制代码
在渲染 dom ,先检查了是否存在渲染位置,如果不存在的话,也就不会注册了;
Vue.prototype.$mount = function (
el,
hydrating
) {
el = el && inBrowser ? query(el) : undefined;
return mountComponent(this, el, hydrating)
};
function mountComponent (
vm,
el,
hydrating
) {
vm.$el = el;
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode;
}
callHook(vm, 'beforeMount');
}
在 beforeMount 这里,基本没做什么事情,只是做了一个 render 方法如果存在就绑定一下 createEmptyVNode 函数;
绑定完毕后,就执行了 beforeMount 钩子;
mounted(渲染dom后)
[JavaScript]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var updateComponent;
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = function () {
var name = vm._name;
var id = vm._uid;
var startTag = "vue-perf-start:" + id;
var endTag = "vue-perf-end:" + id;
mark(startTag);
var 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 = function () {
vm._update(vm._render(), hydrating);
};
}
// 我们在观察者的构造函数中将其设置为vm._watcher,因为观察者的初始补丁可能会调用$ forceUpdate(例如,在子组件的挂载挂钩内),这依赖于已定义的vm._watcher
new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */);
hydrating = false;
// 手动挂载的实例,在自己挂载的调用挂载在其插入的挂钩中为渲染创建的子组件调用
if (vm.$vnode == null) {
vm._isMounted = true;
callHook(vm, 'mounted');
}

在 new Watcher 的时候,调用了 _render 方法,实现了 dom 的渲染,具体 _render 都做了什么,点击查看 vue 源码解析(实例化前) - 初始化全局 API(最终章);
在执行完实例化 Watcher 以后,如果 $node 不存在,就说明是初始化渲染,执行 mounted 钩子;
beforeUpdate(更新数据前)
[JavaScript]
纯文本查看
复制代码
1
2
3
4
5
6
7
Vue.prototype._update = function (vnode, hydrating) {
var vm = this;
if (vm._isMounted) {
callHook(vm, 'beforeUpdate');
}
};

如果当前的 vue 实例的 _isMounted 为 true 的话,直接调用 beforeUpdate 钩子;
_isMounted 在 mounted 钩子执行前就已经设置为 true 了。
执行 beforeUpdate 钩子;
updated(更新数据后)
[JavaScript]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
function callUpdatedHooks (queue) {
var i = queue.length;
while (i--) {
var watcher = queue[i];
var vm = watcher.vm;
if (vm._watcher === watcher && vm._isMounted) {
callHook(vm, 'updated');
}
}
}

因为有多个组件的时候,会有很多个 watcher ,在这里,就是检查当前的得 watcher 是哪个,是当前的话,就直接执行当前 updated 钩子。
beforeDestroy(卸载组件前)
[JavaScript]
纯文本查看
复制代码
1
2
3
4
5
6
7
Vue.prototype.$destroy = function () {
var vm = this;
if (vm._isBeingDestroyed) {
return
}
callHook(vm, 'beforeDestroy');
};

在卸载前,检查是否已经被卸载,如果已经被卸载,就直接 return 出去;
执行 beforeDestroy 钩子;
destroyed(卸载组件后)
[JavaScript]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
vm._isBeingDestroyed = true;
// 从父级那里删除自己
var parent = vm.$parent;
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
remove(parent.$children, vm);
}
// 拆解观察者
if (vm._watcher) {
vm._watcher.teardown();
}
var i = vm._watchers.length;
while (i--) {
vm._watchers[i].teardown();
}
// 从冻结对象的数据中删除引用可能没有观察者。
if (vm._data.__ob__) {
vm._data.__ob__.vmCount--;
}
// 准备执行最后一个钩子
vm._isDestroyed = true;
// 在当前渲染的树上调用destroyed hook
vm.__patch__(vm._vnode, null);
callHook(vm, 'destroyed');

其实这里就是把所有有关自己痕迹的地方,都给删除掉;
执行 destroyed 钩子。
总结到这里,其实每一个生命周期的钩子做了什么,我们已经了解的差不多了,那这样大量的代码看起来可能不是很方便,所以我们做一个总结的 list:
  • beforeCreate :初始化了部分参数,如果有相同的参数,做了参数合并,执行 beforeCreate ;
  • created :初始化了 Inject 、Provide 、 props 、methods 、data 、computed 和 watch,执行 created ;
  • beforeMount :检查是否存在 el 属性,存在的话进行渲染 dom 操作,执行 beforeMount ;
  • mounted :实例化 Watcher ,渲染 dom,执行 mounted ;
  • beforeUpdate :在渲染 dom 后,执行了 mounted 钩子后,在数据更新的时候,执行 beforeUpdate ;
  • updated :检查当前的 watcher 列表中,是否存在当前要更新数据的 watcher ,如果存在就执行 updated ;
  • beforeDestroy :检查是否已经被卸载,如果已经被卸载,就直接 return 出去,否则执行 beforeDestroy ;
  • destroyed :把所有有关自己痕迹的地方,都给删除掉;


文章转载自:https://juejin.cn/post/6844903780736040973