实现 ref

在本小节中,我们将会实现 ref

1. happy path

it('happy path', () => {
    const refFoo = ref(1)
    expect(refFoo.value).toBe(1)
})

下面我们去实现

// ref.ts
export function ref(value) {
  return { value }
}

2. ref 应该是响应式

it('ref should be reactive', () => {
    const r = ref(1)
    let dummy
    let calls = 0
    effect(() => {
        calls++
        dummy = r.value
    })
    expect(calls).toBe(1)
    expect(dummy).toBe(1)
    r.value = 2
    expect(calls).toBe(2)
    expect(dummy).toBe(2)
})

通过对 ref 的说明,我们发现 ref 传入的值大多数情况下是一个原始值。那么我们就不能通过 Proxy 的特性来对值进行封装了,这个时候我们可以采用 class getter setter 的方式来进行实现收集依赖和触发依赖

3.1 happy path 通过 class 实现

class RefImpl {
  private _value: any
  constructor(value) {
    this._value = value
  }
  get value() {
    return this._value
  }
}

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

我们先把 happy path 通过 class 的方式实现

3.2 实现响应式

这里我们要响应式其实还是和 reactive 的套路是一样的,在 get 中收集依赖,在 set 中触发依赖。这个时候我们就可以去复用 reactive 的 track 逻辑了。

// 我们先看看现在 track 的逻辑

export 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)
  }
  if (dep.has(activeEffect)) return
  activeEffect.deps.push(dep)
  dep.add(activeEffect)
}

其实有很多是无法通用的,下面的收集依赖的逻辑我们就可以单独抽离出来了

export 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 (dep.has(activeEffect)) return
  activeEffect.deps.push(dep)
  dep.add(activeEffect)
}

而 trigger 呢?

export function trigger(target, key) {
  const depsMap = targetMap.get(target)
  const deps = depsMap.get(key)
  for (const effect of deps) {
    if (effect.options.scheduler) {
      effect.options.scheduler()
    } else {
      effect.run()
    }
  }
}

同理,我们也可以将 trigger 单独的部分抽离出来,在重构了一个模块后,需要重新运行一遍测试,查看是否功能正常

export function trigger(target, key) {
  const depsMap = targetMap.get(target)
  const deps = depsMap.get(key)
  triggerEffect(deps)
}

export function triggerEffect(deps) {
  for (const effect of deps) {
    if (effect.options.scheduler) {
      effect.options.scheduler()
    } else {
      effect.run()
    }
  }
}

最后,我们就可以在 ref 中

class RefImpl {
  private _value: any
  // 这里我们也需要一个 deps Set 用于储存所有的依赖
  public deps = new Set()
  constructor(value) {
    this._value = value
  }
  get value() {
    // 在 get 中进行依赖收集
    trackEffect(this.deps)
    return this._value
  }
  set value(newValue) {
    this._value = newValue
    // 在 set 中进行触发依赖
    triggerEffect(this.deps)
  }
}

这样我们再去运行测试发现就可以通过了

3.3 相同的值不会触发依赖

r.value = 2
expect(calls).toBe(2)
expect(dummy).toBe(2)

这个实现方法也非常简单:

class RefImpl {
  // other code ...
  set value(newValue) {
    // 在这里进行判断
    if (newValue === this._value) return
    // other code ...
  }
}

其实这里的判断我们可以写一个工具函数:

// shared/index.ts
export function hasChanged(val, newVal) {
  return !Object.is(val, newVal)
}
class RefImpl {
  // other code ...
  set value(newValue) {
    // 在这里用这个工具函数进行判断
    if (hasChanged(this._value, newValue)) {
      this._value = newValue
      triggerEffect(this.deps)
    }
  }
}

这个时候我们再进行测试发现就没有问题了

3. 嵌套 prop 应该是 reactive 的

我们先看看单元测试

it('should make nested properties reactive', () => {
    const a = ref({
        foo: 1,
    })
    let dummy
    effect(() => {
        dummy = a.value.foo
    })
    a.value.foo = 2
    expect(dummy).toBe(2)
    expect(isReactive(a.value)).toBe(true)
})

那这个我们该怎么实现呢?

class RefImpl {
  // other code ...
  constructor(value) {
    // 在这里进行一下判断,如果是 Object 的话,就对其进行 reacitve
    this._value = isObject(value) ? reactive(value) : value
  }
  // other code ...
}

下面我们再跑一下测试,就可以跑通了