Vue3 的响应式心智负担

这数据是不是响应式的?能否解构?是 Reactive 还是 Ref?取值要不要加个 `.value`?

我们部门从去年开始全面使用 Vue3 开发,响应式导致的 bug 一直困扰着日常编码。最常见的:「解构导致的响应式失效」,即便搞懂了其中的门门道道,这个问题也总会以意料之外的形式出现。比如:

// hook.js
export const useTest = () => {
const count = ref(0);
const double = computed(() => {
return count.value * 2;
});
function add() {
++count.value;
}
return {
count,
double,
add
};
};
// App.vue
const {count, double, add} = useTest()

以上是一个自定义的 Composition 方法。很简单,使用的时候,可以直接解构其中的值出来用,调用 add 方法,countdouble 都会自动更新。没有任何问题。

但是,如果使用了状态管理库 - Pinia,同样的代码:

// Pinia
export const useTestStore = defineStore("test", () => {
const count = ref(0);
const double = computed(() => {
return count.value * 2;
});
function add() {
++count.value;
}
return {
count,
double,
add
};
});
// App.vue
const {count, double, add} = useTestStore()
// ERROR!count double 失去响应式

一模一样的定义,使用了 refcomputed 创建响应式数据,返回一个对象。但如果使用「解构」语法,countdouble 就会失去响应式。

你可以一眼看出这是为什么吗?翻看 Pinia 的文档

Note that store is an object wrapped with reactive, meaning there is no need to write .value after getters but, like props in setup, we cannot destructure it

简单来说,即使你 Pinia Store 的定义返回的是一个普通对象,也会被 Pinia 包装成 reactive 对象,而 reactive 对象解构会失去响应式。想要解构?需要:

const store = useTestStore()
const { count, double, add } = storeToRefs(store)

我知道,这是一个 Pinia 的 API 设计问题。你可以说:「是你自己不认真看文档呀」。OK,确实是这样。Wait...but... 可是明明一模一样的代码,表现为什么不一样呢?遇到这个问题,一番折腾,掌握这个「冷」知识,成为了更有经验的开发者。可其他同事可能还不知道,就需要沟通踩坑心路历程,或者组织团队培训。一切就绪,那换一个库呢?换一个场景呢?

维护管理响应式无疑是一个心智负担,我要时刻挂念着某个数据是否是响应式的、能否解构、是 Reactive 还是 Ref,取值要不要加个 .value、某个库返回的可能又有不同...这些负担会随着团队人数,项目复杂度的增加而放大。虽然借助 Lint 和 TypeScript 可以降低犯错概率,但心智负担始终存在。

每当这个时刻,我就很想念 React 的 Immutable Data 的简洁。虽然可能多渲染几次,但数据就是数据,no wrap,no shit。