# 问题

在 vue 组件中,对于 prop 多类型的写法 (opens new window)是:

export default {
  props: {
    text: {
      type: [String, Number],
      default: ''
    }
  }
}

最近看到一种写法:

export default {
  props: {
    text: {
      type: String | Number,
      default: ''
    }
  }
}

当以上面的写法进行 prop 类型校验时,竟然没有报错。

# 定位目标源码

在 main.js 替换 vue 引入为:import Vue from '../node_modules/vue/dist/vue.common.dev.js'

当 vue 实例化的时候,会调用_init方法,该方法在initMixin中挂载到 Vue 原型上;在执行_init 方法时会调用initState方法,该方法主要对 vue 对象中的 props、data、methods等进行初始化处理;在initState方法中有调用initProps方法。

# 分析 initProps 源码

主要源码如下:

function initProps(vm, propsOptions) { 
  var propsData = vm.$options.propsData || {};
  
  var props = vm._props = {};
  // cache prop keys so that future props updates can iterate using Array
  // instead of dynamic object key enumeration.
  var keys = vm.$options._propKeys = [];
  var isRoot = !vm.$parent;
  // root instance props should be converted
  if (!isRoot) {
    toggleObserving(false);
  }
  var loop = function ( key ) {
    keys.push(key);
    var value = validateProp(key, propsOptions, propsData, vm);
    /* istanbul ignore else */
    {
      var hyphenatedKey = hyphenate(key);
      if (isReservedAttribute(hyphenatedKey) ||
          config.isReservedAttr(hyphenatedKey)) {
        warn(
          ("\"" + hyphenatedKey + "\" is a reserved attribute and cannot be used as component prop."),
          vm
        );
      }
      defineReactive$$1(props, key, value, function () {
        if (!isRoot && !isUpdatingChildComponent) {
          warn(
            "Avoid mutating a prop directly since the value will be " +
            "overwritten whenever the parent component re-renders. " +
            "Instead, use a data or computed property based on the prop's " +
            "value. Prop being mutated: \"" + key + "\"",
            vm
          );
        }
      });
    }
    // static props are already proxied on the component's prototype
    // during Vue.extend(). We only need to proxy props defined at
    // instantiation here.
    if (!(key in vm)) {
      proxy(vm, "_props", key);
    }
  };

  for (var key in propsOptions) loop( key );
  toggleObserving(true);
}

上面代码主要执行:

  • 获取当前 vue 实例的 props 属性;
  • 判断是否为根实例节点,是的话则调用toggleObserving将 shouldObserve 设置为 false,否则设置为 true;
  • 遍历 props 属性
    • 调用validateProp进行 props 属性类型校验;
    • 将每个 props 属性设置为响应式。

# 分析 validateProp 源码

主要源码如下:

function validateProp (
  key,
  propOptions,
  propsData,
  vm
) {
  var prop = propOptions[key];
  var absent = !hasOwn(propsData, key);
  var value = propsData[key];
  // 布尔值判断
  var booleanIndex = getTypeIndex(Boolean, prop.type);
  if (booleanIndex > -1) {
    if (absent && !hasOwn(prop, 'default')) {
      value = false;
    } else if (value === '' || value === hyphenate(key)) {
      // only cast empty string / same name to boolean if
      // boolean has higher priority
      var stringIndex = getTypeIndex(String, prop.type);
      if (stringIndex < 0 || booleanIndex < stringIndex) {
        value = true;
      }
    }
  }
  // 检验默认值
  if (value === undefined) {
    value = getPropDefaultValue(vm, prop, key);
    // since the default value is a fresh copy,
    // make sure to observe it.
    var prevShouldObserve = shouldObserve;
    toggleObserving(true);
    observe(value);
    toggleObserving(prevShouldObserve);
  }
  {
    assertProp(prop, key, value, vm, absent);
  }
  return value
}

上面代码主要执行:

  • 判断 type 是否为布尔值,是的话做特殊处理;
  • 检验值。

回到最开始的例子,调试输出validateProp方法中的 propOptions,会得到:

{
	text: {
		type: 0
	}
}

可以看到,当前 prop 的 type 已经变成 0 了。

回到validateProp函数中,当 type 为 0 的时候,代码逻辑不会进行布尔值校验,当前的值不为 undefined,自然也不会进行值的校验。最后,进入assertProp函数。

# 分析 assertProp 源码

主要源码如下:

function assertProp (
  prop,
  name,
  value,
  vm,
  absent
) {
  if (prop.required && absent) {
    warn(
      'Missing required prop: "' + name + '"',
      vm
    );
    return
  }
  if (value == null && !prop.required) {
    return
  }
  var type = prop.type;
  var valid = !type || type === true;
  var expectedTypes = [];
  if (type) {
    if (!Array.isArray(type)) {
      type = [type];
    }
    for (var i = 0; i < type.length && !valid; i++) {
      var assertedType = assertType(value, type[i], vm);
      expectedTypes.push(assertedType.expectedType || '');
      valid = assertedType.valid;
    }
  }

  var haveExpectedTypes = expectedTypes.some(function (t) { return t; });
  if (!valid && haveExpectedTypes) {
    warn(
      getInvalidTypeMessage(name, value, expectedTypes),
      vm
    );
    return
  }
  var validator = prop.validator;
  if (validator) {
    if (!validator(value)) {
      warn(
        'Invalid prop: custom validator check failed for prop "' + name + '".',
        vm
      );
    }
  }
}

该方法主要用于校验 prop 的值是否是正确的类型。

该方法对获取当前 prop 的类型列表进行遍历,然后判断是否为预定义的期望类型,获取 expectedTypes 列表;接着对 type 进行valid = !type || type === true;处理,通过 valid 和 expectedTypes 对当前类型进行校验判断。

回到上面的例子,前面调试知道 type 为 0。那么在 assertProp 方法中,所有的校验都不会执行。

其实,造成 type 变成 0 是因为:String | Number二进制位或进行赋值运算 (opens new window)

The operator ToInt32 converts its argument to one of 232 integer values in the range -231 through 231-1, inclusive. This operator functions as follows:

  1. Call ToNumber (opens new window) on the input argument.
  2. If Result(1) is NaN, +0, -0, +∞, or -∞, return +0.

按位或会将操作数转为 32 位带符号整型二进制序列(小数部分会去掉)进行按位或计算,返回十进制的数值。

转化 32 位带符号整型二进制序列逻辑为:

  • 数值调用Number转化
  • 字符串和 undefined 转为 NaN
  • null 和 false 转为 0
  • true 转为 1

回到上面的例子,String | Number转化为 32 位带符号整型二进制序列都为 NaN,根据 2 可知返回结果为 0。