vue3中ref到底在干什么

原始值指的是 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 了

相关推荐
uhakadotcom2 小时前
视频直播与视频点播:基础知识与应用场景
后端·面试·架构
范文杰3 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪3 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪3 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy4 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom4 小时前
快速开始使用 n8n
后端·面试·github
uhakadotcom4 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom4 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom4 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom4 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试