页面打开一片空白,控制台只有一行很克制的警告:
Invalid prop: type check failed for prop "options". Expected Array, got Object。你检查了数据明明是个数组,为什么模板里就变成了 Object?这篇文章从一次真实的 Vue 3 排障出发,讲清楚script setup里 ref 自动解包到底解到了哪一层。
一、现象:明明返回数组,模板却提示 Object
业务里常见的一种组织方式是"Hook 组合式函数":把一堆逻辑(查询、分页、筛选状态)收进 useSomething(),页面里解构出来直接用。
某个页面结构大致是这样------一个外层 view 引用了组合函数和子组件:
vue
<script setup>
const { baseOptions, tableData, loading } = useSomething();
</script>
<template>
<MySelect :options="baseOptions" />
<MyTable :data-source="tableData" loading="loading" />
</template>
子组件里按常规方式声明了 prop 类型:
vue
<script setup>
defineProps({
options: { type: Array, required: true },
dataSource: { type: Array, default: () => [] },
});
</script>
结果一运行,页面白屏,控制台冒出:
typescript
Invalid prop: type check failed for prop "options". Expected Array, got Object
诡异的是,你在 Hook 里把数据 log 出来,它明明就是 [] 或一个真实数组。
二、根因:嵌套对象里的 ref 不会被自动解包
问题出在 Vue 3 的 ref 解包机制 在 script setup 中的边界上。
Vue 3 有个便利特性:在模板里,顶层 ref 会自动解包 ------你写 {{ count }},哪怕 count 是个 ref(0),渲染的也是 0 这个值,不用写 count.value。这是很多人爱 script setup 的原因之一。
但请注意关键字:顶层(top-level) 。只有 script setup 顶层的绑定、且是解构出来的顶层变量,才会在模板里被自动解包。
如果组合函数返回的是嵌套对象,情况就完全不同了:
js
export function useSomething() {
const baseOptions = ref([]);
const tableData = ref([]);
return { baseOptions, tableData }; // ref 被包在对象里
}
页面里这样解构:
js
const result = useSomething();
// 模板里用 result.baseOptions
此时 result.baseOptions 是一个 ref 对象,而不是数组。模板中 ref 的解包只发生在"顶层绑定"上------
const { baseOptions } = useSomething()解构到顶层 → template 自动解包,正常;const result = useSomething()用result.baseOptions→ 不自动解包,你拿到的是 ref 对象。
上面那个报错的例子,很可能是把 Hook 的返回值整体接管后,把 ref 包装对象直接丢给了子组件 prop:
js
const state = useSomething(); // 嵌套对象里全是 ref
于是 :options="state.baseOptions" 传过去的实际上是个 Ref 对象,而 prop 声明是 Array,类型校验自然炸了------数据确实有,但它不是你以为的形状 。页面空白,是因为模板里拿着 ref 对象去当数组用(length、迭代都取不到正确值),渲染不出来。
三、修复:顶层解构 + 正确的解包位置
修复方式很直接,就是让 ref 落到能被自动解包的位置。
方案 A:顶层解构(最推荐)
把组合函数返回值里你需要的 ref,在 script setup 顶层直接解构出来:
vue
<script setup>
// 顶层解构 → 模板自动解包
const { baseOptions, tableData, loading } = useSomething();
</script>
<template>
<MySelect :options="baseOptions" />
<MyTable :data-source="tableData" :loading="loading" />
</template>
这样 baseOptions 就是被解包后的数组,prop 类型校验通过,页面恢复显示。
关于"为什么顶层解构的解包能生效"要理解一点:Vue 编译 script setup 的模板时,baseOptions 已成为顶层绑定,模板访问它会走 unref(),把 ref 解开。而 state.baseOptions 这种"属性访问"不会被编译器特殊处理。
方案 B:Hook 内部若必须整体返回对象,用 toRefs 摊平
如果你确实想整体接管返回值(比如在一个函数里统一维护状态),可以这样处理,让每个字段仍是可解包的 ref:
js
import { ref, toRefs } from 'vue';
export function useSomething() {
const stateRef = reactive({ baseOptions: [], tableData: [] });
return toRefs(stateRef); // 每个都是 ref
}
但在模板里直接 result.baseOptions 依然不会自动解包------除非再解构一层。所以最省心的还是方案 A:在页面 script setup 顶层解构需要传给子组件 prop 的 ref。传 prop 前,让数据要么已是原始值,要么已是可解包的顶层绑定。
四、另一个相关乌龙:Prop 类型自己写翻了
这次排障还带出一个容易和上面混淆的小坑:type: Array 写成了 Array,但实际传的本来就是数组,却仍报错。
常见原因不是解包,而是类型字符串写错 或默认值/工厂函数写错:
js
defineProps({
options: { type: Array, default: [] }, // ❌ 引用类型默认值必须是工厂函数
});
defineProps({
options: { type: Array, default: () => [] }, // ✅ 正确
});
Vue 会警告"Props with type Object/Array must use a factory function to return the default value"。虽然报错文案不同,但同样会造成页面空白或渲染异常,排查时别漏掉这个更基础的点。
五、方法论:报错前先确认"数据的真实形状"
回看这次排障,最有价值的不是那行修复,而是定位思路:先确认你手里那个值,到底是什么。
- 控制台 log 一下,看
typeof和Array.isArray------它到底是数组还是 ref 包装对象? - 看它来自哪里:是顶层解构出来的,还是从嵌套对象属性里取出来的?
- 如果是嵌套对象,模板里它不会被自动解包,prop 拿到的就是一个 ref 对象。
把"引用怎么传的"和"Vue 在哪一层帮你解包"对上号,这类 Invalid prop 白屏问题通常五分钟内就能定位。
结语
script setup 的 ref 自动解包很爽,但它有明确的边界------只有顶层绑定才会被模板自动解包 。把 ref 塞进嵌套对象再整体传给子组件,就绕开了这个便捷机制,换来一串 Invalid prop 和一次白屏。
记住这句话:传 prop 之前,让数据是"被解包后的原始值"。你就能和大多数诡异的白屏说再见。
你在 Vue 3 里有没有被 ref 解包坑过?欢迎聊聊你遇到过的"Expected X, got Y"查因经历。