Vue3 → 响应式 ref 拓展 - isRef、unRef、toRef和proxyRefs等API原理

648 阅读9分钟

ref -> 响应式数据创建

功能概述

refreactive一样,都是创建响应式数据的方法,reactive是创建复杂数据类型的响应式代理,而ref则是创建简单数据类型的响应式代理;
当然ref也可以处理复杂数据类型的响应式代理,只不过需要进行单独转换

其核心就是通过维护一个对象,该对象只有一个value属性,然后给该对象设置proxy代理;

  • 需要具备的响应式功能
    • reactive类似 只不过在进行依赖收集或触发时只通过单一的.value的方式(reactive收集依赖是需要根据key收集很多的deps,ref只有一个key,也就只有一个deps)
    • 当进行更新时如果值没有改变就不进行依赖触发,不进行更新操作

依赖收集

ref和reactive一样,也是需要进行依赖收集的,只是由于ref只需要维护一个value的属性,其依赖只会是value的,所以不用像reactive那样走很复杂的逻辑(targetMap -> depsMap -> deps)

依赖触发

依赖触发是在set的时候进行的,但是不需要进行复杂的key值捕获

ref常用API拓展

isRef

是用于判断一个对象是否被ref实例对象所包裹;

有了isReactiveisReadonly的基础isRef也是同样的处理方式,通过添加一个public __v_isRef = true的属性进行后续判断

unRef

省略ref调用值时的.value操作,直接进行数据的操作获取;
当使用.value太频繁的时候,不知道后面的值到底有没有.value,此时就可以用该API进行包裹访问

proxyRefs

当访问的是 ref 类型的值时 返回.value;
当访问的不是 ref 类型的值时 直接返回value;
帮助我们脱离ref的value限制,让我们可以直接访问响应式数据,而不需要通过value间接访问

在Vue3中的setup中,不管在模板中有没有使用.value,都可以进行.value的省略,因为setup内部返回的结果就调用了这个API

  • proxyRefs需求分析
    • proxyRefs可以对数据进行getset,并且还有对应的处理
    • get的时候,会自动把.value省略掉
    • set的时候,需要区分是普通值还是ref;proxyUser.age = ref(10)
  • 功能实现
    • 由于需要劫持对象,因此需要使用Proxy来进行代理劫持
    • 在进行get的时候,判断获取的结果是ref还是普通的,如果是ref的话,默认调用.value
    • 对于set的话,同样需要判断set的内容是不是ref,且需要判断set之前的值是什么类型的
      • set的是内容是普通值,且原来的值是ref,需要调用.value来赋值
      • 否则,直接替换即可
    /**
     * 
     * @param obj 
     * @returns 
     */
    export function proxyRefs(obj) {
      return new Proxy(obj, {
        get(target, key) {
          const val = Reflect.get(target, key)
          // 返回的结果类型判断
          return isRef(val) ? val.value : val
        },
        set(target, key, val) {
          // set需要判断新内容和set之前的内容是否为ref 
          if (isRef(target[key]) && !isRef(val)) {
            // 新值是普通值,原来的值是ref类型 - 调用.value来替换
            return target[key].value = val
          } else {
            // 直接替换
            return Reflect.set(target, key, val)
          }
        }
      })
    }
    

shallowRef

  • 只处理基本数据类型的响应式,不进行对象的响应式处理
    • 有一个对象数据,后续功能不会修改该对象中的属性,而是生成新的对象来替换时使用shallowRef
    • 当使用该API代理对象的时候,对象默认是不带有响应式的,需要进行强制更新页面DOM的操作 - triggerRef
const state = shallowRef({ count: 1 })
// 不会触发更改
state.value.count = 2
// 会触发更改
state.value = { count: 2 }
<button @click="changeNage">重命名</button>
<div>{{name}}</div>
type Obj = {
    name: string
}
let name:Ref<Obj> = shallowRef({
    name: "Lbxin"
})

const changeName = () => {
    name.value.name = "Lbxin-11"
    triggerRef(message)
}

toRef - 访问代理:解决响应式数据丢失的问题

可以为源响应式对象上的某个属性新创建一个ref,且ref可以被传递,会保持对源属性的响应式连接


import { ref, toRef } from "vue";
// 使用 toRef 后 两个变量数据 会产生链式关系 互相响应 一个数据发送改变 另一个也会跟随 改变
const user = ref({
  name: "Lbxin",
  age: 22,
});

