2.2.1 • Published 5 months ago

@mini-jackz-vue/reactivity v2.2.1

Weekly downloads
-
License
MIT
Repository
-
Last release
5 months ago

🚀 Welcome to 响应系统的源码简版 -ZSM reactivity# 1.项目结构介绍

1.选用TS+jest(测试框架)
2.测试用例驱动功能代码实现
前置知识点 ts (接口 类型 枚举 泛型  谓词签名(is) 断◊言签名(as)等)

vue核心三件 - 响应系统  渲染 编译

关于WeakMap以及js位运算

2.实现 reactivity

实现最基础的reactive

查看 Vue3 API 文档中的响应性 API 部分,找到reactive的介绍

reactive返回对象的响应式副本

const obj = reactive({ count: 0 })

响应式转换是“深层”的——它影响所有嵌套 property。在基于 ES2015 Proxy 的实现中,返回的 proxy 是不等于原始对象的。建议只使用响应式 proxy,避免依赖原始对象。

类型声明

function reactive<T extends object>(target: T): UnwrapNestedRefs<T>

在实现reactive之前,首先在src/reactivity/__tests__目录下创建reactive的测试文件reactive.spec.ts,并添加以下测试代码:

describe('reactivity/reactive', () => {
  it('Object', () => {
    const original = { foo: 1 }
    // reactive 返回对象的响应式副本
    const observed = reactive(original)
    // observed !== original
    expect(observed).not.toBe(original)
    // observed 的 property 的值与 original 的相等
    expect(observed.foo).toBe(1)
  })
})

为了通过以上测试,在src/reactivity/src目录下创建reactive.ts文件,在其中实现并导出reactive

export function reactive(raw) {
  // 返回 Proxy 的实例
  return new Proxy(raw, {
    // 对原始对象的 property 的 get 和 set 进行代理
    get(target, key) {
      // TODO: 收集依赖
      return Reflect.get(target, key)
    },
    set(target, key, value) {
      // TODO: 触发依赖
      return Reflect.set(target, key, value)
    }
  })
}

实现最基础的effect

effect接受一个函数作为参数,在程序运行时会执行该函数。若该函数中使用了响应式对象的 property,当该 property 的值更新时,会再次执行该函数

在实现effect之前,首先在src/reactivity/__tests__目录下创建effect的测试文件effect.spec.ts,并添加以下测试代码:

describe('effect', () => {
  it('should run the passed function once (wrapped by a effect)', () => {
    // 创建 mock 函数
    const fnSpy = jest.fn(() => {})
    effect(fnSpy)
    // 当程序执行时,传入的函数会被执行
    expect(fnSpy).toHaveBeenCalledTimes(1)
  })

  it('should observe basic properties', () => {
    let dummy
    // 创建响应式对象
    const counter = reactive({ num: 0 })
    // 在传入的函数中使用了响应式对象的 property
    effect(() => (dummy = counter.num))

    expect(dummy).toBe(0)
    // 当该 property 的值更新时,会再次执行该函数
    counter.num = 7
    expect(dummy).toBe(7)
  })
})

为了通过以上测试,在src/reactivity/src目录下创建effect.ts文件,在其中实现一个不完全的effect并导出,在实现过程中抽离出一个ReactiveEffect类,对相关操作进行封装:

// 抽离出一个 ReactiveEffect 类,对相关操作进行封装
class ReactiveEffect {
  private _fn: any

  constructor(fn) {
    // 将传入的函数赋值给实例的私有 property _fn
    this._fn = fn
  }

  // 执行传入的函数
  run() {
    this._fn()
  }
}

// 接受一个函数作为参数
export function effect(fn) {
  // 利用传入的函数创建 ReactiveEffect 类的实例
  const _effect: ReactiveEffect = new ReactiveEffect(fn)

  // 调用 ReactiveEffect 实例的 run 方法,执行传入的函数
  _effect.run()
}

这样就实现了一个不完全的effect,即能够在程序运行时执行传入的函数。之后,在reactive返回的 Proxy 的实例的 get 中收集依赖,在 set 中触发依赖

export function reactive(raw) {
  // 返回 Proxy 的实例
  return new Proxy(raw, {
    // 对原始对象的 get 进行代理
    get(target, key) {
      const res = Reflect.get(target, key)
      // 收集依赖
      track(target, key)
      return res
    },
    // 对原始对象的 set 进行代理
    set(target, key, value) {
      const res = Reflect.set(target, key, value)
      // 触发依赖
      trigger(target, key)
      return res
    }
  })
}

effect.ts文件中实现并导出tracktrigger函数,在实现过程中使用了一个全局的WeakMap类型的变量targetsMap,用于保存程序运行中的所有依赖,以及一个全局的变量activeEffect,用于保存正在执行的ReactiveEffect类的实例

class ReactiveEffect {
  /* 其他代码 */

  run() {
    // 调用 run 方法时,用全局变量 activeEffect 保存当前实例
    activeEffect = this
    this._fn()
  }
}

/**
 * 用于保存程序运行中的所有依赖
 * key 为响应式对象
 * value 为 Map 的实例,用于保存该响应式对象的所有依赖
 */
const targetsMap = new WeakMap()

// 用于保存正在执行的 ReactiveEffect 类的实例
let activeEffect: ReactiveEffect

// 用于收集依赖
export function track(target, key) {
  // 获取当前响应式对象对应的 Map 实例,若为 undefined 则进行初始化并保存到 targetsMap 中
  /**
   * 用于保存当前响应式对象的所有依赖
   * key 为响应式对象的 property
   * value 为 Set 的实例,用于保存与该 property 相关的 ReactiveEffect 类的实例
   */
  let depsMap: Map<any, Set<ReactiveEffect>> | undefined =
    targetsMap.get(target)

  if (!depsMap) {
    depsMap = new Map<any, Set<ReactiveEffect>>()
    targetsMap.set(target, depsMap)
  }

  // 获取当前 property 对应的 Set 实例,若为 undefined 则进行初始化并保存到 depsMap 中
  /**
   * 用于保存与当前 property 相关的函数
   * value 为与该 property 相关的 ReactiveEffect 类的实例
   */
  let dep: Set<ReactiveEffect> | undefined = depsMap.get(key)

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

  // 若 dep 中包括当前正在执行的 ReactiveEffect 类的实例则直接返回
  if (dep.has(activeEffect!)) {
    return
  }

  // 将当前正在执行的 ReactiveEffect 类的实例添加到 dep 中
  dep.add(activeEffect)
}

// 用于触发依赖
export function trigger(target, key) {
  // 获取当前响应式对象对应的 Map 实例
  const depsMap: Map<any, Set<ReactiveEffect>> = targetsMap.get(target)
  // 获取当前 property 对应的 Set 实例
  const dep: Set<ReactiveEffect> = depsMap.get(key)!

  // 遍历 dep,调用每一个 ReactiveEffect 类的实例的 run 方法
  for (const reactiveEffect of dep) {
    reactiveEffect.run()
  }
}

完善effect——返回runner

