Vue3 → computed计算属性的实现

64 阅读4分钟

回顾

effect 函数

主要作用是用来注册副作用函数,同时也允许指定一些选项参数options,例如指定scheduler调度器来控制副作用函数的执行时机和方式;还有像收集依赖的track函数和触发依赖(副作用)的trigger函数;
但是常规的effect函数会立即执行传递给他的副作用函数,有时需要进行进行懒执行的操作,可以通过options进行相关的配置来实现,如options.lazy配置实现懒执行``

参考文献

Vue3 响应式 effect 拓展
Vue3 响应式 reactive、ref、effect 实现浅析

computed 计算属性

功能概述

接受一个 getter 函数,返回一个只读的响应式 ref对象。该 ref 通过 .value 暴露 getter 函数的返回值。它也可以接受一个带有 get 和 set 函数的对象来创建一个可写的 ref 对象。

  • 创建一个只读的计算属性 ref
const count = ref(1)
const plusOne = computed(() => count.value + 1)

console.log(plusOne.value) // 2

plusOne.value++ // 错误

  • 创建一个可写的计算属性 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

  • 内部逻辑

    • computed会对fn进行缓存,只有内容发生变化了,且调用了computed的返回值.value才会去执行fn
      • 需要通过.value来进行访问,可以用class类对valuegetset进行代理拦截
      • 依赖收集还是在get中进行收集,触发可以通过effect class中的scheduler配置中触发;
      • 缓存问题可以通过flag来实现
  • 需求分析

    • computed接收一个fn函数,且初始化时不执行,最后函数返回一个带有.value的对象
    • 第一次调用.value,fn会执行一次,等后续调用不会再执行
    • 改变fn依赖的响应式对象的值时,fn还是不执行
    • 当再次调用.value的时候,fn会再次执行
      懒加载和缓存的特性

computed会对fn进行缓存,只有内容变化了,且调用了computed的返回值的.value的是才会去执行fn

  • 使用方式
    • 传入一个fn或是自定义的getset,返回一个对象,并且需要使用.value来调用里面的内容

测试用例

import { computed } from "./computed";
import { reactive } from "./reactive";

describe('computed', () => {
    it('happy path', () => {
        // 类似ref  采用.value获得值
        // 具有缓存功能

        const user = reactive({
            name: 'Lbxin'
        })

        const name = computed(()=>{
            return user.name
        })

        expect(name.value).toBe('Lbxin')
    });

    it('should computed lazily', () => {
        const user = reactive({
            name: "Lbxin"
        })

        // jest.fn 由于用到了 computed的get属性  需要对getter进行验证 所以用jest.fn进行操作
        const getter = jest.fn(()=>{
            return user.name
        })

        const name = computed(getter)
        expect(getter).not.toBeCalled()

        // name.value触发get操作  进而getter被触发 只不过是有缓存
        expect(name.value).toBe('Lbxin')
        expect(getter).toBeCalledTimes(1)

        name.value  // 再次触发 get操作  computed具有缓存功能 getter 还是被调用了一次
        expect(getter).toBeCalledTimes(1)

        user.name = "Lbxin12" //响应式对象在set的时候回触发set中的trigger -> effect -> get 重新执行
        expect(getter).toBeCalledTimes(1)

        expect(name.value).toBe('Lbxin12')
        expect(getter).toBeCalledTimes(2)

        user.name = "Lbxin12" //响应式对象在set的时候回触发set中的trigger -> effect -> get 重新执行
        expect(getter).toBeCalledTimes(2)

    });
});

内部实现

import { createDep } from "./dep";
import { ReactiveEffect } from "./effect";
import { trackRefValue, triggerRefValue } from "./ref";

export class ComputedRefImpl {
  public dep: any; // 收集getter的依赖
  public effect: ReactiveEffect;

  private _dirty: boolean;
  private _value

  constructor(getter) {
    this._dirty = true;
    this.dep = createDep();
    // 实例化ReactiveEffect对象 当触发响应式对象的getter的时候 会将getter作为依赖收集起来 - 同时解决了设置依赖值时找不到依赖的问题
    this.effect = new ReactiveEffect(getter, () => {
      // scheduler
      // 只要触发了这个函数说明响应式对象的值发生改变了
      // 那么就解锁,后续在调用 get 的时候就会重新执行,所以会得到最新的值
      if (this._dirty) return;

      this._dirty = true;
      // 触发依赖 triggerEffect(this.deps)
      triggerRefValue(this);
    });
  }

  get value() {
    // 收集依赖
    trackRefValue(this);
    // 锁上,只可以调用一次
    // 当数据改变的时候才会解锁
    // 这里就是缓存实现的核心
    // 解锁是在 scheduler 里面做的

      // 当value被更改的时候 期望this._dirty需要改为true - 
      // 在依赖的响应式对象的值发生改变时  需要依赖于 effect 
    if (this._dirty) {
      // 触发过一次就可以
      this._dirty = false;
      // this._value =  this._getter() 不能采用这种方式进行重新计算 无法收集依赖  而是通过收集的依赖中的run方法间接执行
      // 这里执行 run 的话,就是执行用户传入的 fn
      this._value = this.effect.run(); //在调用run的时候会重新执行缓存的依赖fn 即传入的getter
    }
    //实现缓存

    return this._value;
  }
}

export function computed(getter) {
  return new ComputedRefImpl(getter);
}

参考文献

实现mini-vue -- computed