theme: channing-cyan
开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情 大家好,我是初心,本篇是源码系列之ref、toRef、toRefs,如有错误,欢迎指正??在讨论原始值的响应式方案,先看看原始值有哪些吧,目前阶段原始值分别是 Boolean, Number,BigInt, String, Symbol, undefined, null
前言

一、引入ref的概念
由于proxy代理目标必须是非原始值,所以我们没有任何手段拦截对原始值的操作,例如:对于ref是一个函数创建响应式,在Vue2.0中已经规范了架子,采用options data对象形式,所以不需要考虑这个原始值的问题,对于这个问题,Vue3的作者及core核心成员们,想到了一个办法,目前官方说是唯一的办法,使用一个非原始值去 “包裹” 原始值,例如我们可以使用对象来包装let name = 'luanshu';// 无法拦截对值的修改name = '巧君';let name = 'luanshu'; // 无法拦截对值的修改 name = '巧君';let name = 'luanshu'; // 无法拦截对值的修改 name = '巧君';
但是这样会导致两个问题:import { reactive } from 'Vue';const wrapper = {value: 'luanshu'}// 可以使用 Proxy 代理wrapper,简洁实现对原始值的拦截const userName = reactive(wrapper);// 读取valuename.value // luanshu// 修改值可以触发响应式name.value = '巧君';import { reactive } from 'Vue'; const wrapper = { value: 'luanshu' } // 可以使用 Proxy 代理wrapper,简洁实现对原始值的拦截 const userName = reactive(wrapper); // 读取value name.value // luanshu // 修改值可以触发响应式 name.value = '巧君';import { reactive } from 'Vue'; const wrapper = { value: 'luanshu' } // 可以使用 Proxy 代理wrapper,简洁实现对原始值的拦截 const userName = reactive(wrapper); // 读取value name.value // luanshu // 修改值可以触发响应式 name.value = '巧君';
- 用户为了创建一个响应式的原始值,不得不顺带创建一个包裹对象;
- 包裹对象有用户定义,而这以为着不规范,用户可以随意命名,例如wrapper.value 或者 wrapper.val 都是可以的。
如上面的代码,我们把wrapper对象封装到ref函数内部,然后使用reactive函数将包裹对象变成响应式数据并返回,这样就解决了上述两个问题// 封装一个 ref 函数function ref(val){// 在ref函数内部包裹对象const wrapper = {value: val,}// 将包裹对象变成响应式数据return wrapper;}// 封装一个 ref 函数 function ref(val){ // 在ref函数内部包裹对象 const wrapper = { value: val, } // 将包裹对象变成响应式数据 return wrapper; }// 封装一个 ref 函数 function ref(val){ // 在ref函数内部包裹对象 const wrapper = { value: val, } // 将包裹对象变成响应式数据 return wrapper; }
我们都知道在Vue3.0中创建响应式有 ref 和 reactive 函数,现在就面临一个问题了,如何区分是reactive函数创建的响应式还是 ref 函数创建的响应式呢?import { effect } from 'Vue';// 创建原始值的响应式数据const refValue = ref('栾树');effect(()=>{// 在函数副作用下 通过 value 值读取原始值console.log(refValue.value);})// 修改值能够触发副函数重新执行refValue.value = 'luanshu';import { effect } from 'Vue'; // 创建原始值的响应式数据 const refValue = ref('栾树'); effect(()=>{ // 在函数副作用下 通过 value 值读取原始值 console.log(refValue.value); }) // 修改值能够触发副函数重新执行 refValue.value = 'luanshu';import { effect } from 'Vue'; // 创建原始值的响应式数据 const refValue = ref('栾树'); effect(()=>{ // 在函数副作用下 通过 value 值读取原始值 console.log(refValue.value); }) // 修改值能够触发副函数重新执行 refValue.value = 'luanshu';
core核心大佬们 想到使用Object.defineProperty区分import { ref, reactive } from 'Vue';// refconst refValue = ref(1);// reactiveconst reactiveValue = reactive({ value: 1 });import { ref, reactive } from 'Vue'; // ref const refValue = ref(1); // reactive const reactiveValue = reactive({ value: 1 });import { ref, reactive } from 'Vue'; // ref const refValue = ref(1); // reactive const reactiveValue = reactive({ value: 1 });
我们使用Object.defineProperty为包裹对象 wrapper 定义了一个不可枚举且不可写属性 __v_isRef,它的值为true,代表这个是一个ref,而非普通对象,这样就可以通过__v_isRef判断检查一个数据是否是ref。import { reactive } from 'Vue';function ref(val){// 在ref函数内部包裹对象const wrapper = {value: val,}// Object.definePropertyObject.defineProperty(wrapper, '__v_isRef', {value: true,})// 将包裹对象变成响应式数据return reactive(wrapper);}import { reactive } from 'Vue'; function ref(val){ // 在ref函数内部包裹对象 const wrapper = { value: val, } // Object.defineProperty Object.defineProperty(wrapper, '__v_isRef', { value: true, }) // 将包裹对象变成响应式数据 return reactive(wrapper); }import { reactive } from 'Vue'; function ref(val){ // 在ref函数内部包裹对象 const wrapper = { value: val, } // Object.defineProperty Object.defineProperty(wrapper, '__v_isRef', { value: true, }) // 将包裹对象变成响应式数据 return reactive(wrapper); }
二、响应丢失问题
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><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><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: '巧君'}const wrapper = reactive({ flowerName: '栾树', userName: '巧君' }) return { ...wrapper } // 等价于 return { flowerName: '栾树', userName: '巧君' }const wrapper = reactive({ flowerName: '栾树', userName: '巧君' }) return { ...wrapper } // 等价于 return { flowerName: '栾树', userName: '巧君' }
在这段代码里面其实我们修改 newObj 对象的实现方式。可以看到,在现在的 newObj 对象下,具有与 wrapper 对象同名的属性, 而且每个属性的值都是一个对象,例如 flowerName 属性的值是: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.userNameconsole.log(newObj.userName);})// 这个时候就可以触发响应式了wrapper.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 = '巧军';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 = '巧军';
该对象有一个访问器属性 value, 当读取 value 的值时, 最终读取的响应式数据 wrapper 下的同名属性值。也就是说,当在副作用函数内读取 newObj.flowerName时, 等价于间接读取了 wrapper.flowerName 的值。这样这样响应式数据自然是能够与副作用函数建立起响应联系。于是,当我们尝试修改 wrapper.flowerName 的值时,能够触发副作用函数重新执行。 观察 newObj 对象, 可以发现他的结构存在相似之处:{get value(){return wrapper.flowerName;}}{ get value(){ return wrapper.flowerName; } }{ get value(){ return wrapper.flowerName; } }
age 和 userName 这两个属性的结构非常像, 这启发core核心作者将这种结构抽象出来并封装成函数,如下代码所示:import { reactive } from 'Vue';const wrapper = reactive({ age: 25, userName: '巧君' });const newObj = {age: {get value(){return wrapper.age;}},userName: {get value(){return wrapper.userName}}}import { reactive } from 'Vue'; const wrapper = reactive({ age: 25, userName: '巧君' }); const newObj = { age: { get value(){ return wrapper.age; } }, userName: { get value(){ return wrapper.userName } } }import { reactive } from 'Vue'; const wrapper = reactive({ age: 25, userName: '巧君' }); const newObj = { age: { get value(){ return wrapper.age; } }, userName: { get value(){ return wrapper.userName } } }
toRef函数接受两个参数,第一个参数 obj 是一个响应式数据,第二个参数 obj 对象的一个键。该函数会返回一个类似 ref 结构的 wrapper 对象。 有了 toRef 函数后, 我们就可以重新 wrapper 对象:function toRef(obj, key){const wrapper = {get value(){return obj[key];}}return wrapper;}function toRef(obj, key){ const wrapper = { get value(){ return obj[key]; } } return wrapper; }function toRef(obj, key){ const wrapper = { get value(){ return obj[key]; } } return wrapper; }
可以看到,代码变得非常简洁。但如果响应式数据 wrapper 的键非常多,需要花费很大力气来做这一层转换。为此,我们可以封装 toRefs 函数,来批量地完成转换:import { toRef,reactive } from 'Vue';const wrapper = reactive({ age: 25, userName: '栾树' })const newObj = {age: toRef(wrapper, 'age'),userName: toRef(wrapper, 'userName'),}import { toRef,reactive } from 'Vue'; const wrapper = reactive({ age: 25, userName: '栾树' }) const newObj = { age: toRef(wrapper, 'age'), userName: toRef(wrapper, 'userName'), }import { toRef,reactive } from 'Vue'; const wrapper = reactive({ age: 25, userName: '栾树' }) const newObj = { age: toRef(wrapper, 'age'), userName: toRef(wrapper, 'userName'), }
这样我们只需要哦异步操作即可完成对一个对象的转换:import { toRef } from 'Vue';function toRefs(obj){const ret = {};// 使用 for...in 循环遍历对象for(const key in obj){// 逐个调用 toRef 完成转换ret[key] = toRef(obj, key);}}import { toRef } from 'Vue'; function toRefs(obj){ const ret = {}; // 使用 for...in 循环遍历对象 for(const key in obj){ // 逐个调用 toRef 完成转换 ret[key] = toRef(obj, key); } }import { toRef } from 'Vue'; function toRefs(obj){ const ret = {}; // 使用 for...in 循环遍历对象 for(const key in obj){ // 逐个调用 toRef 完成转换 ret[key] = toRef(obj, key); } }
现在,响应式丢失问题贝彻底解决了。解决问题的思路是,将响应式数据转换成类似于 ref 结构的数据。但为了概念上的统一,我们会将通过 toRef 或 toRefs 转换后得到的结果视为真正的 ref 数据,为此需要为 toRef 函数增加一段代码。import { toRefs,reactive } from 'Vue';const wrapper = reactive({ age: 25, userName: '栾树' })const newObj = {...toRefs(wrapper);}console.log(newObj.age.value) // 25import { toRefs,reactive } from 'Vue'; const wrapper = reactive({ age: 25, userName: '栾树' }) const newObj = { ...toRefs(wrapper); } console.log(newObj.age.value) // 25import { toRefs,reactive } from 'Vue'; const wrapper = reactive({ age: 25, userName: '栾树' }) const newObj = { ...toRefs(wrapper); } console.log(newObj.age.value) // 25
可以看到,使用 Object.defineProperty 函数为 wrapper 对象定义了 __v_isRef 属性。这样 toRef 函数的返回值就是真正意义上的 ref 了。通过上述的讲解我们能够注意到, ref 的作用不仅仅是是想原始值的响应式方案, 还是解决响应式丢失的问题。 但上文是想的 toRef 函数存在缺陷,即通过 toRef 函数创建的 ref 是可读的,入下面的代码所示:function toRef(obj,key){const wrapper = {get value () {return obj[key];}}// 定义一个 __v_isRef 属性Object.defineProperty(wrapper, '__v_isRef', {value: true})return wrapper;}function toRef(obj,key){ const wrapper = { get value () { return obj[key]; } } // 定义一个 __v_isRef 属性 Object.defineProperty(wrapper, '__v_isRef', { value: true }) return wrapper; }function toRef(obj,key){ const wrapper = { get value () { return obj[key]; } } // 定义一个 __v_isRef 属性 Object.defineProperty(wrapper, '__v_isRef', { value: true }) return wrapper; }
这是因为 toRef 返回的 wrapper 对象的 value 属性只有 getter, 没有 setter。为了功能的完整性,我们应该为它加上setter函数,所以最终的实现如下:import { reactive, toRef } from 'Vue';const wrapper = reactive({ age: 25, userName: '巧君' });const refWrapper = toRef(wrapper, 'age');refWrapper.value = 18; // 无效import { reactive, toRef } from 'Vue'; const wrapper = reactive({ age: 25, userName: '巧君' }); const refWrapper = toRef(wrapper, 'age'); refWrapper.value = 18; // 无效import { reactive, toRef } from 'Vue'; const wrapper = reactive({ age: 25, userName: '巧君' }); const refWrapper = toRef(wrapper, 'age'); refWrapper.value = 18; // 无效
可以看到,当设置 value 属性的值时,最终设置的是响应式数据的同名属性值,这样就能正确的触发响应式了。function toRef(obj, key){const wrapper = {get(){return obj[key];},// 允许设置值set(val){obj[key] = val}}Object.defineProperty(wrapper, '__v_isRef', {value: true,})return wrapper;}function toRef(obj, key){ const wrapper = { get(){ return obj[key]; }, // 允许设置值 set(val){ obj[key] = val } } Object.defineProperty(wrapper, '__v_isRef', { value: true, }) return wrapper; }function toRef(obj, key){ const wrapper = { get(){ return obj[key]; }, // 允许设置值 set(val){ obj[key] = val } } Object.defineProperty(wrapper, '__v_isRef', { value: true, }) return wrapper; }
三、自动脱ref
toRefs 函数的确解决了响应式丢失问题,但同时也带来了新的问题,由于 toRefs 会吧响应式数据的第一层属性值转换为 ref, 因此必须通过 value 属性值访问, 如以下代码:其实这增加了用户的心智负担,因为通常情况下用户在模板中访问数据的,例如:// 创建一个普通对象const weapper = {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);// 创建一个普通对象 const weapper = { 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);// 创建一个普通对象 const weapper = { 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);
因此我们需要自动脱 ref 的能力,所谓的自动脱 ref,指的是属性的访问行为, 即如果读取的属性是一个ref, 则直接将该 ref 对应的 value 属性值返回,例如:<template><div><p>{{ flowerName }} / {{ userName }}</p>{{ '用户不希望编写以下的代码' }}<p>{{ flowerName.value }} / {{ userName.value }}</p></div></template><template> <div> <p>{{ flowerName }} / {{ userName }}</p> {{ '用户不希望编写以下的代码' }} <p>{{ flowerName.value }} / {{ userName.value }}</p> </div> </template><template> <div> <p>{{ flowerName }} / {{ userName }}</p> {{ '用户不希望编写以下的代码' }} <p>{{ flowerName.value }} / {{ userName.value }}</p> </div> </template>
可以看到,即使 newWrapper.flowerName 是一个ref, 也无法通过 newWrapper.flowerName.value 来访问它的值,需要使用 Proxy 为 newWrapper 创建一个代理对象, 通过代理来实现最终目标,这时就用到了上文中介绍的 ref 标识,即 __v_isRef 属性, 如下面的代码表示:console.log(newWrapper.flowerName); // 栾树console.log(newWrapper.flowerName); // 栾树console.log(newWrapper.flowerName); // 栾树
在上面的代码中,我们定义了 proxyRefs 函数,该函数接受一个对象作为参数,并返回改对象的代理对象。代理对象的作用是拦截get操作,当读取的函数是一个 ref 时,则直接返回改 ref 的 value 值,这样就实现了自动脱ref 实际上,我们在编写Vue.js时,组件中的setup函数所返回的数据会传递给 proxyRefs 函数来进行处理// proxyRefsfunction 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;}})}// 调用 proxyRefsconst weapper = {flowerName: '栾树',userName: '巧君'};const newWrapper = proxyRefs({...toRefs(weapper)});console.log(newWrapper.flowerName); // 栾树// 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 const weapper = {flowerName: '栾树',userName: '巧君'}; const newWrapper = proxyRefs({ ...toRefs(weapper) }); console.log(newWrapper.flowerName); // 栾树// 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 const weapper = {flowerName: '栾树',userName: '巧君'}; const newWrapper = proxyRefs({ ...toRefs(weapper) }); console.log(newWrapper.flowerName); // 栾树
这也是为什么我们可以在模板中直接访问一个 ref 值,而无须通过 value 属性来访问:既然读取属性的值有自动脱落 ref 的能力,对应地,设置属性值耶应该自动为ref设置的能力,例如:<template><p>{{ count }}</p></template><script>import { ref } from 'Vue';const ClComponent = {setup(){const count = ref(0);// 返回的这个对象会传递给 proxyRefsreturn {count}}}</script><template> <p>{{ count }}</p> </template> <script> import { ref } from 'Vue'; const ClComponent = { setup(){ const count = ref(0); // 返回的这个对象会传递给 proxyRefs return { count } } } </script><template> <p>{{ count }}</p> </template> <script> import { ref } from 'Vue'; const ClComponent = { setup(){ const count = ref(0); // 返回的这个对象会传递给 proxyRefs return { count } } } </script>
实现此功能很简单,只需要天啊及对应的 set 拦截函数即可:wrapper.flowerName = 'luanshu';wrapper.flowerName = 'luanshu';wrapper.flowerName = 'luanshu';
如上面的代码表示,我们为 proxyRefs 函数返回的代理对象添加了 set 函数。如果设置的属性是一个ref, 则简洁设置该 ref 的 value 属性值即可。实际上,自动脱 ref 不仅存在上述场景。在Vue.js中, reactive 函数也有自动脱 ref的能力,哈哈 reactive 就留在下一次的技术分享吧!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);}})}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); } }) }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); } }) }
总结
我们首先介绍 ref 的概念, ref 本质上是一个 “包裹对象”。因为 JavaScript 的 Proxy 无法提供对原始值的代理,所以我们需要使用一层对象作为包裹,间接实现原始值的响应式方案。由于 “包裹对象” 本质上与普通对象没有任何区别, 因此为了区分 ref 与普通响应式对象,我们还未 “包裹对象” 定义了一个值为 true 的属性,即__v_isRef, 用它作为 ref 的标识 ref出了能够用于原始值的响应式之外,还能用解决响应式丢失的问题。为了解决该问题,我们实现了 toRef 以及 toRefs 这两个函数。它们本质上是对响应式数据做了一层包装,或者叫做 “访问代理” 最后,讲述了自动脱 ref 的能力。为了减轻用户的心智负担,我们自动对暴露到模板中的响应式数据进行脱 ref 处理。这样,用户在模块中使用响应式数据时,就无须关心一个值是不是 ref 了。感谢您的来访,获取更多精彩文章请收藏本站。

© 版权声明
THE END
暂无评论内容