🤯vue3核心源码剖析(五)

565 阅读9分钟

🚀 实现 computed 和 proxyRefs 功能 且利用 Jest 测试

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

前言

上篇对ref实现的暂告一段落,还没看过上一篇的看这里🎉

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

接下来我们来实现一个简单的计算属性computed,并且利用 Jest 测试它的变化。

本篇文章内容包括:

  1. 讲解 isRef 的实现思路和代码实现
  2. 讲解 unref 的实现思路和代码实现
  3. 讲解 proxyRefs 的实现思路和代码实现
  4. 讲解 computed 的实现思路和代码实现

🤣 如有错漏,请多指教 ❤

实现 isRef

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

以下是测试用例:

test('isRef', () => {
  expect(isRef(ref(1))).toBe(true)

  expect(isRef(0)).toBe(false)
  expect(isRef(1)).toBe(false)
  // an object that looks like a ref isn't necessarily a ref
  expect(isRef({ value: 0 })).toBe(false)
})

我们可以定义一个isRef函数,入参传入一个对象,内部通过有意的访问一个只有 ref 才有的公有属性,来判断这个对象是否是ref对象。

class RefImpl<T> {
  ...
  public __v_isRef = true // 标识是ref对象
  constructor(value: any) {
    ...
  }
  ...
}
// 检查值是否为一个 ref 对象。
export function isRef(ref: any) {
  return !!(ref && ref.__v_isRef)
}

(ref && ref.__v_isRef)有可能为undefined,所以我们加上!!undefined转为boolean类型。

实现 unref

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

以下是测试用例:

test('isRef', () => {
  expect(isRef(ref(1))).toBe(true)

  expect(isRef(0)).toBe(false)
  expect(isRef(1)).toBe(false)
  // an object that looks like a ref isn't necessarily a ref
  expect(isRef({ value: 0 })).toBe(false)
})

基本的实现原理就是val = isRef(val) ? val.value : val

export function unref(ref: any) {
  return isRef(ref) ? ref.value : ref
}

实现 proxyRefs

虽然 proxyRefs并没有在官方文档给出介绍和api用法,但是它的实际作用是在<template></template>模板里面解构出ref的value,这样我们在模板里面就不需要再书写.value来获取ref的值了。它同时兼容reactive对象的传入。

测试用例

test('proxyRefs', ()=>{
  const user = {
    age: ref(10),
    name: 'ghx'
  }
  const original = {
    k: 'v'
  }
  const r1 = reactive(original)
  // 传入reactive对象
  const p1 = proxyRefs(r1)
  // objectWithRefs对象 (带ref 的object)
  const proxyUser = proxyRefs(user)

  expect(p1).toBe(r1)

  expect(user.age.value).toBe(10)
  expect(proxyUser.age).toBe(10)
  expect(proxyUser.name).toBe('ghx')

  proxyUser.age = 20
  expect(proxyUser.age).toBe(20)
  expect(user.age.value).toBe(20)

  proxyUser.age = ref(10)
  proxyUser.name = 'superman'
  expect(proxyUser.age).toBe(10)
  expect(proxyUser.name).toBe('superman')
  expect(user.age.value).toBe(10)
})

特点

  • 如果入参是reactive对象,那么直接返回reactive。

  • 如果入参是子属性值带ref对象的普通对象,那么返回一个proxy对象,其中的属性值是ref对象的属性,通过调用unref函数获取实际的值(.value)。

set操作我们也得区分两种情况:

赋值操作状态下

  1. 如果属性为ref对象,并且值为普通类型,那么需要把值赋给ref对象的.value属性。
const user = {
  age: ref(10),
  name: 'ghx'
}
const proxyUser = proxyRefs(user)
proxyUser.age = 20
expect(proxyUser.age).toBe(20)
expect(user.age.value).toBe(20)
  1. 否则其他情况下都应该把值直接赋值给属性。
proxyUser.age = ref(10)
proxyUser.name = 'superman'
expect(proxyUser.age).toBe(10)
expect(proxyUser.name).toBe('superman')
expect(user.age.value).toBe(10)

