vue2关于初始化props源码解析

173 阅读2分钟

初始化输入属性。主要包含两个过程

  1. 规范化props。normalizeProps。
  2. 初始化props。initProps

1、规范化props。

function normalizeProps(options, vm) {
  var props = options.props;
  if (!props) { return }
  var res = {};
  var i, val, name;
  // props声明有两种方式,列表和对象形式
  if (Array.isArray(props)) {
    i = props.length;
    while (i--) {
      val = props[i];
      if (typeof val === 'string') {
        name = camelize(val);
        res[name] = { type: null };
      } else {
        warn('props must be strings when using array syntax.');
      }
    }
  } else if (isPlainObject(props)) {
    for (var key in props) {
      val = props[key];
      name = camelize(key);
      // 如果是属性不是对象,构造出一个对象,含有type属性,
      // 比如:props:{propA:Boolean} 最终转化为 props:{propA:{type:Boolean}}
      res[name] = isPlainObject(val)
        ? val
        : { type: val };
    }
  } else {
    warn(
      "Invalid value for option \"props\": expected an Array or an Object, " +
      "but got " + (toRawType(props)) + ".",
      vm
    );
  }
  options.props = res;
}

规范化props,最终转化成每个属性都是对象的大对象。便于后面的初始化。

2、初始化props值。

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);
    {
      // 判断props的属性(小驼峰或者烤串模式经过转换)是否是html等关键字,不允许是关键字
      var hyphenatedKey = hyphenate(key);
      if (isReservedAttribute(hyphenatedKey) ||
        config.isReservedAttr(hyphenatedKey)) {
        warn(
          ("\"" + hyphenatedKey + "\" is a reserved attribute and cannot be used as component prop."),
          vm
        );
      }
      // props属性的响应式注册
      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.
    // 代理vue实例的所有属性,并添加到_props
    if (!(key in vm)) {
      proxy(vm, "_props", key);
    }
  };

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

初始化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;
      }
    }
  }
  // check default value
  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
}

validateProp方法主要有两步:

  1. 通过propsData取值。
  2. 如果获取不到,则获取自身的default属性值。

那么propsData是什么?可以看如下源码:

function initInternalComponent(vm, options) {
  var opts = vm.$options = Object.create(vm.constructor.options);
  // doing this because it's faster than dynamic enumeration.
  var parentVnode = options._parentVnode;
  opts.parent = options.parent;
  opts._parentVnode = parentVnode;

  var vnodeComponentOptions = parentVnode.componentOptions;
  opts.propsData = vnodeComponentOptions.propsData;
  ...
}

propsData是父组件的属性值,这也就解释了父组件向子组件通信的机制。

言归正传,validateProps里面有些细节需要说明一下:

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

关于这一段,大致是这个意思:

1、首先判断prop是否布尔类型或者是否存在布尔类型(因为prop可以声明多种类型)
2、如果存在,进入下一个判断
3、如果父组件没有该属性,且当前组件没有默认值,把值设置为false
4、如果值为空字符串,或者值(烤串形式的值)等于当前属性,进入下一判断
5、判断该属性是否存在字符串类型,如果不存在,或者字符串类型优先级小于布尔类型优先级,把值设置为true
6、不是以上情况的,使用原值
以上第4步,空字符串的情况实际上是这种写法:

<IButton disabled></IButton>

这里的disabled只写属性的时候,就是这种情况,注意这不是directive。 ​

当获取默认值的时候,也有细节需要注意,如果默认值是一个对象,那么需要给对象做响应式处理,也就是下面源码:

...
observe(value);
...

而对每一个prop的响应式处理是在initProps方法的源码里面:

function initProps(vm, propsOptions) {
      ...
      // props属性的响应式注册
      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
          );
        }
      });
    }
    ...
}

3、props校验 其实可以在源码种看到,prop还有个校验的过程:

function validateProp(
  key,
  propOptions,
  propsData,
  vm
) {
  ...
  {
    assertProp(prop, key, value, vm, absent);
  }
  ...
}

assertProp就是校验prop,这个方法里面先根据prop.type注册校验器,最后再执行校验器,prop里面可以设置validator属性作为校验器的。

4、总结
初始化prop主要就是这些过程,有些细节可能还没理解到位,以后继续完善。