怎么给 vue 定义全局的方法?

目前一般有两种做法:一是给直接给 Vue.prototype 上添加,另外一个是通过 Vue.mixin 注册。但是为什么 prototype 和 mixin 里面的方法可以在组件访问到呢?

源码分析

我们都知道 Vue 内部很多操作都是通过虚拟节点进行的,在初始化时候会执行创建虚拟节点的操作,这个就是通过一个叫 createElement 的函数进行的,就是渲染函数的第一个参数,在 createElement 内如果发现一个节点是组件的话,会执行 createComponent 函数

export function createComponent(
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  if (isUndef(Ctor)) {
    return;
  }

  //省略其他逻辑...
  // ......
  const baseCtor = context.$options._base;
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor);
  }

  // ......
}
export function createComponent(
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  if (isUndef(Ctor)) {
    return;
  }

  //省略其他逻辑...
  // ......
  const baseCtor = context.$options._base;
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor);
  }

  // ......
}

这里是创建了一个组件的构造函数,baseCtor 是 Vue 的构造函数,其实下面执行的就是 Vue.extend()方法,这个方法是 Vue 构造函数上的一个静态方法,应该有不少小伙伴都用过这个方法,我们来看下这个方法做了什么事情

/**
 * Class inheritance
 */
Vue.extend = function (extendOptions: Object): Function {
  //省略其他逻辑...
  // ......
  const Sub = function VueComponent(options) {
    this._init(options);
  };
  Sub.prototype = Object.create(Super.prototype);
  Sub.prototype.constructor = Sub;
  Sub.cid = cid++;
  Sub.options = mergeOptions(Super.options, extendOptions);
  // ......
};
/**
 * Class inheritance
 */
Vue.extend = function (extendOptions: Object): Function {
  //省略其他逻辑...
  // ......
  const Sub = function VueComponent(options) {
    this._init(options);
  };
  Sub.prototype = Object.create(Super.prototype);
  Sub.prototype.constructor = Sub;
  Sub.cid = cid++;
  Sub.options = mergeOptions(Super.options, extendOptions);
  // ......
};

创建了一个 Sub 函数,并且继承了将 prototype 指向了 Object.create(Super.prototype),是 js 里一个典型的继承方法,要知道,最终组件的实例化是通过这个 Sub 构造函数进行的,在组件实例内访问一个属性的时候,如果本实例上没有的话,会通过原型链向上去查找,这样我们就可以在组件内部访问到 Vue 的原型。

那么 mixin 是怎么实现的呢?其实上面这段代码还有一个是 mergeOptions 的操作,这个 mergeOptions 函数做的事情是将两个 options 合并在一起,这里就不展开说了,因为里面的东西比较多。这里其实就是把 Super.options 和我们传入的 options 合并在一起,这个 Super 的 options 其实也就是 Vue 的 options,在我们使用 Vue.mixin 这个方法的时候,会把我们传入的 options 添加到 Vue.options 上

export function initMixin(Vue: GlobalAPI) {
  Vue.mixin = function (mixin: Object) {
    this.options = mergeOptions(this.options, mixin);
    return this;
  };
}
export function initMixin(Vue: GlobalAPI) {
  Vue.mixin = function (mixin: Object) {
    this.options = mergeOptions(this.options, mixin);
    return this;
  };
}

这样 Vue.options 上就会有我们添加到属性了,在 extend 的时候这个属性也会扩展到组件构造函数的 options 上,

然后在组件初始化的时候,会执行 init 方法:

Vue.prototype._init = function (options?: Object) {
  //省略其他逻辑
  // ......
  // 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
    );
  }
  // ......
};
Vue.prototype._init = function (options?: Object) {
  //省略其他逻辑
  // ......
  // 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
    );
  }
  // ......
};

里面判断到是组件时会执行 initInternalComponent 这个方法

export function initInternalComponent(
  vm: Component,
  options: InternalComponentOptions
) {
  const opts = (vm.$options = Object.create(vm.constructor.options));
  // doing this because it's faster than dynamic enumeration.
  const parentVnode = options._parentVnode;
  opts.parent = options.parent; // 父Vnode,activeInstance
  opts._parentVnode = parentVnode; // 占位符Vnode
  opts._parentElm = options._parentElm;
  opts._refElm = options._refElm;

  const vnodeComponentOptions = parentVnode.componentOptions; // componentOptions是createComponent时候传入new Vnode()的
  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;
  }
}
export function initInternalComponent(
  vm: Component,
  options: InternalComponentOptions
) {
  const opts = (vm.$options = Object.create(vm.constructor.options));
  // doing this because it's faster than dynamic enumeration.
  const parentVnode = options._parentVnode;
  opts.parent = options.parent; // 父Vnode,activeInstance
  opts._parentVnode = parentVnode; // 占位符Vnode
  opts._parentElm = options._parentElm;
  opts._refElm = options._refElm;

  const vnodeComponentOptions = parentVnode.componentOptions; // componentOptions是createComponent时候传入new Vnode()的
  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;
  }
}

这里又通过 const opts = vm.$options = Object.create(vm.constructor.options) 将组件构造函数的 options 赋值给了 vm.$options,这里的 vm.constructor.options 就是刚才和 Vue.options 合并后的组件构造函数上的 options,这样我们就在组件内部拿到了 Vue.mixin 定义的方法