npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@mini-jackz-vue/reactivity

v2.2.1

Published

```javascript 1.选用TS+jest(测试框架) 2.测试用例驱动功能代码实现 ```

Downloads

5

Readme

🚀 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'