effect执行会返回一个函数,用一个变量runner接受该函数,调用runner时会再次执行传入的函数,同时返回该函数的返回值。

effect的测试文件effect.spec.ts中添加以下测试代码:

describe('effect', () => {
  /* 其他测试代码 */

  it('should return a function to be called manually', () => {
    let foo = 0
    // 用一个变量 runner 接受 effect 执行返回的函数
    const runner = effect(() => {
      foo++
      return 'foo'
    })
    expect(foo).toBe(1)
    // 调用 runner 时会再次执行传入的函数
    const res = runner()
    expect(foo).toBe(2)
    // runner 执行返回该函数的返回值
    expect(res).toBe('foo')
  })
})

为了通过以上测试,需要对effect的实现进行完善。首先,effect执行返回_effect.run,并将其this指向指定为_effect,其次run方法执行返回传入的函数执行的结果:

class ReactiveEffect {
  /* 其他代码 */

  run() {
    activeEffect = this

    // 返回传入的函数执行的结果
    return this._fn()
  }
}

export function effect(fn) {
  const _effect: ReactiveEffect = new ReactiveEffect(fn)

  _effect.run()

  // 返回 _effect.run,并将其 this 指向指定为 _effect
  return _effect.run.bind(_effect)
}

完善effect——接受scheduler

effect接受一个对象作为第二个参数,该对象中可以包括一个scheduler方法。用一个变量runner接受effect执行返回的函数,程序运行时会首先执行传入的函数,而不会调用scheduler方法,之后当传入的函数依赖的响应式对象的 property 的值更新时,会调用scheduler方法而不会执行该函数,只有当调用runner时才会执行该函数。

effect的测试文件effect.spec.ts中添加以下测试代码:

describe('effect', () => {
  /* 其他测试代码 */

  it('scheduler', () => {
    let dummy
    let run: number
    // 创建 mock 函数
    const scheduler = jest.fn(() => {
      run++
    })
    const obj = reactive({ foo: 1 })
    const runner = effect(
      () => {
        dummy = obj.foo
      },
      { scheduler }
    )
    // 程序运行时会首先执行传入的函数,而不会调用 scheduler 方法
    expect(scheduler).not.toHaveBeenCalled()
    expect(dummy).toBe(1)
    // 当传入的函数依赖的响应式对象的 property 的值更新时,会调用 scheduler 方法而不会执行传入的函数
    obj.foo++
    expect(scheduler).toHaveBeenCalledTimes(1)
    expect(dummy).toBe(1)
    // 只有当调用 runner 时才会执行传入的函数
    runner()
    expect(scheduler).toHaveBeenCalledTimes(1)
    expect(dummy).toBe(2)
  })
})

为了通过以上测试,需要对effect的实现和trigger函数进行完善。

class ReactiveEffect {
  /* 其他代码 */

  // 构造函数接受可选的第二个参数,保存为实例的公共变量 scheduler
  constructor(fn, public scheduler?) {
    this._fn = fn
  }
}

// 接受一个函数作为第一个参数,接受一个对象作为第二个参数
export function effect(fn, options: any = {}) {
  // 利用传入的函数创建 ReactiveEffect 类的实例,并将 scheduler 方法传给 ReactiveEffect 类的构造函数
  const _effect: ReactiveEffect = new ReactiveEffect(fn, options.scheduler)

  /* 其他代码 */
}

export function trigger(target, key) {
  /* 其他代码 */

  /**
   * 遍历 dep,判断每一个 ReactiveEffect 类的实例的 scheduler property 是否存在
   * 若不为 undefined 则调用 scheduler 方法,否则调用 run 方法
   */
  for (const reactiveEffect of dep) {
    if (reactiveEffect.scheduler) {
      reactiveEffect.scheduler()
    } else {
      reactiveEffect.run()
    }
  }
}

完善effect——stop

stop接受effect执行返回的函数作为参数。用一个变量runner接受effect执行返回的函数,调用stop并传入runner后,当传入的函数依赖的响应式对象的 property 的值更新时不会再执行该函数,只有当调用runner时才会恢复执行该函数。

effect的测试文件effect.spec.ts中添加以下测试代码:

describe('effect', () => {
  /* 其他测试代码 */

it('stop', () => {
        let dummy;
        const obj = reactive({ prop: 1 });
        const runner = effect(() => {
            dummy = obj.prop;
        });
        obj.prop = 2;
        expect(dummy).toBe(2);
        // 调用 stop 后,当传入的函数依赖的响应式对象的 property 的值更新时不会再执行该函数
        stop(runner);
        obj.prop = 3;
        expect(dummy).toBe(2);
        obj.prop++;
        expect(dummy).toBe(4);
        // 只有当调用`runner`时才会恢复执行该函数
        runner();
        expect(dummy).toBe(4);
    });
})

为了通过以上测试,需要对effect的实现进行完善,实现并导出stop

// 用于记录是否应该收集依赖,防止调用 stop 后触发响应式对象的 property 的 get 时收集依赖
let shouldTrack: boolean = false

class ReactiveEffect {
  /* 其他代码 */

  // 用于保存与当前实例相关的响应式对象的 property 对应的 Set 实例
  deps: Array<Set<ReactiveEffect>> = []
  // 用于记录当前实例状态,为 true 时未调用 stop 方法,否则已调用,防止重复调用 stop 方法
  active: boolean = true

  // 用于执行传入的函数
  run() {
    // 若已调用 stop 方法则直接返回传入的函数执行的结果
    if (!this.active) {
      return this._fn()
    }

    // 应该收集依赖
    shouldTrack = true
    // 调用 run 方法时,用全局变量 activeEffect 保存当前实例
    activeEffect = this

    const res = this._fn()
    // 重置
    shouldTrack = false

    // 返回传入的函数执行的结果
    return res
  }

  // 用于停止传入的函数的执行
  stop() {
    if (this.active) {
      cleanupEffect(this)
      this.active = false
    }
  }
}

// 用于将传入的 ReactiveEffect 类的实例从与该实例相关的响应式对象的 property 对应的 Set 实例中删除
function cleanupEffect(effect: ReactiveEffect) {
  effect.deps.forEach((dep: any) => {
    dep.delete(effect)
  })
}

export function effect(fn, options: any = {}) {
  /* 其他代码 */

  // 用一个变量 runner 保存将 _effect.run 的 this 指向指定为 _effect 的结果
  const runner: any = _effect.run.bind(_effect)
  // 将 _effect 赋值给 runner 的 effect property
  runner.effect = _effect

  // 返回 runner
  return runner
}

export function track(target, key) {
  // 若不应该收集依赖则直接返回
  if (!shouldTrack || activeEffect === undefined) {
    return
  }

  /* 其他代码 */

  dep.add(activeEffect!)
  // 将 dep 添加到当前正在执行的 ReactiveEffect 类的实例的 deps property 中
  activeEffect?.deps.push(dep)
}

// 用于停止传入的函数的执行
export function stop(runner) {
  // 调用 runner 的 effect property 的 stop 方法
  runner.effect.stop()
}

