从使用Vue3到深入原理(二):reactivity模块的实现

1,823 阅读19分钟

前言

:本文过长过干,观看前请备足水分。and,虽然是ts写的,但是并不关心类型,所有的类型注解基本只是为了防报错。

在手写Vue3核心代码之前,作者完全不知道TDD(测试驱动开发)是啥,在完成下文代码之前补习了一下TDD的基本思想以及一款测试框架——jest。所以在阅读之前,最好希望你对jest框架有基本的认知(阅读官方文档半小时能入门),以及明白为何在完成功能之前需要先写测试代码——这有助于减少调试的耗时,细致拆分的测试文件能够将bug扼杀在摇篮之中。

同时,不仅仅是完成代码,为了保证代码的可读性还会对完成功能后的代码进行重构,当然作者的重构能力仅限参考,清谨慎阅读😀。

reactivity的核心流程

以一段reactiveeffect的配合使用的代码来说明响应式的核心流程——依赖收集以及依赖触发。

    const user = reactive({
      age: 10
    })
​
    let nextAge
    effect(() => {
      nextAge = user.age + 1
    })
    console.log(nextAge) //11
    //update
    user.age++
    console.log(nextAge) //12

这里演示了一个最基本功能的数据响应式案例,effect函数和reactive函数都是reactivity模块中的核心API,reactive函数通过Proxy代理传入的对象,在getter和setter阶段会分别执行依赖收集以及依赖的触发的工作。而effect函数接收一个回调函数,初始默认执行一次,并在回调中对应的依赖更新时再次执行。

当然reactivity还包含其他响应式API如ref等,这里先实现最基本的reactive和effect模块。

reactive和effect的实现

分别测试reactive和effect的逻辑,单元测试(jest) 代码如下:

//effect.spec.ts
//effect测试了reactive的响应式
describe('effect', () => {
  it('happy path', () => {
    const user = reactive({
      age: 10
    })
​
    let nextAge
    effect(() => {
      nextAge = user.age + 1
    })
    expect(nextAge).toBe(11)
    //update
    user.age++
    expect(nextAge).toBe(12)
  })
})

reactive和effect的代理和依赖收集及触发功能的实现

第一步,实现reactive的代理中的get和set

//reactive.ts
export function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      //收集依赖 在effect中实现track,因为要获取activeEffect对象
      //...
      return Reflect.get(target, key)
    },
    set(target, key, value) {
      //触发依赖 在effect中实现trigger
      //...
      return Reflect.set(target, key, value)
    }
  })
}

在这里拆分并测试一下reactive函数的逻辑,是可以通过的

//reactive.spec.ts
//测试reactive的代理功能
describe('reactive', () => {
  it('happy path', () => {
    const obj = { a: 1 }
    const reactiveObj = reactive(obj)
    expect(reactiveObj).not.toBe(obj)
    expect(reactiveObj.a).toBe(1)
  }));

第二步,再get和set中分别收集依赖(track)和收集依赖(trigger),这两个函数的实现放在了effect.ts文件中,因为需要利用外部作用域和effect函数共用activeEffect对象(当前活跃的ReactiveEffect对象),这个点后面就知道为什么了。

//effect.ts中的track和trigger的实现
let activeEffect: ReactiveEffect
const targetsMap = new Map
export function track(target, key) {
  //读取targetsMap中target对应的depsMap
  if (!targetsMap.has(target)) targetsMap.set(target, new Map())
  let depsMap = targetsMap.get(target)
  if (!depsMap.has(key)) depsMap.set(key, new Set)
  let deps = depsMap.get(key)
  //将现在的effect对象放入deps
  deps.add(activeEffect)
}
​
export function trigger(target, key, value) {
  //读取targetsMap中target对应的depsMap
  if (!targetsMap.has(target)) targetsMap.set(target, new Map())
  let depsMap = targetsMap.get(target)
  if (!depsMap.has(key)) depsMap.set(key, new Set)
  let deps = depsMap.get(key)
  //通知依赖
  for (let dep of deps) {
    dep.run()
  }
}

第三步,编写effect函数的功能——执行传入的函数,并且能够配合track和trigger实现依赖的触发与收集

class ReactiveEffect {
  fn: Function
  constructor(fn) {
    this.fn = fn
  }
  run() {
    this.fn()
  }
}
​
export function effect(fn: Function) {
  //构造一个ReactiveEffect实例
  let _effect = new ReactiveEffect(fn)
  activeEffect = _effect
  _effect.run()
}

这里不是直接在effect函数中执行fn,而是定义了一个ReactiveEffect对象,这个对象其实是观察者模式中的观察者(Observer),观察者上面定义了run函数,在主体(Subjcet)——也就是之前reactive所代理对象的某个key值发生改变时,会通知所有对应的观察者——即track和trigger中的deps集合中所包含的对象执行其run函数。

image-20220621103820994.png

完善effect的功能

effect返回runner

effect的返回值runner是一个函数,且具有和传入的函数相同的功能和返回值。测试代码如下:

  //测试返回runner的功能
  it('return runner', () => {
    let foo = 1
    let fn = effect(() => {
      foo++
      return 'foo'
    })
    expect(foo).toBe(2)
    const n = fn()
    expect(foo).toBe(3)
    expect(n).toBe('foo')
  });
})

