欢迎关注公众号:《前端 Talkking》
1、前言
在 Vue 中,父组件可以通过 props
属性向子组件传递值,子组件接收到这些值后可以实现各种功能。
Vue 内部是如何实现 props 初始化以及更新的呢?接下来,一起来看看 props 的实现原理吧。
2、源码实现
2.1 props 配置的标准化
props
支持多种数据类型,比如数组,对象、字符串、数值等类型,因此,需要对 props
配置进行标准化处理,源码实现如下:
const instance: ComponentInternalInstance = {
uid: uid++,
vnode,
// 省略部分代码
propsOptions: normalizePropsOptions(type, appContext),
}
根据以上代码可知,标准化 props
是通过 normalizePropsOptions
方法完成的,我们来看它的实现:
export function normalizePropsOptions(
comp: ConcreteComponent,
appContext: AppContext,
asMixin = false
): NormalizedPropsOptions {
const cache = appContext.propsCache
const cached = cache.get(comp)
// 有缓存则直接返回
if (cached) {
return cached
}
const raw = comp.props
const normalized: NormalizedPropsOptions[0] = {}
const needCastKeys: NormalizedPropsOptions[1] = []
// apply mixin/extends props
let hasExtends = false
// 处理extends和mixins这些props
if (__FEATURE_OPTIONS_API__ && !isFunction(comp)) {
const extendProps = (raw: ComponentOptions) => {
if (__COMPAT__ && isFunction(raw)) {
raw = raw.options
}
hasExtends = true
const [props, keys] = normalizePropsOptions(raw, appContext, true)
extend(normalized, props)
if (keys) needCastKeys.push(...keys)
}
if (!asMixin && appContext.mixins.length) {
appContext.mixins.forEach(extendProps)
}
if (comp.extends) {
extendProps(comp.extends)
}
if (comp.mixins) {
comp.mixins.forEach(extendProps)
}
}
if (!raw && !hasExtends) {
if (isObject(comp)) {
cache.set(comp, EMPTY_ARR as any)
}
return EMPTY_ARR as any
}
// 数组形式的props定义
if (isArray(raw)) {
for (let i = 0; i < raw.length; i++) {
if (__DEV__ && !isString(raw[i])) {
warn(`props must be strings when using array syntax.`, raw[i])
}
// 转化为驼峰形式
const normalizedKey = camelize(raw[i])
// props名称合法
if (validatePropName(normalizedKey)) {
// 为标准化后的key对应 的每一个值创建一个空对象
normalized[normalizedKey] = EMPTY_OBJ
}
}
} else if (raw) {
if (__DEV__ && !isObject(raw)) {
warn(`invalid props options`, raw)
}
for (const key in raw) {
const normalizedKey = camelize(key)
if (validatePropName(normalizedKey)) {
const opt = raw[key]
const prop: NormalizedProp = (normalized[normalizedKey] =
isArray(opt) || isFunction(opt) ? { type: opt } : extend({}, opt))
if (prop) {
const booleanIndex = getTypeIndex(Boolean, prop.type)
const stringIndex = getTypeIndex(String, prop.type)
prop[BooleanFlags.shouldCast] = booleanIndex > -1
prop[BooleanFlags.shouldCastTrue] =
stringIndex < 0 || booleanIndex < stringIndex
// if the prop needs boolean casting or default value
if (booleanIndex > -1 || hasOwn(prop, 'default')) {
needCastKeys.push(normalizedKey)
}
}
}
}
}
const res: NormalizedPropsOptions = [normalized, needCastKeys]
if (isObject(comp)) {
cache.set(comp, res)
}
return res
}
normalizePropsOptions
方法有三个参数,其中:
- comp:表示定义组件的对象;
- appContext:表示全局上下文;
- asMixin:表示组件是否处于
mixin
的处理环境中。
其处理过程步骤如下:
-
处理
mixins
和extends
这两个特殊属性,因为它们的作用都是扩展组件,因此需要对它们定义中的props
递归执行normalizePropsOptions
。 -
处理数组形式的
prop
:将数组中的字符串都变成驼峰形式key
,校验key
是否符合命名规范,然后给标准化后的key
都创建一个空对象,例如,export default { props: ["name", "nick-name"], };
标准化的
prop
后的定义如下:export default { props: { name: {}, nickName: {}, }, };
-
处理对象形式的
prop
:标准化每个 prop 属性定义,把数组或者函数形式的 prop 标准化对象形式。例如:
export default {
title: String,
author: [String, Boolean],
};
标准化的 prop
后的定义如下:
export default {
props: {
title: {
type: String,
},
author: {
type: [String, Boolean],
},
},
};
4. 缓存并返回标准化后的结果。
标准化 props
配置的目的是为了支持用户各种 props
的配置写法,标准化统一的对象格式为了后续统一处理。
2.2 props 值的初始化
在执行 setupComponent
函数的时候,会初始化 props
:
export function setupComponent(
instance: ComponentInternalInstance,
isSSR = false
) {
isInSSRComponentSetup = isSSR
const { props, children } = instance.vnode
const isStateful = isStatefulComponent(instance)
// 初始化props
initProps(instance, props, isStateful, isSSR)
// 初始化插槽
initSlots(instance, children)
// 设置有状态的组件实例(通常,我们写的组件就是一个有状态的组件,所谓有状态,就是组件会在渲染过程中把一些状态挂载到组件实例对应的属性上)
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR)
: undefined
isInSSRComponentSetup = false
return setupResult
}
根据以上代码得知,prop
的初始化是通过 initProps
函数完成的,我们来看它的实现:
export function initProps(
instance: ComponentInternalInstance,
rawProps: Data | null,
isStateful: number, // result of bitwise flag comparison
isSSR = false
) {
const props: Data = {}
const attrs: Data = {}
// props的默认值缓存对象
def(attrs, InternalObjectKey, 1)
instance.propsDefaults = Object.create(null)
// 设置props值
setFullProps(instance, rawProps, props, attrs)
// ensure all declared prop keys are present
for (const key in instance.propsOptions[0]) {
if (!(key in props)) {
props[key] = undefined
}
}
// validation
// 验证props是否合法
if (__DEV__) {
validateProps(rawProps || {}, props, instance)
}
if (isStateful) {
// stateful
// 有状态组件,响应式处理
instance.props = isSSR ? props : shallowReactive(props)
} else {
// 函数式组件处理
if (!instance.type.props) {
// functional w/ optional props, props === attrs
instance.props = attrs
} else {
// functional w/ declared props
instance.props = props
}
}
// 普通属性赋值
instance.attrs = attrs
}
根据上面代码可知,该函数主要做了以下几件事情:
- 调用
setFullProps
设置props
的值; - 调用
validateProps
验证props
是否合法; - 把
props
变成响应式; - 把
props
添加到实例instance.props
上; - 普通属性添加到
instance.attrs
上。
接下来我们来看 setFullProps
和 validateProps
是如何实现的。
2.2.1 设置 props
接下来我们看下 setFullProps
方法的实现:
function setFullProps(
instance: ComponentInternalInstance,
rawProps: Data | null,
props: Data,
attrs: Data
) {
// 获取标准化props的配置
const [options, needCastKeys] = instance.propsOptions
// 判断普通属性是否改变了标志位
let hasAttrsChanged = false
let rawCastValues: Data | undefined
if (rawProps) {
for (let key in rawProps) {
// key, ref are reserved and never passed down
// 一些保留的prop比如ref、key是不会传递的
if (isReservedProp(key)) {
continue
}
if (__COMPAT__) {
if (key.startsWith('onHook:')) {
softAssertCompatEnabled(
DeprecationTypes.INSTANCE_EVENT_HOOKS,
instance,
key.slice(2).toLowerCase()
)
}
if (key === 'inline-template') {
continue
}
}
const value = rawProps[key]
// prop option names are camelized during normalization, so to support
// kebab -> camel conversion here we need to camelize the key.
// 连字符形式的props也转成驼峰形式
let camelKey
if (options && hasOwn(options, (camelKey = camelize(key)))) {
if (!needCastKeys || !needCastKeys.includes(camelKey)) {
props[camelKey] = value
} else {
;(rawCastValues || (rawCastValues = {}))[camelKey] = value
}
} else if (!isEmitListener(instance.emitsOptions, key)) {
// Any non-declared (either as a prop or an emitted event) props are put
// into a separate `attrs` object for spreading. Make sure to preserve
// original key casing
if (__COMPAT__) {
if (isOn(key) && key.endsWith('Native')) {
key = key.slice(0, -6) // remove Native postfix
} else if (shouldSkipAttr(key, instance)) {
continue
}
}
if (!(key in attrs) || value !== attrs[key]) {
attrs[key] = value
hasAttrsChanged = true
}
}
}
}
// 对需要转换的props求值
if (needCastKeys) {
const rawCurrentProps = toRaw(props)
const castValues = rawCastValues || EMPTY_OBJ
for (let i = 0; i < needCastKeys.length; i++) {
const key = needCastKeys[i]
props[key] = resolvePropValue(
options!,
rawCurrentProps,
key,
castValues[key],
instance,
!hasOwn(castValues, key)
)
}
}
return hasAttrsChanged
}
根据以上代码可知,可以拆解两个步骤:
-
遍历
rawProps
,拿到每一个key
。由于我们在标准化 props 配置过程中已经把 props 定义的 key 转成了驼峰形式,所以也需要把 rawProps 的 key 转成驼峰形式,然后对比看 prop 是否在配置中定义。如果 rawProps 中的 prop 在配置中定义了,那么把它的值赋值到 props 对象中,如果不是,那么判断这个 key 是否为非事件派发相关,如果是那么则把它的值赋值到 attrs 对象中。另外,在遍历的过程中,遇到 key、ref 这种 key,则直接跳过。遍历 rawProps; -
对需要转换的 props 求值:props 求值;
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.2.2 验证 props
validateProps
验证过程是在非生产环境执行的,我们来看下它的实现:
function validateProps(
rawProps: Data,
props: Data,
instance: ComponentInternalInstance
) {
const resolvedValues = toRaw(props)
const options = instance.propsOptions[0]
for (const key in options) {
let opt = options[key]
if (opt == null) continue
validateProp(
key,
resolvedValues[key],
opt,
!hasOwn(rawProps, key) && !hasOwn(rawProps, hyphenate(key))
)
}
}
function validateProp(
name: string,
value: unknown,
prop: PropOptions,
isAbsent: boolean
) {
const { type, required, validator, skipCheck } = prop
// required!
if (required && isAbsent) {
warn('Missing required prop: "' + name + '"')
return
}
// missing but optional
if (value == null && !required) {
return
}
// type check
if (type != null && type !== true && !skipCheck) {
let isValid = false
const types = isArray(type) ? type : [type]
const expectedTypes = []
// value is valid as long as one of the specified types match
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
}
}
// custom validator
if (validator && !validator(value)) {
warn('Invalid prop: custom validator check failed for prop "' + name + '".')
}
}
validateProps
方法对标准化后的 prop
配置对象进行遍历,调用 validateProp
进行验证。它校验了一下内容:
- 必填校验:如果
prop
配置了required
为 true,那么必须给它传值,否则会发出警告; - 类型校验:如果
prop
配置的是数组类型,那么prop
的值只要匹配到了数组中的其中一个类型就是合法的,否则会发出警告; - 自定义校验器
validator
校验:如果配置了自定义校验器validator
,那么prop
的值必须满足自定义校验器的规则,否则会报警告。
2.3 props 的更新
props
更新时,它的直接反应是会触发子组件的重新渲染。那么,它是如何触发组件的渲染呢?
2.3.1 触发子组件重新更新
组件的重新渲染会触发 patch
流程,然后遍历子节点递归 patch
,如果遇到组件,它会执行 updateComponent
方法:
const updateComponent = (n1: VNode, n2: VNode, optimized: boolean) => {
const instance = (n2.component = n1.component)!
// 根据新旧子组件vnode判断是否需要更新子组件
if (shouldUpdateComponent(n1, n2, optimized)) {
if (
__FEATURE_SUSPENSE__ &&
instance.asyncDep &&
!instance.asyncResolved
) {
updateComponentPreRender(instance, n2, optimized)
return
} else {
// normal update
// 新的子组件vnode赋值给instance.next
instance.next = n2
// 子组件也可能因为数据变化被添加到更新队列里了,移除它们防止对一个子组件重复更新
invalidateJob(instance.update)
// instance.update is the reactive effect.
// 执行子组件的副作用渲染函数
instance.update()
}
} else {
// 不需要更新,只赋值属性
n2.el = n1.el
instance.vnode = n2
}
}
首先,内部对比 props
来判断是否需要更新子组件,如果需要更新,则将新的子组件 vnode
赋值给 instance.next
,然后执行 instance.update()
触发子组件的重新渲染。
但是,虽然子组件重新渲染了,但是子组件实例 instance.props
的数据需要更新才行,不然还是渲染之前的数据,那么是如何更新 instance.props
的呢?
2.3.2 更新 instance.props
执行子组件的 instance.update()
函数,实际上执行的是 componentUpdateFn
组件副作用渲染函数,
const setupRenderEffect = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
// 创建响应式的副作用渲染函数
instance.update = effect(function componentEffect() {
if (!instance.isMounted) {
// 渲染组件
} else {
// 更新组件
let { next, vnode } = instance;
// next 表示新的组件 vnode
if (next) {
// 更新组件 vnode 节点信息
updateComponentPreRender(instance, next, optimized);
} else {
next = vnode;
}
// 渲染新的子树 vnode
const nextTree = renderComponentRoot(instance);
// 缓存旧的子树 vnode
const prevTree = instance.subTree;
// 更新子树 vnode
instance.subTree = nextTree;
// 组件更新核心逻辑,根据新旧子树 vnode 做 patch
patch(
prevTree,
nextTree,
// 如果在 teleport 组件中父节点可能已经改变,所以容器直接找旧树 DOM 元素的父节点
hostParentNode(prevTree.el),
// 参考节点在 fragment 的情况可能改变,所以直接找旧树 DOM 元素的下一个节点
getNextHostNode(prevTree),
instance,
parentSuspense,
isSVG
);
// 缓存更新后的 DOM 节点
next.el = nextTree.el;
}
}, prodEffectOptions);
};
在更新组件的时候,会判断是否有 instance.next,它代表新的组件 vnode,根据前面的逻辑 next 不为空,所以会执行 updateComponentPreRender 更新组件 vnode 节点信息,我们来看一下它的实现:
const updateComponentPreRender = (instance, nextVNode, optimized) => {
nextVNode.component = instance;
const prevProps = instance.vnode.props;
instance.vnode = nextVNode;
instance.next = null;
updateProps(instance, nextVNode.props, prevProps, optimized);
updateSlots(instance, nextVNode.children);
};
上面源码中,updateProps 的主要目标是把父组件渲染时求得的 props 新值,更新到子组件的 instance.props 中。
到这里我们搞清楚了了子组件的 props 是如何更新的,接下来,看为什么需要把 instance.props 变成响应式?
2.3.3 把 instance.props 变成响应式
举一个 🌰:
import { ref, h, defineComponent, watchEffect } from "vue";
const count = ref(0);
let dummy;
const Parent = {
render: () => h(Child, { count: count.value }),
};
const Child = defineComponent({
props: { count: Number },
setup(props) {
watchEffect(() => {
dummy = props.count;
});
return () => h("div", props.count);
},
});
count.value++;
这里,我们定义了父组件 Parent 和子组件 Child,子组件 Child 中定义了 prop count,除了在渲染模板中引用了 count,我们在 setup 函数中通过了 watchEffect 注册了一个回调函数,内部依赖了 props.count,当修改 count.value 的时候,我们希望这个回调函数也能执行,所以这个 prop 的值需要是响应式的,由于 setup 函数的第一个参数是 props 变量,其实就是组件实例 instance.props,所以也就是要求 instance.props 是响应式的。
3、总结
本文分析了 props 的实现原理,总结如下:
props
提供了父子组件数据传递的方式,它允许组件的使用者在外部传递props
,这样组件内部就可以根据这些props
实现各种业务功能了;props
允许用户传入各种类型的数据类型,因此,底层需要对这些props
做标准化处理;props
的初始化包括props
的求值、验证、响应式处理,当传入的props
数据发生变化时,会触发子组件的重新更新。
4、参考资料
[1]vue官网
[2]vuejs设计与实现
[3]vue3源码