完善effect——接受onStop

effect接受一个对象作为第二个参数,该对象中还可以包括一个onStop方法。用一个变量runner接受effect执行返回的函数,调用stop并传入runner时,会执行onStop方法。

effect的测试文件effect.spec.ts中添加以下测试代码

describe('effect', () => {
  /* 其他测试代码 */

  it('events: onStop', () => {
    // 创建 mock 函数
    const onStop = jest.fn()
    const runner = effect(() => {}, {
      onStop
    })

    // 调用 stop 时,会执行 onStop 方法
    stop(runner)
    expect(onStop).toHaveBeenCalled()
  })
})

为了通过以上测试,需要对effect的实现进行完善。

class ReactiveEffect {
  /* 其他代码 */

  // 用于保存当前实例的 onStop 方法
  onStop?: () => void

  stop() {
    if (this.active) {
      cleanupEffect(this)
      // 在调用 stop 方法时,调用 onStop 方法
      if (this.onStop) {
        this.onStop()
      }
      this.active = false
    }
  }
}

export function effect(fn, options: any = {}) {
  const _effect: ReactiveEffect = new ReactiveEffect(fn, options.scheduler)
  // 将 onStop 方法挂载到 ReactiveEffect 类的实例上
  _effect.onStop = options.onStop

  /* 其他代码 */
}

effect接受一个对象作为第二个参数,该对象中可以包括多个属性和方法,在effect的实现中若依次挂载到ReactiveEffect类的实例上将会十分繁琐,因此可以使用Object.assign方法,同时为了提高代码的可读性,可以为其设置别名。在src/shared目录下创建index.ts文件,并添加以下代码:

// 为 Object.assign 方法创建别名
export const extend = Object.assign

利用Object.assign方法对之前的实现做简单优化:

增加lazy懒属性不立即调用副作用函数:

export function effect(fn, options: any = {}) {
  const _effect: ReactiveEffect = new ReactiveEffect(fn)
  // 将第二个参数即 options 对象的属性和方法挂载到 ReactiveEffect 类的实例上
  extend(_effect, options)
 if (!options || !options.lazy) {
        _effect.run();
    }
  /* 其他代码 */
}

实现最基础的readonly

查看 Vue3 API 文档中的响应性 API 部分,找到readonly的介绍:

接受一个对象(响应式或纯对象)或 ref 并返回原始对象的只读代理。只读代理是深层的:任何被访问的嵌套 property 也是只读的。

onst original = reactive({ count: 0 })

const copy = readonly(original)

watchEffect(() => {
  // 用于响应性追踪
  console.log(copy.count)
})

// 变更 original 会触发依赖于副本的侦听器
original.count++

// 变更副本将失败并导致警告
copy.count++ // 警告!

在实现readonly之前,首先在src/reactivity/__tests__目录下创建readonly的测试文件readonly.spec.ts,并添加以下测试代码:

describe('reactivity/readonly', () => {
  it('should make values readonly', () => {
    const original = { foo: 1 }
    // 创建 readonly 响应式对象
    const wrapped = readonly(original)
    console.warn = jest.fn()
    // readonly 响应式对象与原始对象不相等
    expect(wrapped).not.toBe(original)
    expect(wrapped.foo).toBe(1)
    // readonly 响应式对象的 property 是只读的
    wrapped.foo = 2
    expect(wrapped.foo).toBe(1)
    // 修改 readonly 响应式对象的 property 的值时会调用 console.warn 发出警告
    expect(console.warn).toBeCalled()
  })
})

为了通过以上测试,在src/reactivity/src目录下的reactive.ts文件中实现并导出readonly

export function readonly(raw) {
  // 返回 Proxy 的实例
  return new Proxy(raw, {
    // 对原始对象的 get 进行代理
    get(target, key) {
      const res = Reflect.get(target, key)

      return res
    },
    // 对原始对象的 set 进行代理
    set() {
      // TODO: 警告!
      return true
    }
  })
}

reactivereadonly的实现中有较多重复,需要对其中的代码进行优化,抽离重复代码,提高可读性。在src/reactivity/src目录下创建baseHandlers.ts文件,将与创建用于构造 Proxy 的 handlers 相关的代码抽离到其中,并抽离出工具函数和使用全局变量进行缓存:

// 对 get 和 set 进行缓存,防止重复调用工具函数
const get = createGetter()
const set = createSetter()
const readonlyGet = createGetter(true)

// 用于生成 get 函数的工具函数
function createGetter(isReadonly = false) {
  return function (target, key) {
    const res = Reflect.get(target, key)

    // 利用 reactive 进行响应式转换时才进行依赖收集
    if (!isReadonly) {
      // 收集依赖
      track(target, key)
    }

    return res
  }
}

// 用于生成 set 函数的工具函数
function createSetter() {
  return function (target, key, value) {
    const res = Reflect.set(target, key, value)
    // 触发依赖
    trigger(target, key)
    return res
  }
}

// reactive 对应的 handlers
export const mutableHandlers = {
  get,
  set
}

