先记住这个答案
在 Vue 3.5 中,script setup 里直接解构 defineProps 返回值后,同一编译作用域中的相应变量访问可以被转换成 props 属性访问,从而继续参与依赖追踪。这个能力针对特定的编译语法,不适用于任意 reactive 对象的普通解构。把解构值传给 watch 或外部组合式函数时,还要区分传入当前值与提供 getter;需要持续读取变化时通常传 getter,而不是只把当下的值传出去。
- 响应式 props 解构是编译器提供的语法能力
- 普通 reactive 对象解构仍按 JavaScript 值语义处理
- 跨函数边界传递时要区分当前值与 getter
为什么不能照搬到任意对象解构
普通 JavaScript 解构会把当时读到的属性值赋给局部变量。若它是数字或字符串,后来对象属性改变不会自动重新给这个变量赋值。Vue 3.5 的 props 解构之所以不同,是编译器理解了特定声明,并能在适用范围内改写后续访问,而不是改变了 JavaScript 的语言规则。
因此,把一个任意 reactive 对象解构后期待相同行为仍然可能出错;从辅助函数返回一个 props 对象再解构,也不能不加确认地视为同一种转换。需要通用的字段引用时,可以选择 props.xxx 访问、toRef 或明确的 getter,并查看项目编译设置与类型提示。
watch 与组合式函数为什么需要 getter
假设解构出了 pageSize,直接执行 watch(pageSize, callback) 传递的是当前读到的数字,watch 并没有拿到“以后该去哪里读新值”的入口。使用 watch(() => pageSize, callback) 可以让读取发生在被追踪的 getter 中;这是数据源接口的要求,不能仅凭局部变量看起来响应式就省略。
把 pageSize 传给自定义 usePager 也存在同样问题。若组合式函数只接收普通数字,它得到的是这次调用的值;若它约定接收 getter,就应在自己的响应式计算或监听中调用该 getter。仅在函数初始化时调用一次 getter 再保存普通值,仍可能把持续响应关系切断。
版本与默认值的边界怎样回答
Vue 3.4 及更早版本默认没有这项 3.5 的响应式 props 解构行为;特定项目可能使用实验配置或不同编译链,所以排查时应核对实际的编译工具版本。代码审查可以直接看编译产物是否保留了对 props 的访问,避免仅靠口头版本判断。
解构默认值也要按对应语法理解,不能把它等同于允许子组件修改父层输入。props 的单向数据流约定依旧存在。需要可编辑草稿时应明确复制初值与后续同步策略,需要通知父组件更新时使用组件约定的事件或模型接口,而不是在本地修改 props 绑定。
容易答错的地方
- 认为 Vue 3.5 以后所有解构都不丢响应式
- 该能力有明确的宏调用与编译作用域前提,不是对所有对象进行自动引用包装。普通函数、运行时返回对象和任意 reactive 解构都需要各自分析,不能用版本升级代替数据流检查。
- 把值传给外部函数后期待编译器重写它内部逻辑
- 编译器不会自动把任何函数参数变成可订阅的动态引用。函数接口必须明确接受值、ref 或 getter,且消费方式也要匹配;调用处的语法优化不能替代模块间契约。
面试官还会怎么问?
watch(() => pageSize) 与 watch(pageSize) 有什么区别?
前者提供一个可以在追踪过程中重新读取数据的函数,后者先求值再把结果当作数据源传入。对于数字等原始值,这两个接口形态不同,不能因为变量来自 props 解构就认为等价。
为什么同样的代码在测试里和页面里表现不同?
测试环境可能使用不同版本的 SFC 编译器,或没有经过与应用相同的 script setup 转换。应核对运行时、编译插件和测试转换器,而不只是检查 package.json 中某一个 Vue 版本字符串。
为了跨版本维护,最直观的写法是什么?
保留 props 对象并显式读取 props.xxx 通常更容易解释;需要拆分字段引用时使用相应 ref 工具或 getter。选择的目标是数据流明确与工具链一致,不是为了避开一种新语法而统一重写全部组件。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。