响应式原理七:props
props
作为 Vue 核心特性之一,也是父子组件通信的一种方式,在 Vue 项目中经常使用。那么,它的内部是如何实现的呢?这将是本文即将要探究的话题。
规范化
props
对外提供的接口有两种方式,一是使用数组,数组元素只能是字符串,即字符串数组;二是对象。因此,在初始化 props
之前,需要对其进行规范化,作用是将用户传入的 props
规范为对象格式。
在初始化 Vue 实例时,对 options
进行合并配置处理,包括相应特性的规范化处理,比如 props
,其核心逻辑实现在函数 mergeOptions
,具体实现如下:
// src/core/util/options.js
/**
* Merge two option objects into a new one.
* Core utility used in both instantiation and inheritance.
*/
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
...
normalizeProps(child, vm)
...
return options
}
从代码实现中可看出,normalizeProps
实现了 props
的规范化处理,具体实现如下:
// src/core/util/options.js
/**
* Ensure all props option syntax are normalized into the
* Object-based format.
*/
function normalizeProps (options: Object, vm: ?Component) {
const props = options.props
if (!props) return
const res = {}
let i, val, name
if (Array.isArray(props)) {
i = props.length
while (i--) {
val = props[i]
if (typeof val === 'string') {
name = camelize(val)
res[name] = { type: null }
} else if (process.env.NODE_ENV !== 'production') {
warn('props must be strings when using array syntax.')
}
}
} else if (isPlainObject(props)) {
for (const key in props) {
val = props[key]
name = camelize(key)
res[name] = isPlainObject(val)
? val
: { type: val }
}
} else if (process.env.NODE_ENV !== 'production') {
warn(
`Invalid value for option "props": expected an Array or an Object, ` +
`but got ${toRawType(props)}.`,
vm
)
}
options.props = res
}
函数接收两个参数:
options
:Vue 实例组合项对象,即options
,数据类型为对象;vm
:Vue 实例。
作用是对 props
进行规范化,将用户传入的 props
规范为对象格式,以此符合 Vue 框架格式。
从代码实现可看出,props
接收两种数据格式:字符串数组和对象;如果 props
既不是数组,又不是对象,则会在开发环境下抛出告警。
如果 props
是数组,且数组每一个元素只能是字符串,表示 prop
的 key
。遍历数组 props
,对其 key
转换成驼峰化格式,同时设置 type
为 null
,举例如下:
props: ['message']
// 规范化
props: {
message: {
type: null
}
}
如果 props
是对象,对其进行遍历,通过 key
获取对应 prop
的值 val
,同时把 key
转换为驼峰化格式。接着对 val
数据类型进行判断,如果是对象,则直接赋值;否则将其设置为属性 type
的值,即 { type: val }
,举例如下:
props: {
message: String,
user: {
type: String,
default: ''
}
}
// 规范化
props: {
message: {
type: String
},
user: {
type: String,
default: ''
}
}
上面两次提到驼峰化处理,那么它又是如何实现的呢?
// src/shared/util.js
/**
* Create a cached version of a pure function.
*/
export function cached<F: Function> (fn: F): F {
const cache = Object.create(null)
return (function cachedFn (str: string) {
const hit = cache[str]
return hit || (cache[str] = fn(str))
}: any)
}
/**
* Camelize a hyphen-delimited string.
*/
const camelizeRE = /-(\w)/g
export const camelize = cached((str: string): string => {
return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '')
})
从代码实现中可看出,Vue 框架对 props
驼峰化处理做了缓存,即使用变量 cache
来保存,如果存在的话,则直接从 cache
获取且返回;否则调用传入的回调函数进行处理,并将其值缓存,再返回。
初始化
在初始化 Vue 实例时,函数 initState
对 props
做了初始化操作,具体实现如下:
// src/core/instance/state.js
export function initState (vm: Component) {
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
...
}
接着来看函数 initProps
具体是如何实现的?
// src/core/instance/state.js
function initProps (vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {}
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
// root instance props should be converted
if (!isRoot) {
toggleObserving(false)
}
for (const key in propsOptions) {
keys.push(key)
const value = validateProp(key, propsOptions, propsData, vm)
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
const hyphenatedKey = hyphenate(key)
if (isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)) {
warn(
`"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
vm
)
}
defineReactive(props, key, value, () => {
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
)
}
})
} else {
defineReactive(props, key, value)
}
// static props are already proxied on the component's prototype
// during Vue.extend(). We only need to proxy props defined at
// instantiation here.
if (!(key in vm)) {
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
}
函数接收 2 个参数:
vm
:Vue 组件实例;propsOptions
:规范化后的props
。
函数主要做了三件事:
- 校验
props
对象每个prop
; prop
转换为响应式对象;prop
代理到vm
实例。
在分析这三部分逻辑前,先来说明变量定义的作用。
propsData
:赋值为vm.$options.propsData || {}
,作用是父组件传入props
值;props
:赋值为vm._props = {}
,作用是代理props
到vm
实例上;keys
:赋值为vm.$options._propKeys = []
,作用是将props
key
缓存到数组,以便将来更新时能直接从数组获取,而无需再次遍历对象props
。
整体上来看,对 props
初始化的逻辑不是很复杂,即遍历规范化后 props
,然后对每个 prop
做三件事:校验、响应式和代理。那么,接下来我们就详细来分析它们。
校验
遍历 propsOptions
,将 key
保存到数组 keys
,调用函数 validateProp
对其进行校验,具体实现如下:
// src/core/util/props.js
export function validateProp (
key: string,
propOptions: Object,
propsData: Object,
vm?: Component
): any {
const prop = propOptions[key]
const absent = !hasOwn(propsData, key)
let value = propsData[key]
// boolean casting
const 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
const 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.
const prevShouldObserve = shouldObserve
toggleObserving(true)
observe(value)
toggleObserving(prevShouldObserve)
}
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
}
函数接收 4 个参数:
key
:prop
对应的键名key
;propOptions
:规范后生成的props
对象;propsData
:父组件传递props
数据;vm
:Vue 实例。
同样的,validateProp
函数主要也做了三件事:处理 Boolean
类型数据、处理默认值数据和 prop
断言。
在做这三件事之前,先做了一些前期工作。通过 key
从 propOtions
获取 prop
,比如:
// 规范化
props: {
message: {
type: String
},
user: {
type: String,
default: ''
}
}
// 通过 key 获取 prop
props['message'] = {
type: String
}
接着判断 prop
是否缺省,即父组件是否有对 prop
传值,即 const absent = !hasOwn(propsData, key)
,调用函数 hasOwn
判断 key
是否在对象 propsData
上,如果为 false
,表示缺省;否则不缺省,具体实现如下:
// src/core/shared/util.js
/**
* Check whether an object has the property.
*/
const hasOwnProperty = Object.prototype.hasOwnProperty
export function hasOwn (obj: Object | Array<*>, key: string): boolean {
return hasOwnProperty.call(obj, key)
}
最后获取父组件传入 prop
对应的值,即 let value = propsData[key]
。
准备工作做好了,接着来分析下校验阶段所做的三件事。
处理 Boolean
数据类型
调用函数 getTypeIndex
判断 prop.type
是否为 Boolean
类型,如果满足的话,则返回匹配的索引;否则返回 -1。具体实现如下:
// src/core/util/props.js
function getTypeIndex (type, expectedTypes): number {
if (!Array.isArray(expectedTypes)) {
return isSameType(expectedTypes, type) ? 0 : -1
}
for (let i = 0, len = expectedTypes.length; i < len; i++) {
if (isSameType(expectedTypes[i], type)) {
return i
}
}
return -1
}
由于 prop
类型定义时可以是单个原生构造函数,也可以是原生构造函数的数组,举例如下:
export default {
props: {
message: {
type: String
},
value: {
type: [Stirng, Boolean]
}
}
}
于是,如果 expectedTypes
是单个原生构造函数,则调用函数 isSameType
判断其与 type
是否为同一个数据类型,返回其索引;如果 expectedTypes
是原生构造函数数组,则对其进行遍历,同样调用函数 isSameType
判断每个数组元素与 type
是否为同一个数据类型,并且返回其索引。isSameType
具体实现如下:
// src/core/util/props.js
const functionTypeCheckRE = /^\s*function (\w+)/
/**
* Use function string name to check built-in types,
* because a simple equality check will fail when running
* across different vms / iframes.
*/
function getType (fn) {
const match = fn && fn.toString().match(functionTypeCheckRE)
return match ? match[1] : ''
}
function isSameType (a, b) {
return getType(a) === getType(b)
}
通过一个具体的例子来说明类型判断:
props['message'] = {
type: String
}
props['message'].toString() // 'function String() { [native code] }'
props['message'].toString().match(functionTypeCheckRE) => {
0: "function String"
1: "String"
groups: undefined
index: 0
input: "function String() { [native code] }"
}
// 最终返回结果:String
通过函数 getTypeIndex
获取索引 booleanIndex
,如果 booleanIndex > -1
,则说明 prop.type
是 Boolean
类型,那么继续执行后续逻辑。
如果父组件没有传递 prop
数据且没有设置默认值 default
的情况下,则将 prop
的值 value
设置为 false
,此判断是通过 absent && !hasOwn(prop, 'default')
实现。
如果 value === ''
或者 value === hyphenate(key)
即 value
与转换后 key
相等,那么 getTypeIndex
获取 String
类型索引,如果 stringIndex < -1
或者 booleanIndex < stringIndex
,则将 prop
的值设置为 true
,需要注意的是 Boolean
优先级比 String
高,例子如下:
定义子组件 B 如下:
export default {
message: String,
userName: [Boolean, String]
}
然后在父组件引用子组件 B,两种方式分别如下:
<!-- value === '' -->
<template>
<b message="Hello JavaScript" user-name></b>
</template>
<!-- value === hyphenate(key)-->
<template>
<b message="Hello JavaScript" user-name="user-name"></b>
</template>
这个例子中符合上述条件,最终 userName
值为 true
。
响应式
通过函数 validateProp
校验 prop
有效性后,则会将 prop
转换为响应式对象。从代码实现可以看出,最终是调用函数 defineReactive
将 prop
转换为响应式对象,至于其逻辑实现,可参考《响应式原理一:data 初始化》。
不过需要注意的是:在开发环境下,会校验 prop
键名 key
是否为 HTML
保留属性,如果是的话,则会抛出告警。
代理
在初始化 props
最后一步,则是将 props
代理到 vm._props
,即调用函数 proxy
实现代理,具体实现如下:
// src/core/instance/state.js
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
例子如下:
export default {
props: {
message: {
type: String,
}
}
}
当我们访问 prop
message
时,一般是通过 this.message
,那么,在其内部实际的访问时 this._props_message
。需要注意的是这里只是代理根实例 props
,对于非根实例的子组件,props
代理是发生在 Vue.extend
,即在执行 render
函数创建子组件时,具体实现如下:
// src/core/global-api/extend.js
/**
* Class inheritance
*/
Vue.extend = function (extendOptions: Object): Function {
// For props and computed properties, we define the proxy getters on
// the Vue instances at extension time, on the extended prototype. This
// avoids Object.defineProperty calls for each instance created.
...
if (Sub.options.props) {
initProps(Sub)
}
...
return Sub
}
接着看下 initProps
具体实现:
// src/core/global-api/extend.js
function initProps (Comp) {
const props = Comp.options.props
for (const key in props) {
proxy(Comp.prototype, `_props`, key)
}
}
可见,最终还是调用函数 proxy
将 props
代理到 vm._props`。
参考链接
转载自:https://juejin.cn/post/7080529193439592485