// readonly 对应的 handlers
export const readonlyHandlers = {
  get: readonlyGet,
  set(target, key) {
    // 调用 console.warn 发出警告
    console.warn(
      `Set operation on key "${key}" failed: target is readonly.`,
      target
    )
    return true
}

// reactive.ts 之后对reactive和readonly的实现进行优化,抽离出工具函数:
export function reactive(raw) {
  return createReactiveObject(raw, mutableHandlers)
}

export function readonly(raw) {
  return createReactiveObject(raw, readonlyHandlers)
}

// 用于创建 Proxy 实例的工具函数
function createReactiveObject(raw, baseHandlers) {
  // 返回 Proxy 的实例
  return new Proxy(raw, baseHandlers)
}

实现isReactiveisReadonlyisProxy

查看 Vue3 API 文档中的响应性 API 部分,找到isProxyisReactiveisReadonly的介绍:

isProxy检查对象是否是由reactivereadonly创建的 proxy。

isReactive检查对象是否是由reactive创建的响应式代理。

mport { reactive, isReactive } from 'vue'
export default {
  setup() {
    const state = reactive({
      name: 'John'
    })
    console.log(isReactive(state)) // -> true
  }
}

如果该代理是readonly创建的,但包裹了由reactive创建的另一个代理,它也会返回true

import { reactive, isReactive, readonly } from 'vue'
export default {
  setup() {
    const state = reactive({
      name: 'John'
    })
    // 从普通对象创建的只读 proxy
    const plain = readonly({
      name: 'Mary'
    })
    console.log(isReactive(plain)) // -> false

    // 从响应式 proxy 创建的只读 proxy
    const stateCopy = readonly(state)
    console.log(isReactive(stateCopy)) // -> true
  }
}

isReadonly检查对象是否是由readonly创建的只读代理。

① 实现isReactive

在实现isReactive之前,首先在reactive的测试文件reactive.spec.ts中增加关于isReactive的测试代码

describe('reactivity/reactive', () => {
  it('Object', () => {
    const original = { foo: 1 }
    const observed = reactive(original)
    expect(observed).not.toBe(original)
    // 对响应式对象调用 isReactive 返回 true
    expect(isReactive(observed)).toBe(true)
    // 对普通对象调用 isReactive 返回 false
    expect(isReactive(original)).toBe(false)
    expect(observed.foo).toBe(1)
  })
})

为了通过以上测试,在src/reactivity/src目录下的reactive.ts文件中实现并导出isReactive

// 用于检查对象是否是由 reactive 创建的响应式对象
export function isReactive(value): boolean {
  // 获取对象的某个特殊 property 的值,从而触发 get,property 名为 __v_isReactive
  return !!value['__v_isReactive']
}

同时,还需要对src/reactivity/src目录下的baseHandlers.ts文件中的createGetter工具函数做相应修改:

function createGetter(isReadonly = false) {
  return function (target, key) {
    // 当 property 名为 __v_isReactive 时,表明正在调用 isReactive,直接返回 !isReadonly
    if (key === '__v_isReactive') {
      return !isReadonly
    }

    /* 其他代码 */
  }
}

② 实现isReadonly

在实现isReadonly之前,首先在readonly的测试文件readonly.spec.ts中增加关于isReadonly的测试代码

describe('reactivity/readonly', () => {
  it('should make values readonly', () => {
    const original = { foo: 1 }
    const wrapped = readonly(original)
    console.warn = jest.fn()
    expect(wrapped).not.toBe(original)
    // 对 readonly 响应式对象调用 isReactive 返回 false
    expect(isReactive(wrapped)).toBe(false)
    // 对 readonly 响应式对象调用 isReadonly 返回 true
    expect(isReadonly(wrapped)).toBe(true)
    // 对普通对象调用 isReactive 返回 false
    expect(isReactive(original)).toBe(false)
    // 对普通对象调用 isReadonly 返回 false
    expect(isReadonly(original)).toBe(false)
    expect(wrapped.foo).toBe(1)
    wrapped.foo = 2
    expect(wrapped.foo).toBe(1)
    expect(console.warn).toBeCalled()
  })
})

为了通过以上测试,在src/reactivity/src目录下的reactive.ts文件中实现并导出isReadonly

// 用于检查对象是否是由 readonly 创建的 readonly 响应式对象
export function isReadonly(value): boolean {
  // 获取对象的某个特殊 property 的值,从而触发 get,property 名为 __v_isReactive
  return !!value['__v_isReadonly']
}

同时,还需要对src/reactivity/src目录下的baseHandlers.ts文件中的createGetter工具函数做相应修改:

function createGetter(isReadonly = false) {
  return function (target, key) {
    // 当 property 名为 __v_isReactive 时,表明正在调用 isReactive,直接返回 !isReadonly
    if (key === '__v_isReactive') {
      return !isReadonly
    }
    // 当 property 名为 __v_isReadonly 时,表明正在调用 isReadonly,直接返回 isReadonly
    else if (key === '__v_isReadonly') {
      return isReadonly
    }

    /* 其他代码 */
  }
}

③ 实现isProxy

在实现isProxy之前,首先分别在reactive的测试文件reactive.spec.tsreadonly的测试文件readonly.spec.ts中增加关于isProxy的测试代码:

// reactive.spec.ts
describe('reactivity/reactive', () => {
  it('Object', () => {
    const original = { foo: 1 }
    const observed = reactive(original)
    expect(observed).not.toBe(original)
    expect(isReactive(observed)).toBe(true)
    expect(isReactive(original)).toBe(false)
    // 对响应式对象调用 isProxy 返回 true
    expect(isProxy(observed)).toBe(true)
    // 对普通对象调用 isProxy 返回 false
    expect(isProxy(original)).toBe(false)
    expect(observed.foo).toBe(1)
  })
})
// readonly.spec.ts
describe('reactivity/readonly', () => {
  it('should make values readonly', () => {
    const original = { foo: 1 }
    const wrapped = readonly(original)
    console.warn = jest.fn()
    expect(wrapped).not.toBe(original)
    expect(isReactive(wrapped)).toBe(false)
    expect(isReadonly(wrapped)).toBe(true)
    expect(isReactive(original)).toBe(false)
    expect(isReadonly(original)).toBe(false)
    // 对 readonly 响应式对象调用 isProxy 返回 true
    expect(isProxy(wrapped)).toBe(true)
    // 对普通对象调用 isProxy 返回 false
    expect(isProxy(original)).toBe(false)
    expect(wrapped.foo).toBe(1)
    wrapped.foo = 2
    expect(wrapped.foo).toBe(1)
    expect(console.warn).toBeCalled()
  })
})

为了通过以上测试,在src/reactivity/src目录下的reactive.ts文件中实现并导出isProxy

// 用于检查对象是否是由 reactive 或 readonly 创建的响应式对象
export function isProxy(value): boolean {
  // 利用 isReactive 和 isReadonly 进行判断
  return isReactive(value) || isReadonly(value)
}

④ 优化代码

isReactiveisReadonly的实现中使用到的特殊 property 的名为字符串,需要对其进行优化,创建并导出枚举类型ReactiveFlags用于保存这两个字符串:

//reactive.ts
// 用于保存 isReactive 和 isReadonly 中使用的特殊 property 的名
export const enum ReactiveFlags {
  IS_REACTIVE = '__v_isReactive',
  IS_READONLY = '__v_isReadonly'
}
// baseHandlers.ts
function createGetter(isReadonly = false) {
  return function (target, key) {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    }

    /* 其他代码 */
  }
}
// reactive.ts
export function isReactive(value): boolean {
  return !!value[ReactiveFlags.IS_REACTIVE]
}

export function isReadonly(value): boolean {
  return !!value[ReactiveFlags.IS_READONLY]
}

完善reactivereadonly——响应式转换嵌套对象

reactivereadonly的响应式转换是“深层”的,会影响所有嵌套的 property,即嵌套的 property 也应该是响应式的。

分别在reactive的测试文件reactive.spec.tsreadonly的测试文件readonly.spec.ts中添加以下测试代码:

// reactive.spec.ts
describe('reactivity/reactive', () => {
  it('nested reactives', () => {
    const original = { foo: { bar: 1 } }
    const observed = reactive(original)
    // 嵌套对象是响应式的
    expect(isReactive(observed.foo)).toBe(true)
  })
})
// readonly.spec.ts
describe('reactivity/readonly', () => {
  it('should make nested values readonly', () => {
    const original = { foo: { bar: 1 } }
    const wrapped = readonly(original)
    // 嵌套对象是响应式的
    expect(isReadonly(wrapped.foo)).toBe(true)
  })
})

为了通过以上测试,需要对reactivereadonly的实现进行完善,对src/reactivity/src目录下的baseHandlers.ts文件中的createGetter工具函数做如下修改

