手撸mini-vue之ref

122 阅读2分钟

reactive 通过 proxy 进行 get set 操作实现响应式。但是 reactive 只作用于对象。那么像 1,true,'1'这类的单值怎么实现响应式呢?

ref

想实现这些单值的响应式,本质上也是要去思考怎么实现 get set 操作

class RefImpl {
  get value() {
    ...
  }
  
  set value() {
     ...
  }
}

从上面的代码可以看出 用这个class包裹一个 get set,从而实现依赖收集和依赖触发,接下来我们去实现一下 ref 这个功能

happy path 单元测试
descreibe("ref", () => {
  it("happy path", () => {
    const a = ref(1);
    expect(a.value).toBe(1);
  });
})

从以上单元测试可知,用 ref 包裹1 赋值给 a,通过 a.value 得到 1

代码实现
class RefImpl {
  private _value: any;
  
  constructor(value) {
    this._value = value;
  }
  
  get value() {
    return this._value;
  }
}

export function ref(value) {
  return new RefImpl(value);
}
响应式 单元测试
descreibe("ref", () => {
  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);
    // same value should not trigger
    a.value = 2;
    expect(calls).toBe(2);
    expect(dummy).toBe(2);
  });
})

从以上单元测试可知,当 a 的值发生变化,dummy 和 calls 的值也发生变化。当把一个重复的值赋值给 a,不进行触发依赖

代码实现
class RefImpl {
  private _value: any;
  public dep; // 存放依赖的容器
  
  constructor(value) {
    this._value = value;
    this.dep = new Set();
  }
  
  get value() {
    // 判断是否存在 activeEffect
    trackRefValue(this)
    return this._value;
  }
  
  set value(newValue) {
    if (hasChanged(this._value, newValue)) {
      this._value = newValue;
      triggerEffects(this.dep);
    }
  }
}

function trackRefValue(ref) {
  if (isTracking()) {
    trackEffects(ref.dep);
  }
}

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

对 effect.ts 中 dep 收集和触发暴露出来给 ref 调用

// effect.ts

export function trackEffects(dep) {
  // 依赖已经存在 dep 中就不再添加
  if (dep.has(activeEffect)) return;

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

export function triggerEffects(dep) {
  for (let effect of dep) {
    if (effect.scheduler) {
      effect.scheduler();
    } else {
      effect.run();
    }
  }
}

export function isTracking() {
  return shouldTrack && activeEffect !== undefined;
}

公共方法调用

// shared index.ts

export const isObject = (val) => {
  return val !== null && typeof val === "object";
};

export const hasChanged = (val, newVal) => {
  return !Object.is(val, newVal);
};
ref 嵌套 reactive 单元测试
descreibe("ref", () => {
  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++;
    expect(dummy).toBe(2);
  });
})
代码实现
class RefImpl {
  private _value: any;
  private _rawValue: any; // 用于 set 时候和新值做对比
  public dep; // 存放依赖

  constructor(value) {
    this._rawValue = value;
    this._value = convert(value);
    this.dep = new Set();
  }

  get value() {
    // activeEffect 存在,才执行依赖收集
    trackRefValue(this);
    return this._value;
  }

  set value(newValue) {
    if (hasChanged(this._rawValue, newValue)) {
      this._rawValue = newValue;
      this._value = convert(newValue);
      triggerEffects(this.dep);
    }
  }
}

function convert(value) {
  return isObject(value) ? reactive(value) : value;
}

function trackRefValue(ref) {
  if (isTracking()) {
    trackEffects(ref.dep);
  }
}

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

这里声明一个 _rawValue 是为了当 value 被 reactive 包裹后是一个 proxy,而 newValue 是一个普通的对象

源码地址戳这里