🤯vue3核心源码剖析(四)

752 阅读9分钟

🚀实现 shallowReadonly 和 ref 功能 且利用 Jest 测试

山不在高,有仙则名。水不在深,有龙则灵。 —— 刘禹锡《陋室铭》

前言

上篇对 reactive & effect 的补充暂告一段落,还没看过上一篇的看这里 🎉

🚀reactive & effect 且利用 Jest 测试实现数据响应式(一)

🚀reactive & effect 且利用 Jest 测试实现数据响应式(二)

🚀实现 isReactive & isReadonly 且利用 Jest 进行测试

整篇文章的通过TDD测试驱动开发,带你一步一步实现 vue3 源码。

本篇文章内容包括:

  1. 讲解 shallowReadonly 和 shallowReactive 的实现
  2. 讲解 isShallow 的实现思路
  3. 讲解 isProxy 的实现思路
  4. 讲解 toRaw 的实现思路
  5. 讲解 ref 的实现思路

🤣 如有错漏,请多指教 ❤

实现 shallowReadonly

首先要搞清楚 shallowReadonly 是什么东西,以及它的用法,咱们才好往下实现它。

官方说明

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

我们逐句讲解:

  1. 创建一个 proxy

    跟 readonly 一样,我们也需要 new 一个 Proxy,并通过传入 handler。

  2. 使其自身的 property 为只读

    跟 readonly 一样,我们需要传入 handler 的 getter 和 setter,用户对数据进行 set 赋值操作的时候会得到一个警告。

  3. 不执行嵌套对象的深度只读转换

    与 readonly 不一样的地方在于,我们不用通过isObject()判断 get 操作之后得到的res是否是对象然后再次执行 readonly(res)。只需要判断数据是否 isShallow,如果是,直接return res即可。

下面是我们的测试用例:

it('shallowReadonly basic test', () => {
  let original = {
    foo: {
      name: 'ghx',
    },
  }
  let obj = shallowReadonly(original)
  expect(isReadonly(obj)).toBe(true)
  // 因为只做表层的readonly,深层的数据还不是proxy
  expect(isReadonly(obj.foo)).toBe(false)
  expect(isReactive(obj.foo)).toBe(false)
})
  1. 创建一个 proxy需要传入不一样的 handlers
export function shallowReadonly<T>(target: T) {
  return createReactiveObject(target, shallowReadonlyHandlers)
}
  1. 使其自身的 property 为只读

这里我通过 extend 拓展了一下readonlyhandlers,但是 get 操作需要单独处理。

export const shallowReadonlyHandlers: ProxyHandler<object> = extend({}, readonlyHandlers, {
  get: shallowReadonlyGet,
})
  1. 不执行嵌套对象的深度只读转换

    判断数据是否 isShallow,改写createGetter,加多一个入参isShallow

export function createGetter<T extends object>(isReadonly = false, isShallow = false) {
  return function get(target: T, key: string | symbol) {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    }
    let res = Reflect.get(target, key)

    if (!isReadonly) {
      track(target, key as string)
    }
    // 这句是当前功能的关键
    if (isShallow) {
      return res
    }
    if (isObject(res)) {
      return isReadonly ? readonly(res) : reactive(res)
    }
    return res
  }
}

为什么不把 isShallow 判断放在 isReadonly 之前?

这就涉及到后面的 shallowReactive 实现了,shallowReactive 除了要返回 get 到的值之外,同时还要进行依赖收集。

之后创建一个常量用于缓存 createGetter 返回的函数即可。

const shallowReadonlyGet = createGetter(true, true)

实现 shallowReactive

官方说明

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

// shallowReactive的get操作
const shallowGet = createGetter(false, true)
// shallowReactive的set操作
const shallowSet = createSetter(true)

export const shallowReactiveHandlers: ProxyHandler<object> = extend({}, mutableHandlers, {
  get: shallowGet,
  set: shallowSet,
})
export function shallowReactive<T extends object>(target: T) {
  return createReactiveObject<T>(target, shallowReactiveHandlers)
}

上面有讲isShallow,那我们也实现一个判断是否开启shallow模式的函数吧。

实现isShallow

export enum ReactiveFlags {
  IS_REACTIVE = '__v_isReactive',
  IS_READONLY = '__v_isReadonly',
  IS_SHALLOW = '__v_isShallow',
  RAW = '__v_raw'
}
// 检查对象是否 开启 shallow mode
export function isShallow(value: unknown){
  return !!(value as Target)[ReactiveFlags.IS_SHALLOW]
}

上面就是故意触发proxy的get操作,因为createGetter()的入参有个isShallow,这已经为我们标识当前proxy是否是shallow了。