function createGetter(isReadonly = false) {
  return function (target, key) {
    /* 其他代码 */

    const res = Reflect.get(target, key)

    if (!isReadonly) {
      track(target, key)
    }

    // 若 property 的值为对象,则利用 reactive 和 readonly 进行响应式转换
    if (typeof res === 'object' && res !== null) {
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}

由于可能会多次使用到,因此可以将判断一个变量是否为对象抽离成一个isObject函数。在src/shared目录下的index.ts文件中添加以下代码:

// 用于判断一个变量是否为对象
export const isObject = value => typeof value === 'object' && value !== null

之后利用isObject函数完善createGetter工具函数:

function createGetter(isReadonly = false) {
  return function (target, key) {
    /* 其他代码 */

    if (isObject(res)) {
      return isReadonly ? readonly(res) : reactive(res)
    }

    /* 其他代码 */
  }
}

实现shallowReactiveshallowReadonly

查看 Vue3 API 文档中的响应性 API 部分,找到shallowReactiveshallowReadonly的介绍:

shallowReactive创建一个响应式代理,它跟踪其自身 property 的响应性,但不执行嵌套对象的深层响应式转换(暴露原始值)

const state = shallowReactive({
  foo: 1,
  nested: {
    bar: 2
  }
})

// 改变 state 本身的性质是响应式的
state.foo++
// ...但是不转换嵌套对象
isReactive(state.nested) // false
state.nested.bar++ // 非响应式

reactive不同,任何使用ref的 property 都不会被代理自动解包。

shallowReadonly 创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换(暴露原始值)。

const state = shallowReadonly({
  foo: 1,
  nested: {
    bar: 2
  }
})

// 改变 state 本身的 property 将失败
state.foo++
// ...但适用于嵌套对象
isReadonly(state.nested) // false
state.nested.bar++ // 适用

readonly不同,任何使用ref的 property 都不会被代理自动解包

在实现shallowReactiveshallowReadonly之前,首先在src/reactivity/__tests__目录下分别创建shallowReactiveshallowReadonly的测试文件shallowReactive.spec.tsshallowReadonly.spec.ts,分别添加以下测试代码:

// shallowReactive.spec.ts
describe('shallowReactive', () => {
  test('should not make non-reactive properties reactive', () => {
    const props = shallowReactive({ n: { foo: 1 } })
    expect(isReactive(props.n)).toBe(false)
  })
})
// shallowReadonly.spec.ts
describe('reactivity/shallowReadonly', () => {
  test('should not make non-reactive properties reactive', () => {
    const props = shallowReadonly({ n: { foo: 1 } })
    expect(isReactive(props.n)).toBe(false)
  })
})

为了通过以上测试,同时根据之前优化代码的思路,首先对src/reactivity/src目录下的baseHandlers.ts文件中的createGetter工具函数做如下修改:

function createGetter(isReadonly = false, shallow = false) {
  return function (target, key) {
    /* 其他代码 */

    const res = Reflect.get(target, key)

    // 利用 reactive 和 shallowReactive 进行响应式转换时才进行依赖收集
    if (!isReadonly) {
      // 收集依赖
      track(target, key)
    }

    // 若利用 shallowReactive 和 shallowReadonly 进行响应式转换则直接返回
    if (shallow) {
      return res
    }

    /* 其他代码 */
  }
}

之后,在src/reactivity/src目录下的baseHandlers.ts文件中分别构建shallowRreactiveshallowReadonly对应的 handlers,二者分别是由mutableHandlersreadonlyHandlers替换 get property 得到的:

// shallowRreactive 对应的 handlers 是由 mutableHandlers 替换 get property 得到的
export const shallowHandlers = extend({}, mutableHandlers, {
  get: shallowGet
})

// shallowReadonly 对应的 handlers 是由 readonlyHandlers 替换 get property 得到的
export const shallowReadonlyHandlers = extend({}, readonlyHandlers, {
  get: shallowReadonlyGet
})

最后,在src/reactivity/src目录下的reactive.ts文件中实现并导出shallowRreactiveshallowReadonly

export function shallowReactive(raw) {
  return createReactiveObject(raw, shallowHandlers)
}

export function shallowReadonly(raw) {
  return createReactiveObject(raw, shallowReadonlyHandlers)
}

处理几个effect的特例(暂不涉及数组)

1.target对应已注册的proxy无需重新注册

// 例子
let test={num:1}
let a1=reactive(test)
let a2=reactive(test)
export const reactiveMap = new WeakMap<Target, any>();
export const readonlyMap = new WeakMap<Target, any>();
export const shallowReactiveMap = new WeakMap<Target, any>();
export const shallowReadonlyMap = new WeakMap<Target, any>();
export function reactive(target) {
    // 如果目标对象是一个只读的响应数据,则直接返回目标对象
    if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
        return target;
    }
    return createReactiveObject(target, false, mutableHandlers, reactiveMap);
}
export function readonly(target) {
    return createReactiveObject(target, true, readonlyHandlers, readonlyMap);
}
export function shallowReactive(target) {
    return createReactiveObject(target, false, shallowHandlers, shallowReactiveMap);
}
export function shallowReadonly(target) {
    return createReactiveObject(target, true, shallowReadonlyHandlers, shallowReadonlyMap);
}
// 用于创建 Proxy 实例的工具函数
function createReactiveObject(
    target: Target,
    isReadonly: boolean,
    baseHandlers: ProxyHandler<any>,
    proxyMap: WeakMap<Target, any>
) {
    if (!isObject(target)) {
        // 不是对象直接返回
        return target;
    }
    // 查看当前代理对象之前是不是创建过当前代理,如果创建过直接返回之前缓存的代理对象
    // proxyMap 是一个全局的缓存WeakMap
    const existingProxy = proxyMap.get(target);
    if (existingProxy) {
        return existingProxy;
    }
    // 返回 Proxy 的实例
    const proxy = new Proxy(target, baseHandlers);
    proxyMap.set(target, proxy);
    return proxy;
}

2.如果传入的已经是代理了 并且 不是readonly 转换 reactive的直接返回实现

// 例子
let test=reactive(readonly({num:1})) // =====> readonly({num:1})
let test01=readonly(reactive({num:1})) //readonly( reaceive({num1}))
 // 函数 createReactiveObject里
// 如果传入的已经是代理了 并且 不是readonly 转换 reactive的直接返回
    if (target[ReactiveFlags.RAW] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE])) {
        return target;
    }
