Skip to content

合并选项

入口分析

mergeOptions源码位于:src/core/util/options.ts

调用入口: src/core/instance/init.tsVue.prototype._init

ts
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 as any);
} else {
  vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor as any),
    options || {},
    vm
  );
}

Vue.prototype._init中判断了new Vue的时候传入了选项options并且有options._isComponent属性,才会进入初始化内置组件的过程, 正常初始化过程不会有这个属性, 因此进入了else, 进入选项合并

选项合并后, 会将合并后的选项挂到实例的$options身上(vm.$options), 此后可以在其他地方通过vm.$options拿到

解析、合并构造函数选项

源码如下:

ts
/* 解析构造函数选项 */
function resolveConstructorOptions(Ctor: typeof Component) {
  let options = Ctor.options;
  if (Ctor.super) {
    const superOptions = resolveConstructorOptions(Ctor.super);
    const cachedSuperOptions = Ctor.superOptions;
    if (superOptions !== cachedSuperOptions) {
      // super option changed,
      // need to resolve new options.
      Ctor.superOptions = superOptions;
      // check if there are any late-modified/attached options (#4976)
      const modifiedOptions = resolveModifiedOptions(Ctor);
      // update base extend options
      if (modifiedOptions) {
        extend(Ctor.extendOptions, modifiedOptions);
      }
      options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions);
      if (options.name) {
        options.components[options.name] = Ctor;
      }
    }
  }
  return options;
}

function resolveModifiedOptions(
  Ctor: typeof Component
): Record<string, any> | null {
  let modified;
  const latest = Ctor.options;
  const sealed = Ctor.sealedOptions;
  for (const key in latest) {
    if (latest[key] !== sealed[key]) {
      if (!modified) modified = {};
      modified[key] = latest[key];
    }
  }
  return modified;
}

resolveConstructorOptions方法入参为Constructor, 最终会返回构造函数身上的选项options;

  • 首先,判断当前构造函数是否有继承的父类,没有则直接返回当前构造函数的选项
  • 有继承的负类,则递归解析合并选项, 因为有可能在业务中出现Vue.extend的用法
  • 递归解析后得到一个最终的选项,然后用当前构造函数的选项与递归后的选项进行比较,若不同则以递归合并的选项为准进行覆盖原有的选项
  • 接着有检查选项是否有选项被修改,代码为resolveModifiedOptions方法,若有选项被后期修改,resolveModifiedOptions方法返回一个修改后的选项, 否则返回undefined
  • 然后就是将更改的选项挂到构造函数的extendOptions身上
  • 最后将更改后的选项与最终继承的选项进行合并,用合并后的选项覆盖构造函数的options,且构造函数的options返回
  • 载返回options之前,还会判断选项中的name属性, 有name属性则构造函数缓存到选项中components对象中,以name作为 key

mergeOptions

mergeOptions选项合并的代码位于src/core/util/options.ts

源码如下:

ts
/**
 * 选项覆盖策略是处理如何将父选项值和子选项值合并为最终值的函数。
 */
const strats = config.optionMergeStrategies // Object.create(null)

/**
 * Default strategy.
 */
const defaultStrat = function (parentVal: any, childVal: any): any {
  return childVal === undefined ? parentVal : childVal
}

/**
 * Merge two option objects into a new one.
 * Core utility used in both instantiation and inheritance.
 */
export function mergeOptions(
  parent: Record<string, any>,
  child: Record<string, any>,
  vm?: Component | null
): ComponentOptions {
  if (__DEV__) {
    checkComponents(child);
  }

  if (isFunction(child)) {
    // @ts-expect-error
    child = child.options;
  }

  // 格式化数据
  normalizeProps(child, vm); 
  normalizeInject(child, vm);
  normalizeDirectives(child);

  // Apply extends and mixins on the child options,
  // but only if it is a raw options object that isn't
  // the result of another mergeOptions call.
  // Only merged options has the _base property.
  if (!child._base) { // 若不是合并后的选项
    if (child.extends) { // 若是有extends属性, 则递归合处理得到父级选项
      parent = mergeOptions(parent, child.extends, vm)
    }
    if (child.mixins) { // 有混入mixins属性, 也是递归处理得到父级选项
      for (let i = 0, l = child.mixins.length; i < l; i++) {
        parent = mergeOptions(parent, child.mixins[i], vm)
      }
    }
  }

  const options: ComponentOptions = {} as any;
  let key;
  for (key in parent) {
    mergeField(key);
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key);
    }
  }
  function mergeField(key: any) {
    const strat = strats[key] || defaultStrat;
    options[key] = strat(parent[key], child[key], vm, key);
  }
  return options;
}

mergeOptions 接收到的参数依次为 合并后的构造函数选项, 用户传入的选项, vue实例

方法先判断用户传入的选项是否为函数, 为函数会取其身上的 options, 做了一个参数兼容。

格式化用户传入的选项

接着对传入从选项进行 “格式化”,具体为:

ts
normalizeProps(child, vm); 
normalizeInject(child, vm);
normalizeDirectives(child);

三个方法的位置都在src/core/util/options.ts中, 源码如下:

ts
/**
 * Ensure all props option syntax are normalized into the Object-based format.
 */
