实现ref
我们知道读取ref对象的值需要.value, 因此我们来写一个单元测试来简单实现以下它的功能
it("happy path", () => { let obj = ref(1) expect(obj.value).toBe(1) })
我们要读取到obj身上的value属性即可,读属性就想到了get操作,同reactive,我们也定义一个类来实现方法
class RefImpl { private _value constructor(value) { this._value = value } get value() { return this._value } /* set value(){ }*/ } export function ref(value) { return new RefImpl(value) }
当我们触发obj.value时,会调用ref对象身上的get方法,此时返回的数值也就是传进来的值,就实现了.value能拿到ref对象的值
编辑
触发依赖以及收集依赖
基本数据类型
触发依赖与收集依赖,同样是get时触发track, set时触发trigger,先来实现一下case
it('it should be effect', () => { let dummy let obj = ref(1) let count = 0 effect(() => { count++ dummy = obj.value }) expect(count).toBe(1) expect(dummy).toBe(1) obj.value = 2 expect(count).toBe(2) expect(dummy).toBe(2) })
这里的功能是,effect监听ref对象的变化,当我们修改obj.value时,dummy同意跟着变化,用count来监听effect触发的次数,这个后面细说,先埋个伏笔
收集依赖
对于ref与reactive的对比,ref他只有一个value属性,因此它的依赖性只有一个set, 我们知道依赖性存在于dep中,因此我们可以将依赖性收集在dep中,set value时我们再执行即可,遵循这个思路就很清晰了
export function track(target, key) { let depMap = targetMap.get(target) //解决初始化不存在的问题 if (!depMap) { depMap = new Map() targetMap.set(target, depMap) } let dep = depMap.get(key) //解决初始化不存在的问题 if (!dep) { dep = new Set() depMap.set(key, dep) } // 这里我们用下面的isTrack来优化 /*如果没有依赖项,就直接返回 if (!activeEffect) return; //如果不要收集依赖,这里直接返回 if (!shouldTrack) return;*/ if (!isTrack()) return //收集依赖, 我们需要拿到fn 如果依赖中存在activeEffect 就直接return 不再收集了 if (dep.has(activeEffect)) return dep.add(activeEffect) //这里我们将 activeEffect.deps.push(dep) }
观察一下我们的track函数,我们通过key value的形式取到的dep 而我们的ref直接就定义一个dep就可,所以我们可以执行长注释下面这一段逻辑,因此我们将该部分提炼出来
export function trackEffect(dep) { if (!isTrack()) return //收集依赖, 我们需要拿到fn 如果依赖中存在activeEffect 就直接return 不再收集了 if (dep.has(activeEffect)) return dep.add(activeEffect) //这里我们将 activeEffect.deps.push(dep) }
这里写的也就是如果不需要收集依赖以及已经存在依赖,就不需要再收集了,最后收集的依赖存在dep中,因此我们的get value操作就可以这样
get value() { if (isTrack()) { trackEffect(this.dep) } return this._value }
这里的isTrack是我们之前写的,判断依赖项是否存在,以及是否应该收集依赖的操作
export function isTrack() { return shouldTrack && activeEffect }
因为如果依赖项是个undefined或者不应该收集依赖,那么我们就不需要走到收集依赖
到这里我们收集依赖的任务也完成了,接下来实现触发依赖
触发依赖
我们下来看一下trigger函数,我们是先找到dep ,然后依次执行dep里的东西,我们因为已经有dep了,所有可以跳过dep的获取过程了
export function trigger(target, key) { const depMap = targetMap.get(target) const dep = depMap.get(key) for (const effect of dep) { if (effect.scheduler) { effect.scheduler() } else { effect.run() } }
因此将ref触发依赖的逻辑抽离出来
export function tiggerEffect(dep) { for (const effect of dep) { if (effect.scheduler) { effect.scheduler() } else { effect.run() } }
class RefImpl { private _value public dep constructor(value) { this._value = value this.dep = new Set() } get value() { if (isTrack()) { trackEffect(this.dep) } return this._value } set value(newValue) { this._value = newValue tiggerEffect(this.dep) } }
这样我们也就实现了依赖的触发
引用数据类型
当我们的ref是一个对象时,那么它的底层会调用reactive方法来包裹住,
我们就可以在传入值的时候做个处理,先实现一下case
it("ref Object should be a reactive", () => { let obj = ref({ age: 123 }) let dummy let count = 0 effect(() => { count++ dummy = obj.value.age }) expect(count).toBe(1) expect(dummy).toBe(123) obj.value.age = 222 expect(count).toBe(2) expect(dummy).toBe(222) })
顺着思路来实现一下
class RefImpl { private _value public dep constructor(value) { //做对象类型检测 this._value = isObject(value) ? reactive(value) : value this.dep = new Set() } get value() { trackRefValue(this.dep) return this._value } set value(newValue) { if (!isHasChanged(this._value, newValue)) return this._value = newValue tiggerEffect(this.dep) } }
export function isObject(res) { return res !== null && typeof res === 'object' }
这样就能够通过单测了
优化代码
上面我们提到有count来记录effect触发的次数,这里我们来实现一个功能
it('it should be effect', () => { let dummy let obj = ref(1) let count = 0 effect(() => { count++ dummy = obj.value }) expect(count).toBe(1) expect(dummy).toBe(1) obj.value = 2 expect(count).toBe(2) expect(dummy).toBe(2) obj.value = 2 expect(count).toBe(2) expect(dummy).toBe(2) })
即当我们的obj.value更新后的值与他开始一样的话,我们就不让他做依赖的触发,即跳过这次的更新操作,这样可以极大的节省性能
Object.is
Object.is它用于比较两个值是否相等,和其他比较运算符(如 和 )相比, 有一些独特的特性和用途
1.精确的相等性比较: 使用严格相等比较()的规则来确定两个值是否相等。与 操作符相比,它不会进行隐式的类型转换
Object.is(1, "1"); // false Object.is(NaN, NaN); // true
2.处理特殊值: 可以处理一些特殊值的比较,例如NaN与0
Object.is(NaN, NaN); // true Object.is(-0, +0); // false
Object.is(0, -0); // false 0 === -0; // true
总之, 该方法是在进行值的比较时有用的工具,它提供了更加精确的相等性比较,并且可以处理一些特殊的值。但需要注意的是,由于 在处理非基本类型值时会进行引用比较,因此对于引用类型的对象,它并不常用,通常使用 运算符来进行对引用的比较
因此这里我们新旧值对比就可以用该方法
set value(newValue) { if (Object.is(newValue, this._value)) { return } this._value = newValue tiggerEffect(this.dep) }
这样就可以对一些不变的值进行优化了
注意点
这里我们注意一下,当我们对比两个对象类型时,我们的value已经进行Proxy代理了,所以会有问题,我i们可以用个变量来保存一下之前没有代理的普通value, 然后对比这俩就可以
class RefImpl { private _value private _raw public dep constructor(value) { //保存没有代理前的value this._raw = value //做对象类型检测 this._value = isObject(value) ? reactive(value) : value this.dep = new Set() } get value() { trackRefValue(this.dep) return this._value } set value(newValue) { if (!isHasChanged(this._raw, newValue)) return this._raw = newValue this._value = newValue tiggerEffect(this.dep) } }
这样就可以对比最原始的两个数了