//新增枚举
export const enum ReactiveFlags {
    IS_REACTIVE = '__v_isReactive',
    IS_READONLY = '__v_isReadonly',
    RAW = '__v_raw'
}
export interface Target {
    [ReactiveFlags.IS_REACTIVE]?: boolean; // target 是否是响应式
    [ReactiveFlags.IS_READONLY]?: boolean; // target 是否是只读
    [ReactiveFlags.RAW]?: any; // 表示 proxy 对应的源数据,target 已经是 proxy 对象时会有该属性
}
// baseHandles.ts 
// 用于生成 get 函数的工具函数
function createGetter(isReadonly = false, shallow = false) {
    return function (target, key, receiver) {
        //  ReactiveFlags 是在reactive中声明的枚举值,如果key是枚举值则直接返回对应的布尔值
        if (key === ReactiveFlags.IS_REACTIVE) {
            return !isReadonly;
        } else if (key === ReactiveFlags.IS_READONLY) {
            return isReadonly;
        } else if (
            // 如果key是raw  receiver 指向调用者,则直接返回目标对象。
            // 这里判断是为了保证触发拦截 handle 的是 proxy 本身而不是 proxy 的继承者
            // 触发拦的两种方式:一是访问 proxy 对象本身的属性,二是访问对象原型链上有 proxy 对象的对象的属性,因为查询会沿着原型链向下找
            key === ReactiveFlags.RAW &&
            receiver ===
                (isReadonly
                    ? shallow
                        ? shallowReadonlyMap
                        : readonlyMap
                    : shallow
                    ? shallowReactiveMap
                    : reactiveMap
                ).get(target)
        ) {
            return target;
        }
        const res = Reflect.get(target, key, receiver);
        // 利用 reactive 进行响应式转换时才进行依赖收集
        if (!isReadonly) {
            // 收集依赖
            track(target, key);
        }
        if (shallow) {
            return res;
        }
        // 由于 proxy 只能代理一层,如果子元素是对象,需要递归继续代理
        if (isObject(res)) {
            return isReadonly ? readonly(res) : reactive(res);
        }
        return res;
    };
}

3.嵌套effect,外层副作用函数失效

4.同时收集触发依赖造成死循环

5.增加deferStop 防止内部停止不了当前副作用

// 例子 嵌套effect
let test=reactive({num:1})
let test2=reactive({num2:1})
effect(()=>{
  effect(()=>{
    console.log(test2.num2)
  })
  console.log(test.num)
})
//  收集触发依赖死循环
effect(()=>{
  test.num++
})
// 内部停止不了当前副作用
let runner=effect(()=>{
  if(test.num>3){
    stop(runner)
  }
})
// 抽离出一个 ReactiveEffect 类,对相关操作进行封装
export class ReactiveEffect {
    private _fn: any;
    // 用于保存与当前实例相关的响应式对象的 property 对应的 Set 实例
    deps: Array<Set<ReactiveEffect>> = [];
    // 用于记录当前实例状态,为 true 时未调用 stop 方法,否则已调用,防止重复调用 stop 方法
    active: boolean = true;
    parent: ReactiveEffect | undefined = undefined;
    // 防止内部调用 无法stop该副作用
    private deferStop?: boolean;
    constructor(fn, public scheduler?) {
        // 将传入的函数赋值给实例的私有 property _fn
        this._fn = fn;
    }
    // 执行传入的函数
    run() {
        // 若已调用 stop 方法则直接返回传入的函数执行的结果
        if (!this.active) {
            return this._fn();
        }
        let parent: ReactiveEffect | undefined = activeEffect;
        // 防止内嵌产生导致外层依赖无法收集
        let lastShouldTrack = shouldTrack;
        while (parent) {
            // 处理副作用函数get 和 set同时操作造成内存溢出
            if (parent === this) {
                return;
            }
            parent = parent.parent;
        }
        // debugger;
        try {
            this.parent = activeEffect;
            // 应该收集依赖
            shouldTrack = true;
            // 调用 run 方法时,用全局变量 activeEffect 保存当前实例
            activeEffect = this as any;
            // 执行前清除依赖
            cleanupEffect(this);
            //执行副作用函数 触发收集依赖
            const res = this._fn();
            // 返回传入的函数执行的结果
            return res;
        } finally {
            // 储存上一个副作用函数的实例
            activeEffect = this.parent;
            // 重置
            shouldTrack = lastShouldTrack;

            this.parent = undefined;
            // 清除依赖
            if (this.deferStop) {
                this.stop();
            }
        }
    }
    onStop?: () => void;
    // 用于停止传入的函数的执行
    stop() {
        // 防止内部调用 最后副作用函数完成时依赖未完全清除时 导致清除无效
        if (activeEffect == this) {
            this.deferStop = true;
        } else if (this.active) {
            cleanupEffect(this);
            if (this.onStop) {
                this.onStop();
            }
            this.active = false;
        }
    }
}

ref

查看 Vue3 API 文档中的响应性 API 部分,找到ref的介绍。

ref 接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象具有指向内部值的单个 property .value。

示例:

const count = ref(0)
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

如果将对象分配为 ref 值,则通过reactive函数使该对象具有高度的响应式。

类型声明:

interface Ref<T> {
  value: T
}

function ref<T>(value: T): Ref<T>

有时我们可能需要为 ref 的内部值指定复杂类型。想要简洁地做到这一点,我们可以在调用 ref 覆盖默认推断时传递一个泛型参数

const foo = ref<string | number>('foo') // foo 的类型:Ref<string | number>

foo.value = 123 // ok!

如果泛型的类型未知,建议将 ref 转换为Ref<T>

function useState<State extends string>(initial: State) {
  const state = ref(initial) as Ref<State> // state.value -> State extends string
  return state
}

① 实现最基础的ref

在实现ref之前,首先在src/reactivity/__tests__目录下创建ref的测试文件ref.spec.ts,并添加以下测试代码:

describe('reactivity/ref', () => {
  it('should hold a value', () => {
    // 创建 ref 对象
    const a = ref(1)
    // ref 对象的 value property 的值等于传入的值
    expect(a.value).toBe(1)
    // ref 对象的 value property 的值是可变的
    a.value = 2
    expect(a.value).toBe(2)
  })

  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)
    // ref 对象是响应式的
    a.value = 2
    expect(calls).toBe(2)
    expect(dummy).toBe(2)
    // ref 对象的 value property 的 set 具有缓存,不会重复触发依赖
    a.value = 2
    expect(calls).toBe(2)
    expect(dummy).toBe(2)
  })
})

为了通过以上测试,在src/reactivity/src目录下创建ref.ts文件,在其中实现一个不完全的ref并导出,在实现过程中利用Ref接口的实现类,对操作进行封装

// ref 对象的接口
interface Ref {
  value
}

// Ref 接口的实现类,对操作进行封装
class RefImpl {
  private _value

  constructor(value) {
    // 将传入的值赋值给实例的私有 property _value
    this._value = value
  }

  // value property 的 get 返回私有 property _value 的值
  get value() {
    // TODO: 收集依赖

    // 返回实例的私有 property _value 的值
    return this._value
  }

  // value property 的 set 修改私有 property _value 的值
  set value(newVal) {
    // TODO: 触发依赖

    // 对 set 的值进行处理,将结果赋值给实例的私有 property _value
    this._value = newVal
  }
}

export function ref(value?: unknown): Ref {
    // 返回 RefImpl 类的实例,即 ref 对象
    return new RefImpl(value);
}

这样就实现了一个不完全的ref,即能够将传入的值转为 ref 对象。之后,从src/reactivity/src目录下的effect.ts文件中的tracktrigger函数中抽离并导出isTrackingtrackEffectstriggerEffects函数:

