Vue3.0源码系列(七):响应式原理(computed)

上一篇:Vue3.0源码系列(六):响应式原理(ref,isRef,unRef)

vue源码分析系列github地址:github.com/zzq921/my-m…

大家好,今天是周六,上午去了雍和宫为家人祈福,人真的是超级多,为了求一个手链排队40分钟,真的是人麻了。回来倒头大睡,睁开眼就已经到了晚上6点啦。此时,我家的呆呆还在梦乡,睡醒感觉就是通透,舒爽,也该兑现上一篇的承诺,来为大家分享一下VUE3.0当中很重要的一个api,就是computed。computed在我们开发中可以说无处不在,我们在使用的时候,有没有想过,它的底层源码是怎么构建的?它的惰性缓存是怎么实现的那?好了,带着这些疑问,我们进入今天的主角computed底层源码的世界。

首先,我们按照惯例,用单元测试书写我们的要实现的computed的功能点,我们可以看到vue3的computed像比如vue2,书写方式放生了变化。我们需要为computed函数传入一个函数,即computed(fn),如果有多个computed,我们可以书写多次。

单元测试(1):当我们调用,用computed包裹的响应式函数时候,我们期待age.value返回为1。

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

describe('computed',()=>{
  it('happy path',()=>{
    const user = reactive({age:1})
    const age = computed(()=>{
      return user.age
    })
    //单元测试(1)
    expect(age.value).toBe(1)
  })
})
复制代码

这是computed最基本的功能,调用computed,返回我们所需要的值。那我们看看一下源码应该怎么书写?

首先,我们创建了computed函数,把我们的getter函数传入一个为computed构建的类,方便后边的功能做扩展。这样我们就生成了一个computed的类。通过调用.value我们很容易就返回了对应的值,实现了上面简单的单元测试(1)的功能。实现了最简单的computed。

import { ReactiveEffect } from "./effect"

class ComputedIml {
  private _getter: any //私有属性
  constructor(getter) {
   //getter它是我们computed传进来的的方法
    this._getter = getter
  }
  get value() {
     //当外部调用 .value时候,我们执行传进来的方法,就会把值返回出去
    return this._getter()
  }
}
export function computed(getter) {
  return new ComputedIml(getter)
}
复制代码

单元测试(2):当我们没有获取compute的value时候,我们希望传入computed里的函数getter是不执行的

单元测试(3):当我们调用cValue.value时候,执行getter函数,希望我们获取到值为2。

it('should coputed lazily',()=>{
    const user = reactive({
      foo:2
    })
    //通过jest.fn()模拟一函数,并且返回reactive的值
    const getter = jest.fn(()=>{
      return user.foo
    })
    const cValue = computed(getter)
    //单元测试(2)
    expect(getter).not.toHaveBeenCalled()
    //单元测试(3)
    expect(cValue.value).toBe(2)
    expect(getter).toHaveBeenCalledTimes(1)
 })
复制代码

可以看到,通过上面的源码,我们的单元测试(2)和(3)也是可以通过的,通过这两个测试我们可以知道,只有当我们触发competed的获取value时候,我们的getter方法才会触发。才会重新计算返回我们所需要的值。

单元测试(4):当我们再次获取 cValue.value,也就是触发get的时候,我们期望getter还是只执行一次。

单元测试(5):当我们响应式数据变化的时候,我们期望getter还是只执行一次。

单元测试(6):当我们再次调用cValue.value触发get的时候,我们获得最新值。并且getter函数再一次执行。

describe('computed',()=>{
  it('should coputed lazily',()=>{
    const user = reactive({
      foo:2
    })
    const getter = jest.fn(()=>{
      return user.foo
    })
    //单元测试(4)
    cValue.value
    expect(getter).toHaveBeenCalledTimes(1)
    //单元测试(5)
    user.foo = 3
    expect(getter).toHaveBeenCalledTimes(1)
    //单元测试(6)
    expect(cValue.value).toBe(3)
    expect(getter).toHaveBeenCalledTimes(2)

  })
})
复制代码

可以看到,通过单元测试(4),(5),(6)我们是想实现我们computed当中比较重要的功能,那就是惰性求值,即(4)当获取它的value值,也就是触发get的时候,我们的getter不发生变化,也就是不执行。(5)当我们响应式数据发生变化时候,我们可以看到,computed的getter还是没有执行。那么,现在就有人疑问了,到底什么时候我们的computed执行那?什么时候computed的值才能获取最新的值那?可以看(6),即当我们响应式数据发生变化基础上,我们页面或者逻辑中触发了求值的时候,即获取了cValue.value。这是我们的值变成最新的值,getter进行了重新调用。这就是我们computed里面的惰性求值,可以有效的提高我们页面性能。只有当页面需要时候,数据变化时候,才重新求值。

通过上面我们对computed的功能分析,我们脑子就是不是有了自己的实现逻辑那,那么我们就来带大家写一下源码,看看源码中是怎样实现的?基于上面的基础功能进行接下来的编写......,我们看下面的源码,有几点是我们要重点理解的

第一:_dirty这个属性,这是我们computed中的关,也是我们惰性求值的关键,通过这个属性控制我们是否执行getter来获取最新的值。

第二:_effect方法,当依赖的响应式数据发生变化的时候,我们会把_dirty开关打开。

第三:scheduler调度器fn(),它可以在合适的时机执行我们想要执行的逻辑。也就是当trigger动作触发依赖重新执行的时候,有能力去决定执行的时机和方式。

import { ReactiveEffect } from "./effect"

class ComputedIml {
  private _getter: any
  private _dirty: boolean = true
  private _value: any
  private _effect: any
  constructor(getter) {
    this._getter = getter
    //第二个参数就是调度器 scheduler,此逻辑在前面文章中有讲到,欢迎观看https://juejin.cn/post/7083073866980917284
    this._effect = new ReactiveEffect(getter,()=>{
    //默认_dirty为true,当数据发生变化,也就是trigger时候,我们传入调度器,把开关打开。当下一次调用computed的value时候,开关打开,重新执行,获取最新的值返回
      if(!this._dirty) {
        this._dirty = true
      }
    })
  }
  get value() {
  //默认_dirty为true,第一次时候进行执行,然后又把开关关闭。只有再次打开才会执行。
    if(this._dirty){
      this._dirty = false
      //执行run方法,进行触发依赖。将用户传进来的最新的值返回
      this._value = this._effect.run()
    }
    //如果开关关闭,无论调用几次,都返回之前的值。
    return this._value
  }
}

export function computed(getter) {
  return new ComputedIml(getter)
}
复制代码

最后总结一下,computed的实现其实很巧妙,它利用了dirty属性实现缓存,利用effect来实现值的更新,当我们第一次调用时候,dirty为true,返回值,这是我们就把dirty开关关闭。以后无论调用多少次,我们都不会调用传进来的getter了,只有当我们的响应式数据发生变化的时候,这是会触发trigger的的依赖,进行依赖触发,因为我们传入了调度器:scheduler,trigger中判断scheduler存在,就会执行其中的函数,把dirty的开关打开,同时不会触发里面的run逻辑,也就是值不会改变。当我们再次通过.value获取最新的值时候,开关已经打开,就可以执行里面的run方法,把里面的最新值返回啦。 通过上面单元测试的功能分析和源码分析书写,我们就完成了computed的全部功能,大家是不是觉得源码也炒鸡简单哪,如果你觉得学到了什么,欢迎点赞哈。

猜你喜欢

转载自juejin.im/post/7084579219862683685