Vue3: 响应式 props 解构得到的变量将不是响应式?也不会更新?

Vue的 响应性语法糖 文档中 响应式 props 解构 部分提到了:

.value 类似,为了保持响应性,你始终需要以 props.x 的方式访问这些 prop。这意味着你不能够解构 defineProps 的返回值,因为得到的变量将不是响应式的、也不会更新。

真的是这样吗?解构 defineProps 的返回值得到的变量将不是响应式,也不会更新。


例子1 父组件响应式数据改变 => 子组件解构出来的 proxy 对象

  • 父组件每隔一段时间就更新响应式数据,在子组件的 onUpdated 钩子中总是可以看到更新后的数据
  • 就算我们给响应式数据的value重新赋值一个值,在子组件的 onUpated 钩子中也总是可以看到更新后的数据

🔗 Vue SFC Playground

// Parent.vue
<script setup>
import Child from "./Child.vue"
import { onMounted, ref } from "vue";
const person = ref([
  { name: "peter", age: 18 },
  { name: "tom", age: 20 }
])
let cnt = 0;
setInterval(() => {
  person.value[0].name += cnt;
  ++cnt;
}, 2000)

function update() {
  person.value = []
}
</script>
<template>
  <Child :person="person" />
  <button @click="update()">update person.value</button>
</template>
//Child.vue
<script setup>
import { onMounted, onUpdated } from "vue";

const { person } = defineProps(["person"]);

onMounted(() => {
  console.log("mounted person: ", JSON.stringify(person));
})

onUpdated(() => {
  console.log("updated person: ", JSON.stringify(person));
})
</script>
<template>
  <div v-for="item in person" class="flex gap-2 items-center">
    <span>{{item.name}}</span>
    <span>{{item.age}}</span>
  </div>
</template>
<style scoped>
.flex {
  display: flex;
}
.gap-2 {
  gap: 10px;
}
.items-center {
  align-items: center;
}
</style>

例子2:改变子组件解构出来的 proxy => 父组件的响应式数据

  • 子组件每隔一段时间就更新响应式数据,在父组件的 onUpdated 钩子中总是可以看到更新后的数据。在视图上我们也可以看到父组件上的响应式数据和子组件上解构出来的 proxy 在一起变化。

🔗 Vue SFC Playground

//Parent.vue
<script setup>
import Child from "./Child.vue"
import { onMounted, onUpdated, ref } from "vue";
const person = ref([
  { name: "peter", age: 18 },
  { name: "tom", age: 20 }
])

onMounted(() => {
  console.log("mounted person: ", JSON.stringify(person.value));
})

onUpdated(() => {
  console.log("updated person: ", JSON.stringify(person.value));
})
</script>
<template>
  <div v-for="item in person" class="flex gap-2 items-center">
    <span>{{item.name}}</span>
    <span>{{item.age}}</span>
  </div>
  <div>*****************************************************</div>
  <Child :person="person" />
</template>
<style scoped>
.flex {
  display: flex;
}
.gap-2 {
  gap: 10px;
}
.items-center {
  align-items: center;
}
</style>
//Child.vue
<script setup>
import { onMounted, onUpdated } from "vue";

const { person } = defineProps(["person"]);

let cnt = 0;
setInterval(() => {
  person[0].name += cnt;
  ++cnt;
}, 2000)

function update() {
}
</script>
<template>
  <div v-for="item in person" class="flex gap-2 items-center">
    <span>{{item.name}}</span>
    <span>{{item.age}}</span>
  </div>
  <button @click="update()">update person.value</button>
</template>
<style scoped>
.flex {
  display: flex;
}
.gap-2 {
  gap: 10px;
}
.items-center {
  align-items: center;
}
</style>
阅读 4.2k
avatarAI BotBETA

答案

在 Vue 3 中,当使用 defineProps 解构 props 时,得到的变量 person 实际上是响应式的,因为它仍然是对原始 prop 的引用。这意味着,当父组件中的响应式数据改变时,子组件中解构出来的 person 也会相应地更新。

在你的例子中,无论是父组件更新 person.value 还是子组件修改 person[0].name,这些更改都会反映在两个组件的视图中,因为 person 在两个组件中都是对同一个响应式引用的访问。

不过,有一个需要注意的是,尽管解构得到的 person 是响应式的,但在子组件中直接修改它(比如通过 person[0].name += cnt;)可能不是一个好的做法,因为这可能会绕过 Vue 的响应式系统。在 Vue 3 中,推荐使用 Vue 提供的响应式方法来修改状态,比如使用 reactiveref 创建的响应式对象或引用,并通过它们提供的 API(如 .value 或直接修改对象属性)来更新状态。

在你的例子中,由于 person 是一个数组,而数组元素本身也是对象,因此你可以直接修改对象属性而不需要担心响应性问题,因为对象的属性本身就是响应式的。但是,如果你尝试在子组件中重新赋值 person(比如 person = [...]),那么这将不会触发父组件的更新,因为 person 只是一个引用,而不是一个响应式引用本身。在这种情况下,你应该通过触发一个事件来通知父组件进行更新。

3 个回答

所谓“丢失响应性”,它指的是这种情况:👉传送门👈

注意组件一和组件二在解构写法上的区别。


这里涉及的就是 @陟上晴明 提到的 Props Destructure Transform 这一特性:

const { foo, bar } = defineProps(['foo', 'bar']);

这种写法,也和原题中的代码写法是一样的,即直接对 defineProps 的返回值解构。这种情况下父组件改变传入的 props,子组件也能获得最新的值。

但你依然不能这么写:

const props = defineProps(['foo', 'bar']);
const { foo, bar } = props;

注意这里不是直接解构 defineProps 的返回值,而是先用一个中间变量存储、然后再解构这个中间变量。这种情况下父组件再改变 props,子组件就不会得到最新的值了。因为 Props Destructure Transform 这个特性是发生在编译阶段的,Vue 会捕获 script setup 里对 defineProps 的解构;而后面这种写法的解构实际发生在运行阶段,Vue 捕获不到。

而所谓“丢失响应式”,针对的也是后一种这种写法。(当然了,没这个特性之前,前一种写法也一样丢失)


P.S. 多补充一点,为啥文档里要强调 props 的解构会失去响应式,是因为除了 script setup 以外,还有一种最常见的错误写法是这样的:

export default defineComponent({
  props: ['foo', 'bar'],
  setup(props) {
    const { foo, bar } = props;
  }
})

为啥是错误写法应该不难理解,就不解释了。

emmmmm……确实好像一直没有关注过 响应性语法糖 这个功能。但是在文档开头有提示:

已移除的实验性功能

响应性语法糖曾经是一个实验性功能,且已在最新的 3.4 版本中被移除,请阅读废弃原因。

所以我想既然废弃了,那么社区应该会有相关的讨论,同时应该也会有 defineProps 一些改进。
果然 👉 prop sugar · vuejs/rfcs · Discussion #394

这个 Feat 已经在 v3.2.20 被合并。

你说的对
对象的值取出来之后就没法响应变化了。
主要是因为 proxy 或者 defineProperty, 你通过
prop.x = y 它能知道你改 prop
但你写
const x = prop.x
x = y
它只知道你在改 x ,不知道你在改 prop ,也不知道你在使用 prop.
这是响应式必然结果, vue2 也是这样子.
prop.x 是原始类型时候会这样子, prop.x 是对象就还会知道它的变化.

推荐问题
宣传栏