export function track(target, key) {
  // 若不应该收集依赖则直接返回
  if (!isTracking()) {
    return
  }

  /* 其他代码 */

  trackEffects(dep)
}

// 用于判断是否应该收集依赖
export function isTracking() {
  return shouldTrack && activeEffect !== undefined
}

// 用于将当前正在执行的 ReactiveEffect 类的实例添加到 dep 中, 同时将 dep 添加到当前正在执行的 ReactiveEffect 类的实例的 deps property 中
export function trackEffects(dep) {
    // 若 dep 中包括当前正在执行的 ReactiveEffect 类的实例则直接返回
    if (dep.has(activeEffect!)) {
        return;
    }
    // 将当前正在执行的 ReactiveEffect 类的实例添加到 dep 中
    dep.add(activeEffect!);
    // 将 dep 添加到当前正在执行的 ReactiveEffect 类的实例的 deps property 中
    activeEffect!.deps.push(dep);
}

export function trigger(target, key) {
  /* 其他代码 */

  triggerEffects(dep)
}

// 用于遍历 dep,调用每一个 ReactiveEffect 类的实例的 scheduler 方法或 run 方法
export function triggerEffects(dep) {
  for (const reactiveEffect of dep) {
    if (reactiveEffect.scheduler) {
      reactiveEffect.scheduler()
    } else {
      reactiveEffect.run()
    }
  }
}

之后,在RefImpl类中创建私有 property dep用于保存与当前 ref 对象相关的依赖,在 value property 的 get 中收集依赖,在 set 中触发依赖:

class RefImpl {
  private _value
  // 用于保存与当前 ref 对象相关的依赖
  private dep

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

  get value() {
    if (isTracking()) {
      // 收集依赖
      trackEffects(this.dep)
    }
    return this._value
  }

  set value(newVal) {
    // 若 set 的值与之前相同则直接返回
    if (!hasChanged(newVal, this._value)) {
      return
    }

    this._value = newVal
    // 触发依赖
    triggerEffects(this.dep)
  }
}

// share.js
export const hasChanged = (value: any, oldValue: any): boolean => !Object.is(value, oldValue);

② 完善ref

若传入的值是一个对象,需要利用reactive对该对象进行响应式转换。

ref的测试文件ref.spec.ts中添加以下测试代码:

describe('reactivity/ref', () => {
  /* 其他测试代码 */

  it('should make nested properties reactive', () => {
    const a = ref({
      count: 1
    })
    let dummy
    effect(() => {
      dummy = a.value.count
    })
    expect(dummy).toBe(1)
    // ref 对象的 value property 的是一个响应式对象
    a.value.count = 2
    expect(dummy).toBe(2)
  })
})

为了通过以上测试,需要对ref的实现进行完善。首先,在src/reactivity/src目录下的reactive.ts文件中实现并导出toReactive函数:

// 用于对值进行处理,若为对象则利用 reactive 进行响应式处理,否则直接返回
export const toReactive = value => (isObject(value) ? reactive(value) : value)

之后,在RefImpl类中增加私有 property _rawValue用于保存用于保存传入的值和 set 的值,并在赋值给实例的私有 property _value之前利用toReactive函数对值进行处理:

class RefImpl {
  // 用于保存传入的值和 set 的值
  private _rawValue
  private _value
  private dep

  constructor(value) {
    // 将传入的值赋值给实例的私有 property _rawValue
    this._rawValue = value
    // 对传入的值进行处理,将结果赋值给实例的私有 property _value
    this._value = toReactive(value)
    this.dep = new Set()
  }

  get value() {
    if (isTracking()) {
      trackEffects(this.dep)
    }

    return this._value
  }

  set value(newVal) {
    // 若 set 的值与之前不同则修改并触发依赖
    if (hasChanged(newVal, this._rawValue)) {
      // 将 set 的值赋值给实例的私有 property _rawValue
      this._rawValue = newVal
      // 对 set 的值进行处理,将结果赋值给实例的私有 property _value
      this._value = toReactive(newVal)
      // 触发依赖
      triggerEffects(this.dep)
    }
  }
}

实现isRefunref

查看 Vue3 API 文档中的响应性 API 部分,找到isRefunRef的介绍。

isRef检查值是否为一个 ref 对象。

unref如果参数是一个 ref,则返回内部值,否则返回参数本身。这是val = isRef(val) ? val.value : val的语法糖函数。

function useFoo(x: number | Ref<number>) {
  const unwrapped = unref(x) // unwrapped 现在一定是数字类型
}

在实现isRefunRef之前,首先在ref的测试文件ref.spec.ts中增加关于isRefunRef的测试代码:

describe('reactivity/ref', () => {
  it('isRef', () => {
    expect(isRef(ref(1))).toBe(true)
    expect(isRef(reactive({ foo: 1 }))).toBe(false)
    expect(isRef(0)).toBe(false)
    expect(isRef({ bar: 0 })).toBe(false)
  })

  it('unref', () => {
    expect(unref(1)).toBe(1)
    expect(unref(ref(1))).toBe(1)
  })
})

为了通过以上测试,首先在RefImpl类中增加共有 property __v_isRef用于标志实例是一个 ref 对象,之后,在src/reactivity/src目录下的ref.ts文件中实现并导出isRefunRef

class RefImpl {
  // 用于保存传入的值和 set 的值
  private _rawValue
  private _value
  // 用于保存与当前 ref 对象相关的依赖
  private dep
  // 用于标志实例是一个 ref 对象
  public __v_isRef = true
}

// 用于判断一个值是否是 ref 对象
export function isRef(value): boolean {
  return !!value.__v_isRef
}

// 用于获取 ref 对象的 value property 的值
export function unref(ref) {
  return isRef(ref) ? ref.value : ref
}

实现proxyRefs函数

proxyRefs函数接受一个对象作为参数,返回一个对该对象的 get 和 set 进行代理的 Proxy 的实例proxy,若该对象的某个 property 的值是一个 ref 对象,则可直接通过获取proxy的相应 property 的值获取该 ref 对象的传入的值,直接修改proxy的相应 property 的值修改该 ref 对象的传入的值或替换该 ref 对象。

在实现proxyRefs函数之前,首先在ref的测试文件ref.spec.ts中增加关于proxyRefs函数的测试代码:

describe('reactivity/ref', () => {
  it('proxyRefs', () => {
    const obj = {
      foo: ref(1),
      bar: 'baz'
    }
    const proxyObj = proxyRefs(obj)
    expect(proxyObj.foo).toBe(1)
    expect(proxyObj.bar).toBe('baz')

    proxyObj.foo = 2
    expect(proxyObj.foo).toBe(2)

    proxyObj.foo = ref(3)
    expect(proxyObj.foo).toBe(3)
  })
})

为了通过以上测试,在src/reactivity/src目录下的ref.ts文件中实现并导出proxyRefs函数。

