结合源码看Vue Options

711 阅读5分钟

Vue Options及其内部属性详解

在阅读源码的时候,经常被options和$options搞晕,写一篇笔记来记录Vue2.x中options的构造过程及两者之间的区别和联系。

options

  • Vue.options: 全局options
  • Sub.options: 子类构造函数的options
  • vm.$options: 实例对象的options

Vue.options

这是一个全局对象,运用了原型链继承的方式,构造子类的实例对象时,实例对象的options继承了Vue.options,Vue源码中提供了不同版本的入口,我们以runtime运行时版本为例,入口文件在项目目录src/platforms/web/entry-runtime.js下,只关注options的相关代码

构造过程 从入口开始,向上寻找Vue构造函数的初始定义,再反向查看Vue的options发生了哪些变化,下面先不关注Vue的原型链,只关注Vue的引入路线和静态属性options的变化

// 入口 1. src/platforms/web/entry-runtime.js
import Vue from './runtime/index'
export default Vue

// 2. ./runtime/index
import Vue from "core/index";
import platformDirectives from "./directives/index";
import platformComponents from "./components/index";

extend(Vue.options.directives, platformDirectives);
extend(Vue.options.components, platformComponents);

// 3. core/index
import Vue from "./instance/index";
import { initGlobalAPI } from "./global-api/index";

// 为Vue扩展全局静态方法
initGlobalAPI(Vue);

//4. ./instance/index
function Vue(options) {
  if (process.env.NODE_ENV !== "production" && !(this instanceof Vue)) {
    warn("Vue is a constructor and should be called with the `new` keyword");
  }
  this._init(options);
}

我们找到了core/instance/index中定义了Vue构造函数本身,现在来反向查看Vue静态属性发生的变化,分析一下上面的源码,可以提取三句核心代码,首先调用initGlobalAPI初始化Vue的全局API,然后调用extend扩展Vue.options.directivesVue.options.componentsextend是一个最简单的属性覆盖函数

// core/index
initGlobalAPI(Vue);

// ./runtime/index
extend(Vue.options.directives, platformDirectives);
extend(Vue.options.components, platformComponents);

initGlobalAPI

这个函数初始化了Vue的静态方法,比如Vue.useVue.extendVue.mixinVue.nextTick等……,提取options相关逻辑,global中初始化的options属性都是基本属性,与平台无关

  // ASSET_TYPES=['component','directive','filter']
  // builtInComponent={keepAlive}
  Vue.options = Object.create(null);
  ASSET_TYPES.forEach((type) => {
    Vue.options[type + "s"] = Object.create(null);
  });
 Vue.options._base = Vue;
 extend(Vue.options.components, builtInComponents);

// Vue.options = {
// 	components: { keepAlive },
//	directives: {},
//	filters: {},
//	_base: Vue,
// }

注入平台相关的指令和组件

// ./runtime/index
extend(Vue.options.directives, platformDirectives);
extend(Vue.options.components, platformComponents);

// 全局options
// Vue.options = {
// 	components: { 
//		keepAlive, 
//		Transition,
//		TransitionGroup
//	},
//	directives: {
//		modal,
//		show
//  },
//	filters: {},
//	_base: Vue,
// }

初始情况下Vue.options是一个很简单的对象,目的是为了储存全局组件、指令等,Vue提供了一些静态方法来扩展基础构造函数的options,也比较好理解,贴上源码

// global-api/assets.js
import { ASSET_TYPES } from "shared/constants";
import { isPlainObject, validateComponentName } from "../util/index";
// ASSET_TYPES=['component','directive','filter']

export function initAssetRegisters(Vue: GlobalAPI) {
  /**
   * Create asset registration methods.
   */
  ASSET_TYPES.forEach((type) => {
    Vue[type] = function (
      id: string,
      definition: Function | Object
    ): Function | Object | void {
      if (!definition) {
        return this.options[type + "s"][id];
      } else {
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== "production" && type === "component") {
          validateComponentName(id);
        }
        // 全局注册组件的方法
        if (type === "component" && isPlainObject(definition)) {
          definition.name = definition.name || id;
          // 函数内部的this指向Vue
			  // 调用Vue.extend(extendOptions)生成子类构造器
          definition = this.options._base.extend(definition);
        }
        // e.g: Vue.directive('show', show)
        if (type === "directive" && typeof definition === "function") {
          definition = { bind: definition, update: definition };
        }
        // 把扩展后的对象添加到Vue.options中
        this.options[type + "s"][id] = definition;
        return definition;
      }
    };
  });
}
验证

image.png

Sub.options

子构造函数的options其实比较好理解,(一般来说)它继承自Vue.options,源码目录core/global-api/extend,我只提取部分options相关逻辑,这个函数不复杂,就是构建了一个比较基本的继承关系,并且处理了一些全局函数,感兴趣的可以自己看一看源码

传入的options:一个对象,包含了datapropsinjectprovidewatchcomputedcomponentsdirectives等等你在开发时会用到的属性

