Vue 3.0源码系列之ref、toRef、toRefs

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

大家好我是初心本篇是源码系列之ref、toRef、toRefs 本篇也是我坚持原创文章的第04期文章,如有错误欢迎指正👏🏻

在讨论原始值的响应式方案先看看原始值有哪些吧目前阶段原始值分别是 Boolean, NumberBigInt, String, Symbol, undefined, null

前言

一、引入ref的概念

由于proxy代理目标必须是非原始值所以我们没有任何手段拦截对原始值的操作例如

let name = 'luanshu';
// 无法拦截对值的修改
name = '巧君'; 

对于ref是一个函数创建响应式在Vue2.0中已经规范了架子采用options data对象形式所以不需要考虑这个原始值的问题对于这个问题Vue3的作者及core核心成员们想到了一个办法目前官方说是唯一的办法使用一个非原始值去 “包裹” 原始值例如我们可以使用对象来包装

import { reactive } from 'Vue';

const wrapper = {value: 'luanshu'
}

// 可以使用 Proxy 代理wrapper,简洁实现对原始值的拦截
const userName = reactive(wrapper);

// 读取value
name.value // luanshu

// 修改值可以触发响应式
name.value = '巧君'; 

但是这样会导致两个问题:

1.用户为了创建一个响应式的原始值不得不顺带创建一个包裹对象
2.包裹对象有用户定义而这以为着不规范用户可以随意命名例如wrapper.value 或者 wrapper.val 都是可以的。

为了解决这个问题使用函数封装将对象包裹起来封装到该函数中

// 封装一个 ref 函数
function ref(val){// 在ref函数内部包裹对象const wrapper = {value: val,}// 将包裹对象变成响应式数据return wrapper;
} 

如上面的代码我们把wrapper对象封装到ref函数内部,然后使用reactive函数将包裹对象变成响应式数据并返回这样就解决了上述两个问题

import { effect } from 'Vue';

// 创建原始值的响应式数据
const refValue = ref('栾树');

effect(()=>{// 在函数副作用下 通过 value 值读取原始值console.log(refValue.value);
})

// 修改值能够触发副函数重新执行
refValue.value = 'luanshu'; 

我们都知道在Vue3.0中创建响应式有 ref 和 reactive 函数现在就面临一个问题了如何区分是reactive函数创建的响应式还是 ref 函数创建的响应式呢?

import { ref, reactive } from 'Vue';
// ref
const refValue = ref(1);
// reactive
const reactiveValue = reactive({ value: 1 }); 

core核心大佬们 想到使用Object.defineProperty区分

import { reactive } from 'Vue';

function ref(val){// 在ref函数内部包裹对象const wrapper = {value: val,}// Object.definePropertyObject.defineProperty(wrapper, '__v_isRef', {value: true,})// 将包裹对象变成响应式数据return reactive(wrapper);
} 

我们使用Object.defineProperty为包裹对象 wrapper 定义了一个不可枚举且不可写属性 __v_isRef它的值为true,代表这个是一个ref,而非普通对象这样就可以通过__v_isRef判断检查一个数据是否是ref。

二、响应丢失问题

ref除了用于原始值响应式方案之外还能用来解决响应式丢失问题首先我们需要看下响应式丢失问题

<template><div>姓名:{{ userName }} / 花名: {{ flowerName }}</div>
</template>

