前言
在我们平时使用各种框架的时候, 都避免不了使用到一种特性, 就是 生命周期 钩子, 这些钩子, 可以给我们提供很多便利, 让我们在数据更新的每一个阶段, 都可以捕捉到它的变化.
我们最主要讲的是 vue 的生命周期, 先来一份大纲:
- beforeCreate(初始化界面前)
- created(初始化界面后)
- beforeMount(渲染 dom 前)
- mounted(渲染 dom 后)
- beforeUpdate(更新数据前)
- updated(更新数据后)
- beforeDestroy(卸载组件前)
- destroyed(卸载组件后)
今天, 我就来分析一下, vue 在调用到每一个生命周期前, 到底都在做了什么?
正文
来看看官方的生命周期流程图:
这张图其实已经大概的告诉了我们, 每个阶段做了什么, 但是我觉得还有必要详细的去分析一下, 这样在未来如果我们要实现类似于 vue 这种框架的时候, 可以知道在什么时间, 应该去做什么, 怎么去实现.
- beforeCreate(初始化界面前)
- 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 的创建;
- 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;
- }
初始化了一些参数;
- 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 存在的话, 更新组件的事件监听;
- 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(初始化界面后)
- initInjections(vm); // 在数据 / 道具之前解决注入
- initState(vm);
- initProvide(vm); // 解决后提供的数据 / 道具
- callHook(vm, 'created');
- 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 https://cn.vuejs.org/v2/api/#provide-inject ;
- 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 的初始化处理;
- function initProvide (vm) {
- var provide = vm.$options.provide;
- if (provide) {
- vm._provided = typeof provide === 'function'
- ? provide.call(vm)
- : provide;
- }
- }
Provide 和 Inject 作用其实是一样的, 只是处理的方式不一样, 具体区别请看官方文档: provide / inject https://cn.vuejs.org/v2/api/#provide-inject ;
到这里执行完毕后, 就要走到 created 钩子了.
- beforeMount(渲染 dom 前)
- 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 后)
- 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(更新数据前)
- Vue.prototype._update = function (vnode, hydrating) {
- var vm = this;
- if (vm._isMounted) {
- callHook(vm, 'beforeUpdate');
- }
- };
如果当前的 vue 实例的 _isMounted 为 true 的话, 直接调用 beforeUpdate 钩子;
_isMounted 在 mounted 钩子执行前就已经设置为 true 了.
执行 beforeUpdate 钩子;
- updated(更新数据后)
- 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(卸载组件前)
- Vue.prototype.$destroy = function () {
- var vm = this;
- if (vm._isBeingDestroyed) {
- return
- }
- callHook(vm, 'beforeDestroy');
- };
在卸载前, 检查是否已经被卸载, 如果已经被卸载, 就直接 return 出去;
执行 beforeDestroy 钩子;
- destroyed(卸载组件后)
- 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 : 把所有有关自己痕迹的地方, 都给删除掉;
结束语
Vue 生命周期实现, 就先讲到这里了, 里面有些地方, 细节讲的不是很多, 因为这个文章和之前的源码解析方向和目的不一样, 源码讲解的目的是为了让大家一步一步的去了解, 都写了什么, 而这篇文章的目的是为了让大家了解到每个生命周期的阶段, 都做了什么.
如果大家有觉得有问题的地方, 或者写的不好的地方, 还请直接下方评论指出, 谢谢了.
来源: https://juejin.im/post/5c6d48e36fb9a049eb3c84ff