自我介绍:大家好,我是吉帅振的网络日志;微信公众号:吉帅振的网络日志;前端开发工程师,工作4年,去过上海、北京,经历创业公司,进过大厂,现在郑州敲代码。
一、前言
页面可以由一个个组件构建而成,组件是一种抽象的概念,它是对页面的部分布局和逻辑的封装。为了让组件支持各种丰富的功能,Vue.js 设计了 Props 特性,它允许组件的使用者在外部传递 Props,然后组件内部就可以根据这些 Props 去实现各种各样的功能。
二、Props 的初始化
function setupComponent (instance, isSSR = false) {
const { props, children, shapeFlag } = instance.vnode
// 判断是否是一个有状态的组件
const isStateful = shapeFlag & 4
// 初始化 props
initProps(instance, props, isStateful, isSSR)
// 初始化插槽
initSlots(instance, children)
// 设置有状态的组件实例
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR)
: undefined
return setupResult
}
function initProps(instance, rawProps, isStateful, isSSR = false) {
const props = {}
const attrs = {}
def(attrs, InternalObjectKey, 1)
// 设置 props 的值
setFullProps(instance, rawProps, props, attrs)
// 验证 props 合法
if ((process.env.NODE_ENV !== 'production')) {
validateProps(props, instance.type)
}
if (isStateful) {
// 有状态组件,响应式处理
instance.props = isSSR ? props : shallowReactive(props)
}
else {
// 函数式组件处理
if (!instance.type.props) {
instance.props = attrs
}
else {
instance.props = props
}
}
// 普通属性赋值
instance.attrs = attrs
}
初始化 Props 主要做了以下几件事情:设置 props 的值,验证 props 是否合法,把 props 变成响应式,以及添加到实例 instance.props 上。
1.设置 Props
function setFullProps(instance, rawProps, props, attrs) {
// 标准化 props 的配置
const [options, needCastKeys] = normalizePropsOptions(instance.type)
if (rawProps) {
for (const key in rawProps) {
const value = rawProps[key]
// 一些保留的 prop 比如 ref、key 是不会传递的
if (isReservedProp(key)) {
continue
}
// 连字符形式的 props 也转成驼峰形式
let camelKey
if (options && hasOwn(options, (camelKey = camelize(key)))) {
props[camelKey] = value
}
else if (!isEmitListener(instance.type, key)) {
// 非事件派发相关的,且不在 props 中定义的普通属性用 attrs 保留
attrs[key] = value
}
}
}
if (needCastKeys) {
// 需要做转换的 props
const rawCurrentProps = toRaw(props)
for (let i = 0; i < needCastKeys.length; i++) {
const key = needCastKeys[i]
props[key] = resolvePropValue(options, rawCurrentProps, key, rawCurrentProps[key])
}
}
}
instance 表示组件实例;rawProps 表示原始的 props 值,也就是创建 vnode 过程中传入的 props 数据;props 用于存储解析后的 props 数据;attrs 用于存储解析后的普通属性数据。设置 Props 的过程也分成几个步骤:标准化 props 的配置,遍历 props 数据求值,以及对需要转换的 props 求值。
function normalizePropsOptions(comp) {
// comp.__props 用于缓存标准化的结果,有缓存,则直接返回
if (comp.__props) {
return comp.__props
}
const raw = comp.props
const normalized = {}
const needCastKeys = []
// 处理 mixins 和 extends 这些 props
let hasExtends = false
if (!shared.isFunction(comp)) {
const extendProps = (raw) => {
const [props, keys] = normalizePropsOptions(raw)
shared.extend(normalized, props)
if (keys)
needCastKeys.push(...keys)
}
if (comp.extends) {
hasExtends = true
extendProps(comp.extends)
}
if (comp.mixins) {
hasExtends = true
comp.mixins.forEach(extendProps)
}
}
if (!raw && !hasExtends) {
return (comp.__props = shared.EMPTY_ARR)
}
// 数组形式的 props 定义
if (shared.isArray(raw)) {
for (let i = 0; i < raw.length; i++) {
if (!shared.isString(raw[i])) {
warn(`props must be strings when using array syntax.`, raw[i])
}
const normalizedKey = shared.camelize(raw[i])
if (validatePropName(normalizedKey)) {
normalized[normalizedKey] = shared.EMPTY_OBJ
}
}
}
else if (raw) {
if (!shared.isObject(raw)) {
warn(`invalid props options`, raw)
}
for (const key in raw) {
const normalizedKey = shared.camelize(key)
if (validatePropName(normalizedKey)) {
const opt = raw[key]
// 标准化 prop 的定义格式
const prop = (normalized[normalizedKey] =
shared.isArray(opt) || shared.isFunction(opt) ? { type: opt } : opt)
if (prop) {
const booleanIndex = getTypeIndex(Boolean, prop.type)
const stringIndex = getTypeIndex(String, prop.type)
prop[0 /* shouldCast */] = booleanIndex > -1
prop[1 /* shouldCastTrue */] =
stringIndex < 0 || booleanIndex < stringIndex
// 布尔类型和有默认值的 prop 都需要转换
if (booleanIndex > -1 || shared.hasOwn(prop, 'default')) {
needCastKeys.push(normalizedKey)
}
}
}
}
}
const normalizedEntry = [normalized, needCastKeys]
comp.__props = normalizedEntry
return normalizedEntry
}
normalizePropsOptions 主要目的是标准化 props 的配置,这里需要注意,你要区分 props 的配置和 props 的数据。所谓 props 的配置,就是你在定义组件时编写的 props 配置,它用来描述一个组件的 props 是什么样的;而 props 的数据,是父组件在调用子组件的时候,给子组件传递的数据。所以这个函数首先会处理 mixins 和 extends 这两个特殊的属性,因为它们的作用都是扩展组件的定义,所以需要对它们定义中的 props 递归执行 normalizePropsOptions。接着,函数会处理数组形式的 props 定义,例如:
export default {
props: ['name', 'nick-name']
}
如果 props 被定义成数组形式,那么数组的每个元素必须是一个字符串,然后把字符串都变成驼峰形式作为 key,并为normalized 的 key 对应的每一个值创建一个空对象。针对上述示例,最终标准化的 props 的定义是这样的:
export default {
props: {
name: {},
nickName: {}
}
}
如果 props 定义是一个对象形式,接着就是标准化它的每一个 prop 的定义,把数组或者函数形式的 prop 标准化成对象形式,例如:
export default {
title: String,
author: [String, Boolean]
}
注意,上述代码中的 String 和 Boolean 都是内置的构造器函数。经过标准化的 props 的定义:
export default {
props: {
title: {
type: String
},
author: {
type: [String, Boolean]
}
}
}
接下来,就是判断一些 prop 是否需要转换,其中,含有布尔类型的 prop 和有默认值的 prop 需要转换,这些 prop 的 key 保存在 needCastKeys 中。注意,这里会给 prop 添加两个特殊的 key,prop[0] 和 prop[1]赋值,它们的作用后续我们会说。最后,返回标准化结果 normalizedEntry,它包含标准化后的 props 定义 normalized,以及需要转换的 props key needCastKeys,并且用 comp.__props 缓存这个标准化结果,如果对同一个组件重复执行 normalizePropsOptions,直接返回这个标准化结果即可。标准化 props 配置的目的无非就是支持用户各种的 props 配置写法,标准化统一的对象格式为了后续统一处理。
function setFullProps(instance, rawProps, props, attrs) {
// 标准化 props 的配置
if (rawProps) {
for (const key in rawProps) {
const value = rawProps[key]
// 一些保留的 prop 比如 ref、key 是不会传递的
if (isReservedProp(key)) {
continue
}
// 连字符形式的 props 也转成驼峰形式
let camelKey
if (options && hasOwn(options, (camelKey = camelize(key)))) {
props[camelKey] = value
}
else if (!isEmitListener(instance.type, key)) {
// 非事件派发相关的,且不在 props 中定义的普通属性用 attrs 保留
attrs[key] = value
}
}
}
// 转换需要转换的 props
}
该过程主要就是遍历 rawProps,拿到每一个 key。由于我们在标准化 props 配置过程中已经把 props 定义的 key 转成了驼峰形式,所以也需要把 rawProps 的 key 转成驼峰形式,然后对比看 prop 是否在配置中定义。如果 rawProps 中的 prop 在配置中定义了,那么把它的值赋值到 props 对象中,如果不是,那么判断这个 key 是否为非事件派发相关,如果是那么则把它的值赋值到 attrs 对象中。另外,在遍历的过程中,遇到 key、ref 这种 key,则直接跳过。
function setFullProps(instance, rawProps, props, attrs) {
// 标准化 props 的配置
// 遍历 props 数据求值
if (needCastKeys) {
// 需要做转换的 props
const rawCurrentProps = toRaw(props)
for (let i = 0; i < needCastKeys.length; i++) {
const key = needCastKeys[i]
props[key] = resolvePropValue(options, rawCurrentProps, key, rawCurrentProps[key])
}
}
}
在 normalizePropsOptions 的时候,我们拿到了需要转换的 props 的 key,接下来就是遍历 needCastKeys,依次执行 resolvePropValue 方法来求值。我们来看一下它的实现:
function resolvePropValue(options, props, key, value) {
const opt = options[key]
if (opt != null) {
const hasDefault = hasOwn(opt, 'default')
// 默认值处理
if (hasDefault && value === undefined) {
const defaultValue = opt.default
value =
opt.type !== Function && isFunction(defaultValue)
? defaultValue()
: defaultValue
}
// 布尔类型转换
if (opt[0 /* shouldCast */]) {
if (!hasOwn(props, key) && !hasDefault) {
value = false
}
else if (opt[1 /* shouldCastTrue */] &&
(value === '' || value === hyphenate(key))) {
value = true
}
}
}
return value
}
resolvePropValue 主要就是针对两种情况的转换,第一种是默认值的情况,即我们在 prop 配置中定义了默认值,并且父组件没有传递数据的情况,这里 prop 对应的值就取默认值。第二种是布尔类型的值,前面我们在 normalizePropsOptions 的时候已经给 prop 的定义添加了两个特殊的 key,所以 opt[0] 为 true 表示这是一个含有 Boolean 类型的 prop,然后判断是否有传对应的值,如果不是且没有默认值的话,就直接转成 false,举个例子:
export default {
props: {
author: Boolean
}
}
如果父组件调用子组件的时候没有给 author 这个 prop 传值,那么它转换后的值就是 false。接着看 opt[1] 为 true,并且 props 传值是空字符串或者是 key 字符串的情况,命中这个逻辑表示这是一个含有 Boolean 和 String 类型的 prop,且 Boolean 在 String 前面,例如:
export default {
props: {
author: [Boolean, String]
}
}
这种时候如果传递的 prop 值是空字符串,或者是 author 字符串,则 prop 的值会被转换成 true。至此,props 的转换求值结束,整个 setFullProps 函数逻辑也结束了,回顾它的整个流程,我们可以发现它的主要目的就是对 props 求值,然后把求得的值赋值给 props 对象和 attrs 对象中。
2.验证 Props
function initProps(instance, rawProps, isStateful, isSSR = false) {
const props = {}
// 设置 props 的值
// 验证 props 合法
if ((process.env.NODE_ENV !== 'production')) {
validateProps(props, instance.type)
}
}
验证过程是在非生产环境下执行的,我们来看一下 validateProps 的实现:
function validateProps(props, comp) {
const rawValues = toRaw(props)
const options = normalizePropsOptions(comp)[0]
for (const key in options) {
let opt = options[key]
if (opt == null)
continue
validateProp(key, rawValues[key], opt, !hasOwn(rawValues, key))
}
}
function validateProp(name, value, prop, isAbsent) {
const { type, required, validator } = prop
// 检测 required
if (required && isAbsent) {
warn('Missing required prop: "' + name + '"')
return
}
// 虽然没有值但也没有配置 required,直接返回
if (value == null && !prop.required) {
return
}
// 类型检测
if (type != null && type !== true) {
let isValid = false
const types = isArray(type) ? type : [type]
const expectedTypes = []
// 只要指定的类型之一匹配,值就有效
for (let i = 0; i < types.length && !isValid; i++) {
const { valid, expectedType } = assertType(value, types[i])
expectedTypes.push(expectedType || '')
isValid = valid
}
if (!isValid) {
warn(getInvalidTypeMessage(name, value, expectedTypes))
return
}
}
// 自定义校验器
if (validator && !validator(value)) {
warn('Invalid prop: custom validator check failed for prop "' + name + '".')
}
}
顾名思义,validateProps 就是用来检测前面求得的 props 值是否合法,它就是对标准化后的 Props 配置对象进行遍历,拿到每一个配置 opt,然后执行 validateProp 验证。对于单个 Prop 的配置,我们除了配置它的类型 type,还可以配置 required 表明它的必要性,以及 validator 自定义校验器,举个例子:
export default {
props: {
value: {
type: Number,
required: true,
validator(val) {
return val >= 0
}
}
}
}
因此 validateProp 首先验证 required 的情况,一旦 prop 配置了 required 为 true,那么必须给它传值,否则会报警告。接着是验证 prop 值的类型,由于 prop 定义的 type 可以是多个类型的数组,那么只要 prop 的值匹配其中一种类型,就是合法的,否则会报警告。最后是验证如果配了自定义校验器 validator,那么 prop 的值必须满足自定义校验器的规则,否则会报警告。相信这些警告你在平时的开发工作中或多或少遇到过,了解了 prop 的验证原理,今后再遇到这些警告,你就能知其然并知其所以然了。
3.响应式处理
function initProps(instance, rawProps, isStateful, isSSR = false) {
// 设置 props 的值
// 验证 props 合法
if (isStateful) {
// 有状态组件,响应式处理
instance.props = isSSR ? props : shallowReactive(props)
}
else {
// 函数式组件处理
if (!instance.type.props) {
instance.props = attrs
}
else {
instance.props = props
}
}
// 普通属性赋值
instance.attrs = attrs
}
在前两个流程,我们通过 setFullProps 求值赋值给 props 变量,并对 props 做了检测,接下来,就是把 props 变成响应式,并且赋值到组件的实例上。至此,Props 的初始化就完成了。
三、总结
了解 Props 是如何被初始化的,如何被校验的,区分 Props 配置和 Props 传值这两个概念。
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。