function normalizeProps(options: Record<string, any>, vm?: Component | null) {
  const props = options.props
  if (!props) return
  const res: Record<string, any> = {}
  let i, val, name
  if (isArray(props)) {
    i = props.length
    while (i--) {
      val = props[i]
      if (typeof val === 'string') {
        name = camelize(val) // 将 hello-world 转换成 helloWorld
        res[name] = { type: null }
      } else if (__DEV__) {
        warn('props must be strings when using array syntax.')
      }
    }
  } else if (isPlainObject(props)) {
    for (const key in props) {
      val = props[key]
      name = camelize(key)
      res[name] = isPlainObject(val) ? val : { type: val }
    }
  } else if (__DEV__) {
    warn(
      `Invalid value for option "props": expected an Array or an Object, ` +
        `but got ${toRawType(props)}.`,
      vm
    )
  }
  options.props = res
}
ts
/**
 * Normalize all injections into Object-based format
 */
function normalizeInject(options: Record<string, any>, vm?: Component | null) {
  const inject = options.inject
  if (!inject) return
  const normalized: Record<string, any> = (options.inject = {})
  if (isArray(inject)) {
    for (let i = 0; i < inject.length; i++) {
      normalized[inject[i]] = { from: inject[i] }
    }
  } else if (isPlainObject(inject)) {
    for (const key in inject) {
      const val = inject[key]
      normalized[key] = isPlainObject(val)
        ? extend({ from: key }, val)
        : { from: val }
    }
  } else if (__DEV__) {
    warn(
      `Invalid value for option "inject": expected an Array or an Object, ` +
        `but got ${toRawType(inject)}.`,
      vm
    )
  }
}
ts
/**
 * Normalize raw function directives into object format.
 */
function normalizeDirectives(options: Record<string, any>) {
  const dirs = options.directives
  if (dirs) {
    for (const key in dirs) {
      const def = dirs[key]
      if (isFunction(def)) {
        dirs[key] = { bind: def, update: def }
      }
    }
  }
}
js
// 缓存后的函数
const camelize = cached((str) => {
  return str.replace(/-(\w)/g, (_, c) => (c ? c.toUpperCase() : ""));
});

// 缓存函数
function cached(fn) {
  const cache = Object.create(null);
  return function cachedFn(str) {
    const hit = cache[str];
    return hit || (cache[str] = fn(str));
  };
}

console.log(camelize("hello-word")); // helloWord

normalizeProps

normalizeProps 作用是格式化props, 统一将props格式化成为key-value对象的格式, 并覆盖options.props 例如:

js
/* ---------例 1------------- */
props:['title', 'isShow']

/*****转换成 ******/

options.props = {
  title:{type:null, },
  isShow: { type: null}
}

/* ---------例 2------------ */

props:{
  dataType: Number,
  name:String
}

/*****转换成 ******/

options.props = {
  dataType: {
    type: Number
  },
  name:{
    type: String
  }
}

这里面用到一个优化小技巧: 记忆函数, 在将中横线连接的props名称转换为小驼峰props的时候,运用闭包的好处,做了缓存,可以提高性能, 原理如下:

js
// 缓存后的函数
const camelize = cached((str) => {
  return str.replace(/-(\w)/g, (_, c) => (c ? c.toUpperCase() : ""));
});

// 缓存函数
function cached(fn) {
  const cache = Object.create(null);
  return function cachedFn(str) {
    const hit = cache[str];
    return hit || (cache[str] = fn(str));
  };
}

console.log(camelize("hello-word")); // helloWord

normalizeInject

normalizeInject的作用也是格式化inject, 将所有inject标准化为基于对象的格式,并覆盖options.inject, 例如:

js
/* ----------------例 1---------------- */
inject: ['xxxx']
/* 转换为 */
options.inject = {
  xxxxx:{
    from: 'xxxxx'
  }
}

/* ----------------例 2---------------- */
inject: {foo:'xxx'}
/* 转换为 */
options.inject = {
  foo: {
    from:'xxx',
  }
}

这里面用到一个连等赋值的技巧, 原理如下:

js
let obj = {};

let obj2 = (obj.option = {});

obj2.k = { hello: "world" };

console.log(obj); // obj ==> { option: { k: { hello: 'world' } } }

normalizeDirectives

normalizeDirectives作用是格式化指令directives,将原始函数指令规范化为对象格式。并覆盖options.directives, 例如:

js
/* ----------例 1------------ */
directive: {
  pin(el, binding, vnode) {
     el.style.position = 'fixed'
    var s = (binding.arg == 'left' ? 'left' : 'top')
    el.style[s] = binding.value + 'px'
  }
}

/* 转换为 */
options.directive = {
  bind: pin(el, binding, vnode) {
     el.style.position = 'fixed'
    var s = (binding.arg == 'left' ? 'left' : 'top')
    el.style[s] = binding.value + 'px'
  },
  update: pin(el, binding, vnode) {
     el.style.position = 'fixed'
    var s = (binding.arg == 'left' ? 'left' : 'top')
    el.style[s] = binding.value + 'px'
  }
}

最终的选项合并

选项合并策略的最后, 会看当前选项是否是合并过的, 若是未合并过的, 则会将将extends,mixins身上的选项也合入父级,在进入真正的选项合并 核心逻辑就是优先取传入的options, 最后返回合并后的选项

ts
const defaultStrat = function (parentVal: any, childVal: any): any {
  return childVal === undefined ? parentVal : childVal
}

至此, 选项合并结束

总结

Vue选项合并个过程, 先解析构造函数上的选项, 将所有父构造函数的选项取到一起, 再格式化传入的选项, 包括props, inject, directive得到当前传入选项, 接着先合并父级选项, 在合并父级与当前选项, 以当前选项优先级最高的原则进行合并, 最终返回合并后的选项options,且挂在vm.$options上面