准备工作

jest 测试环境搭建

首先初始化项目

yarn init -y

创建src/reactivity目录,新建 index.ts 文件

export function add(a: number, b: number) {
  return a + b 
}

Index.ts 同级目录下创建 tests 目录,并新建 index.spec.ts 文件

import {add}from '../index'

it("init", () => {
  expect(add(1,2)).toBe(3);
})

这时候会出现报错,因为我们没有安装typescript 依赖

yarn add --dev typescript
npx tsc --init

然后是 jest 相关依赖,在使用jest 时运行环境为 node, 但我们通常习惯 ESM,所以需要使用 babel 将ESM 的代码转为 CommonJS,同时需要对 typesctipt 进行转换

yarn add --dev babel-jest @babel/core @babel/preset-env
yarn add --dev @babel/preset-typescript

然后再项目根目录下添加 babel.config.js

module.exports = {
  presets: [['@babel/preset-env', { targets: { node: 'current' } }],
    '@babel/preset-typescript'
  ]
}

此时代码中仍然存在报错提醒,我们需要将 types 类型引入,在 tsconfig.json 中修改 types 字段

"types": ["jest"]

此时代码中报错消失,在 package.json 中添加 script

"scripts": {
    "test": "jest"
  },

执行 yarn test

image-20220629173813510

reactivity

effect 依赖收集

了解过 Vue2 原理的再看 Vue3 的依赖收集原理很容易理解,同样的 get 时收集依赖,set 时出发依赖,区别在于将之前的 Object.defineProperty 替换为了 Proxy,Proxy 的功能比之前强大数倍

Proxy 的语法:new Proxy(target, handler)

target: 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。

handler: 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为

更多内容可以看 MDN——Proxy

采用 TDD(测试驱动开发)的方式来写代码,所以我们从测试用例出发,先写测试用例,将之前的测试文件删除,新建 effect.spec.ts

describe('effect', () => {
  it('happy path', () => {
    //* 这里的reactive 并没有实现,
    //* 我们的目的就是实现 reactive, 让测试用例运行成功
    const user = reactive({ age: 10 })
    
    let nextAge;
    // 副作用函数
    effect(() => {
      nextAge = user.age + 1
    })
    expect(nextAge).toBe(11)

    // user.age 发生改变时触发依赖, 此时 nextAge 就是 12
    user.age++
    expect(nextAge).toBe(12)
  })
})

然后再新建一个 reactive.spec.ts

describe('reactive', () => {
  it('happy path', () => {
    const original = { foo: 1 };
    const observed = reactive({ foo: 1 });
    expect(observed).not.toBe(original);
    expect(observed.foo).toBe(1);
  })
})

然后我们来实现 effect,新建 effect.ts 文件。effect 接收一个函数,首先要将函数执行一次,执行的过程中读取 proxy 对象的值完成依赖收集

class ReactiveEffect {
  _fn: Function
  constructor(fn: Function) {
    this._fn = fn
  }

  run() {
    activeEffect = this
    this._fn()
  }
}

let activeEffect: ReactiveEffect;
export function effect(fn: Function) {
  const _effect = new ReactiveEffect(fn)
  
  _effect.run()
}

然后是 reactive 函数, 新建 reactive.ts 文件,reactive 要做的就是对对象的代理,核心是收集依赖和触发依赖

export function reactive(raw: Record<any, any>) {
  return new Proxy(raw, {
    get(target, key) {
      const res = Reflect.get(target, key);
      // 收集依赖
      track(target, key)
      return res;
    },
    set(target, key, value) {
      const res = Reflect.set(target, key, value);

      // 触发依赖
      trigger(target, key)
      return res
    }
  })
}

然后在来实现 track 和 trigger,在 effect.ts文件中添加这两个方法

// 使用 WeakMap 将 target 作为键,值也是一个 map,key为键,key 对应的依赖是值
const targetMap = new WeakMap<object, Map<keyof any, Set<ReactiveEffect>>>()

export function track<T extends object>(target: T, key: keyof T) { 
  // target -> key -> dep
    // 获取 target 对应的 map
  let depsMap = targetMap.get(target)
    
  // 如果 depsMap 不存在,新建并赋值targetMap
  if (!depsMap) {
    targetMap.set(target, depsMap = new Map())
  }
  
  // 获取 key 对应的 map
  let dep = depsMap.get(key)
  
  // 如果 dep 不存在,新建并赋值 depsMap
  if (!dep) {
    depsMap.set(key, dep = new Set<ReactiveEffect>())
  }
    
  // 将付错用添加到 key 对应的依赖集合中
  dep.add(activeEffect)

}

