一文了解Composition API新特性:ref、toRef、toRefs
在上一篇文章中,我们初步了解了vue3的新特性,今天,我们将延续Composition API的话题,来了解Composition API带来的新特性: ref
、 toRef
和 toRefs
。
下面开始进入本文的讲解✨
一、🙎如何理解ref、toRef和toRefs
1、ref、toRef和toRefs是什么
(1)ref
1)ref是什么
-
ref
可以生成值类型
(即基本数据类型) 的响应式数据; -
ref
可以用于模板和reative; -
ref
通过.value
来修改值(一定要记得加上.value
); -
ref
不仅可以用于响应式,还可以用于模板的DOM
元素。
2)举个例子🌰
假设我们定义了两个值类型的数据,并通过一个定时器来看它响应式前后的效果。接下来我们用代码来演示一下:
<template>
<p>ref demo {{ageRef}} {{state.name}}</p>
</template>
<script>
import { ref, reactive } from 'vue'
export default {
name: 'Ref',
setup(){
const ageRef = ref(18)
const nameRef = ref('monday')
const state = reactive({
name: nameRef
})
setTimeout(() => {
console.log('ageRef', ageRef.value,'nameRef', nameRef.value)
ageRef.value = 20
nameRef.value = 'mondaylab'
console.log('ageRef', ageRef.value,'nameRef', nameRef.value)
},1500)
return{
ageRef,
state
}
}
}
</script>
复制代码
别眨眼,来看下此时浏览器的显示效果:
大家可以看到,控制台先后打印的顺序是响应式前的数据和响应式后的数据。因此,通过 ref
,可以实现值类型的数据响应式。
值得注意的是, ref
不仅可以实现响应式,还可以用于模板的DOM元素。我们用一段代码来演示一下:
<template>
<p ref="elemRef">今天是周一</p>
</template>
<script>
import { ref, onMounted } from 'vue'
export default {
name: 'RefTemplate',
setup(){
const elemRef = ref(null)
onMounted(() => {
console.log('ref template', elemRef.value.innerHTML, elemRef.value)
})
return{
elemRef
}
}
}
</script>
复制代码
此时浏览器的显示效果如下所示:
我们通过在模板中绑定一个 ref
,之后在生命周期中调用,最后浏览器显示出该 DOM
元素。所以说, ref
也可以用来渲染模板中的DOM元素。
(2)toRef是什么
1)toRef是什么
toRef
可以响应对象Object
,其针对的是某一个响应式对象(reactive
封装)的属性prop
。toRef
和对象Object
两者保持引用关系,即一个改完另外一个也跟着改。toRef
如果用于普通对象(非响应式对象),产出的结果不具备响应式。如下代码所示:
//普通对象
const state = {
age: 20,
name: 'monday'
}
//响应式对象
const state = reactive({
age: 20,
name: 'monday'
})
复制代码
2)举个例子🌰
对于一个普通对象来说,如果这个普通对象要实现响应式,就用 reactive
。用了 reactive
之后,它就在响应式对象里面。那么在 一个响应式对象里面,如果其中有一个属性要拿出来单独做响应式的话,就用 toRef
。来举个例子看一看:
<template>
<p>toRef demo - {{ageRef}} - {{state.name}} {{state.age}}</p>
</template>
<script>
import { ref, toRef, reactive, computed } from 'vue'
export default {
name: 'ToRef',
setup() {
const state = reactive({
age: 18,
name: 'monday'
})
// // toRef 如果用于普通对象(非响应式对象),产出的结果不具备响应式
// const state = {
// age: 18,
// name: 'monday'
// }
//实现某一个属性的数据响应式
const ageRef = toRef(state, 'age')
setTimeout(() => {
state.age = 20
}, 1500)
setTimeout(() => {
ageRef.value = 25 // .value 修改值
}, 3000)
return {
state,
ageRef
}
}
}
</script>
复制代码
此时我们来看下浏览器的显示效果:
我们通过 reactive
来创建一个响应式对象,之后呢,如果只单独要对响应式对象里面的某一个属性进行响应式,那么使用toRef
来解决。用 toRef(Object, prop)
的形式来传对象名和具体的属性名,达到某个属性数据响应式的效果。
(3)toRefs是什么
1)toRefs是什么
- 与
toRef
不一样的是,toRefs
是针对整个对象的所有属性,目标在于将响应式对象(reactive
封装)转换为普通对象 - 普通对象里的每一个属性
prop
都对应一个ref
-
toRefs
和对象Object
两者保持引用关系,即一个改完另外一个也跟着改。
2)举个例子🌰
假设我们要将一个响应式对象里面的所有元素取出来,那么我们可以这么处理。代码如下:
<template>
<p>toRefs demo {{state.age}} {{state.name}}</p>
</template>
<script>
import { ref, toRef, toRefs, reactive } from 'vue'
export default {
name: 'ToRefs',
setup() {
const state = reactive({
age: 20,
name: 'monday'
})
return {
state
}
}
}
</script>
复制代码
此时浏览器的显示结果如下:
但是这样子好像有点略显麻烦,因为在模板编译的时候一直要 state.
,这样如果遇到要取很多个属性的时候就有点臃肿了。
既然太臃肿了,那我们换一种思路,把 state
进行解构。代码如下:
<template>
<p>toRefs demo {{age}} {{name}}</p>
</template>
<script>
import { ref, toRef, toRefs, reactive } from 'vue'
export default {
name: 'ToRefs',
setup() {
const state = reactive({
age: 20,
name: 'monday'
})
return {
...state
}
}
}
</script>
复制代码
此时浏览器的显示结果如下:
效果是一样的,看起来清晰了很多。但是呢……天上总不会有无缘无故的馅饼出现,得到一些好处的同时总要失去些原本拥有的东西。
对于解构后的对象来说,如果直接解构 reactive
,那么解构出来的对象会直接失去响应式。我们用一个定时器来检验下效果,具体代码如下:
<template>
<p>toRefs demo {{age}} {{name}}</p>
</template>
<script>
import { ref, toRef, toRefs, reactive } from 'vue'
export default {
name: 'ToRefs',
setup() {
const state = reactive({
age: 20,
name: 'monday'
})
setTimeout(() => {
state.age = 25
}, 1500)
return {
...state
}
}
}
</script>
复制代码
此时浏览器的显示结果如下:
我们等了好几秒之后,发现 age
迟迟不变成25,所以当我们解构 reactive
的对象时,响应式将会直接失去。
所以,就来到了我们的 toRefs
。 toRefs
在把响应式对象转变为普通对象后,不会丢失掉响应式的功能。具体我们用代码来演示一下:
<template>
<p>toRefs demo {{age}} {{name}}</p>
</template>
<script>
import { ref, toRef, toRefs, reactive } from 'vue'
export default {
name: 'ToRefs',
setup() {
const state = reactive({
age: 18,
name: 'monday'
})
const stateAsRefs = toRefs(state) // 将响应式对象,变成普通对象
setTimeout(() => {
console.log('age', state.age, 'name', state.name)
state.age = 20,
state.name = '周一'
console.log('age', state.age, 'name', state.name)
}, 1500)
return stateAsRefs
}
}
</script>
复制代码
此时我们观察浏览器的显示效果:
大家可以看到,用了 toRefs
,普通对象的值成功被取出来了,并且还不会丢失响应式的功能,该改变的值一个也不少。
(4)合成函数返回响应式对象
了解了上面三种类型的使用,我们再来看一种场景:合成函数如何返回响应式对象。下面附上代码:
function useFeatureX(){
const state = reactive({
x: 1,
y: 2
})
//逻辑运行状态,……
//返回时转换为ref
return toRefs(state)
}
复制代码
export default{ setup(){
//可以在不失去响应性的情况下破坏结构
const {x, y} = useFeatureX()
return{
x,
y
}
}}
复制代码
在第一段代码中,我们定义了一个函数,并且用 toRefs
将 state
对象进行返回,之后在组件里面直接调用响应式对象。
通过这样方式,让代码逻辑变得更加清晰明了,复用性更强。
2、最佳使用方式
通过上面的演示可以得出以下几点结论:
- 用
reactive
做对象的响应式,用ref
做值类型的响应式。 -
setup
中返回toRefs(state)
,或者toRef(state, 'xxx')
。 - 为了防止误会产生,
ref
的变量命名尽量都用xxxRef
,这样在使用的时候会更清楚明了。 -
合成函数返回响应式对象时,使用
toRefs
3、深入理解
讲完 ref
、 toRef
和 toRefs
,我们再来思考一个问题:为什么一定要用它们呢?可以不用吗?
(1)为什么需要用ref
-
值类型(即基本数据类型)无处不在,如果不用
ref
而直接返回值类型,会丢失响应式。 - 比如在
setup
、computed
、合成函数等各种场景中,都有可能返回值类型。 -
Vue
如果不定义ref
,用户将自己制造ref
,这样反而会更加混乱。
(2)为何ref需要.value属性
通过上面的分析我们知道, ref
需要通过 .value
来修改值。这看起来是一个很麻烦的操作,总是频繁的 .value
感觉特别琐碎。那为什么一定要 .value
呢?我们来揭开它的面纱。
-
ref
是一个对象,这个对象不丢失响应式,且这个对象用value
来存储值。 - 因此,通过
.value
属性的get
和set
来实现响应式。 - 只有当用于 模板 和 reactive 时,不需要
.value
来实现响应式,而其他情况则都需要。
(3)为什么需要toRef和toRefs
与 ref
不一样的是, toRef
和 toRefs
这两个兄弟,它们不创造响应式,而是延续响应式。创造响应式一般由 ref
或者 reactive
来解决,而 toRef
和 toRefs
则是把对象的数据进行分解和扩散,其这个对象针对的是响应式对象而非普通对象。总结起来有以下三点:
- 初衷: 在不丢失响应式的情况下,把对象数据进行 分解或扩散。
-
前提: 针对的是响应式对象(
reactrive
封装的)而非普通对象。 - 注意: 不创造响应式,而是延续响应式。
二、🙆♀️Composition API实现逻辑复用
1、规则
先来了解几条规则:
-
Composition API
指抽离逻辑代码到一个函数; - 函数的命名约定为
useXxxx
格式(React hooks也是); - 在
setup
中引用useXxx
函数。
2、举个例子🌰
引用一个非常经典的例子:获取鼠标的定位。接下来我们用Composition API来进行封装演示:
定义一个 js
文件,名字为 useMousePosition
,具体代码如下:
import { reactive, ref, onMounted, onUnmounted } from 'vue'
function useMousePosition() {
const x = ref(0)
const y = ref(0)
function update(e) {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => {
console.log('useMousePosition mounted')
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
console.log('useMousePosition unMounted')
window.removeEventListener('mousemove', update)
})
return {
x,
y
}
}
复制代码
再定义一个 .vue
文件,命名为 index.vue
。具体代码如下:
<template>
<p v-if="flag">mouse position {{x}} {{y}}</p>
<button @click="changeFlagHandler">change flag</button>
</template>
<script>
import { reactive } from 'vue'
import useMousePosition from './useMousePosition'
export default {
name: 'MousePosition',
return {
flag: true
},
setup() {
const { x, y } = useMousePosition()
return {
x,
y
}
},
changeFlagHandler() {
this.flag = !this.flag
},
}
</script>
复制代码
此时浏览器的显示效果如下:
了解完 ref
后,我们来实现这个功能看起来会清晰很多。我们先通过 ref
对 x
和 y
做响应式操作,之后通过 .value
来修改值,最终达到时刻获取鼠标定位的效果。同时,如果我们时刻保持着鼠标移动时不断改变值,这样子是非常耗费性能的。所以,我们可以通过一个按钮,来随时控制它的出现与隐藏。
大家可以发现,当隐藏的时候,随后会触发 onUnmounted
生命周期,组件内容随之被销毁。也就是说,使用的时候调用,不使用的时候及时销毁,这样子可以很大程度上提升性能。
三、🙅♀️结束语
通过上文的学习,我们可以知道, ref
、 toRef
和 toRefs
是 vue3
中 Composition API
的新特性,且 vue3
一般通过 ref
、 toRef
和 toRefs
来实现数据响应式。有了这三个内容,实现数据响应式看起来方便许多,而不再像 vue2
中那种处理起来很困难。
到这里,关于 ref
、 toRef
和 toRefs
的内容就讲完啦!希望对大家有帮助!
如有疑问或文章有误欢迎评论区留言或私信我交流~