Vue.js源码学习之Vue的初始化

257 阅读2分钟

vue.js源码:cdn.jsdelivr.net/npm/vue/dis…

new Vue()到底做了什么?

function Vue (options) {
    if (!(this instanceof Vue)
    ) {
      warn('Vue is a constructor and should be called with the `new` keyword');
    }
    this._init(options);
}

new Vue()实际上就是实例化构造函数Vue,整个构造函数入口只有一个this._init(options)。该方法是在initMixin函数中定义:

 function initMixin (Vue) {
    Vue.prototype._init = function (options) {
      var vm = this;
      // a uid
      vm._uid = uid$3++;

      var startTag, endTag;
      /* istanbul ignore if */
      if (config.performance && mark) {
        startTag = "vue-perf-start:" + (vm._uid);
        endTag = "vue-perf-end:" + (vm._uid);
        mark(startTag);
      }

      // a flag to avoid this being observed
      vm._isVue = true;
      // merge options
      if (options && options._isComponent) {
        // optimize internal component instantiation
        // since dynamic options merging is pretty slow, and none of the
        // internal component options needs special treatment.
        initInternalComponent(vm, options);
      } else {
        vm.$options = mergeOptions(
          resolveConstructorOptions(vm.constructor),
          options || {},
          vm
        );
      }
      /* istanbul ignore else */
      {
        initProxy(vm);
      }
      // expose real self
      vm._self = vm;
      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');

      /* istanbul ignore if */
      if (config.performance && mark) {
        vm._name = formatComponentName(vm, false);
        mark(endTag);
        measure(("vue " + (vm._name) + " init"), startTag, endTag);
      }

      if (vm.$options.el) {
        vm.$mount(vm.$options.el);
      }
    };
  }

Vue的初始化就为了完成几件事情:initLifecycle初始化生命周期,initEvents初始化事件,initRender初始化渲染,初始化状态(data,props,computed,watcher)等等。这里需要注意一下初始化的顺序。initState(vm)方法调用是在 callHook(vm, 'beforeCreate')之后,也就是说生命周期钩子函数beforeCreate调用之前,Vue组件中的data,props等数据还没有初始化,这也是为什么我们说beforeCreate生命周期中是无法访问组件数据的原因。

接着我们按照初始化的顺序分别看下对应的源代码:

initLifecycle(vm)

function initLifecycle (vm) {
    var options = vm.$options;

    // locate first non-abstract parent
    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;
  }

initLifecycle()函数就是将Vue的生命周期状态恢复到初始化状态。

initEvents(vm)

function initEvents (vm) {
    vm._events = Object.create(null);
    vm._hasHookEvent = false;
    // init parent attached events
    var listeners = vm.$options._parentListeners;
    if (listeners) {
      updateComponentListeners(vm, listeners);
    }
}
function updateComponentListeners (
    vm,
    listeners,
    oldListeners
  ) {
    target = vm;
    updateListeners(listeners, oldListeners || {}, add, remove$1, createOnceHandler, vm);
    target = undefined;
  }

初始化事件的时候创建了一个null对象,如果配置项中存在listeners,则更新组件监听。

initRender(vm)

function initRender (vm) {
    vm._vnode = null; // the root of the child tree
    vm._staticTrees = null; // v-once cached trees
    var options = vm.$options;
    var parentVnode = vm.$vnode = options._parentVnode; // the placeholder node in parent tree
    var renderContext = parentVnode && parentVnode.context;
    vm.$slots = resolveSlots(options._renderChildren, renderContext);
    vm.$scopedSlots = emptyObject;
    
    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 are exposed for easier HOC creation.
    // they need to be reactive so that HOCs using them are always updated
    var parentData = parentVnode && parentVnode.data;

    /* istanbul ignore else */
    {
      defineReactive$$1(vm, '$attrs', parentData && parentData.attrs || emptyObject, function () {
        !isUpdatingChildComponent && warn("$attrs is readonly.", vm);
      }, true);
      defineReactive$$1(vm, '$listeners', options._parentListeners || emptyObject, function () {
        !isUpdatingChildComponent && warn("$listeners is readonly.", vm);
      }, true);
    }
  }

vm.$createElement实际上就是createElement()方法,返回的是vnode,它是一个虚拟的Node。

callHook(vm, hook)

在使用Vue.js中生命周期是非常高频的调用,所以callHook是非常重要的一个方法。

 function callHook (vm, hook) {
    // #7573 disable dep collection when invoking lifecycle hooks
    pushTarget();
    var handlers = vm.$options[hook];
    var info = hook + " hook";
    if (handlers) {
      for (var i = 0, j = handlers.length; i < j; i++) {
        invokeWithErrorHandling(handlers[i], vm, null, vm, info);
      }
    }
    if (vm._hasHookEvent) {
      vm.$emit('hook:' + hook);
    }
    popTarget();
  }

这个方法中的一头一尾分别是pushTarget()popTarget(),它们的作用分别是将当前对象推入和推出到订阅对象Dep中。callHook()方法主要作用就是通过$emit调用对应的钩子函数回调方法。

initInjections(vm)

  function initInjections (vm) {
    var result = resolveInject(vm.$options.inject, vm);
    if (result) {
      toggleObserving(false);
      Object.keys(result).forEach(function (key) {
        /* istanbul ignore else */
        {
          defineReactive$$1(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
            );
          });
        }
      });
      toggleObserving(true);
    }
  }

provide 和 inject 主要在开发高阶插件/组件库时使用,以允许祖先组件向其所有子孙组件中注入依赖,无论组件层次有多深,并在起上下游关系成立的时间里始终生效。从源码上看,刻意执行了toggleObserving(false),所以inject绑定的数据不一定是响应式的。

initState(vm)

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);
    }
  }

initState(vm)的作用是初始化,从代码中可以看到我们在定义vue组件时常用到的关键字props,data,methods,computed,watch。这里会分别对对应的属性进行初始化。

initProvide(vm)

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

initProvide(vm)initInections(vm)是配套使用的,一个是注入依赖,一个是调用依赖。所以组件中provide应该是一个对象或者是一个返回对象的函数。

执行到这,vue实例初始化就完成了,最后就执行了callHook(vm, 'created'),所以我们可以在组件的生命周期钩子函数created()访问到组件数据,执行组件方法,比如异步请求等等。