export function trigger<T extends object>(target: T, key: keyof T) {
  let depsMap = targetMap.get(target) // 获取 target 对应的 map

  let dep = depsMap!.get(key) as Set<ReactiveEffect> // 获取 key 对应的 set

  for (const effect of dep) {
    // 遍历执行副作用
    effect.run()
  }
}

WeakMap:WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。
这里是弱引用,不会影响对象被垃圾回收

effect 返回 runner

effect 需要具有返回值,返回值就是 effect 接受的回调函数

it('should return runner when call effect', () => {
  // effect(fn) -> function(runner) -> fn -> return
  let foo = 10

  const runner = effect(() => {
    foo++
    return 'foo'
  })

  expect(foo).toBe(11)
  const r = runner()
  expect(r).toBe('foo')
})

由测试用例来实现代码,继续完善上一节的 effect 函数

class ReactiveEffect {
  _fn: Function
  constructor(fn: Function) {
    this._fn = fn
  }

  run() {
    activeEffect = this
    return this._fn()
  }
}

let activeEffect: ReactiveEffect;
export function effect(fn: Function) {
  const _effect = new ReactiveEffect(fn)
  
  _effect.run()

  return _effect.run.bind(_effect)
}

run 方法中将返回值返回,effect函数 中将 run 方法显示绑定 this 返回

执行测试,功能实现

image-20220630164226493

scheduler

这里引用一下 Vue 官方的经典测试用例来测试 scheduler 功能

it("scheduler", () => {
  let dummy;
  let run: any;
  const scheduler = jest.fn(() => {
    run = runner;
  });
  const obj = reactive({ foo: 1 });
  const runner = effect(
    () => {
      dummy = obj.foo;
    },
    { scheduler }
  );
  expect(scheduler).not.toHaveBeenCalled();
  expect(dummy).toBe(1);
  // should be called on first trigger
  obj.foo++;
  expect(scheduler).toHaveBeenCalledTimes(1);
  // // should not run yet
  expect(dummy).toBe(1);
  // // manually run
  run();
  // // should have run
  expect(dummy).toBe(2);
});

测试用例的执行流程大致为:声明一个 scheduler,使用 reactive 声明一个对象 obj,使用 effect 对 obj.foo 进行依赖收集;此时断言传入的 scheduler 没有被执行,但是 fn 执行,所以 dummy 的值为 1;然后对 obj.foo 加一,此时应该调用一次 scheduler,scheduler 函数内部并没有对 dummy 的赋值, 所以 dummy 此时还是 1;然后执行 runner,对 dummy 进行赋值,此时 dummy 值为 2。

scheduler的作用是:当传入scheduler后,target修改的时候,trigger的时候绕过fn,直接执行传入的scheduler。

这就需要在 effect 里判断是否有 scheduler,如果有则执行 scheduler,否则执行 fn

根据这个思路我们来修改我们之前的 effect 函数

class ReactiveEffect {
  private _fn: Function
  public scheduler?: Function

  constructor(fn: Function, scheduler?: Function) {
    this._fn = fn
    this.scheduler = scheduler
  }

  run() {
    activeEffect = this
    return this._fn()
  }
}

let activeEffect: ReactiveEffect;

export function effect(fn: Function, options: any = {}) {
  const _effect = new ReactiveEffect(fn, options.scheduler)

  _effect.run()

  return _effect.run.bind(_effect)
}

然后我们需要在 trigger 函数中去判断执行 scheduler 还是 fn

export function trigger<T extends object>(target: T, key: keyof T) {
  let depsMap = targetMap.get(target)

  let dep = depsMap!.get(key) as Set<ReactiveEffect>

  for (const effect of dep) {
    if (effect.scheduler)
      effect.scheduler()
    else
      effect.run()
  }
}

执行测试

image-20220703115055741

stop

同样的我们还是通过测试用例来完成 stop 功能的实现

it("stop", () => {
  let dummy;
  const obj = reactive({ prop: 1 });
  const runner = effect(() => {
    dummy = obj.prop;
  });
  obj.prop = 2;
  expect(dummy).toBe(2);
  stop(runner);
  obj.prop = 3;
  expect(dummy).toBe(2);

  // stopped effect should still be manually callable
  runner();
  expect(dummy).toBe(3);
});