基本实现

export function proxyRefs<T extends object>(obj: T){
  return isReactive(obj)
    ? obj
    : new Proxy<any>(obj, {
    get(target, key){
      // unref已经处理了obj是否ref的情况所以我们不需要自己if处理,如果是,返回.value,如果不是,直接返回值
      return unref(Reflect.get(target, key)) 
    },
    set(target, key, value){
      // 因为value为普通值类型的情况特殊,要把value赋值给ref的.value
      if (isRef(target[key]) && !isRef(value)) {
        target[key].value = value
        return true
      } else {
        return Reflect.set(target, key, value)
      }
    }
  })
}

实现 computed

<template></template>中放入过多的计算逻辑会让模板难以维护

<h1>My book: </h1>
<div>{{ghx.books.length > 10 ? '多' : '少'}}<div>
<div>{{ghx.books.length > 10 ? '多' : '少'}}<div>
<div>{{ghx.books.length > 10 ? '多' : '少'}}<div>
<div>{{ghx.books.length > 10 ? '多' : '少'}}<div>
<div>{{ghx.books.length > 10 ? '多' : '少'}}<div>
<div>{{ghx.books.length > 10 ? '多' : '少'}}<div>

这时候的template就不单单是简单的,它包含了一些逻辑,让整体上的template看起来更加复杂,所以我们为了解决这个问题,我们需要使用computed。

特点

  1. 具有对数据的响应依赖关系的缓存功能。computed的依赖没有更新的情况下,再次发生ref对象的get操作并不会导致数据的重新计算。
  2. 懒计算。computed的依赖被更新,也不会立即重新计算结果,而是当computed返回的ref对象发生get操作的时候才会计算结果。

基本用法

// 第一种:
const count = ref(1)
const plusOne = computed(() => count.value + 1)

console.log(plusOne.value) // 2

plusOne.value++ // 错误

// 第二种:
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

computed可以接收一个具有get和set方法的对象作为参数,也可以接收一个get方法作为参数,都返回一个ref对象。

这就要用到函数重载了。

  • 当computed只接受一个get方法的时候,那么该computed为只读,写入数据将会得到一个错误
  • 当computed接受具有get和set方法的对象的时候,那么该computed为可读写

我们以通关的形式来实现computed,总共有两关,第一关简单实现computed类,第二关实现computed的懒计算和缓存功能

  1. 第一关的测试用例
it('should return updated value', () => {
  const value = reactive({ foo: 1 })
  const cValue = computed(() => value.foo)
  expect(cValue.value).toBe(1)
})

创建一个computed函数,顺便定义get和set的类型,对不同函数签名进行重载

type ComputedGetter<T> = (...args: any[])=> T
// v 是 要带赋值的值
type ComputedSetter<T> = (v: T) => void
interface WritableComputedOptions<T> {
  get: ComputedGetter<T>;
  set: ComputedSetter<T>;
}
export function computed<T>(option: WritableComputedOptions<T>): any
export function computed<T>(getter: ComputedGetter<T>): ComputedRefImpl<T>
// 实现函数签名
export function computed<T>(getterOrOption: ComputedGetter<T> | WritableComputedOptions<T>){
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>
  if (isFunction(getterOrOption)) {
    getter = getterOrOption
    setter = () => console.error('错误, 因为是getter只读, 不能赋值')
  } else {
    getter = getterOrOption.get
    setter = getterOrOption.set
  }
  return new ComputedRefImpl(getter, setter)
}

接着我们来实现ComputedRefImpl类。

我们希望的computed,它是可以使用.value来访问值的,并能控制value的读写操作,所以我们在内部定义一个私有属性_value,然后通过取值函数getter和存值函数setter控制我们的value,当用户触发get操作的时候,我们会传入的getter函数,把返回值传回给_value,当用户触发set操作的时候,set value(newValue)的newValue会传给用户传进来的setter函数。

