likes
comments
collection
share

Vue3+pinia踩坑,reactive 与 ref 的区别。

作者站长头像
站长
· 阅读数 18

下面是Vue3官方中对于 reactive 局限性的描述:

Vue3+pinia踩坑,reactive 与 ref 的区别。 我相信所有的人看到这段文本都不会意识到它具体意味着什么,我如果不是踩过坑,我也不太能够体会这段话的含义。本着“自己掉坑了不想别人继续掉坑里”的想法,我写了这篇文章。

掉坑

下面是复现坑的简略版本代码

//  @/stores/test.ts
import { reactive,ref } from "vue";
import { defineStore } from "pinia";

export const useStore = defineStore("test", () => {
  const dataList = reactive([{ name: "Tom", age: 18 }]);
  return { dataList };
});

// @/components/App.vue
<template>
  <button @click="handleClick">更新</button>
  <ul>
    <li :key="item.name" v-for="item of store.dataList">
      {{ item.name }}:{{ item.age }}
    </li>
  </ul>
</template>
<script lang="ts" setup>
import { useStore } from "@/stores/test";
const store = useStore();
function handleClick() {
  store.$patch({
    dataList:[{name:"Jack",age:20}]
  });
}
</script>

渲染后的UI:

Vue3+pinia踩坑,reactive 与 ref 的区别。

此时,点击更新按钮,我们就可以发现并没有发生更新,显然这是违反直觉的,因为我已经调用store.$patch去进行更新了,为什么UI并没有同步更新呢?

如果我们稍微修改一下代码:

// @/stores/test.ts
  /*省略其它无关代码*/
     //const dataList = reactive([{ name: "Tom", age: 18 }]);
     const dataList = ref([{ name: "Tom", age: 18 }]);
  /*省略其它无关代码*/

此处我们将reactive 用 ref 进行替换,此时刷新网页后,我们就可以发现,单机更新按钮后,UI发生了改变。

Vue3+pinia踩坑,reactive 与 ref 的区别。

为什么会这样?

导致这种区别的主要有两个方面的原因,第一个是因为 ref 与 reactive 之间的差异,第二个原因则是因为 pinia 的原因。

先探究第一个原因

众所周知,在Vue3中,ref 在 reactive 中会被自动解包,且解包后它们引用同一个源,例如下面的写法:

const msg = ref('Hello World!')
const obj = reactive({msg})
console.log(obj.msg === msg.value)//true
obj.msg="dog" //会导致 msg.value 的值也发生改变,并触发与 msg 有关的副作用

这很符合直觉,而且Vue3的文档也向我们说明了这件事情。但有时候,msg不单单只是"Hello World!"这种的基本数据类型,它也有可能会是一个引用数据类型。例如:

const msg = ref([{name:"Tom",age:18}])
const obj = reactive({msg})
console.log(obj.msg === msg.value)//true
obj.msg=[{name:"Jack",age:20}]//同样的,会导致 msg.value 的值也发生改变,并触发与 msg 有关的副作用

这种写法可能就会导致一些问题,一部分用户对于引用数据类型,可能并不喜欢用 ref,而喜欢用 reactive。此时写法就变成了:

const msg = reactive([{name:"Tom",age:18}])//对于引用数据类型用 reactive
const obj = reactive({msg})
obj.msg=[{name:"Jack",age:20}]//此时如果这样更新,msg将不会发生改变,这导致的结果是“响应式连接丢失”

此处回到了文章一开头的Vue3文档所说的关于reactive的局限性的说明。也即,reactive 与 ref 间差异的地方是:一个 reactive 包裹的数据如果传递给另一个 reactive 数据的某个属性时,修改该 reactive 的属性的值为其它值时,其与一开始引用的 reactive 的响应性会断开,此种写法无法影响到一开始的 reactive,而如果使用的是 ref,则响应式连接依然存在,能够影响到ref。

这意味着一种可能,就是如果两个 reactive 的某个属性如果引用的是同一个ref,则修改某个 reactive,会导致另一个 reactive 也发生改变。(reactive 与 reactive 之间,通过引用同一个 ref 连接到了一起)

const msg = ref([{name:"Tom",age:18}])
//注意 obj 和 obj2 ,引用了同一个 ref.
const obj = reactive({msg})
const obj2 = reactive({a:msg})
obj.msg=[{name:"Jack",age:20}]
//在这两个 reactive 某个属性引用同一个ref的情况下,
//更新obj.msg会导致obj2.a也发生更新,并触发其相关副作用
console.log(obj2.a)//[{name:"Jack",age:20}]