测试用例执行流程:使用 reactive 声明一个对象 obj,使用 effect 为 obj.prop 添加依赖(将 obj.prop的值赋给 dummy),然后给 obj.prop 赋值,此时 dummy 的值应该为 2;然后使用 stop 函数取消 obj.prop 的这个依赖,再给 obj.prop 加 1,此时 dummy 不会再被赋值,值仍然为 2;然后重新执行 runner,此时 dummy 又被重新赋值为 obj.prop。

然后我们来编写 stop 方法,它仍然是 effect 文件导出的一个模块

export function stop(runner: any) {
  runner.effect.stop()
}

这个 runner 就是我们之前的 run 方法的包装,我们可以将之前的 effect 绑定在runner 函数之上

export function effect(fn: Function, options: any = {}) {
  const _effect = new ReactiveEffect(fn, options.scheduler)

  _effect.run()
  
  const runner: any = _effect.run.bind(_effect)
  runner.effect = _effect

  return runner
}

然后我们在 ReactiveEffect 上声明 stop 方法和 deps 数组

class ReactiveEffect {
  private _fn: Function
  public scheduler?: Function
  deps: Set<ReactiveEffect>[] = []

  constructor(fn: Function, scheduler?: Function) {
    this._fn = fn
    this.scheduler = scheduler
  }

  run() {
    activeEffect = this
    return this._fn()
  }

  stop() {
    this.deps.forEach(dep => {
      dep.delete(this)
    })
  }
}

其次,需要在收集依赖的时候同时将依赖保存到 effect 上

export function track<T extends object>(target: T, key: keyof T) {
  // target -> key -> dep

  let depsMap = targetMap.get(target)

  if (!depsMap) {
    targetMap.set(target, depsMap = new Map())
  }

  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, dep = new Set<ReactiveEffect>())
  }
  
  // 如果没有 activeEffect 不执行依赖收集
  if (!activeEffect) return;

  // 收集依赖
  dep.add(activeEffect)
  // 反向收集依赖
  activeEffect.deps.push(dep)
}

此时执行测试

image-20220703160920809

可以看到已经完成了测试用例,但是,这时候代码还不够完善,有很大的优化空间。

为了代码的可读性,我们将 stop 的逻辑抽离出来作为一个函数

// 清除依赖 
function cleanupEffect(effect: ReactiveEffect) {
  effect.deps.forEach(dep => {
    dep.delete(effect)
  })
}

此外还有一个性能问题,每次当我们调用 stop 的时候,都会遍历清除,但是,只需要清除一次之后就不需要再清除了,所以我么你可以通过提供一个状态来控制 stop 的次数

stop() {
  if (this.active) {
    cleanupEffect(this)
    this.active = false
  }
}

再次执行测试,测试仍然成功。

然后,与 stop 相关的还有一个 onStop 事件,我们还是通过测试用例来入手

it("events: onStop", () => {
  const onStop = jest.fn();
  const runner = effect(() => {}, {
    onStop,
  });

  stop(runner);
  expect(onStop).toHaveBeenCalled();
});

onStop 是一个回调函数,当触发 stop 时会被调用

export function effect(fn: Function, options: any = {}) {
  const _effect = new ReactiveEffect(fn, options.scheduler)

  _effect.onStop = options.onStop

  _effect.run()
  

  const runner: any = _effect.run.bind(_effect)
  runner.effect = _effect

  return runner
}

然后在 ReactiveEffect 内声明 onStop,在调用 cleanupEffect 之后调用

stop() {
  if (this.active) {
    cleanupEffect(this)
    if (this.onStop) {
      this.onStop()
    }
    this.active = false
  }
}

执行测试,通过

image-20220703164943737

这里的 onStop 赋值可以优化一下,后面还有会很多属性复制,所以我们直接将这里的赋值逻辑优化一下

// /src/shared/index.ts
export const extend = Object.assign

// /src/reactivity/effect.ts
export function effect(fn: Function, options: any = {}) {
  const _effect = new ReactiveEffect(fn, options.scheduler)

  // 将 options 的内容合并到 effet 上
  extend(_effect, options)

  _effect.run()
  
  const runner: any = _effect.run.bind(_effect)
  runner.effect = _effect

  return runner
}

虽然上面的测试用例通过了,但是,如果将测试用例中的 obj.prop = 3改为obj.prop++再执行测试用例会发现测试失败。

(下面的代码在 isReactive 之后完善,所以会有较大变化, 可以看完 isReactive 之后再看这里)