实现也很简单,修改后的代码如下:

class ReactiveEffect {
  private fn: Function
  constructor(fn) {
    this.fn = fn
  }
  run() {
    activeEffect = this
    return this.fn()
  }
}
​
export function effect(fn: Function) {
  //构造一个ReactiveEffect实例
  let _effect = new ReactiveEffect(fn)
  _effect.run()
  //bind是因为run函数内部有this值的指向问题
  return _effect.run.bind(_effect)
}

scheduler

effect的第二个参数是可选参数——配置对象options,其中的scheduler是一个函数对象,如果有scheduler的话,effect只会在初始化时执行fn,在之后的触发更新阶段都会执行scheduler。测试代码如下:

  //测试scheduler功能
  it('scheduler ', () => {
    let run: any
    let scheduler = function () {
      run = runner
    }
    let foo = reactive({ bar: 1 })
    let bee
    let runner = effect(() => {
      bee = foo.bar + 1
      return 'runner'
    }, {
      scheduler: scheduler
    })
    expect(bee).toBe(2)
    //trigger,执行的不是fn,而是scheduler
    foo.bar++//foo.bar == 2 ,没有执行fn,所以bee还是2 ,执行了scheduler,所以run可以使用
    expect(bee).toBe(2)
    const n = run()//执行run,即执行fn函数,此时bee == foo.bar +1 ==3,且n为fn的返回值
    expect(bee).toBe(3)
    expect(n).toBe('runner')
  });

实现如下,只需要改变effect的二个参数为可选参数,以及改写ReactiveEffect对象的构造函数并向外暴露scheduler,在trigger中判断并调用即可。

//effect第二个参数为可选参数
export function effect(fn: Function, options?: any) {
  //构造一个ReactiveEffect实例
  let _effect = new ReactiveEffect(fn, options)
  _effect.run()
  return _effect.run.bind(_effect)
}
//向外暴露scheduler
class ReactiveEffect {
  private fn: Function
  public scheduler
  constructor(fn, options={}) {
    this.fn = fn
    this.scheduler = options.scheduler
  }
  run() {
    activeEffect = this
    this.fn()
    return this.fn
  }
}
//trigger中调用
export function trigger(target, key, value) {
  //读取targetsMap中target对应的depsMap
  if (!targetsMap.has(target)) targetsMap.set(target, new Map())
  let depsMap = targetsMap.get(target)
  if (!depsMap.has(key)) depsMap.set(key, new Set)
  let deps = depsMap.get(key)
  //通知依赖
  for (let dep of deps) {
    if (dep.scheduler) dep.scheduler()
    else dep.run()
  }
}

stop和onStop

effect向外暴露一个stop函数,通过 stop(runner)(runner是effect的返回值,即effect对象上的run方法)取消触发effect对应的依赖,再次执行runner则又可以恢复其依赖的触发。

而onStop是effect的配置选项,会在用户调用 stop(runner)时调用对应effect对象的onStop回调,这一部分的测试代码如下,这部分遇到了一些插曲,分享一些bug的解决过程。

  //测试stop和onStop
  it('stop and onStop', () => {
    let foo = reactive({ bar: 1 })
    let dummy
    let str = ''
    let onStop = jest.fn(() => { str += 'stop!' })
    let runner = effect(() => {
      dummy = foo.bar
    }, { onStop })
    expect(dummy).toBe(1)
    stop(runner)
    foo.bar = 1
    //foo.bar++ 如果是这种写法,测试不通过,问题后续再讲
    expect(dummy).toBe(1)//dummy失去了响应性
    expect(onStop).toBeCalledTimes(1)//onStop被调用
    runner()
    expect(dummy).toBe(2)//dummy恢复响应性
  });