我们改写以下createGetter,加多一层判断,如果get操作的key值是ReactiveFlags.IS_SHALLOW,那么直接返回入参isShallow的状态。

export function createGetter<T extends object>(isReadonly = false, isShallow = false) {
  return function get(target: T, key: string | symbol) {
    // isReactive和isReadonly 都是根据传入的参数 `isReadonly`来决定是否返回true | false的
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (key === ReactiveFlags.IS_SHALLOW) {
      //  主要代码:
      return isShallow
    } else if (key === ReactiveFlags.RAW) {
      return target
    }
    let res = Reflect.get(target, key)

    if (!isReadonly) {
      track(target, key as string)
    }
    if (isShallow) {
      return res
    }
    if (isObject(res)) {
      return isReadonly ? readonly(res) : reactive(res)
    }
    return res
  }
}

实现 isProxy

官方说明

检查对象是否是由 reactive 或 readonly 创建的 proxy。

判断 reactive 和 readonly 生成的 proxy,在上一章节已经讲了,所以这里实现起来比较容易。

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

实现 toRaw 功能

官方说明

返回 reactive 或 readonly 代理的原始对象。这是一个“逃生舱”,可用于临时读取数据而无需承担代理访问/跟踪的开销,也可用于写入数据而避免触发更改

总而言之就是可以通过toRaw获取到reactive或者readonly的原始值。

以下是测试用例

it('toRaw', () => {
  const original = { foo: 1 }
  const observed = reactive(original)
  // 输出的结果必须要等于原始值
  expect(toRaw(observed)).toBe(original)
  expect(toRaw(original)).toBe(original)
})
it('nested reactive toRaw', () => {
  const original = {
    foo: {
      name: 'ghx',
    },
  }
  const observed = reactive(original)
  const raw = toRaw(observed)
  expect(raw).toBe(original)
  expect(raw.foo).toBe(original.foo)
})

怎么获取原始数据呢?

可以在 get 操作直接 return get()的第一个参数target

那我们怎么才能知道什么时候应该 return target呢?我们需要一个标识,这个标识也是之前讲过,判断 isReadonly 和 isReactive 也是用的这个标识。

在 ReactiveFlags 定义一个枚举成员RAW,用于标记当前 get 操作是因为 raw 引起的

export enum ReactiveFlags {
  IS_REACTIVE = '__v_isReactive',
  IS_READONLY = '__v_isReadonly',
  IS_SHALLOW = '__v_isShallow',
  RAW = '__v_raw',
}

之后我们就可以通过故意触发proxy的 get 操作,让 get 操作 return 原始值了。

// 返回 reactive 或 readonly 代理的原始对象
export function toRaw<T>(observed: T): T {
  // observed存在,触发get操作,在createGetter直接return target
  const raw = observed && (observed as Target)[ReactiveFlags.RAW]
  return raw ? toRaw(raw) : observed
}

实现 ref

官方说明

  1. 接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象仅有一个 .value property,指向该内部值。
  1. 如果将对象分配为 ref 值,则它将被 reactive 函数处理为深层的响应式对象。

内部值一般指值类型(string,number,boolean。。。)

为什么要有 ref 呢,reactive 不行吗?

因为reactive用的是 proxy,而proxy只能针对对象去监听数据变化,基本数据类型并不能用 proxy。

所以我们想到了class里面的取值函数 getter 和存值函数 getter,他们都能在数据变化的时候对数据加以操作。

我们依旧如此编写一个测试用例来驱动开发

it('should hold a value', () => {
  const a = ref(1)
  expect(a.value).toBe(1)
  a.value = 2
  expect(a.value).toBe(2)
})

定义一个 RefImpl 类,实例化的就是 ref 对象

class RefImpl<T> {
  private _value: T
  constructor(value: any) {
    this._value = value
  }
  get value() {
    return this._value
  }
  set value(newValue: any) {
    this._value = newValue
  }
}

很好,上面的测试用例我们已经跑通了

但是光跑通上面的简单用例还不行,我们必须让 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)
  // same value should not trigger
  a.value = 2
  expect(calls).toBe(2)
})

在实现reactive数据响应式的时候我们就已经实现了依赖收集和触发依赖功能,为了对代码能有更好的可读性和性能优化,我们会选择复用reactive的依赖收集和触发依赖的相关代码逻辑。

但是这些代码逻辑已经写死在 trigger()和 track()函数里面了,为了代码能复用,我们可以把这些逻辑抽离出来。

定义一个trackEffect函数,对依赖收集的逻辑进行封装