如果不是 ref ,而是 reactive ,则无法实现这种连接。

const msg = reactive([{name:"Tom",age:18}])
const obj = reactive({msg})
const obj2 = reactive({a:msg})
obj.msg=[{name:"Jack",age:20}]//连接断开了,此处修改不会影响到其它reactive
console.log(obj2.a)//[{name:"Tom",age:18}]

综上所述,ref 显然比之于 reactive 要强大,因为它可以建立一种响应式连接,这种连接甚至能够使得重新赋值这个操纵会同步到其它引用同一个ref的响应式对象中。这也是为什么 Vue3 推荐使用 ref 的原因(哪怕是对于引用数据类型)。

回到问题的一开始,在了解到 ref 与 reactive 的这种差异后,我们好像已经可以理解为什么使用 ref 替换 reactive 能够使得点击更新按钮后UI更新了。同时我们也能够察觉到 pinia 的实现恐怕有些问题。

第二个原因,pinia 的问题

ref 替换 reactive 之后,更新的问题得到了解决。而我们已经了解到 ref 比之于 reactive 最大的优势在于其能够在多个 reactive 之间建立响应式连接。那么我们这里可以大胆的推断,使用 useStore() 后返回的 store 与 pinia 内部保存状态的某个对象其实是两个 reactive,只有这样才会导致使用 reactive 与 ref 之间效果的不同。(看过 pinia 源码的我大胆推断说出答案)

实际上,store 与 pinia 内部保存状态的某个对象确实是两个不同的 reactive,因此在使用 ref 的时候,这两个 reactive 能够依托 ref 的特性而建立连接,而当使用 reactive 的时候,这种连接就容易断开。

下面是console.log(pinia)的打印输出,

Vue3+pinia踩坑,reactive 与 ref 的区别。

约定:

  • pinia 指的是 createPinia() 返回的结果
  • store 指的是 useStore() 返回的结果

pinia 的状态保存的位置有下面这些:

  • pinia.state.value[id]:获取到某id对应的 store 的 state
  • pinia_s.get(id):获取到某id对应的 store
  • store.$state:一个getter/setter,其从相应的 pinia.state.value[id]中获取值

主要有两个保存状态的地方,一个是 store,另一个则是 pinia.state.value[id] (后面用piniaState 指代)。

回到掉坑的例子中:

在初始化的时候,store.dataList 与 piniaState.dataList 指向的是同一个目标,也即用户在defineStore()中返回的 dataList 所对应的值。

但当调用 store.$patch() 后,其内部更新的实际上是 piniaState.dataList,此时如果一开始的 dataList 我们是通过 reactive 创建的话,那么 piniaState.dataList = newDataList 这一操作就会导致 store.dataList 与 piniaState.dataList 间的连接断开,store.dataList 指向的值依然是旧值,而piniaState.dataList 指向的值则是新值。

一般情况下,我们传递给组件的 props 都是从 store 上读取的,这就导致了一个问题(也即我踩的坑),当我们使用 store.$patch 去进行更新的时候,会发现更新失败了,UI没有发生任何改变。

这显然是有问题的,我用 pinia 去进行状态管理,我竟然还需要在创建 state 的时候注意使用的是 ref 还是 reactive,这显然是不合理的。而这种不合理的原因在于 pinia 在进行状态管理的时候,使用了两个 reactive。一个是给用户使用的 store,另一个则是内部使用的 piniaState,且 pinia 默认了用户创建状态使用的是 ref,而 ref 可以将两个 reactive 连接起来。

解决方案

  • pinia 修复该问题,pinia 将通过 store 访问的 state 指向 piniaState,统一用一个对象进行 state 管理。
  • 将从store读取数据的(store.dataList)写法改为从 store.$state 读取。
  • 用 ref 代替 reactive 去创建 state。
  • 不使用 defineStore(id,setupFunction) 的语法,而使用 defineStore(id,options) 的语法。(options语法内部使用 ref 包裹 state)
  • 写个 pinia 插件,同步一下 store 与 piniaState:
pinia.use(function ({ store, pinia }) {
  Object.entries(toRefs(store.$state)).forEach(([key, ref]) => {
    store[key] = ref;
  });
});

总结

  1. ref 与 reactive 的一个区别是,ref 能够在 reactive 间建立连接。
  2. pinia 的源码实现存在问题(v2.1.6),setupFuncion 语法中用 ref 创建state 与 reactive 创建 state 存在差异,其语义有差别。
转载自:https://juejin.cn/post/7278931167519801381
评论
请登录