实现 ref

188 阅读3分钟

这是我参与「掘金日新计划 · 2 月更文挑战」的第 1 天,点击查看活动详情

实现 ref 简版

目标:学习ref 看看 reactive 实现区别是什么 从源码的角度去分析 为什么是 ref 是.value 取值 ref 包裹对象取值为什么不需要 .value

下面是我实现的简单版本的 ref

  1. 当调用ref 的时候 返回了一个 createRef 并执行这段函数

    createRef 里面他进行了一个判断 两个参数 分别表示 传的值 和 是否 shallow 这里我们只需要关心 rawValue

  2. 进来进行isRef判断如果是一个 可以看见 其实就是通过一个标识来确认是不是ref

  3. 最后用了一个 RefImpl类 返回 其实大致可以猜一下 标识可能就在 RefImpl 进行加工了

export function ref(value: any) {
  return createRef(value, false);
}

function createRef(rawValue: any, shallow: boolean) {
  if (isRef(rawValue)) {
    return rawValue;
  }

  return new RefImpl(rawValue, shallow);
}

export function isRef(r: any) {
  return !!(r && r.__v_isRef === true);
}

接下来就是关注 RefImpl这个类到底做了什么 来看下面这段代码

  1. 可以看见在 constructor 接收了外面传的值 这时候 就如刚刚所想的一样 isRef 是在这个类上的添加的
  2. 接下来就是判断 是不是一个对象呀 是的话 toReactive 里面 用 reactive 代理进行包装 不是就返回简单数据类型在 这个__value 这里就可以解释 ref 为啥包裹一个对象还是响应式 但是 我们可以思考一下 用ref 包裹 一个对象 多执行一次 和 包裹简单数据类型 少执行一次 谁好一点呢
  3. RefImpl 中使用 get set 来更新 返回值 get value 中进行了收集依赖 当 .value 的时候 我们就返回this.__value
  4. set 的时候 会进行判断 新旧两个值是否相等 不相等进行 判断是不是一个复杂数据类型 是的话使用 reactive 包装 然后触发更新
class RefImpl {
    
  __v_isShallow: any;
  dep: undefined;
  __v_isRef: boolean;
  _value: any;

  constructor(value: any, __v_isShallow: boolean) {
    this.__v_isShallow = __v_isShallow;
    this.dep = undefined; 
    this.__v_isRef = true; // 标识
    this._value = __v_isShallow ? value : toReactive(value);
  }

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

  set value(newValue) {
    // 判断是否需要更新 如果新值和旧值不相等
    if (hasChanged(newValue, this._value)) {
      // 如果是一个对象 那么就进行包装
      this._value = isObject(newValue) ? reactive(newValue) : newValue;
      // 触发更新
      triggerRefValue(this);
    }
  }
}

我的单元测试

describe('Ref', () => {
  it('create Ref', () => {
    const initRef = ref('sakana');
    const initNum = ref(1);

    expect(initRef.value).toBe('sakana'); // 通过
    expect(initNum.value).toBe(1); // 通过

    initNum.value = 2;
    expect(initNum.value).toBe(2); // 通过
    initRef.value = 'sakana2';
    expect(initRef.value).toBe('sakana2'); // 通过

    expect(isRef(initRef)).toBe(true);
  });

  it('create Ref with object', () => {
    const initRef = ref({ name: 'sakana' });
    let name;
    effect(() => {
      name = initRef.value.name;
    });
    initRef.value.name = 'sakana2';
    expect(initRef.value.name).toBe('sakana2'); // 通过
    expect(name).toBe('sakana'); // 通过
  });
});

思考

在我们面试vue3的过程中 我们会被这样一个问题 为什么 reactive 解构会失去响应式?

揭秘:

  • 之前上一篇实现 reactive 中 我们其实是知道 reactive 使用了 Proxy 加 Reflect 然后自己的一套 get set 函数处理规则 通过 Proxy 返回的是一个新对象
  • 当我们 尝试 解构 reactive 直接取出对象的值 那么 这个值就不在属于是 Proxy 代理 内部的 get set 收集依赖 和触发更新就不会在进行 所以 就会失去响应式