const newAge = toRef(user.value, "age");

newAge.value = 20;
console.log(user.value.age); // 20

user.value.age = 18;
console.log(newAge.value); // 18
内部实现
  • 实现思路
    • 接收两个参数,第一个是响应式数据,第二个是响应式数据的一个键,通过返回类似Ref结构的对象进行响应式获取
  • 代码逻辑
function toRef(obj,key){
  const result = {
    get value(){
      return obj[key]
    },
    set value(val){
      obj[key] = val
    }
  }

  // 为了与真正的ref数据同步,需要增加__v_isRef的标识
  Object.defineProperties(result,'__v_isRef',{
    value: true
  })


  return result 
}

const newObj = {
  userName: toRef(userInfo,'name'),
  userAge: toRef(userInfo,'age'),
}

toRefs - 访问代理:解决响应式数据丢失的问题

将一个响应式对象转换为普通对象,但是内部的数据还是响应式的


import { ref, toRefs } from "vue";
// 使用 toRef 后 两个变量数据 会产生链式关系 互相响应 一个数据发送改变 另一个也会跟随 改变
const user = ref({
  name: "Lbxin",
  age: 22,
});
const newAge = toRefs(user.value);

newAge.age.value = 20;
console.log(user.value.age); // 20

user.value.age = 18;
console.log(newAge.value); // 18
内部实现
  • 解决思路
    • 将响应式数据转换为类似于ref结构的数据
    • 在某些方面上来说,toRefs转换后的结果要视为真正的ref数据
  • 实现代码
function toRefs(obj) {
  const result = {};
  for (const key in obj) {
    if (Object.hasOwnProperty.call(obj, key)) {
      // 通过调用`toRef`来实现响应式转换
      result[key] = toRef(obj, key);
    }
  }
}

const newObj = {...toRefs(userInfo)}
存在的问题
  • 通过toRefs转换的响应式数据的访问还是需要通过繁琐的xxx.value的方式进行实现,而常规的数据是可以直接通过键值对的方式进行访问的,这时就需要用到下文说的proxyRefs来包裹实现了,内部是在读取ref的值时,直接返回refvalue属性值,这样就实现了自动脱ref
    • Vue中的setup函数返回的数据会传递给ProxyRefs函数进行处理
  • 同样的,在进行设置值时也需要通过繁琐的xxx.value的方式进行实现,为了解决这个问题,可以通过proxyRefsSet拦截进行实现,在进行设置时如果是__v_isRef标识,则直接设置xxx.value = newValue属性值即可;

ref的作用不仅仅是实现基本数据类型的响应式方案,它还用来解决响应式数据丢失的问题(丢失一般都是拓展运算、浅克隆等造成的)
自动脱ref的操作不仅仅在上述的场景中有应用,在reactive函数中也有自动脱ref的能力

ref及相关API内部实现与测试

原理解析

Vue3响应式数据通过ref、reactive实现、监听函数通过effect实现,reactive借助proxy实现,computed借助effect实现

ref和effect内部是借助track跟踪和trigger触发代理来实现响应式的;

测试用例

