浅析ref与reactvie的区别
2024-12-09 11:13:16

最近新入职的公司需要使用Vue3做项目,所以我这个V3的排斥者也只能选择了这个方法。

对于V3,除了一年前我稍微了解了一下,后续就没有跟进,我个人对技术追星并未有太多的兴趣。

直到我见识了这家公司项目的写法,我只能说,V3确实有那么点的意思。

正文

本文虽说是探讨ref和reactive的区别,但不会深入探讨具体的源码解析,只更倾向于如何在项目中更好的使用。

毕竟,我们不是造轮子的,我们是用轮子的。

在现在这个前端大爆炸的时代里,我们已经不缺一个好用的框架,更缺的是一个用好框架的思路。

refs与reactive的区别

不管怎么说,在正式讲使用心得之前,还是要简单说说二者的区别。

ref

响应式改变需要使用变量.value,被其包裹的变量可以是简单类型,也可以是引用类型。

在开发中,我们常用来包裹一些特殊的响应式变量,通用形式的变量我个人不喜欢用ref,如果是引用类型的话,更倾向于是用reactive()

1
2
3
4
5
6
7
8
9
10
11
12
13
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
</script>

<template>
<button @click="increment">
{{ count }}
</button>
</template>

reactive

同样的代码,reactive不需要用变量.value这种写法,可以直接引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
<script setup>
import { reactive } from 'vue'
const state = reactive({ count: 0 })
function increment() {
count.value++
}
</script>

<template>
<button @click="increment">
{{ count }}
</button>
</template>

不过,reactive有一定的局限性。

  1. 有限的值类型:它只能用于对象类型 (对象、数组和如 MapSet 这样的集合类型)。它不能持有如 stringnumberboolean 这样的原始类型

  2. 不能替换整个对象:由于 Vue 的响应式跟踪是通过属性访问实现的,因此我们必须始终保持对响应式对象的相同引用。这意味着我们不能轻易地“替换”响应式对象,因为这样的话与第一个引用的响应性连接将丢失

    1
    2
    3
    4
    5
    let state = reactive({ count: 0 })

    // 上面的 ({ count: 0 }) 引用将不再被追踪
    // (响应性连接已丢失!)
    state = reactive({ count: 1 })
  3. 对解构操作不友好:当我们将响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,我们将丢失响应性连接

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const state = reactive({ count: 0 })

    // 当解构时,count 已经与 state.count 断开连接
    let { count } = state
    // 不会影响原始的 state,这里的响应式无效
    count++

    // 该函数接收到的是一个普通的数字
    // 并且无法追踪 state.count 的变化
    // 我们必须传入整个对象以保持响应性
    callSomeFunction(state.count)

refs与reactive的使用方式

二者都是V3提供的响应式语法糖,但在开发过程中,很多时候二者会混用,实际上,不该那么用。

个人参考了别人的案例,整理了一下规则

  1. 只用ref声明响应式变量
  2. 只有当第三方库的操作对象需要具备响应性时,才使用shallowRef
  3. 只有当逻辑块完整时,才使用reactive包裹住那块的业务

ref声明响应式变量

虽然reactive声明的变量也具备响应性,但是,想要将v3用的简单,不推荐混用ref与reactive。

因此,无论是对象、数组、数字、字符串、布尔值、HTML引用,都用ref来声明

  • 不推荐

    refreactive混着用,这样的响应式经常会无意识在使用变量时候要不要使用.value产生心智负担,影响开发效率。

    1
    2
    3
    4
    const tableData = reactive([]);
    const obj = reactive({})
    const a = ref([])
    const b = ref(1)
  • 推荐

    只用ref声明响应式变量,这样,相关的变量我们直接就用.value来改变响应式的变量即可

    1
    2
    3
    4
    5
    6
    7
    const arr = ref([])
    const obj = ref({})
    const num = ref(0)
    const str = ref('')
    const bool = ref(false)
    const nil = ref(null)
    const udf = ref()

shallowRef操作第三方库的对象

什么是第三方库的操作对象?指的是那些不是开发时声明出来的,而是第三方库API创建并暴露出来给你操作的对象。

比如Echart初始化之后会产生一个对象,允许你调用其setOption方法来更新图表,这个对象就属于第三方库的操作对象。

通常情况下,对于第三方库的操作对象,是不需要添加响应性的:

1
2
3
4
let mychart = null
onMounted(()=>{
mychart = echarts.init(document.getElement('chart'))
})

但是在有些场景下,你需要让这个对象具备响应性:比如你需要以props的形式将这个操作对象传递给子组件,并且这个操作对象还可能发生变化,你希望子组件也能跟着变化。

那么这时候你应该只使用shallowRef这个API,为这些你不知道底细的第三方库产生的对象提供响应性支持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let mychart = shallowRef(null)
onMounted(()=>{
createNewChart()
})

function onClick(){
createNewChart()
}