首先,实现stop部分——向外暴露一个stop函数,接受runner,该如何清除依赖呢?我们的做法是通过在收集依赖的同时反向记录依赖所存在的位置:即观察者也会收集主体的dep。

image-20220621171115229.png

//trigger部分的修改实现
export function track(target, key) {
  //......
  //反向记录,每个Effect对象都会记录自己所在的deps
  activeEffect.deps.add(dep)
}
class ReactiveEffect {
  //...
  public deps: Set<any>
}

通过类内部访问这个deps属性是最好的,但是stop函数只有runner参数,因此我们将对应的ReactiveEffect对象挂载到runner上

export function stop(runner) {
  runner._effect.stop()
}
export function effect(fn: Function, options?: any) {
  //构造一个ReactiveEffect实例
  let _effect = new ReactiveEffect(fn, options)
  effect.run()
  const runner:any = _effect.run.bind(_effect)
  runner._effect = _effect
  return runner
}

之后再类内部实现stop方法如下,而onStop只需要在里面调用即可

class ReactiveEffect {
  private fn: Function
  public scheduler?
  public onStop?: Function
  public deps: Set<any>
  constructor(fn, options?) {
    this.fn = fn
    this.scheduler = options.scheduler
    this.onStop = options.onStop
    this.deps = new Set
  }
  //...
  stop() {
    //找到deps并一一删除
    this.deps.forEach(dep => dep.delete(this))
    //执行onStop回调
    this.onStop && this.onStop()
  }
}

最后测试一哈,通过!但是在这有作者在测试中发现的一个小问题可以优化:

一点小bug

如下,只是改动了foo.bar的赋值方式,就发现测试不通过,并不是stop不起作用,原因在于foo.bar++等同于 foo.bar = foo.bar + 1的操作,会再次触发一次get和set,此时的activeEffect还是之前的effect,所以又执行了依赖收集和触发。

  //测试stop和onStop
  it('stop and onStop', () => {
    let foo = reactive({ bar: 1 })
    let dummy
    let str = ''
    let onStop = jest.fn(() => { str += 'stop!' })
    let runner = effect(() => {
      dummy = foo.bar
    }, { onStop })
    expect(dummy).toBe(1)
    stop(runner)
    foo.bar++//foo.bar++ 如果是这种写法,测试不通过
    expect(dummy).toBe(1)//dummy失去了响应性
    expect(onStop).toBeCalledTimes(1)//onStop被调用
    runner()
    expect(dummy).toBe(2)//dummy恢复响应性
  });

如何解决

可以在stop中清除活动对象activeEffect并在收集依赖时判断activeEffect的存在性。当然也有另外的解决方式(官方做法)——在ReactiveEffect实例对象上添加是否需要被tracking的标志isTracking,并在执行runner时置为true,在stop后被置为false,在收集依赖时判断该标志即可。感兴趣的同学可以自行尝试。

export function track(target, key) {
  //...
  //当前如果没有活动对象
  if (!activeEffect) return
  //...
}
class ReactiveEffect {
  //...
  stop() {
    //找到deps并一一删除
    this.deps.forEach(dep => dep.delete(this))
    //清空活动对象
    activeEffect = null
    //执行onStop回调
    this.onStop && this.onStop()
  }
}

最后对代码进行了一些重构小优化,完整代码如下:

