原始值指的是 Boolean、Number、 Big?nt、String、Symbol、undefined 和 null 等类型的值。在 JavaScript 中,原始值是按值传递的,而非按引用传递。这意味着,如 果一个函数接收原始值作为参数,那么形参与实参之间没有引用关 系,它们是两个完全独立的值,对形参的修改不会影响实参。另外, JavaScript 中的 Proxy 无法提供对原始值的代理,因此想要将原始值 变成响应式数据,就必须对其做一层包裹,那就引入了ref的概念
1.ref的概念
由于 Proxy 的代理目标必须是非原始值,所以我们没有任何手段 拦截对原始值的操作,例如:
ini
let str = 'vue'
str = 'vue3'
对于这个问题,我们能够想到的唯一办法是,使用一个非原始值 去"包裹"原始值,例如使用一个对象包裹原始值:
ini
let wrapper = {
value:"vue"
}
const name =reactive(wrapper)
name.value
//修改值
name.value = 'vue3'
但是这样会导致两个问题
- 用户为了创建一个响应式的原始值,不得不顺带创建一个包裹对 象;
- 包裹对象由用户定义,而这意味着不规范。用户可以随意命名, 例如 wrapper.value、wrapper.val 都是可以的。
为了解决这两个问题,我们可以封装一个函数,将包裹对象的创 建工作都封装到该函数中:
kotlin
function ref(val){
const wrapper = {value:val}
//将包裹对象变成响应式数据
return reactive(wrapper)
}
如上面的代码所示,我们把创建 wrapper 对象的工作封装到 ref 函数内部,然后使用 reactive 函数将包裹对象变成响应式数据并返 回。这样我们就解决了上述两个问题,测试如下代码
scss
const refVal = ref(1)
effect(() => {
//在副作用函数中通过value属性读取原始值
console.log(refVal.value)
})
//修改值能够出发副作用函数重新执行
refVal.value = 2
上面这段代码能够按照预期工作。现在是否一切都完美了呢?并 不是,接下来我们面临的第一个问题是,如何区分 refVal 到底是原 始值的包裹对象,还是一个非原始值的响应式数据,如以下代码所 示:
ini
const refVal1 = ref(1)
const refVal2 = reactive({value:1})
那我们如何判断是ref函数创建的对象,还是reactive创建的对象呢,答案很简单,如下代码
javascript
function ref(val){
const wrapper = {value:val}
//在对象上定义一个标识
Object.defineProperty(wrapper,'__v_isRef',{
value:true,
})
//将包裹对象变成响应式数据
return reactive(wrapper)
}
那我们在通过不可枚举且不可写的属性__v_isRef,就可以区分到底是不是ref了
2.响应丢失问题
ref除了能够用于原始值的响应式方案歪,还可以用来解决响应丢失,那首先我们需要知道是什么响应丢失
javascript
export default {
setup(){
const obj = reactive({
foo:1,
bar:2
})
//将数据暴露在模版中
return {
...obj
}
}
}
然后我们在模版中访问setup中暴露出来的数据
xml
<template>
<div @click="change">{{foo}}</div>
<div>{{bar}}</div>
</template>
然而这样做会导致响应丢失问题,当我们修改响应式数据的值的时候,不会触发重新渲染
ini
const change = () => {
obj.foo = 100
}
为什么会导致响应式丢失呢,这是由于展开运算符...导致的,return {...obj}
等价于return { foo:1,bar:2}
,可以发现,这就是返回了一个普通对象,他不具备响应能力,他没有和渲染函数建立响应联系的,所以当我们修改值的时候,触发渲染,我们换一种方式来描述响应丢失的问题
ini
const obj = reactive({foo:1,bar:2})
const newObj = {
...obj
}
effect(() => {
console.log(newObj.foo)
})
//很显然,此时修改obj.foo不会触发响应
obj.foo = 100
如上所示,我们将obj解构得到一个新的对象newObj,它是一个普通对象,不具备响应能力,副作用函数并不会收集普通函数的响应,当我们修改obj.foo,也不会触发副作用函数执行。
那如何解决这个问题呢,我们能不能想办法能够在副作用函数内,即使通过普通对象newObj来访问属性值,也能够建立响应联系?
csharp
const obj = reactive({foo:1,bar2})
const newObj = {
foo:{
get value(){
return obj.foo
}
},
bar:{
get value(){
return obj.bar
}
},
}
effect(() => {
//在副作用函数中通过新对象newObj读取foo的值
console.log(newObj.foo.value)
})
//这时就可以触发响应
obj.foo =100
在上面代码中,我们修改了newObj对象的实现方式,可以看到,在newObj对象下,具有obj对象同名的属性,而且每个属性值都是一个对象,该对象下面有一个访问器属性value,当读取value的值,最后读到的都收响应式对象obj的同名属性值,那就是读取newObj的值就是读取了obj的值,那自然能够触发响应。
观察newObj对象,发现其结构存在相似的地方
csharp
foo:{
get value(){
return obj.foo
}
},
bar:{
get value(){
return obj.bar
}
},
那我们可以把这种结构抽象出来并封装成函数toRef,代码如下
csharp
function toRef(obj,key){
const wrapper = {
get value(){
return obj[key]
},
}
return wrapper
}
toRef接受两个参数,一个参数obj是一个响应式数据,第二个参数是obj对象的一个键,那我们重新实现newObj
css
const newObj = {
foo:toRef(obj,'foo'),
bar:toRef(obj,'bar')
}
可以看到,代码就变得简洁了,但是如果是obj的键非常多,那我们海狮要费很大力气,所以我们可以封装toRefs函数
scss
function toRefs(obj){
const ret ={}
for(const key in obj){
ret[key] = toRef(obj,key)
}
return ret
}
现在,我们只需要一步操作即可完成对一个对象的转换
ini
const newObj = {...toRefs(obj)}
现在,响应丢失的问题我们就解决了,解决思路是,将响应式数据转换成类似于ref结构的数据,但为了概念上得统一,我们会将通过toRef或toRefs转换后得到的结果视为真正的ref数据,那我们就需要像以前一样,加上我们的标识
javascript
function toRef(obj,key){
const wrapper = {
get value(){
return obj[key]
},
set value(val){
obj[key] = val
}
}
Object.defineProperty(wrapper,'__v_isRef',{
value:true,
})
return wrapper
}
3.自动脱ref
toRefs函数的确解决了响应式丢失的问题,但同时也带来了新的问题,由于toRefs会把响应式数据的第一层转换为ref,因此必须通过value属性来访问,如下
php
const obj = reactive({ foo:1, bar:2 })
const newObj = { ...toRefs(obj)}
newObj.foo.value //1
newObj.bar.value //2
但是这其实增加了书写的麻烦,在模版中使用,用户肯定不希望编写如下代码
css
<p>{{foo.value}}</p>
因此,我们需要自动脱ref的能力,所谓自动脱ref,指的是属性的访问行为
,即如果读取的属性是一个ref,则直接将ref对应的value属性值放回,例如
arduino
newObj.foo //1
可以看到,即使newObj.foo是一个ref,也不需要通过.value来访问,来实现此功能,需要用到Proxy为newObj创建一个代理对象,通过代理来实现最终目标,那我们家的标识就有用了
javascript
function proxyRefs(target){
return new Proxy(target,{
get(target,key,receiver){
const value = Reflect.get(target,key,receiver)
//自动脱ref,如果读取的值ref,则返回他的value属性值
return value.__v_isRef ? value.value : value
},
})
}
//调用proxyRefs创建代理
const newObj = proxyRefs({...toRefs(obj)})
在上面代码中,我们定义了proxyRefs函数,该函数接受的是一个对象作为参数,并返回了该对象的代理,代理对象的作用是拦截get操作,当读取的属性是一个ref时,则返回ref的value属性值,那就实现了自动脱ref, 实际上,当我们编写template模版时,就是调用了proxyRefs函数,这就是为什么我们可以直接早模版中访问ref的值,而无须通过value属性来访问
css
<p>{{count}}</p>
既然读取属性的值有自动脱 ref 的能力,对应地,设置属性的值 也应该有自动为 ref 设置值的能力,例如
ini
newObj.foo = 100 //应该生效
那我们只需要再代理set拦截函数即可
javascript
function proxyRefs(target){
return new Proxy(target,{
get(target,key,receiver){
const value = Reflect.get(target,key,receiver)
//自动脱ref,如果读取的值ref,则返回他的value属性值
return value.__v_isRef ? value.value : value
},
set(target,key,newValue,receiver){
const value = Reflect.get(target,key,receiver)
if(value.__v_isRef){
value.value = newValue
return true
}
return Reflect.set(target,key,newValue,receiver)
}
})
}
实际上,自动脱ref不仅仅用在模版中,在reactive函数也有自动脱ref的能力
scss
const count = ref(0)
const obj = reactive({ count })
console.log(obj.count) // 0
可以看到,obj.count 本应该是一个 ref,但由于自动脱 ref 能力的存在,使得我们无须通过 value 属性即可读取 ref 的值。这么 设计旨在减轻用户的心智负担,因为在大部分情况下,用户并不知道 一个值到底是不是 ref。有了自动脱 ref 的能力后,用户在模板中使 用响应式数据时,将不再需要关心哪些是 ref,哪些不是 ref。
4.总结
首先介绍了 ref 的概念。ref 本质上是一个"包裹 对象"。因为 JavaScript 的 Proxy 无法提供对原始值的代理,所以我们 需要使用一层对象作为包裹,间接实现原始值的响应式方案。由于"包 裹对象"本质上与普通对象没有任何区别,因此为了区分 ref 与普通响 应式对象,我们还为"包裹对象"定义了一个值为 true 的属性,即 __v__isRef,用它作为 ref 的标识。 ref 除了能够用于原始值的响应式方案之外,还能用来解决响应 丢失问题。为了解决该问题,我们实现了 toRef 以及 toRefs 这两个 函数。它们本质上是对响应式数据做了一层包装,或者叫作"访问代 理"。 最后,我们讲解了自动脱 ref 的能力。为了减轻用户的心智负 担,我们自动对暴露到模板中的响应式数据进行脱 ref 处理。这样, 用户在模板中使用响应式数据时,就无须关心一个值是不是 ref 了