前言
Vue3探秘系列文章链接:
不止响应式:Vue3探秘系列— 虚拟结点vnode的页面挂载之旅(一)
不止响应式:Vue3探秘系列— diff算法的完整过程(三)
计算属性:Vue3探秘系列— computed的实现原理(六)
Hello~大家好。我是秋天的一阵风
在构建现代前端应用时,组件化开发已成为主流趋势,而在Vue.js这类框架中,组件之间的通信至关重要, props
作为父组件向子组件传递数据的主要方式之一,其重要性不言而喻。
Props
在 Vue.js 中扮演着关键角色。它们允许数据从父组件向下流动到子组件,从而实现了组件间的通信。无论是简单的文本还是复杂的对象结构,props
都可以轻松地将这些数据传递给子组件,并确保数据流的单一方向性,这有助于维护应用的状态一致性。
本文将深入探讨 props
的初始化和更新流程,以及这些流程背后的机制。
一、案例代码
为了让你更直观地理解,我们来举个例子,假设有这样一个 BlogPost
组件,它是这样定义的:
<div class="blog-post">
<h1>{{title}}</h1>
<p>author: {{author}}</p>
</div>
<script>
export default {
props: {
title: String,
author: String
}
}
</script>
然后我们在父组件使用这个 BlogPost
组件的时候,可以给它传递一些 Props
数据:
<blog-post title="Vue3 publish" author="yyx"></blog-post>
从最终结果来看,BlogPost
组件会渲染传递的 title
和 author
数据。
二、初始化
不知道同学们还记不记得,在之前介绍 setup
初始化流程时有一个setupComponent
函数,在这个函数里面会调用 initProps
方法。
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;
}
Props
的初始化,就是通过 initProps
方法来完成的,我们来看一下它的实现:
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;
}
从注释中可以看到,initProps
中主要做了四件事,分别是:设置 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
用于存储解析后的普通属性数据。
normalizePropsOptions: 将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;
}
-
首先会处理
mixins
和extends
这两个特殊的属性,因为它们的作用都是扩展组件的定义,所以需要对它们定义中的props
递归执行normalizePropsOptions
。 -
接着处理数组形式的
props
,数组形式的props
必须是字符串,然后将字符串变成驼峰形式的key
,并为这些key创建一个EMPTY_OBJ
空对象 -
处理对象形式的
props
-
对含有布尔类型和有默认值的
prop
转换 -
最后,返回标准化结果
normalizedEntry
,它包含标准化后的props
定义normalized
,以及需要转换的props key needCastKeys
,并且用comp.\_\_props
缓存这个标准化结果,如果对同一个组件重复执行normalizePropsOptions
,直接返回这个标准化结果即可。
遍历 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
中的 prop
在配置中定义了,那么把它的值赋值到props
对象中。
如果不是,那么判断这个 key
是否为非事件派发相关,如果是那么则把它的值赋值到 attrs
对象中。另外,在遍历的过程中,遇到 key
、ref
这种 key
,则直接跳过。
对需要转换的 props 求值
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])
}
}
}
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;
}
- 遍历
needCastKeys
数组进行求值 - 在
resolvePropValue
中主要是对有默认值和布尔类型的情况进行处理
(2) 验证 props
是否合法 : validateProps
function initProps(instance, rawProps, isStateful, isSSR = false) {
const props = {}
// 设置 props 的值
// 验证 props 合法
if ((process.env.NODE_ENV !== ‘production’)) {
validateProps(props, instance.type)
}
}
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 + '".'
);
}
}
校验主要是三个方面:是否必填、type类型是否符合、是否通过自定义校验器
关于自定义校验器,你可以在官网这里学习 Props
(3) 将props
变成响应式 :shallowReactive
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;
}
(4) 添加到实例 instance.props
上
instance.props = props;
三、Props的更新
当Props数据发生改变的时候会发生什么事呢?答案很简单,就是触发组件的重新渲染,以我们之前的例子来说,不仅引用了BlogPost的父组件会重新渲染,子组件BlogPost自身也会重新渲染。
那么问题来了,父组件被重新渲染这个好理解,因为我们在父组件的模板中引用了props变量,也就是title和author。那子组件是如何被触发重新渲染的呢?
(1)updateComponent
在之前的文章中不止响应式:Vue3探秘系列— 组件更新会发生什么(二)提到,组件的重新渲染会触发patch过程,也就是根据新旧子树进行patch
,patch
方法里面如果又遇到了组件节点,就会进入updateComponent
方法
const updateComponent = (n1, n2, parentComponent, optimized) => {
const instance = (n2.component = n1.component);
// 根据新旧子组件 vnode 判断是否需要更新子组件
if (shouldUpdateComponent(n1, n2, parentComponent, optimized)) {
// 新的子组件 vnode 赋值给 instance.next
instance.next = n2;
// 子组件也可能因为数据变化被添加到更新队列里了,移除它们防止对一个子组件重复更新
invalidateJob(instance.update);
// 执行子组件的副作用渲染函数
instance.update();
} else {
// 不需要更新,只复制属性
n2.component = n1.component;
n2.el = n1.el;
}
};
当 shouldUpdateComponent
为true
时,会将新的子组件 vnode
赋值给 instance.next
,然后执行 instance.update
触发子组件的重新渲染。
但是问题又来了,子组件实例在重新渲染的时候,会拿instance.props
的数据进行渲染,如果这个数据还是旧的,那还是渲染了旧数据,这毫无意义。所以我们还要知道instance.props
是如何更新的。
(2)setupRenderEffect
执行instance.update
函数,实际上是执行 componentEffect
组件副作用渲染函数:
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
节点信息,我们来看一下它的实现:
(3)updateComponentPreRender
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
就是关键。
(4) updateProps
function updateProps(instance, rawProps, rawPrevProps, optimized) {
const {
props,
attrs,
vnode: { patchFlag },
} = instance;
const rawCurrentProps = toRaw(props);
const [options] = normalizePropsOptions(instance.type);
if ((optimized || patchFlag > 0) && !((patchFlag & 16) /* FULL_PROPS */)) {
if (patchFlag & 8 /* PROPS */) {
// 只更新动态 props 节点
const propsToUpdate = instance.vnode.dynamicProps;
for (let i = 0; i < propsToUpdate.length; i++) {
const key = propsToUpdate[i];
const value = rawProps[key];
if (options) {
if (hasOwn(attrs, key)) {
attrs[key] = value;
} else {
const camelizedKey = camelize(key);
props[camelizedKey] = resolvePropValue(
options,
rawCurrentProps,
camelizedKey,
value
);
}
} else {
attrs[key] = value;
}
}
}
} else {
// 全量 props 更新
setFullProps(instance, rawProps, props, attrs);
// 因为新的 props 是动态的,把那些不在新的 props 中但存在于旧的 props 中的值设置为 undefined
let kebabKey;
for (const key in rawCurrentProps) {
if (
!rawProps ||
(!hasOwn(rawProps, key) &&
((kebabKey = hyphenate(key)) === key || !hasOwn(rawProps, kebabKey)))
) {
if (options) {
if (
rawPrevProps &&
(rawPrevProps[key] !== undefined ||
rawPrevProps[kebabKey] !== undefined)
) {
props[key] = resolvePropValue(
options,
rawProps || EMPTY_OBJ,
key,
undefined
);
}
} else {
delete props[key];
}
}
}
}
if (process.env.NODE_ENV !== "production" && rawProps) {
validateProps(props, instance.type);
}
}
updateProps
主要的目标就是把父组件渲染时求得的 props
新值,更新到子组件实例的 instance.props
中。
在编译阶段,我们除了捕获一些动态 vnode
,也捕获了动态的 props
,所以我们可以只去比对动态的 props
数据更新。
当然,如果不满足优化的条件,我们也可以通过setFullProps
去全量比对更新 props
,并且,由于新的 props
可能是动态的,因此会把那些不在新props
中但存在于旧 props
中的值设置为 undefined
。
四、为什么 instance.props
需要变成响应式呢?
假设有这么一个场景,定义一个父组件parent和子组件Child,子组件Child接收的一个props,变量为count。
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++;
在子组件Child
中,不仅渲染模板用到了count
,还使用watchEffect
注册了一个函数,函数内依赖了props.count。
假如我们想要的效果是,当count
被修改时,这个回调函数也会执行,那么前提条件就是count
必须是响应式数据。
所以答案很简单,将instance.props
设置为响应式数据就是为了让我们可以在子组件中监听props
的变化。
那么问题又来了,为什么要使用 shallowReactive
呢?
shallowReactive
和普通的 reactive
函数的主要区别是处理器函数不同,我们来回顾 getter
的处理器函数:
function createGetter(isReadonly = false, shallow = false) {
return function get(target, key, receiver) {
if (key === "__v_isReactive" /* IS_REACTIVE */) {
return !isReadonly;
} else if (key === "__v_isReadonly" /* IS_READONLY */) {
return isReadonly;
} else if (
key === "__v_raw" /* RAW */ &&
receiver ===
(isReadonly
? target["__v_readonly" /* READONLY */]
: target["__v_reactive" /* REACTIVE */])
) {
return target;
}
const targetIsArray = isArray(target);
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver);
}
const res = Reflect.get(target, key, receiver);
if (
isSymbol(key)
? builtInSymbols.has(key)
: key === `__proto__` || key === `__v_isRef`
) {
return res;
}
if (!isReadonly) {
track(target, "get" /* GET */, key);
}
if (shallow) {
return res;
}
if (isRef(res)) {
return targetIsArray ? res : res.value;
}
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res);
}
return res;
};
}
shallowReactive
创建的 getter
函数,shallow
变量为 true
,那么就不会执行后续的递归 reactive
逻辑。也就是说,shallowReactive 只把对象 target 的最外一层属性的访问和修改处理成响应式。
之所以可以这么做,是因为 props
在更新的过程中,只会修改最外层属性,所以用 shallowReactive
就足够了
总结
好的,到这里我们这一节的学习也要结束啦,通过这节课的学习,你应该要了解 Props 是如何被初始化的,如何被校验的,你需要区分开 Props 配置和 Props 传值这两个概念;你还应该了解 Props 是如何更新的以及实例上的 props 为什么要定义成响应式的。