手写mini-vue3:实现reactivity模块中的-ref

399 阅读5分钟

实现ref

mini-vue3的同步代码实现点击这里

mini-vue3的所有文章点击这里

实现ref

在vue3中我们想要一个数据变成响应式数据,可以通过reactive也可以通过ref。但是因为reactive本质上是使用了proxy实现的响应式,所以reactive不能将基本数据类型的数据作为参数使其变成响应式。所以当我们想要将基本数据类型变成响应式是我们可以使用refref可以基本数据类型和对象都变成响应式的。

实现通过.value获取值

// ref.spec.ts
import { ref } from './ref'
describe('ref', () => {
    it('happy path', () =>{
        const a = ref(1)
        // 获取a的值需要通过 .value的形式获取
        expect(a.value).toBe(1)
    })
})
// ref.ts
class RefImplement {
    private _value
    
    constructor(value) {
        this._value = value
    }
    
    get value() {
        return this._value
    }
    
    set value(newValue) {
        this._value = newValue
    }
}

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

想要通过.value获取值,我们可以创建一个RefImplement的类,然后对value编写相应的gettersetter操作,这样就可以实现通过.value获取值。

实现ref的响应式

在前面文章实现的effect函数中,如果传入的fn参数中有访问了ref对象的.value,那么当这个ref对象的.value发生变化时,fn应该被重新执行。同时当赋值相同的value时,不会触发trigger

// ref.spec.ts
 it("should be reactive", () => {
    const a = ref(1);
    let dummy;
    let calls = 0;
    effect(() => {
      calls++;
      dummy = a.value;
    });
    expect(calls).toBe(1);
    expect(dummy).toBe(1);
    // 修改值
    a.value = 2;
    // 重新执行函数
    expect(calls).toBe(2);
    expect(dummy).toBe(2);
    // 相同value不会触发trigger
    a.value = 2;
    expect(calls).toBe(2);
    expect(dummy).toBe(2);
  });

想要实现ref的响应式,那么就需要在get value的时候收集依赖,在set value的时候触发依赖。因为收集依赖和触发依赖的操作,在effct.ts文件中有实现过,但是这里并不能直接使用effect.ts的依赖收集和触发依赖函数。所以需要对之前的tracktrigger函数进行修改。

// effect.ts
...
export function isTracking() {
    return shouldTrack && activeEffect !== undefined
}

function track(target, key) {
    if (!isTracking()) return
    let depsMap = targetMap.get(target);
    if (!depsMap) {
      depsMap = new Map();
      targetMap.set(target, depsMap);
    }

    let dep = depsMap.get(key);

    if (!dep) {
      dep = new Set();
      depsMap.set(key, dep);
    }
    // 将后续的代码封装成一个函数
    trackEffect(dep);
}

export function trackEffect(dep) {
    if (activeEffect) {
      dep.add(activeEffect);
      activeEffect.deps.push(dep);
    }
}

export function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  const dep = depsMap.get(key);
  if (!dep) return;

  triggerEffect(dep);
}

export function triggerEffect(dep) {
  for (const effect of dep) {
    // 在后面的实现中,被收集的依赖会有run方法,在该方法中会执行真正需要执行的函数
    if (typeof effect.scheduler === "function") {
      effect.scheduler();
    } else {
      effect.run();
    }
  }
}

effect.ts文件中,我们封装了isTracking函数,该函数用来判断是否应该进行依赖收集。并在track函数中调用trackEffect函数,在trigger函数中调用triggerEffect函数。这样做的好处是,在实现ref的依赖收集和触发依赖时,可以直接使用trackEffecttriggerEffect函数进行收集。

// ref.ts
import { isTracking, trackEffect, triggerEffect } from './effect'

class RefImplement {
    private deps // 用于收集依赖
    private _value
    constructor(value) {
        this._value = value
        this.deps = new Set()
    }
    
    get value() {
        //判断是否需要收集依赖
        if (isTracking()) {
            // 需要进行依赖收集,直接调用trackEffect函数并将deps传递进去即可
            trackEffect(this.deps)
        }
        return this._value
    }
    
    set value(newValue) {
        // 判断修改的值是否和之前一致,如果一致直接返回,不进行触发依赖
        if (this._value === newValue) return
        this._value = newValue
        triggerEffect(this.deps)
    }
}