import { extend } from "./shared/extend"class ReactiveEffect {
  private fn: Function
  public scheduler?
  public onStop?: Function
  public deps: Set<any> = new Set
  active: boolean = true
  constructor(fn) {
    this.fn = fn
  }
  run() {
    activeEffect = this
    this.active = true
    return this.fn()
  }
  stop() {
    if (this.active) {
      cleanEffect(this)
      //执行onStop回调
      this.onStop && this.onStop()
      this.active = false
    }
  }
}
function cleanEffect(effect) {
​
  //找到deps并一一删除
  effect.deps.forEach(dep => dep.delete(effect))
​
  //清空活动对象
  activeEffect = null
}
export function effect(fn: Function, options?: any) {
  //构造一个ReactiveEffect实例
  let _effect = new ReactiveEffect(fn)
  //将options的属性赋予_effect
  extend(_effect, options)
  _effect.run()
  const runner: any = _effect.run.bind(_effect)
  runner._effect = _effect
  return runner
}
let activeEffect: ReactiveEffect | null
const targetsMap = new Mapexport function track(target, key) {
  //读取targetsMap中target对应的depsMap
  if (!targetsMap.has(target)) targetsMap.set(target, new Map())
  let depsMap = targetsMap.get(target)
  if (!depsMap.has(key)) depsMap.set(key, new Set)
  let dep = depsMap.get(key)
  //当前如果没有活动对象
  if (!activeEffect) return
  //将现在的effect对象放入deps
  dep.add(activeEffect)
  //反向记录,每个Effect对象都会记录自己所在的deps
  activeEffect.deps.add(dep)
}
​
export function trigger(target, key, value) {
  //读取targetsMap中target对应的depsMap
  if (!targetsMap.has(target)) targetsMap.set(target, new Map())
  let depsMap = targetsMap.get(target)
  if (!depsMap.has(key)) depsMap.set(key, new Set)
  let dep = depsMap.get(key)
  //通知依赖
  for (let effect of dep) {
    if (effect.scheduler) effect.scheduler()
    else effect.run()
  }
}
​
export function stop(runner) {
  runner._effect.stop()
}

实现readonly

readonly方法可以将一个普通对象或者响应式对象变为readonly对象,测试代码如下:

import { readonly } from "../reactive";
​
describe('readonly', () => {
  it('happy path', () => {
    const raw = { foo: 1 }
    let dummy = readonly(raw)
    expect(dummy).not.toBe(raw)
    expect(dummy.foo).toBe(1)
    console.warn = jest.fn()
    dummy.foo = 2
    expect(console.warn).toBeCalledTimes(1)
  });
});

实现如下,和reactive的实现逻辑基本一致,但是不需要触发和收集依赖(因为不会改变对象中的值)。

export function readonly(obj) {
  return new Proxy(obj, {
    get(target, key) {
      const res = Reflect.get(target, key)
      return res
    },
    set(target, key, value) {
      //抛出警告
      console.warn(`${target}为readonly对象,不能对${key.toString()}做修改`)
    }
  })
}

重构&优化

到这里测试是可以通过的,但是代码结构显然有些臃肿,readonly和reactive的重合部分过多,且缺乏可读性。我们可以首先将两者的get和set抽离出去,定义两个高阶函数createGetter和createSetter函数,通过传入的isReadonly标识将readonly和reactive的逻辑分离

import { track, trigger } from "./effect"
export function reactive(obj) {
  return new Proxy(obj, {
    get: createGetter(),
    set: createSetter()
  })
}
export function readonly(obj) {
  return new Proxy(obj, {
    get: createGetter(true),
    set: createSetter(true)
  })
}
​
function createGetter(isReadonly: boolean = false) {
  return function get(target, key) {
    const res = Reflect.get(target, key)
    if (!isReadonly) {
      //收集依赖
      //track函数传入target和key,通过targetsMap找到target对应的依赖depsMap,再根据key设置对应的deps的fn
      track(target, key)
    }
    return res
  }
​
}
function createSetter(isReadonly: boolean = false) {
  return function set(target, key, value) {
​
    if (!isReadonly) {
      const res = Reflect.set(target, key, value);
      //触发依赖
      //同样是通过targetsMap和depsMap,依次触发deps中的回调函数
      trigger(target, key, value);
      return res
    }
    else {
      //抛出警告
      console.warn(`${target}为readonly对象,不能对${key.toString()}做修改`);
      return true
    }
  };
}

上面的代码还可以进一步抽离,可以看出reactive和readonly实现差别在于Proxy的第二个参数handlers上,定义reactive和eadonly对应的handlers为mutableHandlers和readonlyHandlers,将它们放到一个文件——baseHandlers.ts内。Vue3的源码也是这样的处理,对于不同类型的handlers作了语义上的划分。

//reactive.ts
import { mutableHandlers, reaonlyHandlers } from "./baseHandlers"export function reactive(obj) {
  return new Proxy(obj, mutableHandlers)
}
​
export function readonly(obj) {
  return new Proxy(obj, reaonlyHandlers)
}
//baseHandlers.ts
import { track, trigger } from "./effect";
​
export const reaonlyHandlers = {
  get: createGetter(true),
  set: createSetter(true)
}
export const mutableHandlers = {
  get: createGetter(),
  set: createSetter()
}
export function createGetter(isReadonly: boolean = false) {
  return function get(target, key) {
    //...
  };
}
export function createSetter(isReadonly: boolean = false) {
  return function set(target, key, value) {
    //...
  };
}