export class ComputedRefImpl<T> {
  private _value!: T
  public _getter: ComputedGetter<T>
  constructor(
    getter: ComputedGetter<T>,
    private setter: ComputedSetter<T>
  ) {
    this._getter = getter
  }
  get value() {
    this._value = this.getter()
    return this._value
  }
  set value(newValue: T) {
    this.setter(newValue)
  }
}

到此上面的测试用例就能跑通了。

  1. 第二关的测试用例

看起来有点复杂,我们通过注释讲解一下每一步吧。

it('should compute lazily', () => {
  const value = reactive({ foo: 1 })   // 创建一个reactive对象
  const getter = jest.fn(() => value.foo) // 通过jest.fn()创建一个模拟函数,后续会检测被调用该函数次数
  const cValue = computed(getter)   // 创建一个computed对象,并传入getter函数

  // lazy功能
  expect(getter).not.toHaveBeenCalled() // 因为还没触发cValue的get操作,所以getter是不会被调用的。

  expect(cValue.value).toBe(1)   // cValue的get操作被触发,getter执行
  expect(getter).toHaveBeenCalledTimes(1) // getter被调用一次
  // 缓存功能
  // should not compute again
  cValue.value                   // cValue的get操作被触发,又因为value.foo并没有发生改变
  expect(getter).toHaveBeenCalledTimes(1) // 这里的getter还是被调用了一次

  // should not compute until needed
  value.foo = 2                    // 这里的value.foo发生了改变,但是cValue的get操作还没被触发
  expect(getter).toHaveBeenCalledTimes(1) // 所以这里getter仍然只会被调用一次

  // now it should compute
  expect(cValue.value).toBe(2)     // 这里的cValue的get操作被触发,getter执行
  expect(getter).toHaveBeenCalledTimes(2) // 这里getter被调用了两次
  // should not compute again
  cValue.value                     // cValue的get操作被触发,又因为value.foo并没有发生改变
  expect(getter).toHaveBeenCalledTimes(2) // 这里的getter还是被调用了两次
})

要实现缓存功能,依赖没有发生改变不能让computed重新计算,我们就需要在get操作的时候,添加一把,_dirty,dirty有肮脏的意思,当内部依赖的数据发生改变的时候,就说明肮脏了,_dirty等于true,这时候才能重新计算。

我们声明_dirty为类的私有成员变量。

export class ComputedRefImpl<T> {
  private _value!: T
  private _dirty = true // 避免已经不是第一次执行get操作的时候再次调用compute
  public _getter: ComputedGetter<T>
  constructor(
    getter: ComputedGetter<T>,
    private setter: ComputedSetter<T>
  ) {
    this._getter = getter
  }
  get value() {
    // 如何给dirty重新赋值为true, 触发依赖,调用effect的scheduler()
    if(this._dirty){
      this._dirty = false
      this._value = this.getter()
    }
    return this._value
  }
  set value(newValue: T) {
    this.setter(newValue)
  }
}

后来,为了收集computed的回调函数到依赖集中去,我们为ComputedRefImpl添加_effect,用于存储ReactiveEffect对象(ReactiveEffect到底是什么,在这里有讲到)。底层做了让track可以收集computed的回调函数这件事。

export class ComputedRefImpl<T> {
  private _value!: T
  private _dirty = true // 避免已经不是第一次执行get操作的时候再次调用compute
  private _effect: ReactiveEffect // 进行依赖收集
  constructor(
    getter: ComputedGetter<T>,
    private setter: ComputedSetter<T>
  ) {
    this._effect = new ReactiveEffect(getter)
  }
  get value() {
    if(this._dirty){
      this._dirty = false
      this._value = this._effect.run()
    }
    return this._value
  }
  set value(newValue: T) {
    this.setter(newValue)
  }
}

value.foo = 2的时候,会执行trigger,触发依赖,调用用户的getter函数。

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

但是在这里我们并不希望用户的getter的函数被触发,我们要实现懒计算,就是让用户访问computed返回的ref的时候重新计算,所以为了value.foo = 2的时候不让trigger导致用户的getter函数被触发,并且希望_dirty重新变为true,我们通过scheduler()来把_dirty重新赋值为true。(重新变为true,在下一次computed的返回值ref函数触发get操作的时候才能调用effect.run()重新计算)