这是因为,obj.prop++的操作是一个 get + set 的操作,完整的表达式应该是obj.prop = obj.prop + 1,在 get 的过程中会重新触发依赖收集,所以导致上面的 stop 失效。

解决这个问题可以通过再声明一个全局变量来控制,当变量为 false 时停止收集依赖

// global
let shouldTrack: boolean;

// class ReactiveEffect
run() {
  if (!this.active)
    // 如果被 stop 直接执行返回
    return this._fn()

  // 否则进行依赖收集
  shouldTrack = true
  activeEffect = this
  const result = this._fn()
  shouldTrack = false // 依赖收集结束之后将状态重置

  return result
}

// track
export function track<T extends object>(target: T, key: keyof T) {
  // target -> key -> dep

  let depsMap = targetMap.get(target)

  if (!depsMap) {
    targetMap.set(target, depsMap = new Map())
  }

  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, dep = new Set<ReactiveEffect>())
  }

  // 如果没有激活的 effect 中断执行
  if (!activeEffect) return;
  // 如果不应该收集依赖, 中断执行
  if (!shouldTrack) return

  // 收集依赖
  dep.add(activeEffect)
  // 反向收集依赖
  activeEffect.deps.push(dep)
}

此时执行测试,测试通过

readonly

readonly和 reactive 类似,只不过,readonly不需要进行依赖收集,并且不可被 set,我们只需要在 set 时报错即可

仍然从测试用例入手

it("readonly", () => { 
  const original = { foo: 1, bar: { baz: 2 } };
  const wrapped = readonly(original);

  expect(wrapped).not.toBe(original);
  expect(wrapped.foo).toBe(1);
})

讲一个对象 readonly 包装之后,值是一样的,但是对象的比较是不同的

export function readonly(raw: Record<any, any>) {
  return new Proxy(raw, {
    get: createGetter(true),
    set() {
      // readonly 不可 set, 抛出异常
      // TODO 自定义异常
      return true
    }
  })
}

这里因为 get 的逻辑用到了两次,所以提取出来

function createGetter(isReadOnly: boolean = false) {
  return function get(target: Record<any, any>, key: string | symbol) {
    const res = Reflect.get(target, key);
    // 如果不是 readonly 收集依赖
    if (!isReadOnly)
      track(target, key);
    return res;
  }
}

然后我们来完善 readonly set 时的报错

it("warn when set", () => {
  console.warn = jest.fn()

  const user = readonly({
    age: 10
  })

  // @ts-expect-error
  user.age = 11
  expect(console.warn).toBeCalled()
})

在 readonly 被 set 时调用 console.warn

export const readonlyHandlers = {
  get: readonlyGet, // 将原来的creater 提取出去
  set(target: Record<any, any>, key: string, value: any) {
    // readonly 不可 set, 抛出异常
    console.warn(`key: ${key}设置失败, 因为${target}是 readonly 类型`)
    return true
  }
}

isReactive & isReadonly

这两个API 用于判读是否是响应式对象和是否为只读对象。这两个 API 的实现思路是一样的,在 proxy 代理 getter 的时候判断是否为 isReactive 和 isReadonly,然后返回对应的结果即可。

首先是 siReactive, 还是先来写测试用例

it('happy path', () => {
  const original = { foo: 1 };
  const observed = reactive({ foo: 1 });
  expect(observed).not.toBe(original);
  expect(observed.foo).toBe(1);
  expect(isReactive(observed)).toBe(true)
  expect(isReactive(original)).toBe(false)
})

这里沿用之前的测试用例,添加了两行isReactive 相关的断言,分别对普通对象和 reactive 对象进行判断。

然后回到 reactive.ts 文件中实现这个方法,内容很简单,返回代理对象的指定字段即可

export function isReactive(value: any) {
  return !!value[ReactiveFlags.IS_REACTIVE]
}

export enum ReactiveFlags {
  IS_REACTIVE = '__v_isReactive',
  IS_READONLY = '__v_isReadonly',
}

然后再 getter 中添加判断条件

// 返回 getter
function createGetter(isReadOnly: boolean = false) {
  return function get(target: Record<any, any>, key: string | symbol) {
    if (key === ReactiveFlags.IS_REACTIVE)
      return !isReadOnly

    const res = Reflect.get(target, key);
    // 如果不是 readonly 收集依赖
    if (!isReadOnly)
      track(target, key);
    return res;
  }
}