最后再增加一点可读性和小优化(不用每次get和set操作都要createGetter和createSetter)捏

//reactive.ts
import { mutableHandlers, reaonlyHandlers } from "./baseHandlers"export function reactive(obj) {
  return createActiveObject(obj, mutableHandlers)
}
​
export function readonly(obj) {
  return createActiveObject(obj, reaonlyHandlers)
}
​
function createActiveObject(obj: any, handlers) {
  return new Proxy(obj, handlers)
}
//baseHandlers.ts
const get = createGetter()
const set = createSetter()
const readonlyGet = createGetter(true)
const readonlySet = createSetter(true)
export const reaonlyHandlers = {
  get,
  set
}
export const mutableHandlers = {
  readonlyGet,
  readonlySet
}

重构之后别忘了测试一下之前全部的测试代码捏( ̄▽ ̄)*。

实现isReactive,isReadonly和isProxy

先写单元测试:

describe('isReactive', () => {
  it('happy path', () => {
    const foo = reactive({ bar: 1 })
    const dummy = {}
    expect(isReactive(foo)).toBe(true)
    expect(isReactive(dummy)).toBe(false)
  });
}) 
describe('isReadonly', () => {
  it('happy path', () => {
    const foo = readonly({ bar: 1 })
    const dummy = {}
    expect(isReadonly(foo)).toBe(true)
    expect(isReadonly(dummy)).toBe(false)
  });
}) 
describe('isProxy', () => {
  it('happy path', () => {
    const dummy = { bar: 1 }
    const foo = reactive(dummy)
    const bee = readonly(dummy)
    expect(isProxy(foo)).toBe(true)
    expect(isProxy(bee)).toBe(true)
    expect(isProxy(dummy)).toBe(false)
  });
});

isReactive、isReadonly和isProxy用来识别对象的类型,实现的方式非常巧妙。以isReactive举例,通过访问被检测对象的特定属性,以及在reactive中对应的get handler中处理响应式对象访被问特定属性时根据isReadonly标识符返回值,即可实现对reactive对象的判断。

isReadonly和isProxy方法实现也基本一致,完整代码如下:

//reactive.ts
//创建了一个枚举对象
export const enum ReactiveFlags {
  IS_REACTIVE = '_v_isReactive',
  IS_READONLY = '_v_isReadonly',
  IS_PROXY = '_v_isProxy'
}
export function isReactive(target) {
  return Boolean(target[ReactiveFlags.IS_REACTIVE])
}
​
export function isReadonly(target) {
  return Boolean(target[ReactiveFlags.IS_READONLY])
}
​
export function isProxy(target) {
  return Boolean(target[ReactiveFlags.IS_PROXY])
}
export function createGetter(isReadonly: boolean = false) {
  return function get(target, key) {
    if (key == ReactiveFlags.IS_REACTIVE) return !isReadonly
    else if (key == ReactiveFlags.IS_READONLY) return isReadonly
    else if (key == ReactiveFlags.IS_PROXY) return true
    //...
  };
}

实现reactive和readonly的深层嵌套逻辑

我们知道reactive和readonly代理的嵌套对象内的深层对象也是reactive/readonly类型的,测试代码如下:

//reactive
  test('nested reactive', () => {
    const original = {
      nested: {
        foo: 1
      },
      arr: [{ bar: 1 }]
    }
    const observed = reactive(original)
    expect(isReactive(observed.nested)).toBe(true)
    expect(isReactive(observed.arr[0])).toBe(true)
  })
  
//readonly
  test('nested reactive', () => {
    const original = {
      nested: {
        foo: 1
      },
      arr: [{ bar: 1 }]
    }
    const observed = readonly(original)
    expect(isReadonly(observed.nested)).toBe(true)
    expect(isReadonly(observed.arr[0])).toBe(true)
    observed.nested.foo = 2
    expect(observed.nested.foo).not.toBe(2)
  })

实现的话只需要在get中递归返回使每个对象类型的属性reactive化或readonly化对象即可:

function createGetter(isReadonly: boolean = false) {
  return function get(target, key) {
    //...
        if (isObject(res)) return isReadonly ? readonly(res) : reactive(res)
    //...
  };
}
function isObject(obj){
  return obj != null && typeof obj == 'object'
}

实现shallowReactive和shallowReadonly

前面实现了reactive和readonly的深层嵌套转换功能,现在来实现只会转换浅层对象的shallowReactive和shallowReadonly,单测如下,把上次单测的断言结果取反即可:

