2026.01.26
组件 Props 更新 -> 变更检测与更新渲染的实现
在之前的章节中,我们已经实现了组件的挂载机制。当父组件传递给子组件的 Props 发生变化时,子组件需要进行相应的更新渲染。本节将详细分析 Props 更新的完整流程。
在之前的章节中,我们已经实现了组件的挂载机制。当父组件传递给子组件的 Props 发生变化时,子组件需要进行相应的更新渲染。本节将详细分析 Props 更新的完整流程。
问题场景
让我们先通过一个示例来观察 Props 更新的场景:
<body>
<div id="app"></div>
<button id="btn">age++</button>
<script type="module">
import { h, createApp, ref } from '../dist/vue.esm.js'
const Child = {
props: ['age'],
setup(props) {
return () => {
return h('div', {}, `子组件的 props age: ${props.age}`)
}
}
}
const Comp = {
setup() {
const age = ref(0)
btn.onclick = () => {
age.value++
console.log(age.value)
}
return () => {
return h(Child, { age: age.value })
}
}
}
createApp(Comp).mount('#app')
</script>
</body>
核心问题:
当点击按钮时,age.value 会发生变化,但子组件并没有相应更新。这是因为我们之前的 processComponent 还没有处理更新的逻辑。
实现 updateComponent
1. 分离组件挂载与更新逻辑
首先,将组件的挂载和更新逻辑分离开来:
/**
* 处理组件的挂载和更新
*/
function processComponent(n1, n2, container, anchor) {
if (n1 == null) {
mountComponent(n2, container, anchor)
}
else {
updateComponent(n1, n2)
}
}
设计要点:
n1 == null:表示首次挂载,调用mountComponentn1 != null:表示更新,调用updateComponent
2. 组件实例复用
之前元素更新是复用 DOM 元素(el),而组件更新需要复用组件实例。为了让更新时能够访问到组件实例,我们需要在挂载时将实例保存到虚拟节点上:
function mountComponent(vnode, container, anchor) {
// 创建组件实例
const instance = createComponentInstance(vnode)
// 将组件实例保存到虚拟节点上,方便后续复用
vnode.component = instance
// 初始化组件状态
setupComponent(instance)
setupRenderEffect(instance, container, anchor)
}
核心设计:
vnode.component:存储组件实例- 更新时通过
n1.component获取旧实例,复用到新的虚拟节点n2.component
3. 挂载 el 属性
在实现组件更新之前,我们需要确保虚拟节点上正确挂载了 el 属性:
function setupRenderEffect(instance, container, anchor) {
const componentUpdateFn = () => {
if (!instance.isMounted) {
const { vnode } = instance
const subTree = render.call(instance.proxy)
patch(null, subTree, container, anchor)
vnode.el = subTree.el
instance.isMounted = true
}
else {
const { vnode, render } = instance
const prevSubTree = instance.subTree
const subTree = render.call(instance.proxy)
patch(prevSubTree, subTree, container, anchor)
vnode.el = subTree.el
instance.subTree = subTree
}
}
// ...
}
设计要点:
- 首次挂载时,
vnode.el = subTree.el将子树的根元素挂载到组件虚拟节点 - 更新时同样同步
el属性 - 这样虚拟节点就拥有了 DOM 元素的引用,便于后续复用
4. Props 变更检测
现在来实现 updateComponent,首先需要判断 Props 是否发生变化:
/**
* 检测 Props 是否发生变化
*/
function hasPropsChanged(prevProps, nextProps) {
const nextKeys = Object.keys(nextProps)
// Props 数量不同,说明有变化
if (Object.keys(prevProps).length !== nextKeys.length) {
return true
}
// Props 值不同,说明有变化
for (const key of nextKeys) {
if (nextProps[key] !== prevProps[key]) {
return true
}
}
return false
}
/**
* 判断组件是否需要更新
*/
export function shouldComponentUpdate(n1, n2) {
const { props: prevProps, children: prevChildren } = n1
const { props: nextProps, children: nextChildren } = n2
/**
* 任意一个存在 slot 就需要更新
*/
if (prevChildren || nextChildren) {
return true
}
/**
* 旧的 props 为空,新的 props 不为空
*/
if (!prevProps) {
return !!nextProps
}
/**
* 旧的 props 不为空,新的 props 为空
*/
if (!nextProps) {
return true
}
/**
* 两者都存在,检测 props 值是否变化
*/
return hasPropsChanged(prevProps, nextProps)
}
检测逻辑:
- 插槽变化:任意一个存在插槽,就需要更新
- Props 数量变化:新旧 Props 数量不同,需要更新
- Props 值变化:逐个比较 Props 的值,不同则更新
- Props 从无到有/从有到无:都视为需要更新
5. 组件更新逻辑
/**
* 更新组件
*/
function updateComponent(n1, n2) {
const instance = (n2.component = n1.component)
/**
* Props、slots 发生改变的时候才更新
* 不发生变化时,复用元素即可
*/
if (shouldComponentUpdate(n1, n2)) {
instance.next = n2
instance.update()
}
else {
/**
* 没有任何属性发生变化时,进行元素复用
*/
n2.el = n1.el
instance.vnode = n2
}
}
设计要点:
n2.component = n1.component:复用旧的组件实例instance.next = n2:保存新的虚拟节点,用于后续更新- 不需要更新时,直接复用
el和更新vnode引用
6. 更新前预渲染处理
调用 instance.update() 后会进入 componentUpdateFn,需要处理新的虚拟节点:
function componentUpdateFn() {
if (!instance.isMounted) {
// 首次挂载逻辑
}
else {
let { vnode, render, next } = instance
if (next) {
/**
* 有 next 说明是更新,复用组件实例完成 Props 的更新
*/
updateComponentPreRender(instance, next)
}
else {
/**
* 如果没有 next,还是使用之前的 vnode
*/
next = vnode
}
const prevSubTree = instance.subTree
const subTree = render.call(instance.proxy)
patch(prevSubTree, subTree, container, anchor)
next.el = subTree.el
instance.subTree = subTree
}
}
工作流程:
- 检查是否存在
next(新的虚拟节点) - 如果存在,调用
updateComponentPreRender更新组件实例 - 重新渲染子树
- Patch 更新新旧子树
7. Props 更新实现
/**
* 更新组件前的预处理
*/
function updateComponentPreRender(instance, nextVNode) {
instance.vnode = nextVNode
instance.next = null
updateProps(instance, nextVNode)
}
/**
* 更新 Props
*/
export function updateProps(instance, nextVNode) {
const { props, attrs } = instance
const rawProps = nextVNode.props
/**
* 设置新的 Props 和 Attrs
*/
setFullProps(instance, rawProps, props, attrs)
/**
* 删除之前有但现在没有的属性
*/
for (const key in props) {
if (!hasOwn(rawProps, key)) {
delete props[key]
}
}
for (const key in attrs) {
if (!hasOwn(rawProps, key)) {
delete attrs[key]
}
}
}
更新逻辑:
- 更新 vnode 引用:
instance.vnode = nextVNode - 清空 next:
instance.next = null,避免重复更新 - 设置新 Props:调用
setFullProps更新 Props 和 Attrs - 删除旧 Props:遍历旧的 Props,删除新 Props 中不存在的属性
总结
至此,我们完成了组件 Props 更新的完整机制:
1. 组件实例复用
- 通过
vnode.component存储组件实例 - 更新时复用旧的组件实例到新的虚拟节点
2. Props 变更检测
- 检测 Props 数量和值的变化
- 检测插槽的变化
- 检测 Props 的有无变化
3. 组件更新流程
updateComponent:判断是否需要更新componentUpdateFn:处理新的虚拟节点并重新渲染updateComponentPreRender:更新组件实例的 Props
4. Props 更新实现
- 设置新的 Props 和 Attrs
- 删除旧的 Props 和 Attrs 中不存在的属性
5. 优化策略
- 不需要更新时,直接复用 DOM 元素和虚拟节点
- 只在 Props 或 Slots 真正变化时才触发更新
这套机制确保了组件在 Props 变化时能够正确更新,同时通过组件实例复用和变更检测优化了性能,是 Vue 响应式系统的重要组成部分。