export function proxyRefs(objectWithRefs) {
  // 返回 Proxy 的实例
  return new Proxy(objectWithRefs, {
    // 对传入的对象的 property 的 get 和 set 进行代理
    get: function (target, key) {
      // 获取传入的对象的 property 的值,再调用 unref 进行处理
      return unref(Reflect.get(target, key))
    },
    set: function (target, key, value) {
      const oldValue = target[key]
      // 若传入的对象的 property 的值是一个 ref 对象,而 set 的值不是一个 ref 对象,则修改该 ref 对象的值,否则直接修改                property 的值
      if (isRef(oldValue) && !isRef(value)) {
        oldValue.value = value
        return true
      } else {
        return Reflect.set(target, key, value)
      }
    }
  })
}

实现computed

查看 Vue3 API 文档中的响应性 API 部分,找到computed的介绍。

computed接受一个 getter 函数,并根据 getter 的返回值返回一个不可变的响应式 ref 对象

const count = ref(1)
const plusOne = computed(() => count.value + 1)

console.log(plusOne.value) // 2

plusOne.value++ // 错误

或者,接受一个具有 get 和 set 函数的对象,用来创建可写的 ref 对象。

const count = ref(1)
const plusOne = computed({
  get: () => count.value + 1,
  set: val => {
    count.value = val - 1
  }
})

plusOne.value = 1
console.log(count.value) // 0

类型声明:

// 只读的
function computed<T>(
  getter: () => T,
  debuggerOptions?: DebuggerOptions
): Readonly<Ref<Readonly<T>>>

// 可写的
function computed<T>(
  options: {
    get: () => T
    set: (value: T) => void
  },
  debuggerOptions?: DebuggerOptions
): Ref<T>
interface DebuggerOptions {
  onTrack?: (event: DebuggerEvent) => void
  onTrigger?: (event: DebuggerEvent) => void
}
interface DebuggerEvent {
  effect: ReactiveEffect
  target: any
  type: OperationTypes
  key: string | symbol | undefined
}

① 实现最基础的computed

在实现computed之前,在src/reactivity/__tests__目录下创建computed的测试文件computed.spec.ts,并添加以下测试代码:

describe('reactivity/computed', () => {
  it('should return updated value', () => {
    const value = reactive({ foo: 1 })
    // 接受一个 getter 函数创建只读响应式 ref 对象,
    const cValue = computed(() => value.foo)
    expect(cValue.value).toBe(1)
    value.foo = 2
    expect(cValue.value).toBe(2)
  })
})

为了通过以上测试,在src/reactivity/src目录下创建computed.ts文件,在其中实现一个最基础的computed并导出,在实现过程中利用Ref接口的实现类,对操作进行封装,同时利用了effect的实现中抽离出的ReactiveEffect类,因此需要将src/reactivity/src目录下的effect.ts文件中的ReactiveEffect类导出:

// effect.ts
export class ReactiveEffect {
  /* 具体实现 */
}

// computed.ts
// Ref 接口的实现类
class ComputedImpl {
  // 用于保存 ReactiveEffect 类的实例
  private _effect: ReactiveEffect

  constructor(getter) {
    // 利用 getter 函数创建 ReactiveEffect 类的实例
    this._effect = new ReactiveEffect(getter)
  }

  // value property 的 get 返回调用私有 property _effect 的 run 方法的返回值,即调用 getter 函数的返回值
  get value() {
    return this._effect.run()
  }
}

export function computed(getter) {
  // 返回 RefImpl 类的实例,即 ref 对象
  return new ComputedImpl(getter)
}

② 完善computed

computed会懒执行 getter 函数,同时响应式 ref 对象的 value property 的 get 具有缓存。

处理穿参若为{ get(){ },set(){ } }时

computed的测试文件computed.spec.ts中添加以下测试代码:

describe('reactivity/computed', () => {
  it('should compute lazily', () => {
    const value = reactive({ foo: 1 })
    const getter = jest.fn(() => value.foo)
    const cValue = computed(getter)

    // 在获取 ref 对象的 value property 的值时才执行 getter
    expect(getter).not.toHaveBeenCalled()
    expect(cValue.value).toBe(1)
    expect(getter).toHaveBeenCalledTimes(1)
    // 若依赖的响应式对象的 property 的值没有更新,则再次获取 ref 对象的 value property 的值不会重复执行 getter
    cValue.value
    expect(getter).toHaveBeenCalledTimes(1)
    // 修改依赖的响应式对象的 property 的值时不会执行 getter
    value.foo = 1
    expect(getter).toHaveBeenCalledTimes(1)

    // 在依赖的响应式对象的 property 的值没有更新后,获取 ref 对象的 value property 的值再次执行 getter
    expect(cValue.value).toBe(1)
    expect(getter).toHaveBeenCalledTimes(2)
    cValue.value
    expect(getter).toHaveBeenCalledTimes(2)
  })
})

为了通过以上测试,需要对computed的实现进行完善。

import { effect } from './effect';
import { isFunction } from '../shared/index';
// Ref 接口的实现类
class ComputedImpl {
    private _runner;
    // 用于保存 getter 函数的执行结果
    private _value;
    // 用于记录是否 不使用缓存
    private _dirty = true;
    private _setter?;
    constructor(getter, setter) {
        this._setter = setter;
        // 利用 getter 函数和一个方法创建 ReactiveEffect 类的实例
        this._runner = effect(getter, {
            lazy: true,
            scheduler: () => {
                if (!this._dirty) {
                    // 用于关闭缓存
                    this._dirty = true;
                }
            }
        });
    }

    // value property 的 get 返回调用私有 property _effect 的 run 方法的返回值,即调用 getter 函数的返回值
    get value() {
        if (this._dirty) {
            // 调用 ReactiveEffect 类的实例的 run 方法,即执行 getter 函数,将结果赋值给 _value property
            this._value = this._runner.effect.run();
            this._dirty = false;
        }

        return this._value;
    }
    set value(newValue) {
        this._setter(newValue);
    }
}
/* 处理参数为 {
get(){},
set(newVal){
   ....
}
} */
export function computed(getterOrOption) {
    let getter, setter;
    if (isFunction(getterOrOption)) {
        getter = getterOrOption;
        setter = () => {
            console.warn('computed is readonly');
        };
    } else {
        getter = getterOrOption.get;
        setter = getterOrOption.set;
    }
    // 返回 RefImpl 类的实例,即 ref 对象
    return new ComputedImpl(getter, setter);
}

// ../shared/index (新增判断是否为函数)
export const isFunction = (val: unknown): val is Function =>
  typeof val === 'function'
2.2.1

5 months ago

2.2.0

2 years ago

2.1.0

2 years ago

2.0.6

2 years ago

2.0.5

2 years ago

2.0.4

2 years ago

2.0.3

2 years ago

2.0.2

2 years ago

2.0.1

2 years ago

2.0.0

2 years ago

1.0.12

2 years ago

1.0.11

2 years ago

1.0.10

2 years ago

1.0.9

2 years ago

1.0.8

2 years ago

1.0.7

2 years ago

1.0.6

2 years ago

1.0.5

2 years ago

1.0.4

2 years ago

1.1.0

2 years ago