Vuejs设计与实现 —— ref 原始值的响应式方案

简介: Vuejs设计与实现 —— ref 原始值的响应式方案

image.png

前言

原始值指的是 Boolean、Number、BigInt、String、Symbol、undefined、null 等类型的值,在 JavaScript 中,原始值是按值传递的,引用类型是按引用传递的,这意味着,如果一个函数接收了一个原始值作为参数,那么形参和实参之间是没有引用关系的,它们是完全独立的两个值。在 JavaScriptproxy 无法对原始值提供代理,因此想要将原始值变成响应式数据,就必须要对原始值做一层包裹,这也就是 ref 要做的事。

为什么要引入 ref

用户自定义包裹对象的问题

根据上面提到的,如果要对原始值实现响应式数据,就必须要使用一个非原始值对象去 "包裹" 原始值,如:

// 原始值
let name = 'hello vue3'
// 包裹对象
const nameWrapper = {
    value: 'hello vue3'
}
// 响应式数据
const name = reactive(nameWrapper)
// 触发响应式数据修改
name.value = 'hello vue3 reactive'
复制代码

创建包裹对象的工作 看起来没有问题,但其实是有问题的:

  • 用户为了创建一个响应式的原始值,不得不创建一个包裹对象
  • 包裹对象的内容由用户自己定义,意味着不规范,即可以被随意命名,如 wrapper.valuewrapper.val 都可以

引入 ref 解决问题

封装 ref 函数

为了解决上述的问题,可以 将创建包裹对象的工作 都封装到 ref 函数中,如:

// 封装 ref 函数
function ref(val){
    // 创建包裹对象
    const wrapper = {
       value: val
    }
    // 创建响应式数据
    return reactive(wrapper)
}
// 使用 ref
const name = ref('hello vue3')
effect(() => {
    // 在副作用函数中通过 value 属性读取原始值
    console.log(name.value)
})
// 修改 name.value 的值,触发 effect 副作用函数重新执行
nam.value = 'hello world'
复制代码

优化 ref 函数

上面的代码已经可以实现最初的需求了,但是还是存在对应的缺陷,例如如何区分如下的两个响应式数据,是原始值的包裹对象,还是非原始值的响应式数据呢?

const name1 = ref('hello world')
const name2 = ref('hello vue3')
复制代码

因此,需要对 ref 函数进行优化,如下:

function ref(val){
    // 创建包裹对象
    const wrapper = {
       value: val
    }
    // 使用 Object.definedProperty 在 wrapper 对象上定义一个不可枚举、不可写的属性 __v_isRef: true
    Object.definedproperty(wrapper, '__v_isRef', {
        value: true
    })
    // 创建响应式数据
    return reactive(wrapper)
}
复制代码

通过 Object.definedProperty 方法为包裹对象 wrapper 设置一个不可枚举、不可写的属性 __v_isRef 且值为 true,用于去代表该对象是一个 ref ,而非原始值的响应式数据对象。

ref 用于解决响应式丢失问题

什么是响应式丢失?

在编写 Vue.js 组件时,通常都需要把数据暴露到模板当中使用,如:

export default {
    setup(){
        const state = reactive({foo: 1, bar: 2})
        return { ...state }
    }
}
复制代码

可以看到这里使用了 展开运算符(...)的方式将响应式数据暴露到外部,然后这么做会丢失响应式,以下两段代码是等价的:

return { ...state }
    等价于
return {foo: 1, bar: 2}
复制代码

如果这么看,很容易发现 响应式丢失的原因 是向外暴露了一个普通对象,它不具有任何响应式的能力.

实现 toRef 函数处理响应式丢失

实际上就是通过返回一个类似 ref 结构的 wrapper 对象,但通过为 wrapper 对象实现 getter 函数,并在其中返回具体的响应式数据,即保证与原响应式数据之间的联系,实现如下:

function toRef(obj, key){
    const wrapper = {
        get value(){
             return obj[key]
        },
        // 允许设置
        set value(val){
            obj[key] = val
        }
    };
    // __v_isRef 定义,表明是一个 ref 类型
    Object.definedProperty(wrapper, '__v_isRef', {
        value: true
    });
    return wrapper;
}
// 使用
const state = reactive({foo: 1, bar: 2})
const newState = {
    foo: toRef(state, 'foo'),
    bar: toRef(state, 'bar')
}
复制代码

封装 toRefs 函数

第一版本的 toRef 函数存在的不足,就是只能对的单个 key 进行处理,如果传入的响应式数据键非常多,那效果就不高了,于是通过封装  toRefs 函数实现批量转换:

