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 了

相关推荐
无限大.4 小时前
前端知识速记:节流与防抖
前端
十八朵郁金香5 小时前
【VUE案例练习】前端vue2+element-ui,后端nodo+express实现‘‘文件上传/删除‘‘功能
前端·javascript·vue.js
学问小小谢5 小时前
第26节课:内容安全策略(CSP)—构建安全网页的防御盾
运维·服务器·前端·网络·学习·安全
LCG元5 小时前
Vue.js组件开发-实现全屏图片文字缩放切换特效
前端·javascript·vue.js
还是鼠鼠6 小时前
图书管理系统 Axios 源码__新增图书
前端·javascript·vscode·ajax·前端框架·node.js·bootstrap
customer087 小时前
【开源免费】基于SpringBoot+Vue.JS体育馆管理系统(JAVA毕业设计)
java·vue.js·spring boot·后端·开源
还是鼠鼠9 小时前
图书管理系统 Axios 源码 __删除图书功能
前端·javascript·vscode·ajax·前端框架·node.js·bootstrap
轻口味9 小时前
Vue.js `Suspense` 和异步组件加载
前端·javascript·vue.js
m0_zj11 小时前
8.[前端开发-CSS]Day08-图形-字体-字体图标-元素定位
前端·css
还是鼠鼠11 小时前
图书管理系统 Axios 源码__编辑图书
前端·javascript·vscode·ajax·前端框架