Vue2.x的Prop校验源码分析

141 阅读3分钟

分析一下Vue2.0版本中Prop校验相关的源码,实际源码与以下代码会有一些微小的出入

概述

  1. Vueprop选项的值,首先通过normalize操作将简化的配置改为最全的对象语法配置

  1. 然后调用initProp方法对prop选项进行初始化,其中主要有以下几个操作:


    2.1 使用validateProp方法获取prop的值并赋值到vm._prop上(具体操作请往下看)


    2.2 增加开发环境提示,包括:

    • 使用keyslot等保留属性名时给出警告
    • 子组件内直接修改prop值时给出警告

    2.3 让vm去代理vm._prop上的值


  1. validateProp中的具体操作:


    3.1 获取父组件中传递的value


    3.2 如果有定义Boolean类型,则:

    • 在没传值且未定default值时,设置为false
    • 如果传的值为''或与属性名一致,则在以下两种情况时设置值为true
      • 未同时定义String类型
      • 同时定义了StringBoolean类型,但Boolean在前

    3.3 判断valueundefined,则:

    • 使用getPropDefaultValue方法获取默认值
    • 该方法内会获取prop设置的default值,未定义则直接返回undefined
    • 如果default值为对象或数组则会给出一个'用函数来返回对象或数组'的提示
    • vm._prop上有值,则使用vm._prop上的值
    • 最后判断default值是否为函数,为函数时再判断一下是否有定义Function类型等操作
    • 注意:default为函数时可以使用this来获取vm实例上的属性

    3.4 若在开发环境规划总,则调用assertProp方法进行类型校验

    • 首先校验required设置
    • 若未定义typetypetrue则校验type通过
    • 校验type选项(校验详细规则往下看)
    • 调用validator函数并返回校验结果

    3.5 返回value


  1. type校验规则


    概述:内部使用的assertType方法获取校验结果和类型,并将类型保存到一个数组中,只有在收集到类型时才会根据校验结果是否为false来给出提示


    收集校验类型:这里用的是/^\s*function (\w+)/正则来获取typetoString()结果的\w+捕获值,比如type值为Date时,将获取到Date类型值


    注意:使用class关键字定义的类作为type的值时,虽然校验结果可能为false,但是因为其toString()结果class xxx不符合上面的正则表达式,所以收集不到校验类型,故而不会给出警告


    4.1 String|Number|Boolean|Function|Symbol|BigInt,这些类型先使用typeof校验,不行再使用intanceof


    4.2 Object,使用的是Object.prototype.toString.call(value)方式,主要是区分纯对象与Date、数组等其他内置类型


    4.3 Array,使用的是Array.isArray方法来做校验


    4.4 其他类型,使用的是instanceof操作符来做校验

1.定义Vue等准备工作

export default function Vue(options) {
  // 初始化选项
  // 在实际源码中,会对options进行一个normalize操作
  // 在这里,就不分析normalize操作了
  // 默认prop配置就是最全的那个写法
  this._init(options);
}

Vue.prototype._init = function (options) {
  const vm = this;
  vm.$options = {
    // 这是prop数据,就是父组件给子组件传递的数据
    propsData: options.propsData,
    // 这是prop配置,就是子组件上的props选项
    props: options.props,
  };
  initState(vm);
};

function initState(vm) {
  const opts = vm.$options;
  // 当定义了props选项,才会进入props初始化及校验逻辑
  if (opts.props) {
    initProps(vm, opts.props);
  }
}

2.初始化props

这一步主要是获取prop的值,并将其放在_props上,再在vm上代理_props上的属性

其中的validateProp方法是获取prop值的关键

// propsOptions为子组件上的选项配置
function initProps(vm, propsOptions) {
  // props传入数据
  const propsData = vm.$options.propsData;
  // props最终数据
  const props = (vm._props = {});
  // 遍历props选项
  for (const key in propsOptions) {
    // 用validateProp方法获取prop属性的值,这个值可能与传入数据不相同
    const value = validateProp(key, propsOptions, propsData, vm);
    // 开发环境中的报错提示
    if (process.env.NODE_ENV !== 'production') {
      // 这里是提示开发者不要使用已经被Vue内部使用的prop名
      const hyphenatedKey = hyphenate(key);
      if (['key', 'ref', 'slot', 'slot-scope', 'is'].includes(hyphenatedKey)) {
        console.warn(
          `${hyphenatedKey}是保留的属性,不能被用作组件的prop`
        );
      }
      // 这里是提示开发者不要在子组件中直接修改prop的值
      defineReactive(props, key, value, () => {
        console.warn(`避免直接修改prop值:${key}`);
      })
    } else {
      // 在生产环境中,不做上述的错误提示
      defineReactive(props, key, value);
    }
    // 这步就是让vm去代理vm._props对象
    if (!(key in vm)) {
      // 在vm上取key的值时,从vm._props上取
      proxy(vm, `_props`, key);
    }
  }
}

3.获取属性值

名称为validate,实则是在获取key属性的值,其中对布尔类型的prop做了一些特殊处理

其中通过getPropDefaultValue方法来获取prop的默认值

