详解 Vue 生命周期实现

栏目: JavaScript · 发布时间: 6年前

内容简介:在我们平时使用各种框架的时候,都避免不了使用到一种特性,就是我们最主要讲的是今天,我就来分析一下,

在我们平时使用各种框架的时候,都避免不了使用到一种特性,就是 生命周期 钩子,这些钩子,可以给我们提供很多便利,让我们在数据更新的每一个阶段,都可以捕捉到它的变化。

我们最主要讲的是 vue 的生命周期,先来一份大纲:

  • beforeCreate(初始化界面前)
  • created(初始化界面后)
  • beforeMount(渲染dom前)
  • mounted(渲染dom后)
  • beforeUpdate(更新数据前)
  • updated(更新数据后)
  • beforeDestroy(卸载组件前)
  • destroyed(卸载组件后)

今天,我就来分析一下, vue 在调用到每一个生命周期前,到底都在做了什么?

正文

来看看官方的生命周期流程图:

详解 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 存在并且 _isComponenttrue ,那么就调用 initInternalComponent 方法,这个方法最主要是优化内部组件实例化,因为动态选项合并非常缓慢,并且没有内部组件选项需要特殊处理;

如果不满足上述条件,就调用 mergeOptions 方法去做属性合并,最后的返回值赋值给 $optionsmergeOptions 的实现原理,在 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 ;

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 后,紧接着就做了 propsmethodsdatacomputedwatch 的初始化处理;

function initProvide (vm) {
  var provide = vm.$options.provide;
  if (provide) {
    vm._provided = typeof provide === 'function'
      ? provide.call(vm)
      : provide;
  }
}
复制代码

ProvideInject 作用其实是一样的,只是处理的方式不一样,具体区别请看官方文档: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 实例的 _isMountedtrue 的话,直接调用 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 :初始化了 InjectProvidepropsmethodsdatacomputedwatch ,执行 created
  • beforeMount :检查是否存在 el 属性,存在的话进行渲染 dom 操作,执行 beforeMount
  • mounted :实例化 Watcher ,渲染 dom ,执行 mounted
  • beforeUpdate :在渲染 dom 后,执行了 mounted 钩子后,在数据更新的时候,执行 beforeUpdate
  • updated :检查当前的 watcher 列表中,是否存在当前要更新数据的 watcher ,如果存在就执行 updated
  • beforeDestroy :检查是否已经被卸载,如果已经被卸载,就直接 return 出去,否则执行 beforeDestroy
  • destroyed :把所有有关自己痕迹的地方,都给删除掉;

结束语

Vue 生命周期实现,就先讲到这里了,里面有些地方,细节讲的不是很多,因为这个文章和之前的源码解析方向和目的不一样,源码讲解的目的是为了让大家一步一步的去了解,都写了什么,而这篇文章的目的是为了让大家了解到每个生命周期的阶段,都做了什么。

如果大家有觉得有问题的地方,或者写的不好的地方,还请直接下方评论指出,谢谢了。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

CSS3 Solutions

CSS3 Solutions

Marco Casario / Apress / 2012-8-13 / GBP 35.50

CSS3 brings a mass of changes, additions, and improvements to CSS across a range of new modules. Web designers and developers now have a whole host of new techniques up their sleeves, from working wit......一起来看看 《CSS3 Solutions》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

MD5 加密
MD5 加密

MD5 加密工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具