下面对官网这句话进行详细剖析
attrs 和 slots 都是有状态的对象,它们总是会随着组件自身的更新而更新。此外还需注意,和 props 不同,attrs 和 slots 的属性都不是响应式的。
attrs和slots会随着组件自身的更新而更新 这意味着当父组件向子组件传递的attrs(非props的透传属性)或插槽内容发生变化时,子组件中的attrs和slots的内容会实时更新。
示例代码
父组件:
<template>
<ChildComponent class="updated-class" :title="dynamicTitle">
<template #default>
<p>Dynamic Slot Content: {{ dynamicContent }}</p>
</template>
</ChildComponent>
</template>
<script>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
export default {
components: { ChildComponent },
setup() {
const dynamicTitle = ref('Initial Title');
const dynamicContent = ref('Initial Slot Content');
setTimeout(() => {
dynamicTitle.value = 'Updated Title'; // 更新 attrs
dynamicContent.value = 'Updated Slot Content'; // 更新插槽
}, 2000);
return { dynamicTitle, dynamicContent };
},
};
</script>
子组件:
<template>
<div>
<p>Title Attr: {{ attrs.title }}</p>
<p>Class Attr: {{ attrs.class }}</p>
<slot></slot>
</div>
</template>
<script>
export default {
setup(props, { attrs }) {
return { attrs };
},
};
</script>
attrs和slots不是响应式的 虽然attrs和slots的内容会更新,但它们自身不是响应式对象,无法通过watch或computed自动追踪变化。
<script>
export default {
setup(props, { attrs }) {
console.log(attrs); // 初始打印的 attrs
// 不会触发响应式更新
const untrackedAttr = attrs.title;
console.log(untrackedAttr); // 只打印初始值,后续变化不会自动更新
return { attrs, untrackedAttr };
},
};
</script>
为什么不会更新?
通过 const title = attrs.title; 提取 attrs.title 的值时,title 是一个静态的本地变量,它只在声明时取了 attrs.title 的初始值,后续父组件更新 attrs.title 时,title 不会同步更新。
- 避免解构使用:
解构会丢失更新,需要始终通过
attrs.x或slots.x的形式访问。
<script>
export default {
setup(props, { attrs }) {
const { title } = attrs; // 解构后 title 不会更新
return { title, attrs }; // attrs 仍然能更新
},
};
</script>
- 在
onBeforeUpdate中处理副作用 如果想要在attrs或slots更新时执行一些逻辑(副作用),需要使用onBeforeUpdate钩子捕获更新时机。
示例代码
<script>
import { onBeforeUpdate } from 'vue';
export default {
setup(props, { attrs, slots }) {
onBeforeUpdate(() => {
console.log('Attrs Updated:', attrs);
console.log('Slots Updated:', slots);
});
return { attrs, slots };
},
};
</script>
- 想要解构并动态更新的效果,可以理解为需要对
attrs的属性(如title)进行 依赖收集。当attrs.title更新时,触发依赖通知,更新解构后的title变量,实现响应式。
如何实现依赖收集?
方式 1:使用 Vue 的响应式工具
可以用 ref 或 computed 包装解构后的值,实现响应式。
setup(props, { attrs }) {
const title = computed(() => attrs.title); // 动态计算 attrs.title
return { title };
},
方式 2:手动在生命周期中同步更新
setup(props, { attrs }) {
const title = ref(attrs.title); // 创建响应式变量
onBeforeUpdate(() => {
title.value = attrs.title; // 在组件更新前同步 attrs.title
});
return { title };
},
方式 3:通过代理实现响应式监听
使用 reactive 对 attrs 进行浅代理,对 attrs 的属性访问触发依赖收集。
setup(props, { attrs }) {
const reactiveAttrs = reactive(attrs); // 通过 reactive 包装 attrs
const { title } = toRefs(reactiveAttrs); // 将 title 解构为响应式变量
return { title };
},
- 如果
attrs频繁更新且属性较多,手动依赖收集可能带来额外性能开销。优先通过computed或onBeforeUpdate钩子实现更新。 attrs是 Vue 提供的一个特殊的 非响应式对象,reactive包装attrs,使得其属性具备响应式能力。reactive(attrs)的作用是为attrs添加响应式系统的依赖收集和通知更新能力。toRefs的设计目标是将一个响应式对象的每个属性解构为单独的响应式ref。使用toRefs将reactiveAttrs.title转化为独立的响应式ref,方便解构使用。toRefs需要包装在响应式对象上工作。如果输入的对象(如attrs)不是响应式对象,toRefs返回的title只是一个普通的变量,不具备响应式能力。
const reactiveAttrs = { title: 'Initial Title' }; // 普通对象
const { title } = toRefs(reactiveAttrs); // title 不是响应式的
console.log(title.value); // undefined,因为 reactiveAttrs 不是响应式对象
推荐写法:
- 始终使用
reactive(attrs)包装attrs后,再使用toRefs解构其属性。