function toRefs(obj){
    const ret = {};
    for(const key in obj){
       // 组个调用 toRef 完成批量转换
       ret[key] = toRef(obj, key)
    }
    return ret
}
复制代码

为什么需要自动脱 ref

toRefs 带来的问题

toRefs 函数解决了响应式丢失的问题,但也带来了新的问题,那就是它会把响应式数据的第一层属性值转换为 ref,意味着必须要通过 value 属性去访问值,这其实增加了用户的心智负担,因此,需要提供自动脱 ref 的能力。

自动脱 ref

所谓自动脱 ref 指的是属性的访问行为,即如果读取的属性是一个 ref,则直接将该 ref 对应的 value 属性值返回。

可以基于前面定义的 __v_isRef 标识,通过使用 ProxytoRefs 返回的对象创建代理对象,即通过代理来实现目标,上述内容封装成 proxyRefs 函数,如下:

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){
      // 通过 target[key] 访问真实值
      const value = target[key]
      if(value.__v_isRef){
        value.value = newValue
        return true
      }
      return Reflect.set(target, key, newValue, receiver)
    }
  });
}
复制代码

实际上,在编写 Vuej.s 组件时,组件中的 setup 函数所返回的数据会传递给 proxyRefs 函数进行处理,这也就是为什么在模板中直接访问 ref 时,不需要通过 value 属性访问和设置。

Vue.js 中,关于自动脱 ref 不仅存在上述场景外,reactive 函数也有自动脱 ref 的能力,例如:

const count = ref(0)
const obj = reactive(count)
// 直接访问
obj.count // 0
复制代码

总结

ref 的作用场景总结如下:

  • 处理 原始值 类型的响应式
  • 解决 响应式 数据丢失 为了解决上述问题,引入了 ref 的概念,并且封装了 toRef、 toRefs、proxyRefs 等函数,其中 toRef、 toRefs 是解决具体问题,但同时产生了新的用户心智负担,如 ref 的响应式数据,必须要通过 .value 的方式访问。因此,又通过 proxyRefs 函数实现 自动脱 ref,以减少用户心智负担。



目录
相关文章
|
3月前
|
JavaScript 前端开发 开发者
Vue.js 框架大揭秘:响应式系统、组件化与路由管理,震撼你的前端世界!
【8月更文挑战第27天】Vue.js是一款备受欢迎的前端JavaScript框架,以简洁、灵活和高效著称。本文将从三个方面深入探讨Vue.js:响应式系统、组件化及路由管理。响应式系统为Vue.js的核心特性,能自动追踪数据变动并更新视图。例如,通过简单示例代码展示其响应式特性:`{{ message }}`,当`message`值改变,页面随之自动更新。此外,Vue.js支持组件化设计,允许将复杂界面拆分为独立且可复用的组件,提高代码可维护性和扩展性。如创建一个包含标题与内容的简单组件,并在其他页面中重复利用。
77 3
|
3月前
|
JavaScript 开发者
vue学习之响应式数据绑定
响应式数据绑定
47 0
|
19天前
|
API
vue3知识点:响应式数据的判断
vue3知识点:响应式数据的判断
25 3
|
23天前
|
缓存 JavaScript UED
优化Vue的响应式性能
【10月更文挑战第13天】优化 Vue 的响应式性能是一个持续的过程,需要不断地探索和实践,以适应不断变化的应用需求和性能挑战。
29 2
|
27天前
|
JavaScript 前端开发
Vue 2 和 Vue 3 之间响应式区别
10月更文挑战第7天
32 2
|
1月前
|
JavaScript 前端开发 网络架构
如何使用Vue.js构建响应式Web应用
【10月更文挑战第9天】如何使用Vue.js构建响应式Web应用
|
1月前
|
JavaScript 前端开发
如何使用Vue.js构建响应式Web应用程序
【10月更文挑战第9天】如何使用Vue.js构建响应式Web应用程序
|
1月前
|
JavaScript 前端开发 数据安全/隐私保护
前端技术分享:使用Vue.js构建响应式表单
【10月更文挑战第1天】前端技术分享:使用Vue.js构建响应式表单
|
19天前
|
JavaScript 前端开发 API
vue3知识点:Vue3.0中的响应式原理和 vue2.x的响应式
vue3知识点:Vue3.0中的响应式原理和 vue2.x的响应式
23 0
|
2月前
vue2的响应式原理学“废”了吗?继续观摩vue3响应式原理Proxy
该文章对比了Vue2与Vue3在响应式原理上的不同,重点介绍了Vue3如何利用Proxy替代Object.defineProperty来实现更高效的数据响应机制,并探讨了这种方式带来的优势与挑战。
vue2的响应式原理学“废”了吗?继续观摩vue3响应式原理Proxy