export class ComputedRefImpl<T> {
  private _value!: T
  private _dirty = true // 避免已经不是第一次执行get操作的时候再次调用compute
  private _effect: ReactiveEffect // 进行依赖收集
  constructor(
    getter: ComputedGetter<T>,
    private setter: ComputedSetter<T>
  ) {
    this._effect = new ReactiveEffect(getter, ()=>{
      // 把dirty重新赋值为true
      if(!this._dirty){
        this._dirty = true
      }
    })
  }
  get value() {
    // 如何给dirty重新赋值为true, 触发依赖,调用effect的scheduler()
    if(this._dirty){
      this._dirty = false
      this._value = this._effect.run()
    }
    return this._value
  }
  set value(newValue: T) {
    this.setter(newValue)
  }
}

到这里,测试用例就跑通了。

总结

最难最绕的就是computed的实现思路

整体来看computed的实现也是一个发布-订阅者模式。

最后@感谢阅读

完整代码

// In computed.ts

import { isFunction } from "../shared"
import { ReactiveEffect } from "./effect"

export class ComputedRefImpl<T> {
  private _value!: T
  private _dirty = true // 避免已经不是第一次执行get操作的时候再次调用compute
  private _effect: ReactiveEffect // 进行依赖收集
  constructor(
    getter: ComputedGetter<T>,
    private setter: ComputedSetter<T>
  ) {
    this._effect = new ReactiveEffect(getter, ()=>{
      // 把dirty重新赋值为true
      if(!this._dirty){
        this._dirty = true
      }
    })
  }
  get value() {
    // 如何给dirty重新赋值为true, 触发依赖,调用effect的scheduler()
    if(this._dirty){
      this._dirty = false
      this._value = this._effect.run()
    }
    return this._value
  }
  set value(newValue: T) {
    this.setter(newValue)
  }
}

type ComputedGetter<T> = (...args: any[])=> T
// v 是 赋值 = 右边的值
type ComputedSetter<T> = (v: T) => void
interface WritableComputedOptions<T> {
  get: ComputedGetter<T>;
  set: ComputedSetter<T>;
}
// 函数重载
export function computed<T>(option: WritableComputedOptions<T>): any
export function computed<T>(getter: ComputedGetter<T>): ComputedRefImpl<T>
// 实现签名
export function computed<T>(getterOrOption: ComputedGetter<T> | WritableComputedOptions<T>) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>
  // 区分入参是getter还是option
  if(isFunction(getterOrOption)){
    getter = getterOrOption
    setter = () => console.error('错误, 因为是getter只读, 不能赋值')
  }else{
    getter = getterOrOption.get
    setter = getterOrOption.set
  }
  return new ComputedRefImpl(getter, setter)
}

// In ref.ts

// 检查值是否为一个 ref 对象。
export function isRef(ref: any) {
  return !!(ref && ref.__v_isRef)
}
// 如果参数是一个 ref,则返回内部值,否则返回参数本身。这是 val = isRef(val) ? val.value : val 的语法糖函数。
export function unref(ref: any) {
  return isRef(ref) ? ref.value : ref
}
// 通常用在vue3 template里面ref取值,在template里面不需要.value就可以拿到ref的值
export function proxyRefs<T extends object>(obj: T){
  return isReactive(obj)
    ? obj
    : new Proxy<any>(obj, {
    get(target, key){
      // unref已经处理了是否ref的情况所以我们不需要自己if处理,如果是,返回.value,如果不是,直接返回值
      return unref(Reflect.get(target, key)) 
    },
    set(target, key, value){
      // 因为value为普通值类型的情况特殊,要把value赋值给ref的.value
      if (isRef(target[key]) && !isRef(value)) {
        target[key].value = value
        return true
      } else {
        return Reflect.set(target, key, value)
      }
    }
  })
}