function createNewChart(){
// 销毁旧对象
if(mychart.value){
mychart.value.dispose()
}

// 启用新对象
mychart.value = echarts.init(document.getElement('chart'))
}
  • 为什么不能使用ref给这些对象提供响应性?因为refshallowRef的区别在于,ref会遍历整个对象,给对象的每个属性都创建响应性,无论是多深的对象,你给任何一个属性赋值,都会刷新界面。而shallowRef相反,shallowRef只有浅层的响应式处理,只有给其.value赋值时,才会触发界面刷新。
  • 这就导致一些问题,当第三方库的操作对象也在监听内部数据自我更新时,就会产生一种:“你更新了,我监听到了,我更新;我更新了,你监听到了,你又更新;你又更新了,我监听到了,我又又更新…”的死循环中,然后导致页面崩溃。
  • 所以对于你不知道底细的对象(通常情况下也就只有第三方库会产生),直接使用shallowRef创建响应性。

reactive包裹完成的逻辑块

什么是 组合式函数,指的是那些将响应式变量封装起来的函数。官方文档传送门:组合式函数 | Vue.js (vuejs.org)

组合式API最大的优势在于函数级别的复用,这也是最不同于V2的地方,V2想要复用一块完成的逻辑块,大多用mixinjs混合,但是很多情况下并非特别好用。

我看到项目中这套配合动态表单的组合式风格,简直是惊为天人。

这样的代码逻辑太过清晰易读,简直比之前V2写的低代码还牛逼。

  1. 逻辑模块聚合原则:相同逻辑的代码必须写在一起
  2. 业务代码优先原则:在无复用的需求下,不需要将业务代码高度封装成组合式函数。
  3. 核心模块保留原则:一个vue组件改动最频繁且无复用的逻辑代码,属于核心代码,应该保留在vue组件里而不是ts文件里。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**@table - 用reactive包装,将变量和方法都封在这一个逻辑块中,这块的算是最核心的代码*/
const table = reactive({
column:[],
data:[],
listLoading: false,
request: ()=>{
// 封装请求,这里我们赋值也不需要从.value什么的拿,直接用table.data就能获取列表的值,简直方便,省的ref那种搞法了。
},
add:()=>{},
edit:()=>{},
delete:() =>{}
})

/**@module XX1模块 - 日常开发时优先采用这种命令式直接写业务逻辑,不要包裹到table中,便于特殊处理,如果业务逻辑过于复杂,就抽离到一个单独的TS文件中*/
function todoA(){}

/**@module XX1模块 - 钩子这块直接放置所有需要触发的机制*/
onMounted(()=>{
table.request()
})

如果你是选项式的粉丝,你甚至可以这样玩:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**@module 模块1*/
const datasetMod = reactive(useOptAPI({
data(){
return {
dataset:{}
}
},
methods:{
async req2GetData(){}
}
}))

/**@module 模块2*/
const fileListMod = reactive(useOptAPI({
data(){
return {
atvFile:"",
fileList:[],
fileListRef:null
}
},
computed:{
filterFileList(){}
},
watch:{
atvFile(){}
},
methods:{
async req2GetFileList(){},
async onNextFile(){},
async onPrevFile(){}
}
}))

当然,后边这个写法有点玩票的性质,前者的写法亲自体验之后,惊为天人,设计这套写法的人,抽的太干净了,稍微设计一下,几乎就是低代码的写法。

核心逻辑配合动态表单,所有的业务代码简直是快速生成的利器。

选项式与组合式

以下这段话说的比较虚,并不像上边那么务实,算是这么长时间使用之后,个人的一点心得。

选项式写法,即传统V2写法,按照已经框好的代码块,变量和方法都被放在了规定的位置。

组合式写法,相对前者更为自由,没有V2规定好的变量块和逻辑块,更偏向于传统JS式的写法,开发者只要将对应的逻辑块组到一块就可以,这样更便于代码阅读。

这两种写法均被Vue3支持,可根据自己习惯而定。

根据之前项目踩坑的经验来看,如果团队的代码能力不足,不建议使用组合式写法。

不规范且不同习惯的组合式代码放在一起,在后期维护的时候会非常让人崩溃,vue2的写法虽然死板一些,但是某种意义上也增加了规范,至少有一定的可读性。

我个人认为,组合式的写法想要用的过瘾,需要以下几点

  1. 团队有一定的前端开发基础,大体的开发人员水准不会太差,这样能保证组合式代码写出来的可读性不会太差。
  2. 有一份不错的动态组件库,直接用半个配置化写法整合所有的代码,这样可以大规模减少非业务的代码,减少代码阅读的心智负担
  3. 前端有一份通用的表单处理规范,这样阅读他人的代码逻辑时候,心智负担不会太高,再写好一个完美的个例之后,便于快速复用,其他人能很快读懂。

总结

  1. 为了避免认知混乱,基本上都使用ref进行声明响应式变量
  2. 为了避免页面卡死,对于那些你不知道层级结构的数据,使用shallowRef为它创建响应性
  3. reactive推荐用来包裹一些完整的逻辑块,在reactive中写方法引用变量会非常方便,而且也便于阅读

结语

vue3的升级,果然不仅仅是一次写法上的升级,动态组件库配合组合式写法,简直超神。

一篇业务模块的文件中,几乎没有多少业务代码,而且相对V2的难于抽离,V3的可以直接将弹窗封在子组件中,这种感觉真不错。

参考

Vue的ref、shallowRef、reactive到底要怎么用!!?

我们团队是如何用好vue3 setup组合式API的?