//reactive
  test('nested reactive', () => {
    const original = {
      nested: {
        foo: 1
      },
      arr: [{ bar: 1 }]
    }
    const observed = reactive(original)
    expect(isReactive(observed.nested)).toBe(false)
    expect(isReactive(observed.arr[0])).toBe(false)
  })
  
//readonly
  test('nested readonly', () => {
    const original = {
      nested: {
        foo: 1
      },
      arr: [{ bar: 1 }]
    }
    const observed = readonly(original)
    expect(isReadonly(observed.nested)).toBe(false)
    expect(isReadonly(observed.arr[0])).toBe(false)
    observed.nested.foo = 2
    expect(observed.nested.foo).toBe(2)
  })

同readonly的做法一样,在创建shallowHandlers的get时,定义一个shallow标识符,根据标识符决定是否深层递归收集依赖。

function createGetter(isReadonly: boolean = false, isShallow: boolean = false) {
  return function get(target, key) {
    if (key == ReactiveFlags.IS_REACTIVE) return !isReadonly
    else if (key == ReactiveFlags.IS_READONLY) return isReadonly
    const res = Reflect.get(target, key);
    //先判断是否会深层递归,再判断readonly
    if (!isShallow && isObject(res)) return isReadonly ? readonly(res) : reactive(res)
    if (!isReadonly) {
      //收集依赖
      //track函数传入target和key,通过targetsMap找到target对应的依赖depsMap,再根据key设置对应的deps的fn
      track(target, key);
    }
    return res;
  };
}

之后根据createGetter创建shallowReactiveGet和shallowReadonlyGet,再利用之前定义好的extend(Object.assign的语义化赋值)重写mutableHandlers和readonlyHandlers的get,得到最终的shallowReactiveHandlers和shallowReadonlyHandlers:

const get = createGetter()
const set = createSetter()
const readonlyGet = createGetter(true)
const readonlySet = createSetter(true)
const shallowReactiveGet = createGetter(false, true)
const shallowReadonlyGet = createGetter(true, true)
export const mutableHandlers = {
  get,
  set
}
export const readonlyHandlers = {
  get: readonlyGet,
  set: readonlySet
}
export const shallowReactiveHandlers = extend({}, mutableHandlers, { get: shallowReactiveGet })
export const shallowReadonlyHandlers = extend({}, readonlyHandlers, { get: shallowReadonlyGet })

然后再导出shallowReactive和shallowReadonly方法:

export function shallowReactive(obj) {
  return createActiveObject(obj, shallowReactiveHandlers)
}
​
export function shallowReadonly(obj) {
  return createActiveObject(obj, shallowReadonlyHandlers)
}

测试通过,别忘了把之前所有的单元测试再测一遍噢( •̀ ω •́ )y

实现ref

ref是reactivity模块中一个重要的api,它会将普通值封装成一个响应式对象,同value属性来调用值,如果传入的是一个对象,则会调用reactivity方法进行封装。

测试代码分为三个部分:happy path、与effect配合实现响应式、处理对象

describe('ref', () => {
  test('happy path', () => {
    const foo = ref(1)
    expect(foo.value).toBe(1)
  })
  test('ref make value reactive', () => {
    const foo = ref(1)
    let dummy
    let fn = jest.fn(() => {
      dummy = foo.value
    })
    effect(fn)
    expect(dummy).toBe(1)
    expect(fn).toBeCalledTimes(1)
    foo.value = 2
    expect(dummy).toBe(2)
    expect(fn).toBeCalledTimes(2)
    //赋旧值不触发依赖
    foo.value = 2
    expect(dummy).toBe(2)
    expect(fn).toBeCalledTimes(2)
  })
  test('make nested properties reactive', () => {
    const foo = ref({ bar: 1 })
    let dummy
    effect(() => {
      dummy = foo.value.bar
    })
    foo.value.bar++
    expect(dummy).toBe(2)
  })
});

首先实现happy path,实现ref函数返回一个具有value值的对象,我们可以定义一个类RefImpl(ref接口)来实现ref的各个逻辑。返回一个实例,实例上具有value的get和set方法:

class RefImpl {
  private _value
  constructor(value) {
    this._value = value
  }
  get value() {
    return this._value
  }
  set value(newValue) {
    this._value = newValue
  }
}
export function ref(value) {
  return new RefImpl(value)
}