举个🌰,我们常写这段代码,就是导出了一个options,Vue内部会调用Vue.extend,传入这个对象来生成一个构造函数

// a.vue
export default {
    name: "AComponents",
    data() {},
    props: [],
    computed: {},
    ...
}

源码如下:

// core/global-api/extend
Vue.extend = function(expandOptions) {
    extendOptions = extendOptions || {};
    const Super = this;
    const SuperId = Super.cid;
    
    // ...
    
    // 经典的继承
    const Sub = function VueComponent(options) {
      // 相当于 Vue.call(this)
      this._init(options);
    };
    Sub.prototype = Object.create(Super.prototype);
    Sub.prototype.constructor = Sub;
    
    Sub.cid = cid++;
    // 合并options
    Sub.options = mergeOptions(Super.options, extendOptions);
    Sub["super"] = Super;
    
    // 把自己也添加到options.components
    // 提供组件的递归调用功能
    if (name) {
      Sub.options.components[name] = Sub;
    }
    
    // ...
    Sub.superOptions = Super.options;
    Sub.extendOptions = extendOptions;
    Sub.sealedOptions = extend({}, Sub.options);
    
    // ...
    
    return Sub
}

mergeOptions是Vue内部一个比较重要的合并函数,它定义了一系列的合并规则strats,按照不同的策略去合并不同的参数,并返回一个新的options

// core/util/options
function mergeOptions(parent, child, vm) {
      // ...  
      //返回新的options
      const options = {};
      let key;
      for (key in parent) {
        mergeField(key);
      }
      for (key in child) {
        if (!hasOwn(parent, key)) {
          mergeField(key);
        }
      }
      
      // strats[key] key属性的合并函数
      function mergeField(key) {
        const strat = strats[key] || defaultStrat;
        options[key] = strat(parent[key], child[key], vm, key);
      }
      return options;
}

源码中为每个(或每几个)属性都定义了合并函数strat,这里提取出关于全局属性的合并代码,使用Object.create函数来实现简单的原型链继承,让子组件也能访问到全局定义的componentsdirectivesfilters

// core/util/options
// ASSET_TYPES=['component','directive','filter']
function mergeAssets(
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): Object {
  // 构造原型链
  const res = Object.create(parentVal || null);
  if (childVal) {
    process.env.NODE_ENV !== "production" &&
      assertObjectType(key, childVal, vm);
    return extend(res, childVal);
  } else {
    return res;
  }
}

ASSET_TYPES.forEach(function (type) {
  strats[type + "s"] = mergeAssets;
});
验证
// 定义另一个子组件,验证components的合并规则
const comp = {
    name:"comp"
}

// 子构造函数
const Sub = Vue.extend({
    props: ['list', 'type'],
    data() {
        return {
            a: 1
        }
    },
    computed: {
        validList() {
            return this.list.filter(item => item.visible)
        }
    },
    components: {comp},
    directives: {
        focus: {
            inserted(el){
                el.focus()
            }
        }
    }
})

打印结果如下,可以看到Sub.options.components和Sub.options.directives的原型链上都添加了全局定义的属性,propsdatacomputed等属性的合并规则这里就不贴了,感兴趣可以去阅读源码查看

image.png

vm.$options

实例对象的options:实际上由其构造函数的options以及传入的options构成

$options的初始化过程:组件的构造函数Sub在实例化时传入一个options参数,会跟父构造函数的options进行合并

// _init函数
Vue.prototype._init = function(options) {
	//...

        // vm.$options初始化
	vm.$options = mergeOptions(
            resolveConstructorOptions(vm.constructor),
            options || {},
            vm
       );
}

在实例化的过程中又调用了一次mergeOptions,来合并实例对象与构造函数的options,因为Sub.options是一个静态属性,在new Sub(options)时不会将options继承给vm,要手动调用mergeOptions来新建一个实例化对象独有的$options

resolveConstructorOptions函数会去获取当前vm构造函数的options进行合并,也就是Sub.options

但是为什么不直接这样写呢:

const Sub = vm.constructor
vm.$options = mergeOptions(
            Sub.options,
            options || {},
            vm
       );

一般开发过程中,基本不会有什么问题,但是如果你调用Vue Api去构造组件,这样写就会出问题,举个🌰:

// 先创建构造函数
const Sub = Vue.extend({
    data() {},
    props: [],
    components: {}
    ...
})

// 然后使用全局混入mixin,mixin也是调用了mergeOptions来合并对象
// Sub并不知道Vue.options改变了,所以这个directives无法应用到Sub上
Vue.mixin({
    directives: {
        ...
    }
})

// 再实例化vm,这个实例化也无法应用全局mixin的directives
const vm = new Sub()

应该很少有人写这样的代码,全局混入函数一般在入口文件就会调用,不得不佩服Vue考虑的周全性

总结

理解源码之后会发现Vue.options、Sub.options、vm.$options实际上就是一个继承链的关系,源码中的关键的函数mergeOptions使用策略模式对不同的属性进行合并,感兴趣的可以看一看完整代码,相信会对你有很大的帮助