如果是代理对象,并且访问的是指定字段,并且不是 readonly,那么就会返回 true;如果不是代理对象,那么取指定字段时会是 undefined,经过取非再取非之后就是一个 boolean 值。

执行测试

image-20220705165242530

isReadonly同理,只是将读取的字段修改一下

最终的代码如下

// 返回 getter
function createGetter(isReadOnly: boolean = false) {
  return function get(target: Record<any, any>, key: string | symbol) {
    if (key === ReactiveFlags.IS_REACTIVE)
      return !isReadOnly
    else if (key === ReactiveFlags.IS_READONLY)
      return isReadOnly

    const res = Reflect.get(target, key);
    // 如果不是 readonly 收集依赖
    if (!isReadOnly)
      track(target, key);
    return res;
  }
}

reactive 嵌套

使用过Vue 的应该都知道,Vue 的响应式是深层的,即深层对象也会被进行响应式处理,但是上面的逻辑我们只实现了一层的响应式代理,下面我们就来讲代理转为递归的。

首先是测试用例,我们需要判断对象的子级对象是被也被代理

it('nested reactive', () => {
  const original = {
    nested: { foo: 1 },
    array: [{bar: 2}]
  }

  const observed = reactive(original)
  expect(isReactive(observed.nested)).toBe(true)
  expect(isReactive(observed.array)).toBe(true)
  expect(isReactive(observed.array[0])).toBe(true)
})

实现这个很简单,只需要在返回值的时候判断值的类型是否为对象类型即可

// 返回 getter
function createGetter(isReadOnly: boolean = false) {
  return function get(target: Record<any, any>, key: string | symbol) {
    if (key === ReactiveFlags.IS_REACTIVE)
      return !isReadOnly
    else if (key === ReactiveFlags.IS_READONLY)
      return isReadOnly

    const res = Reflect.get(target, key);

    if (isObject(res)) {
      // 如果是对象类型, 递归代理
      return reactive(res)
    }

    // 如果不是 readonly 收集依赖
    if (!isReadOnly)
      track(target, key);
    return res;
  }
}

const isObject = val => val !== null && typeof val === 'object'

同理 readonly 的代理任然需要递归处理

it('nested readonly', () => {
  const original = { foo: 1, bar: { baz: 2 } };
  const wrapped = readonly(original);
  
  expect(isReadonly( wrapped )).toBe(true)
  expect(isReadonly(original)).toBe(false)
  expect(isReadonly( original.bar )).toBe(false)
  expect(isReadonly( wrapped.bar )).toBe(true)
})

此时执行测试是有问题的,因为经过我们的封装后,reactive 和 readonly 的 getter 调用的是同一个,所以我们在刚才的地方要加上判断。

// 返回 getter
function createGetter(isReadonly: boolean = false) {
  return function get(target: Record<any, any>, key: string | symbol) {
    if (key === ReactiveFlags.IS_REACTIVE)
      return !isReadonly
    else if (key === ReactiveFlags.IS_READONLY)
      return isReadonly

    const res = Reflect.get(target, key);

    if (isObject(res)) {
      // 如果是对象类型, 递归代理, 根据类型不同调用不同得到方法
      return isReadonly ? readonly(res) : reactive(res)
    }

    // 如果不是 readonly 收集依赖
    if (!isReadonly)
      track(target, key);
    return res;
  }
}

shallowReadonly 浅只读

浅只读只有第一层是只读类型的,不进行深层代理

describe('shallowReadonly', () => {
  it('should not make non-reactive properties reactive', () => {
    const props = shallowReadonly({ n: { foo: 1 } })

    expect(isReadonly(props)).toBe(true)
    expect(isReadonly(props.n)).toBe(false)
  })

  it("warn when set", () => {
    console.warn = jest.fn()

    const user = shallowReadonly({
      age: 10
    })

    // @ts-expect-error
    user.age = 11
    expect(console.warn).toBeCalled()
  })
})

继续增强原来的 getter,增加 shallow 标识,如果是 shallow 的话直接返回

// 返回 getter
function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Record<any, any>, key: string | symbol) {
    if (key === ReactiveFlags.IS_REACTIVE)
      return !isReadonly
    else if (key === ReactiveFlags.IS_READONLY)
      return isReadonly

    const res = Reflect.get(target, key);

    if (shallow) {
      // 如果只是浅层代理直接返回
      return res
    }

    if (isObject(res)) {
      // 如果是对象类型, 递归代理, 根据类型不同调用不同得到方法
      return isReadonly ? readonly(res) : reactive(res)
    }

    // 如果不是 readonly 收集依赖
    if (!isReadonly)
      track(target, key);
    return res;
  }
}