接下来实现ref的响应式功能,这部分和reactive的实现类似,被观察主体Subject是RefImpl实例对象,上面应该有一个dep属性用来记录所有的依赖,以及在value被get时收集依赖、被set时触发依赖。并且收集依赖和促发依赖的逻辑在track和trigger中已经实现,将其抽离成两个函数trackEffect和triggerEffect:

//effect.ts
export function trackEffect(dep) {
  //当前如果没有活动对象
  if (!activeEffect) return
  //将现在的effect对象放入deps
  dep.add(activeEffect)
  //反向记录,每个Effect对象都会记录自己所在的deps
  activeEffect.deps.add(dep)
}
export function triggerEffect(dep) {
  //通知依赖
  for (let effect of dep) {
    if (effect.scheduler) effect.scheduler()
    else effect.run()
  }
}

别忘了在set时检查新旧值是否不同,完善后的ref部分代码如下:

import { trackEffect, triggerEffect } from "./reactivity/effect"class RefImpl {
  private _value
  private dep: Set<any>
  constructor(value) {
    this._value = value
    this.dep = new Set
  }
  get value() {
    //收集依赖
    trackEffect(this.dep)
    return this._value
  }
  set value(newValue) {
    //判断是新值还是旧值
    if (Object.is(newValue, this._value)) return
    this._value = newValue
    //触发依赖
    triggerEffect(this.dep)
  }
}
export function ref(value) {
  return new RefImpl(value)
}

最后ref在处理对象时会调用reactive方法,只需要在构造RefImpl实例时判断即可。但是由于引入了reactive代理对象,所以在value为对象的情况下,新旧value比较时,旧的value已经变成Proxy对象,因此直接比较普通对象代理对象是不合理的。我们的处理如下:在RefImpl实例上添加rawValue属性,记录旧对象的源对象。

import { trackEffect, triggerEffect } from "./reactivity/effect"
import { reactive } from "./reactivity/reactive"
import { isObject } from "./reactivity/shared/isObject"class RefImpl {
  private _value
  private dep: Set<any>
  private rawValue
  constructor(value) {
    //判断是普通值还是对象
    this._value = isObject(value) ? reactive(value) : value
    this.rawValue = value
    this.dep = new Set
  }
  get value() {
    //收集依赖
    trackEffect(this.dep)
    return this._value
  }
  set value(newValue) {
    //判断是新值还是旧值
    if (Object.is(newValue, this.rawValue)) return
    this._value = newValue
    //触发依赖
    triggerEffect(this.dep)
  }
}
export function ref(value) {
  return new RefImpl(value)
}

到这里所有的单元测试都是可以通过的,我们来对这部分代码做一些重构工作。可以看到构造函数和set中根据传入值来修改value值的逻辑是复用的,将这部分抽离成createRefValue函数。以及将判断新旧值是否相同抽离成更有语义化的表达hasChanged函数,并将其放入shared文件夹中供之后使用。

//effect.ts
function createRefValue(ref, value) {
  //判断是普通值还是对象
  ref._value = isObject(value) ? reactive(value) : value
}
//shared/hasChanged.ts
export const hasChanged = function (A, B) {
  return Object.is(A, B)
}

重构完成之后别忘了重新测试一下噢(●'◡'●)

一些思考:到这里大家就发现,对于普通值,如果将ref解构赋予另外的变量,则该变量不会是响应式的(没有通过实例属性访问时不会触发依赖),而如果value值是对象,解构出的value依然具有响应性(此时是一个reactive对象)。这也是我们在开发时经常遇到的情况。

  test('deconstruct', () => {
    const foo = ref({ bar: 1 })
    const bee = ref(1)
    let dummy
    const fn = jest.fn(() => {
      dummy = foo.value.bar
      dummy = bee.value + 1
    })
    effect(fn)
    expect(fn).toBeCalledTimes(1)
    let a = foo.value
    let b = bee.value
    a.bar = 2
    expect(fn).toBeCalledTimes(2)
    b = 3
    expect(fn).toBeCalledTimes(2)
  })

实现isRef和unRef

实现isRef只需要在RefImpl中定义一个_v_isRef属性即可。而unRef则是 isRef(obj)?:obj.value:obj的语法糖实现,用来对ref对象解包,测试和代码实现如下:

//ref.spec.ts 
test('isRef and unRef', () => {
    const foo = ref(1)
    const bar = reactive({ a: 1 })
    expect(isRef(foo)).toBe(true)
    expect(isRef(bar)).toBe(false)
    expect(unRef(foo)).toBe(1)
  })