RefImplement类的实现中,我们使用了deps作为依赖收集的容器。当需要收集依赖或者触发依赖时,出需要将这个容器作为参数传递给trackEffecttriggerEffect即可。

实现ref的嵌套属性响应式

ref不仅仅可以将一个基本数据类型转成响应式,还可以将一个对象类型的数据变成响应式的。所以我们在RefImplement类的constructor中需要对传入的value值进行判断。

// ref.spec.ts
it("should make nested properties reactive", () => {
    // 接收一个对象
    const a = ref({
      count: 1,
    });
    let dummy;
    effect(() => {
      dummy = a.value.count;
    });
    expect(dummy).toBe(1);
    // 值改变时
    a.value.count = 2;
    // dummy也发生变化
    expect(dummy).toBe(2);
  });
// ref.ts
import { reactive } from './reactive'

function isObject(value) {
    reuturn value !== null && typeof value === 'object'
]

class RefImplement {
    ...
    private raw
    constructor(value) {
        // raw用来保存value原始值,用于之后判断set的值是否和之前一样
        this.raw = value
        // conver函数用于判断当前value是否为对象
        // 如果是对象则,将该对象包裹一层reactive,如果不是直接返回
        this._value = conver(value)
        this.deps = new Set()
    }
    get value() {
        ...
    }
    set value(newValue) {
        if (isChange(this.raw, newValue)) return
        this.raw = newValue
        this._value = conver(newValue)
        triggerEffect(this.deps)
    }
}

function conver(value) {
    return isObject(value) : reactive(value) : value
}

function isChange(raw, newValue) {
    return raw === newValue
}

为了实现对象类型的响应式,需要在constructor中判断传入得value是否为对象,如果是对象,则将该对象使用reactive函数,如果不是则直接返回。同时,还用一个raw属性用来记录没被处理过的value,该属性主要是用来判断newValue和value是否相等

实现isRef

isRef是用来判断一个数据是否是ref类型。

// ref.ts
class RefImplement {
    public __v_isRef = true
}

export function isRef(value) {
    if (!value) return false
    return !!value.__v_isRef
}

实现isRef其实非常简单,只需要在创建ref实例的时候给ref实例添加上一个__v_isRef属性即可。然后在isRef函数中判断,是否有该属性即可。

实现unref

unRef方法实际上就是一个语法糖,当一个值是ref类型,那么unRef会返回ref.value。当一个值不是ref类型,那么直接返回该值。

// ref.ts
...
export function unRef(ref) {
    return isRef(ref) ? ref.value : ref
} 

实现proxyRefs

proxyRefs主要是用于对一个对象进行ref解包。在vue3中,我们在template中使用setup函数返回的对象的属性值,不管属性值对应的值是不是ref类型,都不需要写.value。而vue3就是通过proxyRefs来实现的。

// ref.spec.ts
it("proxyRefs", () => {
    const user = {
      age: ref(10),
      name: "aaa",
    };

    const proxyUser = proxyRefs(user);
    expect(user.age.value).toBe(10);
    expect(proxyUser.age).toBe(10);
    expect(proxyUser.name).toBe("aaa");
    // 直接通过.age修改值
    proxyUser.age = 20;
    // proxyUser.age改变,user.age.value和proxy.age都需要改变
    expect(proxyUser.age).toBe(20);
    // 通过user.age.value还是能获取到更新后的值
    expect(user.age.value).toBe(20);

    proxyUser.age = ref(30);
    expect(proxyUser.age).toBe(30);
    expect(user.age.value).toBe(30);
  });
// ref.ts

export function proxyRefs(objWithRef) {
    return new Proxy(objWithRef, {
        get(target, key, receiver) {
            return unRef(Reflect.get(target, key, receiver))
        },
        set(target, key, newValue, receiver) {
            if (isRef(target[key]) && !isRef(newValue)) {
                return Reflect.set(target, key, ref(newValue), receiver)
            }
            return Reflect.set(target, key, newValue, receiver)
        }
    })
}

实际上proxyRefs就是返回一个proxy对象,在getter中,只需要通过unRef对获取值时进行解包。而在setter操作时,就需要注意一下。当出现修改的新值不是ref类型,而之前这个值是ref类型的情况时,需要将该新值变成ref类型