describe('ref', () => {
    it('happy path', () => {
        // 用ref包裹一个数字1  期待.value返回值1
        const r = ref(1)
        expect(r.value).toBe(1)
    });

    it('should be a reactive', () => {
        // 在effect中获取用ref包裹起来的r.value的值 期望dunny返回值为1
        // 在进行重复复制同样的值时 期望结果值不再变化
        const r = ref(1)
        let dunny,
            calls = 0;
            effect(() => {
                calls++;
                dunny = r.value
            })

            expect(calls).toBe(1)
            expect(dunny).toBe(1)

            r.value = 2
            expect(calls).toBe(2)
            expect(dunny).toBe(2)
            
            //验证在数据没有变化的前提下  effect中额的逻辑不会执行
            r.value = 2
            expect(calls).toBe(2)
            expect(dunny).toBe(2)
    });

    it('should make nested propeerties reactive', () => {
        // ref也可以进行传递对象进行响应式代理
        const r = ref({
            count: 1
        })
        let dummy
        effect(() => {
            dummy = r.value.count
        })

        expect(dummy).toBe(1)
        r.value.count = 2

        expect(dummy).toBe(2)
    });

    it('isRef', () => {
        // 判断当前数据是否为ref类型
        const r = ref(1)
        const user = reactive({
            name: "Lbxin"
        })

        expect(isRef(r)).toBe(true)
        expect(isRef(user)).toBe(false)
        expect(isRef(1)).toBe(false)
    });

    it('unRef', () => {
        // 如果参数为ref,则返回内部的值,否则返回参数本身
        const r = ref(1)
        const user = reactive({
            name: "Lbxin"
        })

        expect(unRef(r)).toBe(1)
        expect(unRef(1)).toBe(1)
    });

    it('proxyRefs', () => {
        // 当访问的是 ref 类型的值时  返回.value
        // 当访问的不是 ref 类型的值时 直接返回value
        const user = {
            age: ref(20),
            name: "Lbxin"
        }

        const proxyUser = proxyRefs(user)
        expect(proxyUser.age).toBe(20)
        expect(proxyUser.name).toBe('Lbxin')
        expect(user.age.value).toBe(20)

        proxyUser.age = ref(18)
        expect(proxyUser.age).toBe(18)
        expect(user.age.value).toBe(18)

        proxyUser.age = ref(22)
        expect(proxyUser.age).toBe(22)
        expect(user.age.value).toBe(22)

    });
});

内部实现

class RefImpl {
    private _value:any;
    public dep;
    public __v_isRef = true //用于表明是否是ref类的响应式代理
    private _rawValue:any; //保存旧值 防止reactive转换后与value不一样
    constructor(value){
        this._rawValue = value
        // 这里需要对传入的值进行数据类型判断 对象时需要依赖reactive
        this._value = convert(value)
        
        // dep 初始化 
        this.dep = new Set()
    }

    get value() {
        // 依赖收集
        trackRefValue(this);
        return this._value
    }

    set value(newValue) {
        // hasChanged 只有在新值发生变化的时候才进行依赖触发
        if(hasChanged(newValue,this._rawValue)){
            this._rawValue = newValue
            this._value = convert(newValue)
            
            // 触发依赖更新
            triggerEffects(this.dep)
        }
    }
}
export function trackRefValue(ref) {
    // 和effect一样 当没有通过effect进行代理监听时  就不用进行依赖收集  不然会找不到需要手机的effect实例 即 activeEffect为undefined
    if (isTracking()) {
        // 进行依赖收集 ref是单一的.value进行操作的 所以直接进行依赖收集即可
        // 在trackEffects中需要进行添加的前置判断 没有再进行依赖收集
        trackEffects(ref.dep);
    }
}

export function triggerRefValue(ref) {
    triggerEffects(ref.dep);
}


function convert(value) {
    //对象类型的数据需要转换为 reactive 对象代理
    return isObject(value) ? reactive(value) : value;
}

export function ref(value) {
    return new RefImpl(value)
}

export function isRef(value) {
    // 判断当前数据是否为ref类型
    return !!value['__v_isRef']
}

export function unRef(value) {
    // 如果参数为ref,则返回内部的值,否则返回参数本身
    return isRef(value) ? value.value : value
}

export function proxyRefs(obj) {
  return new Proxy(obj, {
    get(target, key) {
      const val = Reflect.get(target, key)
      // 返回的结果类型判断
      return isRef(val) ? val.value : val
    },
    set(target, key, val) {
      // set需要判断新内容和set之前的内容是否为ref 
      if (isRef(target[key]) && !isRef(val)) {
        // 新值是普通值,原来的值是ref类型 - 调用.value来替换
        return target[key].value = val
      } else {
        // 直接替换
        return Reflect.set(target, key, val)
      }
    }
  })
}

export function proxyRefs(value) {
    return new Proxy(value, {
        get(target,key){
            return unRef(Reflect.get(target,key))
        },
        set(target,key,value){
            if(isRef(target[key]) && !isRef(value)){
                target[key] = value
            } else {
                return Reflect.set(target,key,value)
            }
        }
    })
}

注意点

  • 当ref传入的是对象的时候需要进行复杂处理
    • 传入值为对象的时候可以利用reactive进行代理处理 - convert函数的左右
    • _rawValue的私有属性,保存最新的value值,方便后续的新旧值进行比较(set时用到),当没有此值的时候,新的值就被处理成了reactive,无法进行相等比较了 - hasChange函数的作用