12.4 props 与组件的被动更新
props 是父组件交给子组件的数据;由父组件自更新引起的子组件更新叫被动更新,实现的关键是把实例上的 props 做成浅响应。
一、props 的两个来源
在虚拟 DOM 层面,组件的 props 与普通 HTML 标签的属性差别不大,但组件需要显式声明自己接收哪些 props:
| 来源 | 位置 | 说明 |
|---|---|---|
| 传递的 props | vnode.props | 父组件写在子组件节点上的属性 |
| 声明的 props | 组件选项 props | 子组件声明自己会接收哪些 props |
const MyComponent = {
// 组件接收名为 title 的 props,类型为 String
props: { title: String },
render() { /* ... */ }
}
const CompVNode = {
type: MyComponent,
props: { title: 'A Big Title', other: 'other' } // 父组件传递
}
没有定义在组件
props选项中的数据会被归入 attrs(Vue.js 3 的约定),attrs 通常透传到组件的根元素上。 本例的props: { title: String }只用来判断「声明了哪些 props」,不处理默认值与类型校验。
二、resolveProps:拆出 props 与 attrs
const [props, attrs] = resolveProps(propsOption, vnode.props)
function resolveProps(options, propsData) {
const props = {}
const attrs = {}
// 遍历为组件传递的 props 数据
for (const key in propsData) {
if (key in options) {
// 在组件 props 选项中有定义 → 合法 props
props[key] = propsData[key]
} else {
// 否则作为 attrs
attrs[key] = propsData[key]
}
}
return [props, attrs]
}
三、props 用 shallowReactive 包装
const instance = {
state,
// 将解析出的 props 包装为浅响应并定义到实例上
props: shallowReactive(props),
isMounted: false,
subTree: null
}
用
shallowReactive而不是reactive:props 本质上是父组件的数据,深层响应式由父组件那边负责;子组件这一层只要在 props 属性被替换时能触发重新渲染就够了。 正因为是浅响应,更新 props 时直接给instance.props[k]赋值,就能触发子组件重新渲染。
四、渲染上下文 renderContext
有了 props 之后,this.foo(自身状态)与 this.title(props)都要能访问,于是用一个代理统一拦截读取/设置:
const renderContext = new Proxy(instance, {
get(t, k) {
const { state, props } = t
if (state && k in state) {
return state[k] // 优先自身状态
} else if (k in props) {
return props[k] // 其次 props
} else {
console.error('不存在')
}
},
set(t, k, v) {
const { state, props } = t
if (state && k in state) {
state[k] = v
} else if (k in props) {
console.warn(`Attempting to mutate prop "${k}". Props are readonly.`)
} else {
console.error('不存在')
}
}
})
// 渲染函数与所有生命周期钩子的 this 都换成 renderContext
const subTree = render.call(renderContext, renderContext)
created && created.call(renderContext)
mounted && mounted.call(renderContext)
beforeUpdate && beforeUpdate.call(renderContext)
updated && updated.call(renderContext)
props 是只读的:通过
this.title = xxx写 props 会走set的 props 分支并给出警告。
五、被动更新 patchComponent
流程:
- 父组件自更新 → 重新执行父组件的渲染函数 → 生成新的子组件 vnode(带新的
vnode.props); patch发现新旧 vnode 的type(组件选项对象)相同,进入patchComponent;- 用
hasPropsChanged判断 props 是否真的变化,没变就什么都不做; - 变了就更新
instance.props;因为 props 是浅响应,赋值会触发子组件的渲染副作用 → 子组件重新渲染。
function patchComponent(n1, n2, container) {
// 关键:实例同时挂到新 vnode 上,否则下一次更新取不到实例
const instance = (n2.component = n1.component)
const { props } = instance
if (hasPropsChanged(n1.props, n2.props)) {
const [nextProps] = resolveProps(n2.type.props, n2.props)
// 更新 props
for (const k in nextProps) {
props[k] = nextProps[k]
}
// 删除不存在的 props
for (const k in props) {
if (!(k in nextProps)) delete props[k]
}
}
}
function hasPropsChanged(prevProps, nextProps) {
const nextKeys = Object.keys(nextProps)
// 数量变了 → 有变化
if (nextKeys.length !== Object.keys(prevProps).length) return true
for (let i = 0; i < nextKeys.length; i++) {
const key = nextKeys[i]
// 有不相等的 props → 有变化
if (nextProps[key] !== prevProps[key]) return true
}
return false
}
组件实例只创建一次,
n2.component = n1.component不能漏。 被动更新不需要显式调用子组件的更新函数,触发交给响应式系统完成。 值比较用的是!==,对象类型的 props 每次都是新对象会被判定为「变了」;此外本例没有处理 attrs 与 slots 的更新(attrs 的更新原理与 props 类似)。
六、测试
const CompVNode = {
type: MyComponent,
props: {
title: ref('A Big Title'),
other: 'other'
}
}
setTimeout(() => {
CompVNode.props.title.value = 'A Small Title'
}, 1200)
// 用 effect 包住渲染调用,模拟「父组件更新」
effect(() => {
renderer.render(CompVNode, document.querySelector('#app'))
})
- 子组件渲染函数里写
this.title.value,读取 props 建立响应联系; - 修改 ref 的
.value触发外层 effect → 重新执行渲染 → 新旧 vnode 类型相同 → 进入patchComponent更新 props。
相关
- 12.3 组件实例与组件的生命周期:实例对象的定义,以及挂载与更新的区分
- 12.5 setup 函数的作用与实现:setup 的第一个参数,就是这里解析出的 props 的只读版本