<script> import { reactive } from 'Vue';export default{setup(){const wrapper = reactive({ flowerName: '栾树', userName: '巧君' })// 1s 后修改响应式数据的值 不会触发重新渲染setTimeout(()=>{wrapper.flowerName = 'luanshu';},1000)// 这里丢失响应式return {...wrapper}},
} </script> 

然而这样做会丢失响应式其表现是当我们修改响应式数据的值时不会触发重新渲染为什么会丢失响应式呢这里是由运算符…导致的实际上下面这段代码

const wrapper = reactive({ flowerName: '栾树', userName: '巧君' })

return {...wrapper
}

// 等价于
return {flowerName: '栾树',userName: '巧君'
} 

可以发现这其实是返回的一个普通对象它不具备任何响应式能力普通对象暴露到模板中使用不会渲染函数和响应式数据之间建立响应式联系的

如何解决这个问题呢换句话说有没有办法能够帮忙解决实现在函数副作用内即使通过普通对象来访问值呢也可以建立联系其实是有的嘿嘿

import { reactive } from 'Vue';

const wrapper = reactive({ flowerName: '栾树',userName: '巧君'
});

// 通过对象访问器属性 value 当读取到 value 值时 其实读取的是 wrapper 对象下对应的属性值
const newObj = {flowerName: {get value(){return reactive.flowerName}},userName: {get value(){return reactive.userName}}
}

effect(()=>{// 在副函数作用域访问 newObj.userName console.log(newObj.userName);
})

// 这个时候就可以触发响应式了
wrapper.userName = '巧军'; 

在这段代码里面其实我们修改 newObj 对象的实现方式。可以看到在现在的 newObj 对象下具有与 wrapper 对象同名的属性 而且每个属性的值都是一个对象例如 flowerName 属性的值是

{get value(){return wrapper.flowerName;}
} 

该对象有一个访问器属性 value 当读取 value 的值时 最终读取的响应式数据 wrapper 下的同名属性值。也就是说当在副作用函数内读取 newObj.flowerName时 等价于间接读取了 wrapper.flowerName 的值。这样这样响应式数据自然是能够与副作用函数建立起响应联系。于是当我们尝试修改 wrapper.flowerName 的值时能够触发副作用函数重新执行。

观察 newObj 对象 可以发现他的结构存在相似之处

import { reactive } from 'Vue';

const wrapper = reactive({ age: 25, userName: '巧君' });

const newObj = {age: {get value(){return wrapper.age;}},userName: {get value(){return wrapper.userName}}

} 

age 和 userName 这两个属性的结构非常像 这启发core核心作者将这种结构抽象出来并封装成函数如下代码所示

function toRef(obj, key){const wrapper = {get value(){return obj[key];}}return wrapper;
} 

toRef函数接受两个参数第一个参数 obj 是一个响应式数据第二个参数 obj 对象的一个键。该函数会返回一个类似 ref 结构的 wrapper 对象。 有了 toRef 函数后 我们就可以重新 wrapper 对象

import { toRef,reactive } from 'Vue';

const wrapper = reactive({ age: 25, userName: '栾树' })

const newObj = {age: toRef(wrapper, 'age'),userName: toRef(wrapper, 'userName'),
} 

可以看到代码变得非常简洁。但如果响应式数据 wrapper 的键非常多需要花费很大力气来做这一层转换。为此我们可以封装 toRefs 函数来批量地完成转换:

import { toRef } from 'Vue';

function toRefs(obj){const ret = {};// 使用 for...in 循环遍历对象for(const key in obj){// 逐个调用 toRef 完成转换ret[key] = toRef(obj, key);}
} 

这样我们只需要哦异步操作即可完成对一个对象的转换

import { toRefs,reactive } from 'Vue';

const wrapper = reactive({ age: 25, userName: '栾树' })

const newObj = { ...toRefs(wrapper);
}

console.log(newObj.age.value) // 25 

现在响应式丢失问题贝彻底解决了。解决问题的思路是将响应式数据转换成类似于 ref 结构的数据。但为了概念上的统一我们会将通过 toRef 或 toRefs 转换后得到的结果视为真正的 ref 数据为此需要为 toRef 函数增加一段代码。

function toRef(obj,key){const wrapper = {get value () {return obj[key];}}// 定义一个 __v_isRef 属性Object.defineProperty(wrapper, '__v_isRef', {value: true})return wrapper;
} 

可以看到使用 Object.defineProperty 函数为 wrapper 对象定义了 __v_isRef 属性。这样 toRef 函数的返回值就是真正意义上的 ref 了。通过上述的讲解我们能够注意到 ref 的作用不仅仅是是想原始值的响应式方案 还是解决响应式丢失的问题。

但上文是想的 toRef 函数存在缺陷即通过 toRef 函数创建的 ref 是可读的入下面的代码所示

import { reactive, toRef } from 'Vue';

const wrapper = reactive({ age: 25, userName: '巧君' });

const refWrapper = toRef(wrapper, 'age');

refWrapper.value = 18; // 无效 

这是因为 toRef 返回的 wrapper 对象的 value 属性只有 getter, 没有 setter。为了功能的完整性我们应该为它加上setter函数所以最终的实现如下

function toRef(obj, key){const wrapper = {get(){return obj[key];},// 允许设置值set(val){obj[key] = val}}Object.defineProperty(wrapper, '__v_isRef', {value: true,})return wrapper;
} 

可以看到当设置 value 属性的值时最终设置的是响应式数据的同名属性值这样就能正确的触发响应式了。

三、自动脱ref

toRefs 函数的确解决了响应式丢失问题但同时也带来了新的问题由于 toRefs 会吧响应式数据的第一层属性值转换为 ref, 因此必须通过 value 属性值访问 如以下代码:

// 创建一个普通对象
constweapper = {flowerName: '栾树',userName: '巧君'
}

console.log(weapper.flowerName); // 栾树
console.log(weapper.userName); // 巧君

// 通过 toRefs 包装
const newWrapper = {...toRefs(weapper)
}
// 必须通过 value 访问值
console.log(newWrapper.flowerName.value);
console.log(newWrapper.userName.value); 

其实这增加了用户的心智负担因为通常情况下用户在模板中访问数据的例如

<template><div><p>{{ flowerName }} / {{ userName }}</p>{{ '用户不希望编写以下的代码' }}<p>{{ flowerName.value }} / {{ userName.value }}</p></div>
</template> 

因此我们需要自动脱 ref 的能力所谓的自动脱 ref指的是属性的访问行为 即如果读取的属性是一个ref 则直接将该 ref 对应的 value 属性值返回例如

console.log(newWrapper.flowerName); // 栾树 

可以看到即使 newWrapper.flowerName 是一个ref, 也无法通过 newWrapper.flowerName.value 来访问它的值需要使用 Proxy 为 newWrapper 创建一个代理对象 通过代理来实现最终目标这时就用到了上文中介绍的 ref 标识即 __v_isRef 属性 如下面的代码表示

// 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;}})
}

// 调用 proxyRefs
constweapper = {flowerName: '栾树',userName: '巧君'};
const newWrapper = proxyRefs({...toRefs(weapper)
});
console.log(newWrapper.flowerName); // 栾树 

在上面的代码中我们定义了 proxyRefs 函数该函数接受一个对象作为参数并返回改对象的代理对象。代理对象的作用是拦截get操作当读取的函数是一个 ref 时则直接返回改 ref 的 value 值这样就实现了自动脱ref

实际上我们在编写Vue.js时组件中的setup函数所返回的数据会传递给 proxyRefs 函数来进行处理

<template><p>{{ count }}</p>
</template>

<script> import { ref } from 'Vue';

const ClComponent = {setup(){const count = ref(0);// 返回的这个对象会传递给 proxyRefsreturn {count}}
} </script> 

这也是为什么我们可以在模板中直接访问一个 ref 值而无须通过 value 属性来访问既然读取属性的值有自动脱落 ref 的能力对应地设置属性值耶应该自动为ref设置的能力例如

wrapper.flowerName = 'luanshu'; 

实现此功能很简单只需要天啊及对应的 set 拦截函数即可

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 读取真实值const value = target[key];// 如果值是 ref 则设置其对应的value属性值if(value.__v_isRef){value.value = newValue;return true;}return Reflect.set(target, key, newValue, receiver);}})
} 

如上面的代码表示我们为 proxyRefs 函数返回的代理对象添加了 set 函数。如果设置的属性是一个ref, 则简洁设置该 ref 的 value 属性值即可。实际上自动脱 ref 不仅存在上述场景。在Vue.js中 reactive 函数也有自动脱 ref的能力哈哈 reactive 就留在下一次的技术分享吧

总结

我们首先介绍 ref 的概念 ref 本质上是一个 “包裹对象”。因为 JavaScript 的 Proxy 无法提供对原始值的代理所以我们需要使用一层对象作为包裹间接实现原始值的响应式方案。由于 “包裹对象” 本质上与普通对象没有任何区别 因此为了区分 ref 与普通响应式对象我们还未 “包裹对象” 定义了一个值为 true 的属性即__v_isRef, 用它作为 ref 的标识

ref出了能够用于原始值的响应式之外还能用解决响应式丢失的问题。为了解决该问题我们实现了 toRef 以及 toRefs 这两个函数。它们本质上是对响应式数据做了一层包装或者叫做 “访问代理”

最后讲述了自动脱 ref 的能力。为了减轻用户的心智负担我们自动对暴露到模板中的响应式数据进行脱 ref 处理。这样用户在模块中使用响应式数据时就无须关心一个值是不是 ref 了。

最后

最近还整理一份JavaScript与ES的笔记一共25个重要的知识点对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识提升工作效率。



有需要的小伙伴可以点击下方卡片领取无偿分享

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: vue