分析一下Vue2.0版本中Prop校验相关的源码,实际源码与以下代码会有一些微小的出入
概述
Vue
的prop
选项的值,首先通过normalize
操作将简化的配置改为最全的对象语法配置
-
然后调用
initProp
方法对prop
选项进行初始化,其中主要有以下几个操作:
2.1 使用
validateProp
方法获取prop
的值并赋值到vm._prop
上(具体操作请往下看)
2.2 增加开发环境提示,包括:
- 使用
key
、slot
等保留属性名时给出警告 - 子组件内直接修改prop值时给出警告
2.3 让
vm
去代理vm._prop
上的值 - 使用
-
validateProp
中的具体操作:
3.1 获取父组件中传递的
value
值
3.2 如果有定义
Boolean
类型,则:- 在没传值且未定
default
值时,设置为false
- 如果传的值为
''
或与属性名一致,则在以下两种情况时设置值为true
- 未同时定义
String
类型 - 同时定义了
String
和Boolean
类型,但Boolean
在前
- 未同时定义
3.3 判断
value
为undefined
,则:- 使用
getPropDefaultValue
方法获取默认值 - 该方法内会获取
prop
设置的default
值,未定义则直接返回undefined
- 如果
default
值为对象或数组则会给出一个'用函数来返回对象或数组'的提示 - 若
vm._prop
上有值,则使用vm._prop
上的值 - 最后判断
default
值是否为函数,为函数时再判断一下是否有定义Function
类型等操作 - 注意:
default
为函数时可以使用this
来获取vm
实例上的属性
3.4 若在开发环境规划总,则调用
assertProp
方法进行类型校验- 首先校验
required
设置 - 若未定义
type
或type
为true
则校验type通过 - 校验type选项(校验详细规则往下看)
- 调用
validator
函数并返回校验结果
3.5 返回
value
值 - 在没传值且未定
-
type
校验规则
概述:内部使用的
assertType
方法获取校验结果和类型,并将类型保存到一个数组中,只有在收集到类型时才会根据校验结果是否为false
来给出提示
收集校验类型:这里用的是
/^\s*function (\w+)/
正则来获取type
的toString()
结果的\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
}
}