然后将 handler 进行包装,这里跟 readonly 的 setter 一样,只修改 get 即可

// baseHandlers.ts
const shallowReadonlyGet = createGetter(true, true)

export const shallowReadonlyHandlers = extend({}, readonlyHandlers, {
  get: shallowReadonlyGet
})

// reactive.ts
export function shallowReadonly<T extends object>(raw: T): Readonly<T> {
  return createActiveObject<Readonly<T>>(raw, shallowReadonlyHandlers)
}

image-20220710132516973

isProxy

实现 isProxy 非常简单,就我们之前实现的 isReactive 和 isReadonly,只要满足其中之一即为已经代理的对象

it('isProxy', () => {
  const original = { foo: 1, bar: { baz: 2 } };
  const wrapped = readonly(original);
  const observed = reactive(original);

  expect(isProxy(wrapped)).toBe(true);
  expect(isProxy(observed)).toBe(true);
})

代码实现

export function isProxy(value: unknown) {
  return isReadonly(value) || isReactive(value);
}

ref

ref和 reactive 类似,也是一个实现相应是的 API,区别在于 ref 针对基础类型,reactive 针对的是引用类型,但是其实 ref 也可以传参引用类型,但是其背后还是会转到 reactive 来完成。

收线我们来看一下 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);
});

被 ref 修饰过的数据需要通过.value来访问,赋值同样也是;ref也需要进行依赖收集和依赖触发。

然后我们来根据测试用来完成代码,由于之前的依赖收集针对的是多依赖,但是这里 ref 只有一个value, 所有只有一个 dep,不再需要之前的 Map 结构,所以这里对之前的依赖收集重构一下(重构只要要运行测试保证之前的功能不受影响)

export function track<T extends object>(target: T, key: keyof T) {
  if (!isTracking()) return
  // target -> key -> dep
  let depsMap = targetMap.get(target)

  if (!depsMap) {
    targetMap.set(target, depsMap = new Map())
  }

  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, dep = new Set<ReactiveEffect>())
  }

  trackEffects(dep)
}

function trackEffects(dep: Set<ReactiveEffect>) {
  if (dep.has(activeEffect))
    // 如果已经被收集, 中断
    return
  // 收集依赖
  dep.add(activeEffect)
  // 反向收集依赖
  activeEffect.deps.push(dep)
}

同样的一来触发的过程我们也可以抽离出来

export function trigger<T extends object>(target: T, key: keyof T) {
  let depsMap = targetMap.get(target)

  let dep = depsMap!.get(key) as Set<ReactiveEffect>

  trackEffects(dep)
}

export function triggerEffects(dep: Set<ReactiveEffect>) {
  for (const effect of dep) {
    if (effect.scheduler)
      effect.scheduler()
    else
      effect.run()
  }
}

由于有了这两个函数的封装,所以这里 ref 的实现也变得非常简单,只需要在 get 时触发 track,set 时触发 trigger 即可

class RefImpl {
  private _value: any;
  public dep: Set<ReactiveEffect>;

  constructor(value: any) {
    this._value = value;
    this.dep = new Set();
  }
  get value() {
    // 在 tracking 阶段收集依赖
    if(isTracking())
      trackEffects(this.dep)
    return this._value
  }
  
  set value(newValue) {
    // 先修改 value 再触发依赖
    this._value = newValue;
    triggerEffects(this.dep)
  }
}


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

此时运行测试,通过

image-20220718214845721

此时并没有结束,我们测试用例中还有一个条件没有写入,当设置重复值的时候不要再次触发 trigger

// ...省略之前的测试代码
// same value should not trigger
a.value = 2;
expect(calls).toBe(2);
expect(dummy).toBe(2);

这里也很简单,只需要判断 set 的新值和旧值是否相同即可

set value(newValue) {
  if (hasChange(newValue, this._value))
    return
  // 先修改 value 再触发依赖
  this._value = newValue;
  triggerEffects(this.dep)
}

// shared/index.ts
export const hasChange = (newValue: any, value: any) => Object.is(newValue, value)

上面我们已经说过了,ref 可以借用 reactive 对引用类型进行处理,所以接下来我们完善一下对象类型的调用。

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 的类型,对不同的类型进行不同的处理即可

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