​
//ref.ts
class RefImpl {
  //...
  readonly _v_isRef = true
  //...
}
export function isRef(obj) {
  return Boolean(obj._v_isRef)
}
export function unRef(obj) {
  return isRef(obj) ? obj.value : obj
}
​

实现proxyRefs

proxyRefs用来对含有ref对象的对象做解包处理,达到不用.value就能读取ref对象值的效果。使用场景主要是在template中使用setup返回的ref对象。单元测试如下:

  test('proxyRefs', () => {
    const foo = { name: 'foo', age: ref(18) }
    const dummy = proxyRefs(foo)
    expect(foo.age.value).toBe(18)
    expect(dummy.age).toBe(18)
​
    dummy.age = 19
    expect(dummy.age).toBe(19)
    expect(foo.age.value).toBe(19)
​
    dummy.age = ref(100)
    expect(dummy.age).toBe(100)
    expect(foo.age.value).toBe(100)
  })

proxyRefs是对一个对象中含有的ref对象进行浅层解包,使用proxy对该对象进行代理,在get时对对象属性进行unRef解包,在set时对ref对象的value值修改或者对整个属性进行替换。将该handlers抽离为一个独立的handlers

export function proxyRefs(objWithRefs) {
  return new Proxy(objWithRefs, objWithRefsHandlers)
}
export const objWithRefsHandlers = {
  get(target, key) {
    return unRef(Reflect.get(target, key))
  },
  set(target, key, value) {
    return isRef(target[key]) && !isRef(value)
      ? target[key].value = value
      : Reflect.set(target, key, value)
  }
}

实现computed

computed也是响应式的一个核心功能,先将它的功能测试点列出:

  1. computed默认接受一个回调函数getter,返回封装了该函数返回值的对象,类似于ref对象,用.value可以调用该返回值。
  2. computed具有缓存的效果,多次读取computed对象的value会读取缓存值,而不是重复执行getter的所有行为。
  3. computed的行为是lazily地,只有在读取computed对象的value时才会执行getter。

测试代码如下,copy了官方的一部分测试代码。

describe('computed', () => {
  it('should return updated value', () => {
    const value = reactive<{ foo?: number }>({})
    const cValue = computed(() => value.foo)
    expect(cValue.value).toBe(undefined)
    value.foo = 1
    expect(cValue.value).toBe(1)
  })
​
  it('should compute lazily', () => {
    const value = reactive({})
    const getter = jest.fn(() => value.foo)
    const cValue = computed(getter)
​
    // lazy
    expect(getter).not.toHaveBeenCalled()
​
    expect(cValue.value).toBe(undefined)
    expect(getter).toHaveBeenCalledTimes(1)
​
    // should not compute again
    cValue.value
    expect(getter).toHaveBeenCalledTimes(1)
​
    // should not compute until needed
    value.foo = 1
    expect(getter).toHaveBeenCalledTimes(1)
​
    // now it should compute
    expect(cValue.value).toBe(1)
    expect(getter).toHaveBeenCalledTimes(2)
​
    // should not compute again
    cValue.value
    expect(getter).toHaveBeenCalledTimes(2)
  })
});

computed函数的实现如下:向外暴露computed函数,返回一个ComputedRefImpl实例,在类中定义获取value的get方法。为了实现缓存的效果,添加一个状态变量dirty(为false代表没有被污染,需要执行getter来污染)来判断当前是否直接读取缓存的value。

同时,也需要在合适的时候(getter中的响应式对象触发依赖时)改变这个状态变量。因此我们需要effect的介入,但是由于直接使用effect函数会初始化一次以及后续需要在触发依赖时控制变量dirty(传入的getter无法做到,需要自定义一个scheduler),这是我们不需要的,因此直接用更底层的ReactiveEffect对象以及scheduler来实现。

import { ReactiveEffect } from './effect'
import { extend } from './shared/extend'
class ComputedRefImpl {
  private _value
  private dirty = false
  private effect: ReactiveEffect
  constructor(getter) {
    this.effect = new ReactiveEffect(getter)
    const scheduler = () => {
      this.dirty = false
    }
    extend(this.effect, { scheduler })
  }
  get value() {
    if (!this.dirty) {
      this._value = this.effect.run()
      this.dirty = true
    }
    return this._value
  }
}
export function computed(getter) {
  return new ComputedRefImpl(getter)
}

computed部分的代码量不多,但是涉及到的知识点还是比较多,这部分值得多多思考。