页面白屏 + Invalid prop: type check failed for prop "options"?原来是 script setup 的 ref

页面打开一片空白,控制台只有一行很克制的警告: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"查因经历。

相关推荐
小码哥_常5 小时前
一套自己部署的 IM,客户端是同一份 Flutter
前端·后端
蜗牛攻城狮8 小时前
微前端四大方案选型对比与最佳实践:qiankun、micro-app、wujie、Module Federation
前端·微前端·qiankun·前端架构·micro-app·wujie
她说可以呀9 小时前
Vue插槽
前端·javascript·vue.js
qq_448011169 小时前
Vue + Jinja2两套模版与法冲突造成undefinedError
前端·javascript·vue.js
aixingpan9 小时前
aixingpan.cn API开发文档:前端查询夏令时和时区接口指南
前端·php
2601_9583529010 小时前
23.5×19mm 换掉一整条音频链?|A-68 集成了什么:ADC / DAC / 24.576MHz 时钟 / AEC / 波束降噪,主控仅需 6 根线
前端·音视频·ai降噪·回声消除·语音模块
志尊宝10 小时前
AI原生前端开发(039):AI辅助的Nuxt 4全栈开发
前端·vue.js·ai-native
FRing0311 小时前
JavaScript入门到精通三十二(深拷贝、浅拷贝、递归函数)
开发语言·前端·javascript
excel12 小时前
双 Token 与前端使用 Crypto 公私钥鉴权换 Token,哪种方案更好?
前端·后端
外收内放12 小时前
Python基础语法练习题(73)
java·前端·python