这里已经实现了代理,但是还有一个问题,就是 set 的时候,如果传入的值是一个对象,那么 this._value的值是一个 proxy 类型,即便是相同的对象在比较是否改变时也会返回 true,所以我们需要在比较时返回代理对象的原对象

class RefImpl {
  private _value: any;
  private _rawValue: any;
  public dep: Set<ReactiveEffect>;

  constructor(value: any) {
    this._rawValue = value;
    this._value = isObject(value) ? reactive(value) : value;
    this.dep = new Set();
  }
  get value() {
    // 收集依赖
    trackRefValue(this)
    return this._value
  }
  
  set value(newValue) {
    if (hasChange(newValue, this._rawValue)) {
      this._rawValue = newValue;
      // 先修改 value 再触发依赖
      this._value = isObject(newValue) ? reactive(newValue) : newValue;
      triggerEffects(this.dep)
    }
  }
}

到这里 ref 的功能已经实现了。

isRef

isRef 用于判断目标是否为 ref 响应式对象

it("isRef", () => {
  const a = ref(1);
  const user = reactive({
    age: 1,
  });
  expect(isRef(a)).toBe(true);
  expect(isRef(1)).toBe(false);
  expect(isRef(user)).toBe(false);
});

这里判断是否为代理对象的思路和前面的判断 reactive 对象一样,添加一个标识字段即可,只要是使用 ref 代理的都会有这个标识,在判断时只需要返回标识即可。

export function isRef(value: any) {
  return !!value.__v_isRef
}

class RefImpl {
  private _value: any;
  private _rawValue: any;
  public dep: Set<ReactiveEffect>;
  private __v_isRef = true;

  constructor(value: any) {
    this._rawValue = value;
    this._value = convert(value);
    this.dep = new Set();
  }
  
  // 省略原来的代码
}

unRef

unRef 用于返回被 ref 代理的原始对象

it("unRef", () => {
  const a = ref(1);
  expect(unRef(a)).toBe(1);
  expect(unRef(1)).toBe(1);
});

这里的实现也很简单,我们之前的 RefImpl 对象中已经保存了原始value,这里只需要判断是否为 ref 对象然后分别返回对应结果即可。

export function unRef(value: any) {
  return isRef(value) ? value._rawValue : value
}

proxyRefs

proxyRefs 用于在模板中使用 ref 值,如果你写过 Vue3 的代码,那你一定记得在 script 中定义的 ref,如果在 script 中使用需要用 ref.value的形式,但在 template 中则可以直接使用 ref 变量取值,这其中便是使用了 proxyRefs。

那么如何实现 proxyRefs 呢,我们先来分析一下测试用例。

it("proxyRefs", () => {
  const user = {
    age: ref(10),
    name: "xiaohong",
  };
  const proxyUser = proxyRefs(user);
  expect(user.age.value).toBe(10);
  expect(proxyUser.age).toBe(10);
  expect(proxyUser.name).toBe("xiaohong");

  (proxyUser as any).age = 20;
  expect(proxyUser.age).toBe(20);
  expect(user.age.value).toBe(20);

  proxyUser.age = ref(10);
  expect(proxyUser.age).toBe(10);
  expect(user.age.value).toBe(10);
});

user对象有两个属性,age 是一个 ref 对象,name 是一个普通字符串,proxyUser是使用 proxyRefs 处理的对象。

首先第一组断言,判断访问 user.age是否能拿到value 而不是 ref 对象,访问 user.name也要能直接拿到原始值;第二组断言,对 proxyUser 的 age 属性进行复制,是否能否同步修改 ref 对象的值;第三组断言,修改 age 属性为一个新的 ref 对象,判断值是否能够同步。

那么我们开始来实现功能

export function proxyRefs(objectWithRef: any) {
  return new Proxy(objectWithRef, {
    get(target, key) {
      return unRef(Reflect.get(target, key))
    }
  })
}

借助我们之前的 unRef 函数,我们可以方便的返回 get 取值操作,此时第一组断言已经通过测试,接下来我们继续完成后续的断言。

set时是判断赋值的目标是否为一个 ref 对象,如果是则给 ref.value 赋值,否则直接给属性赋值。注意,如果要赋值的 newValue 是一个 ref 对象,则需要直接赋值。

export function proxyRefs<T extends object>(objectWithRef: T): T {
  return new Proxy(objectWithRef, {
    get(target, key) {
      return unRef(Reflect.get(target, key))
    },
    set(target: Record<any, any>, key: string, newValue) {
      if (isRef(target[key]) && !isRef(newValue)) {
        // 如果原值是 ref 并且 newValue 不是 ref,  赋值给 .value,
        return target[key].value = newValue;
      }
      // target[key] 是原始值,或者 newValue 是 ref 对象直接赋值
      return Reflect.set(target, key, newValue)
    }
  })
}