并通过assertProp方法对prop值的合法性做了校验与提示

// key为prop名
// propsOptions为prop选项
// propsData为传入的prop数据
// vm就是组件实例对象
function validateProp(key, propsOptions, propsData, vm) {
  // prop为prop属性在子组件上的配置
  const prop = propsOptions[key];
  // absent为true时表示没有传递prop属性值
  const absent = !hasOwn(propsData, key);
  // value为父组件传给子组件的值
  let value = propsData[key];
  // 获取Boolean类型在prop配置type中的位置
  const booleanIndex = getTypeIndex(Boolean, prop.type);
  // 如果配置了Boolean类型
  if (booleanIndex > -1) {
    if (absent && !hasOwn(prop, "default")) {
      // 未传值且未定义default值时,设置为false
      value = false;
    } else if (value === "" || value === hyphenate(key)) {
      // 传的值为空字符或与属性名相同
      const stringIndex = getTypeIndex(String, prop.type);
      if (stringIndex < 0 || booleanIndex < stringIndex) {
        // 且未定义String类型或String类型定义在Boolean之后
        // 此时设置值为true
        value = true;
      }
    }
  }
  // value仍为undefined
  if (value === undefined) {
    // 获取默认值
    value = getPropDefaultValue(vm, prop, key);
    // 对value进行监听,该方法未在此实现,故注释掉了
    // observe(value);
  }
  if (process.env.NODE_ENV !== 'production') {
    // 开发环境下,对属性值类型进行校验
    assertProp(prop, key, value, vm, absent)
  }
  return value;
}

4.获取默认值

首先判断有没有定义default,其次判断default是不是一个函数

其中要判断一下default为对象的时候要提示用函数来返回

function getPropDefaultValue(vm, prop, key) {
  if (!hasOwn(prop, 'default')) {
    // 未定义default选项则返回undefined
    return undefined;
  }
  const def = prop.default;
  if (process.env.NODE_ENV !== 'production' && isObject(def)) {
    // 开发环境下的一个提示语,此处是简化后的提示语
    console.log('类型Object或Array的默认值应当用一个函数来返回')
  }
  if (vm && vm.$options.propsData && vm.$options.propsData[key] === undefined && vm._props[key] !== undefined) {
    // 如果vm._props上有值,则使用该值
    return vm._props[key]
  }
  // 判断default是否为函数以及是否有定义Function类型
  // 若为函数且未定义Function类型,则使用call调用default函数并传入vm作为this
  // 不为函数或定义了Function类型,则返回default值
  return typeof def === 'function' && getType(prop.type) !== 'Function' ? def.call(vm) : def;
}

5.校验与提示

这个方法对prop的合法性进行了校验

首先判断了required

然后是通过assertType方法收集类型并判断是否合法

如果type定义为合法,则还需要通过validator来判断

function assertProp(prop, key, value, vm, absent) {
  // 定义了required但是没有传
  if (prop.required && absent) {
    console.log('定义了required但是没有传值')
  }
  // 值为undefined或null,且未定义required,则无需再校验
  if (value == null && !prop.required) {
    return
  }
  // 校验type设定
  let type = prop.type;
  // 如果type值未定义或者值为true,则表示通过校验
  let valid = !type || type === true;
  // 保存获取到的类型(用class关键字定义的类是不会获取到这里的)
  const expectedTypes = [];
  if (type) {
    if (!Array.isArray(type)) {
      type = [type];
    }
    // 当有一个校验结果为false时,不继续校验
    for (let i = 0; i < type.length && !valid; i++) {
      // 获取校验结果和类型
      const assertedType = assertType(value, type[i], vm);
      expectedTypes.push(assertedType.expectedType || '')
      valid = assertedType.valid
    }
  }
  // true表示有收集到类型
  const hasExpectedTypes = expectedTypes.some(t => t)
  // 如果校验不通过且有收集到类型,则给出提示语
  if (!valid && hasExpectedTypes) {
    console.log(key + '的值类型不对');
    return;
  }
  // 校验validator定义
  let validator = prop.validator;
  if (validator) {
    if (!validator(value)) {
      console.log(key + '的值未通过validator校验')
    }
  }
}

6.收集类型与判断是否合法

const simpleCheckRE = /^(String|Number|Boolean|Function|Symbol|BigInt)$/;
function assertType(value, type, vm) {
  let valid;
  // 设定的类型
  const expectedType = getType(type);
  if (simpleCheckRE.test(expectedType)) {
    // 这里是使用typeof来校验一些基本的类型
    const t = typeof value;
    valid = t === expectedType.toLowerCase();
    if (!valid && t === 'object') {
      valid = value instanceof type;
    }
  } else if (expectedType === 'Object') {
    // 定义为Object时,校验值是否为纯对象
    valid = isPlainObject(value)
  } else if (expectedType === 'Array') {
    valid = Array.isArray(value);
  } else {
    try {
      // 其他类型用instanceof来校验
      valid = value instanceof type
    } catch (error) {
      console.warn(type + "不是一个构造函数");
    }
  }

  return {
    valid,
    expectedType
  }
}