前面章节通过初始化data选项的initData函数学习了 Vue 的响应式系统。
下面继续学习props选项的初始化。
props的初始化#
回到initState函数,查看初始化props选项的代码,在core/instance/state.js文件中。
if (opts.props) initProps(vm, opts.props);通过调用initProps函数初始化props选项,还需要传入实例对象vm和合并选项后的props属性。
下面看下initProps函数的代码。
function initProps(vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {}; // ! 获取 props 数据来源
const props = (vm._props = {});
// cache prop keys so that future props updates can iterate using Array
// instead of dynamic object key enumeration.
const keys = (vm.$options._propKeys = []);
const isRoot = !vm.$parent;
// ...
}声明常量propsData存储vm.$options.propsData属性,这是外界传进来的props数据。
声明常量props存储实例对象的vm._props属性,初始值为空对象,这个属性用来存储初始化后props的值。
声明常量keys存储vm.$options._propsKeys属性,初始值为空数组,这是props的key组成的数组。
声明常量isRoot存储表达式!vm.$parent的值,如果值为true,表示该组件是根组件,因为根组件没有$parent属性。
继续看下面的代码,只列出前面和后面的代码,中间的代码暂时省略。
// ! 不是根组件时,不进行监听,因为可能传过来的引用类型的值已经是响应式数据
if (!isRoot) {
toggleObserving(false); // ! 关闭监听开关 shouldObserve = false
}
// ...
toggleObserving(true); // ! 开启监听开关 shouldObserve = truetoggleObserving函数前面已经讲解过了,是控制变量shouldObserve的值的函数,而变量shouldObserve又是创建监听器对象的关键参数,是observe函数中创建监听器对象的五个必要条件之一。
这里先设置shouldObserve为false,最后把它的值重置为true。
下面看下中间的代码。
for (const key in propsOptions) {
keys.push(key);
const value = validateProp(key, propsOptions, propsData, vm); // ! 校验 props
// ...
}中间代码是一个for循环语句,遍历的是通过参数传进来的合并选项后的props选项。在遍历的过程中把key值都放入上面定义的keys数组中。
声明变量value存储validateProp函数的返回值,validateProp函数是检验props传入值的函数,返回值是验证后的prop的值,后面再详细讲解。
继续看下面的代码。
if (process.env.NODE_ENV !== "production") {
const hyphenatedKey = hyphenate(key);
// ! prop 的名字是保留的属性,发出警告
if (
isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)
) {
warn(
`"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
vm
);
}
}在非生产环境中,声明常量hyphenatedKey存储通过hyphenate函数把key值转换成连字符和小写字母的形式的值,然后再判断这个值是否是内置的属性名,如果是的话会发出警告。
看下hyphenate函数的代码,这个函数把驼峰形式的变量转换成连字符形式。
const hyphenateRE = /\B([A-Z])/g;
export const hyphenate = cached((str: string): string => {
return str.replace(hyphenateRE, "-$1").toLowerCase();
});示例代码:
const key = "propName";
hyphenate(key); // 'prop-name'继续看下面的代码。
// ! 定义 props 的属性,不是响应式
defineReactive(props, key, value, () => {
if (!isRoot && !isUpdatingChildComponent) {
// ! 组件直接修改 props 属性,发出警告
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
);
}
});还是在非生产环境中,使用defineReactive函数把props对象下面的key值定义成value值,props也就是实例对象的_props属性,它们都是来自同一个引用。
另外,还传入第四个参数customSetter函数,这个函数的作用是,在不是根组件或者不是更新子组件的时候,如果修改了props的属性会发出警告。
继续看下面的代码。
else {
// ! 定义 props 的属性
// ! 因为 shouldObserve = false, 不会变成响应式
defineReactive(props, key, value)
}现在回到生产环境,还是使用defineReactive函数把props对象下面的key值定义成value值。
因为在for循环之前因为把shouldObserve的值设置为false,所以defineReactive函数是不会把props的数据变成响应性数据的。为什么要这样做呢?因为传过来的props值一般都是响应式数据,这里就不需要定义了。
再看下for循环中的剩下的最后代码。
if (!(key in vm)) {
proxy(vm, `_props`, key); // ! 代理 props 上的属性 vm.xxx = vm._props.xxx
}调用proxy函数把props的数据代理到实例对象中,这和initData函数中的逻辑一样。
到这里,props选项的初始化就已经完成。下面看下props的属性是怎么验证的。
Props的校验#
在初始化props选项时,使用for循环语句遍历props选项,在每次循环时都会调用validateProp函数验证外界传过来的值是否符合要求。
for (const key in propsOptions) {
keys.push(key);
const value = validateProp(key, propsOptions, propsData, vm); // ! 校验 props
// ...
}下面看下validateProp函数的代码,在core/util/props.js文件中
export function validateProp(
key: string,
propOptions: Object, // ! 合并后的 props 选项
propsData: Object, // ! 数据来源对象
vm?: Component
): any {
// ...
}先看下函数的四个参数:
1.key:遍历合并选项后的props选项的key。 2.propOptions:合并选项后的props选项。 3.propsData:外界传进来的props数据。 4.vm:实例对象,这个参数是可选的。
然后看下函数的代码,先看前面部分的。
const prop = propOptions[key]; // ! 获取定义的 prop 的属性包括 type 默认值等
const absent = !hasOwn(propsData, key); // ! 外界没有传值进来
let value = propsData[key]; // ! 获取传进来的值
// boolean casting
const booleanIndex = getTypeIndex(Boolean, prop.type); // ! 获取 Boolean 类型的索引声明常量prop存储用户定义的pros属性,里面包括类型type、默认值default、是否传值required等。声明常量absent存储表达式!hasOwn(propsData, key)的值,当表达式输出的值是true时,表示外界没有传值进来。声明变量value存储外界传入的值。声明常量booleanIndex存储通过调用getTypeIndex函数获取到的类型的索引。
getTypeIndex#
下面看下getTypeIndex函数的代码,这个函数获取类型在数组中的索引。
// ! 获取匹配的类型索引。如果值大于 -1 时,说明匹配到了值
function getTypeIndex(type, expectedTypes): number {
// ! 期望的类型不是数组时,直接比较,是相同类型返回 0,不是返回 1
if (!Array.isArray(expectedTypes)) {
return isSameType(expectedTypes, type) ? 0 : -1;
}
// ! 期望的类型是数组时,需要遍历,然后一一比较,返回匹配的类型在数组中的索引,如果都没有匹配到,返回 -1
for (let i = 0, len = expectedTypes.length; i < len; i++) {
if (isSameType(expectedTypes[i], type)) {
return i;
}
}
return -1;
}这个函数传入的参数是类型type和用户定义(期望)的类型expectedTypes。
首先判断期望的类型是不是数组,如果不是数组,通过isSameType函数进行比较,确认它们的类型是否相同,如果相同,返回值是0。如果不相同,返回值是-1。如果期望的类型是数组,需要遍历数组,然后把数组中的每个期望类型和传入的类型进行比较,后面详细讲解。
props定义的类型的示例代码:
props: {
name: {
type: Number
},
msg: {
type: [String, Number, Boolean]
}
}先看下isSameType函数是怎么比较的,查看它的代码。
// ! 判断类型是否相同
function isSameType(a, b) {
return getType(a) === getType(b);
}
// 获取 type 的字符串类型的值,比如 String => 'String'
function getType(fn) {
const match = fn && fn.toString().match(/^\s*function (\w+)/);
return match ? match[1] : "";
}isSameType函数通过调用getType函数获取type字符串类型的值,然后判断它们是否全等。
我们看下getType函数的代码,它的作用是获取类型的值,JavaScript 的类型(大写的)的值都是类,也就是构造函数,它是一个函数类型的值。这里匹配出函数字符串化后的名称,如果匹配到的话,返回这个函数名。如果匹配不到返回空字符串。比如String类型会返回String字符串类型的值。
这里为什么要type是字符串类型的名称呢?因为在一些浏览器中,不同iframes之间的相同类型的构造函数名称是不同的。
继续回到getTypeIndex函数,看下面的代码。
// ! 期望的类型是数组时,需要遍历,然后一一比较,返回匹配的类型在数组中的索引
// ! 如果都没有匹配到,返回 -1
for (let i = 0, len = expectedTypes.length; i < len; i++) {
if (isSameType(expectedTypes[i], type)) {
return i;
}
}
return -1;如果期望的类型值是一个数组类型,需要遍历数组,获取到数组的每个类型,然后这些类型和传入的类型的值进行比较,如果发现它们全等,就返回期望的类型在数组中的索引。如果遍历之后没有发现值全等的,返回值是-1。
小结,getTypeIndex函数就是输入一个类型,然后拿这个类型的值和期望的类型的值(用户定义的)进行比较,看这个类型是不是我们期望的类型(它们的值全等),或者这个类型是不是我们期望的类型之一(包含在期望的类型中)。如果是的话,返回一个大于-1的值。
了解了getTypeIndex函数的作用后,继续看validateProp函数下面的代码。
// ! 优先验证 Boolean 类型的值
if (booleanIndex > -1) {
// ! 外界没有传值,且没有默认值,
if (absent && !hasOwn(prop, "default")) {
value = false; // ! 值为 false
} else if (value === "" || value === hyphenate(key)) {
// ...
}
}这里优先验证Boolean类型的值,即优先处理是布尔值数据的类型,也就是booleanIndex大于-1时。
首先判断外界有没有传入值且它有没有设置默认值,如果外界没有传入值,然后它又没有设置默认值,那么就把这个prop数据value设为false。
示例代码:
propsData: {
// ... 外界没有传值
}
props: {
propName: {
type: Boolean; // 只设置了 Boolean,没有设置默认值
}
}
// result
propName: false;继续看下面的代码。
// ! 外界传入空字符串 或者 名字由驼峰转连字符后与值为相同字符串 (someProp="some-prop)
else if (value === '' || value === hyphenate(key)) {
// only cast empty string / same name to boolean if
// boolean has higher priority
const stringIndex = getTypeIndex(String, prop.type) // ! 获取 String 类型的索引
// ! 没有定义 String 类型
// ! 或者 Boolean 类型的索引排在 String 类型的索引前 (类型是数组时 [Boolean String]))
if (stringIndex < 0 || booleanIndex < stringIndex) {
value = true // ! 值为 true
}
}如果外界传入的值是一个空的字符串或者传入的值和key转换成连字符的形式后的值相同时,先看下这个prop是否设置了类型是String,尝试获取stringIndex在期望的数组类型中的索引,如果没有获取到stringIndex(说明没有设置),或者获取到了但是booleanIndex索引在stringIndex索引的前面时,把这个prop的值value设置为true。
示例代码:
propsData: {
// propName: '' // 传值一 传进来的值是空字符串
propName: "prop-name"; // 传值二 传进来的值是 'prop-name'
}
props: {
propName: {
// type: Boolean // 类型一 没有设置 String 类型
type: [Boolean, String]; // 类型二 没有设置 String 类型,但是 Boolean 排在 String 前面
}
}
// result
propName: true;继续看下面的代码。
// check default value
// ! 处理 prop 未传值的情况
if (value === undefined) {
value = getPropDefaultValue(vm, prop, key); // ! 获取默认值(非响应式)
// since the default value is a fresh copy,
// make sure to observe it.
const prevShouldObserve = shouldObserve; // ! 缓存原来的值
toggleObserving(true); // ! 不管原来 shouldObserve 是否 true, 这里都先设置为 true
observe(shouldObserve); // ! 把默认值设为响应式
toggleObserving(prevShouldObserve); // ! 还原成原先的值
}如果value未定义,也就是外界没有传值,调用getPropDefaultValue函数获取设置的默认值并赋值给value。
先不管getPropDefaultValue函数的逻辑,继续看下面的代码。
声明常量prevShouldObserve缓存shouldObserve原先的值,然后调用toggleObserving函数把shouldObserve的值设置为true。然后调用observe函数监听value,把它变成响应式数据,最后把shouldObserve还原成原来的值。
这里的逻辑就是在外界没有传入值,但是有设置默认值时,把prop的值设置为默认值。
getPropDefaultValue#
下面看下getPropDefaultValue函数是怎么获取默认值的,查看它的代码。
function getPropDefaultValue(
vm: ?Component,
prop: PropOptions,
key: string
): any {
// no default, return undefined
if (!hasOwn(prop, "default")) {
return undefined;
}
}首先看下getPropDefaultValue函数的三个参数,分别是实例对象vm,定义的属性prop,以及合并选项后props的key。
下面看函数体代码,首先判断定义的属性里面有没有设置默认属性default,如果没有,函数返回undefined,也就是说外界没有传入值又没有设置默认值,这时validateProp函数下面的代码是无效的。
observe(value); // observe(undefined) 是无效的继续看getPropDefaultValue函数下面的代码,在有设置默认值的时候。
const def = prop.default; // ! 获取默认值
// warn against non-factory defaults for Object & Array
// ! 对象和数组类型的默认值必须使用函数返回值,否则会报错
if (process.env.NODE_ENV !== "production" && isObject(def)) {
warn(
'Invalid default value for prop "' +
key +
'": ' +
"Props with type Object/Array must use a factory function " +
"to return the default value.",
vm
);
}声明变量def存储prop.default的值,即默认值。
然后在非生产环境下判断def是不是一个非null非undefined的普通对象,如果是的话报错,提示对象和数组类型的prop的默认值必须使用函数的返回值。因为prop的默认值只能是原始类型或者函数类型。
示例代码:
props: {
name: {
type: String,
default: 'Hale'
},
info: {
type: Object,
default: function() {
return {}
}
}
}继续看下面的代码。
if (
vm &&
vm.$options.propsData &&
vm.$options.propsData[key] === undefined && // ! 此时,外界还没传值
vm._props[key] !== undefined // ! 已经定义非 undefined 的默认值
) {
return vm._props[key]; // ! 返回上次的默认值
}如果上一次数组更新或者创建时外界没有给该组件传值且组件存在非undefined的默认值时,返回之前的值(默认值),而不用重新再去获取。
这样可以避免触发无意义的响应,因为默认值如果是一个通过函数创建的对象,那么每次都会重新获取新生成的对象,虽然对象的引用和以前的不一样,但是它们的值是一样的,这样导致页面也没有变化。所以每次都去获取新的引用类型的默认值就没有任何意义,而且还会损耗性能。
继续看getPropDefaultValue函数剩下的代码。
// ! 默认值是函数类型,但是要求的类型不为 Function(类型是数组或者对象) 时需要求值
return typeof def === "function" && getType(prop.type) !== "Function"
? def.call(vm)
: def;如果默认值是函数类型,但是它期望的类型不是Function类型,说明这是数组或者对象的默认值,需要对默认的函数进行求值获取返回的值。
如果默认值是函数类型,而它要求的类型也是Function类型,说明这个prop的类型就是函数函数,不需要对默认值函数进行求值。
回到validateProp函数,继续看下面的代码。
// ! 验证类型,只有在非生产环境进行,且跳过 WEEX 环境的某种判断
if (
process.env.NODE_ENV !== "production" &&
// skip validation for weex recycle-list child component props
!(__WEEX__ && isObject(value) && "@binding" in value)
) {
assertProp(prop, key, value, vm, absent);
}
return value;如果在非生产环境中且在WEEX的某种环境中,使用assertProp函数验证prop的类型。
这里才是真正开始校验prop的类型。前面的代码都不算验证类型,只是在prop是布尔类型时,设置合理的布尔值以及获取prop的默认值。
assertProp#
下面看下assertProp函数的代码。
function assertProp(
prop: PropOptions,
name: string,
value: any,
vm: ?Component,
absent: boolean
) {
// ...
}首先看下函数的五个参数。
1.prop:定义prop的属性。 2.name:遍历合并选项后的props选项的key,就是prop的名称。 3.value:外界传入的值,也就是prop的值。 4.vm:实例对象。 5.absent:外界是否传入值,为true表示没有传入值。
下面看下函数体的代码,注意assertProp函数是验证prop类型的函数,只有在非生产环境中才会执行,验证类型如果不通过会报错。
// ! 有 required 时必须传数据,否则报错
if (prop.required && absent) {
warn('Missing required prop: "' + name + '"', vm);
return;
}
// ...如果定义的属性中设置了required为true,但是外界没有传入值,会报错,提示没有要求的prop值。
继续看下面的代码。
// ! 非必需传数据且值为 null 或者 undefined 时,直接返回
if (value == null && !prop.required) {
return;
}如果定义的属性中没有设置require的值(这时required的值是undefined),而且外界传入的值是null或者undefined时,直接返回,不需要验证。
继续看下面的代码。
// ! 判断类型是否与期望相符
let type = prop.type; // ! 获取类型
let valid = !type || type === true; // ! 没有设置类型或者类型的值为 true 时,不需要校验,直接判定为 true
const expectedTypes = []; // ! 期望类型的集合
if (type) {
// ! 数组类型
if (!Array.isArray(type)) {
type = [type];
}
// ! 遍历数组的所有类型,并验证每一种类型, 当出现一种类型验证失败后 即 !valid 时,循环结束
for (let i = 0; i < type.length && !valid; i++) {
const assertedType = assertType(value, type[i]); // ! 类型验证的返回值
expectedTypes.push(assertedType.expectedType || ""); // ! 把返回放入到数组中,后面统一处理
valid = assertedType.valid; // ! 本次循环验证的结果
}
}这里判断外界传入的值的类型是否和用户定义(期望)的类型相同。
定义变量type存储用户定义的类型。定义变量valid,它是一个布尔值,当用户没有定义类型或者定义的类型的值是true时,遍历valid的值为true,这同时也是它的初始值。定义常量expectedTypes存储类型验证的结果组成集合,初始值为空数组。
先判断是否存在定义的类型,如果存在,继续下面的逻辑。判断定义的类型是不是一个数组,如果不是数组,把它包装成一个数组。如果已经是一个数组了,遍历这个数组,验证数组中的每一种类型,如果出现验证任意一种类型失败的情况,循环结束。
声明常量assertedType存储通过调用assertType函数验证prop类型的返回值。这里先不管assertType函数是怎么验证类型的,继续看下面的逻辑。把验证后的返回值assertedType的expectedType属性放入到expectedTypes数组中,如果expectedTypes属性没有值,返回空字符串。
最后设置变量valid的值,它的值是每次循环时,验证的返回值assertedType的valid属性,这个属性表示本次循环时验证是否成功,如果验证成功则继续循环,进行下一轮验证。如果验证失败,即valid的值为false时,结束循环。
到了这里大概可以猜出验证函数的返回值assertedType是什么样子了,它应该是一个对象,至少有两个属性,分别是expectedType和valid。expectedType的值应该就是期望的类型,比如String、Boolean等,它是值是函数类型。而valid的值应该是个布尔值。
assertType#
下面就看下assertType函数的代码,这个函数是具体验证类型的方法。
function assertType(
value: any,
type: Function
): {
valid: boolean,
expectedType: string,
} {
// ...
}先看下它的两个参数,分别是prop传入的值value,和我们定义的类型type,type是一个函数。
下面看下函数的代码。
let valid;
const expectedType = getType(type); // ! 获取期待的类型声明变量valid,它是一个布尔值,也是函数的一个返回值,表示本次验证类型是否通过。
声明常量expectedType存储字符串类型的type。因为我们传入的参数type的值是一个函数类型的值,需要把它的值转换成函数名,也就是字符串类型,理由在前面已经讲过了。
示例代码:
const type1 = String; // String 是函数类型
getType(type1); // 'String' 是字符串类型
const type2 = Boolean; // Boolean 是函数类型
getType(type2); // 'Boolean' 是字符串类型继续看下面的代码。
// ! 可通过 typeof 来验证的数据类型
if (simpleCheckRE.test(expectedType)) {
const t = typeof value; // ! t 的值是字符串小写
valid = t === expectedType.toLowerCase(); // ! 验证类型
// ...
}判断正则表达式simpleCheckRE是否和期望的类型expectedType匹配,如果匹配,继续下面的逻辑。
先看下正则表达式simpleCheckRE的代码。
// ! 可通过 typeof 来验证的数据类型正则
const simpleCheckRE = /^(String|Number|Boolean|Function|Symbol)$/;上面这些都是可以通过typeof来判断的类型,比如String、Number、Boolean、Function、Symbol。
声明变量t存储通过typeof获取的外界传入的数据value的类型。然后比较t和期望的类型expectedType是否相同。因为这里的expectedType是首字母大写的,在比较前需要把它转换成小写的,然后把比较结果赋值给变量valid。
示例代码:
typeof 'value' // 'string'
expectedType = 'String'
expectedType.toLowerCase() // 'string'
valid = 'value' ==== type.toLowerCase()继续看下面的代码。
// for primitive wrapper objects
// ! 包装类型的验证,验证原型,比如 const str = new String('123'),str 是 String 的实例对象
if (!valid && t === "object") {
valid = value instanceof type;
}如果上面的比较结果是false,即不相同,再判断t的值是不是object,如果是的话,说明传入的值value是一个包装类型的值,需要判断这个值的原型,调用instanceof判断value的原型是不是type,最后把比较结果赋值给valid变量。
什么是包装类型?看下面的示例代码。
const a = "1"; // 原始类型
const b = new String("1"); // 包装类型
typeof a; // 'string'
typeof b; // 'object'
b instanceof String; // true继续看下面的代码。
// 对象类型
else if (expectedType === 'Object') {
valid = isPlainObject(value)
// ! 数组验证
} else if (expectedType === 'Array') {
valid = Array.isArray(value)
}当期望的类型是对象Object时,使用isPlainObject方法验证传入的值value的类型,并把验证结果赋值给变量valid。
当期望的类型是数组Array时,使用Array.isArray方法验证传入的值value的类型,并把验证结果赋值给变量valid。
继续看剩下的代码。
// ! 其他验证 => 自定义类型 (原型验证)
else {
valid = value instanceof type
}如果期望类型即不是可以通过typeof判断的类型,也不是数组对象,那么这个值可能是一个自定义的类型,使用instanceof判断传入的值value的原型是不是自定义的类型type。
示例代码:
// 自定义类型
function Person(name, age) {
this.name = name;
this.age = age;
}
const p = new Person("Hale", 18);
p instanceof Person; // true最后函数返回的一个对象,这个对象包含两个属性。
valid:验证的结果,是一个布尔值。expectedType:定义(期望)的类型,是一个字符串。
// ! 返回验证结果和期望的类型
return {
valid,
expectedType,
};学习了assertType函数的代码后,我们知道了类型是怎么验证的。
再次回到assertProp函数中,看下面的代码。
// ! 当验证失败时报错
if (!valid) {
warn(getInvalidTypeMessage(name, value, expectedTypes), vm);
return;
}当类型验证失败后,也就是valid的值是false时,会报错。
继续看assertProp函数剩下的代码。
// ! 自定义验证
const validator = prop.validator; // ! 获取用户自定义的验证器
// ! 把 value 作为参数传入并执行自定义验证器,
// ! 当它返回值为 false 时,验证失败,报错
if (validator) {
if (!validator(value)) {
warn(
'Invalid prop: custom validator check failed for prop "' + name + '".',
vm
);
}
}除了提高验证JavaScript的内置类型和自定义类型,props还提供了自定义验证。它不是验证值的类型,而是验证代码逻辑,即用户自定义的逻辑。
自定义验证让用户声明一个函数,然后把外界传入的值作为参数传入到函数中进行验证。
声明常量validator存储获取到的用户自定义的验证器,先判断自定义验证器是否存在,如果存在,把外界传入的值value作为参数传入到验证器函数中执行,如果验证器函数返回值是true,说明验证成功,不用进行其他操作。如果返回值是false,说明验证失败,然后报错。
示例代码:
propsData: {
list: ['apple', 'banana', 'orange']
}
props: {
list: {
validator: function(value) {
return value.includes('apple') // true
}
}
}到这里,props选项的验证的代码就全部讲解完成,props选项的初始化也讲解完成。