export type Dep = Set<ReactiveEffect>
export function trackEffect(dep: Dep) {
  // 避免不必要的add操作
  if (dep.has(activeEffect)) return
  // 将activeEffect实例对象add给deps
  dep.add(activeEffect)
  // activeEffect的deps 接收 Set<ReactiveEffect>类型的deps
  // 供删除依赖的时候使用(停止监听依赖)
  activeEffect.deps.push(dep)
}

定义一个triggerEffect函数,对触发依赖的逻辑进行封装

export function triggerEffect(dep: Dep) {
  for (let effect of dep) {
    if (effect.scheduler) {
      effect.scheduler()
    } else {
      effect.run()
    }
  }
}

我们就可以在 RefImpl 类的 get 和 set 进行依赖收集和触发依赖,不过在此之前,我们还需要定义一个dep公有成员函数,用于存储这一个ref对象的依赖

class RefImpl<T> {
  private _value: T
  public dep?: Dep = undefined
  constructor(value: any) {
    this._value = value
    this.dep = new Set()
  }
  get value() {
    trackRefValue(this)
    return this._value
  }
  set value(newValue: any) {
    // 触发依赖

    // 注意这里先赋值再触发依赖
    this._value = newValue
    triggerEffect(this.dep as Dep)
  }
}
function trackRefValue(ref: RefImpl<any>) {
  // 有时候根本就没有调用effect(),也就是说activeEffect是undefined的情况
  //   isTracking 的实现在之前的章节讲到
  if (isTracking()) {
    // 依赖收集
    trackEffect(ref.dep as Dep)
  }
}

当 ref 对象被赋予相同的值的时候,我们要做到不触发依赖的行为。

怎么才能判断是不是相同的值呢?

我们必须给 RefImpl 存入一个原始值_rawValue,它存放着 ref 的入参(ref 原本传入的值),我们只需要把当前执行 set 赋值操作的value_rawValue进行比较,如果相同,我们就不触发依赖。

比较的方式我们选择使用ES6 的 Object.is

export function hasChanged(value: any, oldValue: any) {
  return !Object.is(value, oldValue)
}

比较结果为 true,说明value已经改变了,这时我们应该更新_rawValue的值,并触发依赖。

class RefImpl<T> {
  private _value: T
  public dep?: Dep = undefined
  private _rawValue: T
  constructor(value: any) {
    this._value = value
    this._rawValue = value
    this.dep = new Set()
  }
  get value() {
    trackRefValue(this)
    return this._value
  }
  set value(newValue: any) {
    // 触发依赖

    // 对比旧的值和新的值,如果相等就没必要触发依赖和赋值了,这也是性能优化的点
    if (hasChanged(newValue, this._rawValue)) {
      // 注意这里先赋值再触发依赖
      this._value = newValue
      this._rawValue = newValue
      triggerEffect(this.dep as Dep)
    }
  }
}

如果将对象分配为 ref 值,则它将被 reactive 函数处理为深层的响应式对象。

reactive 函数的深层响应式对象功能,在之前的篇章里我们已经实现了。

我们只需要判断value是否是对象,是对象就用reactive处理,不是对象就直接给_value

function convert(value: any) {
  return isObject(value) ? reactive(value) : value
}
class RefImpl<T> {
  private _value: T
  public dep?: Dep = undefined
  private _rawValue: T
  constructor(value: any) {
    this._value = convert(value)
    this._rawValue = value
    this.dep = new Set()
  }
  get value() {
    trackRefValue(this)
    return this._value
  }
  set value(newValue: any) {
    // 触发依赖

    // 对比旧的值和新的值,如果相等就没必要触发依赖和赋值了,这也是性能优化的点
    if (hasChanged(newValue, this._rawValue)) {
      // 注意这里先赋值再触发依赖
      this._value = convert(newValue)
      this._rawValue = newValue
      triggerEffect(this.dep as Dep)
    }
  }
}

至此,测试用例已全部跑通。

总结

shallowReactiveshallowReadonly只能对表层的数据提供reactivereadonly操作,对于深层的数据我们并只能暴露原始值。

isProxy 实现的如此简单是因为我们在之前有刻意的封装函数,希望我们在写代码的时候也要有这种意识,有效的封装函数会是代码写的更简单。

ref 和 reactive 的区别:

  1. ref 一般接受的是值类型,reactive 接受的是引用类型,虽然 ref 能接受对象类型,但是其内部还是使用了 reactive(),所以某些情况下直接使用 reactive 会更好一些。(为什么?你问 proxy 这个大哥给不给你传入值类型啊!)
  2. ref 需要通过.value 才能拿到值。

还有其他区别欢迎补充!

最后@感谢阅读