likes
comments
collection
share

12_实现ref功能

作者站长头像
站长
· 阅读数 15

12_实现ref功能

一、单元测试

首先建立ref.spec.ts,然后来看一下refhappy path

// src/reactivity/tests/ref.spec.ts

describe('ref', function () {
  it('happy path', () => {
    const a = ref(1);
    expect(a.value).toBe(1);
    a.value = 2;
    expect(a.value).toBe(2);
  });
})

分析单测,我们可以看出,主要就是两个关注点:

  1. 通过ref声明的响应式变量可以通过.value的形式来读取,也就是get操作
  2. 同时也可以设置值,也就是set操作

二、代码实现 happy path

建立ref.ts

// src/reactivity/ref.ts

// + 这里同样封装一个类来实现各种操作
class RefImpl {
  private _value: any;

  constructor(value: any) {
    this._value = value;
  }

  get value() {
    return this._value;
  }

  set value(newVal: any) {
    this._value = newVal;
  }
}

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

我们在class顶层定义一个_value来存储传进来的value,然后后续操作这个_value就够了,可以看到happy path的实现是比较简单的。

12_实现ref功能

三、完善逻辑 v2.0

首先来看第一个单测的逻辑。

// src/reactivity/tests/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不应该再次触发更新
  // a.value = 2;
  // expect(calls).toBe(2);
});

先运行一下单测,看看哪里会出问题,然后针对性的去结局问题。

12_实现ref功能

可以看到第二个用例测试失败,我们期望calls2,实际值为1,说明effect没有调用第二次,也就是当a 的值发生变化后,依赖没有被重新触发。这很容易理解,因为我们根本就没去收集依赖。

那接下来,我们就来完善这部分内容?其实也不难,回去看effect就能知道,我们已经做过相关的内容了。只需要抽离封装部分代码,然后复用即可。

// src/reactivity/effect.ts

// + 抽离dep的收集逻辑
export function trackEffects(dep) {
  if (dep.has(activeEffect)) return;

  dep.add(activeEffect);
  activeEffect.deps.push(dep);
}

// + 抽离dep的触发逻辑
export function triggerEffects(dep) {
  for (const effect of dep) {
    if (effect.scheduler) {
      // ps: effect._fn 为了让scheduler能拿到原始依赖
      effect.scheduler(effect._fn);
    } else {
      effect.run();
    }
  }
}

再回到ref.ts,将effect中抽离的trackEffectstriggerEffects集成进来。

// src/reactivity/ref.ts

import { triggerEffects, trackEffects } from './effect';

class RefImpl {
  private _value: any;
  public dep;

  constructor(value: any) {
    this._value = value;
    this.dep = new Set();
  }

  get value() {
    trackEffects(this.dep);
    return this._value;
  }

  set value(newVal: any) {
    this._value = newVal;
    triggerEffects(this.dep);
  }
}

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

再跑一遍单测。

12_实现ref功能

白给,用例1出现了错误,activeEffect没了,是undefined

分析一下,大概就是两个问题:

  1. 为什么activeEffect会是undefined

    activeEffect是在run的时候去赋值的,也就是必须要有相关的effect。在第一个测试用例中,可以看出我们并没有相关的依赖,所以也就不存在依赖收集的情况。

  2. 该怎么解决这个问题? 那这么说,我们此时并不需要去收集依赖。实际上,我们之前的isTracking就是用来判断,是否应该收集依赖。加上即可。

if (isTracking()) {
  trackEffects(this.dep);
}
return this._value;
12_实现ref功能

可以看到,用例1通过了。

那我们继续放开下面两行,继续完善用例2最后的逻辑。那还是老样子,先跑一下单测,确定一下问题所在。

12_实现ref功能

依旧很轻松可以看出,设置同样的值,calls加一,意思是effect还是被触发了一遍,这跟我们预想的并不一致。

那我们就需要在triggerEffects前判断,新设置的值是否有变化。

if (Object.is(newVal, this._value)) {
  this._value = newVal;
  triggerEffects(this.dep);
}

可能后续会经常用到这类函数,所以我们考虑封装一下进shared里面。

// src/shared/index.ts

export const hasChanged = (val, newVal) => {
  return !Object.is(val, newVal);
};

那继续回去用起来。

if (hasChanged(newVal, this._value)) {
  this._value = newVal;
  triggerEffects(this.dep);
}

逻辑差不多完善,跑一下单测。

12_实现ref功能

通过,美滋滋。


四、完善逻辑 v3.0

// src/reactivity/tests/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;
  expect(dummy).toBe(2);
});

其实这里的处理就很简单了,当我们用_value去存储value的时候,只需要先判断一下是否是对象,如果是对象,就用reactive包裹即可。

class RefImpl {
  private _value: any;
  public dep;

  constructor(value: any) {
    // + 复用isObject
    this._value = isObject(value) ? reactive(value) : value;
    this.dep = new Set();
  }

  get value() {
    trackRefValue(this);
    return this._value;
  }

  set value(newVal: any) {
    if (hasChanged(newVal, this._value)) {
      this._value = newVal;
      triggerEffects(this.dep);
    }
  }
}
12_实现ref功能

至此,三个单元测试全部通过,ref的基本实现也已经完成,喝彩!!!

ps

这是一个 早起俱乐部

⭐️ 适合人群:所有想有所改变的人,可以先从早起半小时开始!抽出30分钟,从初心开始!! ⭐️ 没有任何其它意味,只是本人想寻找一起早起、志同道合的小伙伴。