齐活收工。

computed

computed ref 类似,都是通过.value 来访问值,区别在于 computed 具有缓存

那么我们先从最基本的功能来写 computed 的测试用例

it("happy path", () => {
  const value = reactive({
    foo: 1,
  });

  const getter = computed(() => {
    return value.foo;
  });

  expect(getter.value).toBe(1);
});

这是一个最基本的功能,只是简单的获取了目标属性的值

export class ComputedImpl<T> {
  private _getter: () => T
  constructor(getter: () => T) {
    this._getter = getter;
  }

  get value() {
    return this._getter()
  }
}

export function computed<T>(getter: () => T) {
  return new ComputedImpl(getter)
}

接下来我们再来看一下懒加载功能

众所周知,computed 只有在第一次获取计算属性值的事后才会执行 getter 函数

it("should compute lazily", () => {
  const value = reactive({
    foo: 1,
  });
  const getter = jest.fn(() => {
    return value.foo;
  });
  const cValue = computed(getter);

  // lazy
  expect(getter).not.toHaveBeenCalled();

  expect(cValue.value).toBe(1);
  expect(getter).toHaveBeenCalledTimes(1);
}

这个用例目前为止是能通过的,但是如果再次调用计算属性,getter 函数还会执行,这个就不太合理了,因为 computed 是具有缓存性的,后续再次调用应该调用次数也只有 1

it("should compute lazily", () => {
  const value = reactive({
    foo: 1,
  });
  const getter = jest.fn(() => {
    return value.foo;
  });
  const cValue = computed(getter);

  // lazy
  expect(getter).not.toHaveBeenCalled();

  expect(cValue.value).toBe(1);
  expect(getter).toHaveBeenCalledTimes(1);
  
  // should not compute again
  cValue.value;
  expect(getter).toHaveBeenCalledTimes(1);
}

此时的单测结果就会报错了

image-20220727220537056

这里我们就需要继续完善我们的代码了,我们只需要在第一次调用之后将返回值保存并添加标志位即可

export class ComputedImpl<T> {
  private _getter: () => T
  private _dirty: boolean = true;
  private _value: T | null = null;
  constructor(getter: () => T) {
    this._getter = getter;
  }

  get value() {
    if (this._dirty) {
      this._dirty = false
      this._value = this._getter()
    }
    return this._value
  }
}

export function computed<T>(getter: () => T) {
  return new ComputedImpl(getter)
}

当依赖值发生变化时,计算属性的值不变,此时再此获取 computed 的值,getter 会重新执行并获取到最新的值,此时再完善一下测试用例

it("should compute lazily", () => {
  const value = reactive({
    foo: 1,
  });
  const getter = jest.fn(() => {
    return value.foo;
  });
  const cValue = computed(getter);

  // lazy
  expect(getter).not.toHaveBeenCalled();

  expect(cValue.value).toBe(1);
  expect(getter).toHaveBeenCalledTimes(1);

  // should not compute again
  cValue.value;
  expect(getter).toHaveBeenCalledTimes(1);

  // should not compute until needed
  value.foo = 2;
  expect(getter).toHaveBeenCalledTimes(1);

  // now it should compute
  expect(cValue.value).toBe(2);
  expect(getter).toHaveBeenCalledTimes(2);

  // should not compute again
  cValue.value;
  expect(getter).toHaveBeenCalledTimes(2);
});

那么这里的实现就要用到我们之前的 reactiveEffect,当依赖发生变化之后调用回调函数,这里的回调函数我们用来重置_dirty的值,让下一次获取值的时候重新执行函数。

export class ComputedImpl<T> {
  private _getter: () => T
  private _dirty: boolean = true;
  private _value: T | null = null;
  private _effect: ReactiveEffect;
  constructor(getter: () => T) {
    this._getter = getter;

    this._effect = new ReactiveEffect(getter, () => {
      if (!this._dirty) {
        this._dirty = true
      }
    })
  }

  get value() {
    if (this._dirty) {
      this._dirty = false
      this._value = this._effect.run()
    }
    return this._value
  }
}

export function computed<T>(getter: () => T) {
  return new ComputedImpl(getter)
}

此时计算属性的流程已经测试通过了


前端小白