都用过ref和computed,但你懂它的原理吗?

  • Post author:
  • Post category:其他


What’s up man!

在上一节我们通过实现一个简易的

reactive

函数,大致了解了Vue3响应式处理的操作,尽管对源码的还原度没有那么高,比如在源码中对数组的方法包裹等。但是对于我们了解Vue的处理和响应式操作来说是没有问题的,更何况这就是对源码的核心逻辑的抽离,让我们读起来更加的通俗易懂。

还有就是我的水平也有限😭,完全的复现 Vue3 源码难度较大;

其次我认为大量边缘条件处理反而对于学习来说意义性并不是那么大。

那么这一小节,我们将继续补全上节没有说到的另外两个响应式API:

ref

函数和

computed

函数。

注: 这小节的编写参考了源码的方式(不过还是有出入的),并对一些晦涩的地方给出了一些解释,全篇大概7900字左右,建议先阅读一下上一篇,在阅读了上一篇的基础之上理解这一篇会更快一些



1. 复盘

在此之前,让我们先对上一节讨论的内容做一个小小的回忆:

如果没看,可以去看一下哈:

通过实现最简reactive,学习Vue3响应式核心 – 掘金

要实现响应式操作就要用到我们常用的响应式API,如

reactive

函数


  • reactive

    函数要接收复杂类型,对于普通类型则会直接返回。

  • reactive

    函数会返回一个

    proxy

    实例对象,与此同时会对这个

    proxy

    进行缓存。
  • 在创建

    proxy

    的时候会对所代理的源对象创建

    getter



    setter

    的拦截器,在进行获取或者修改的操作的时候就会触发拦截器。


  • getter

    拦截器中要收集状态的依赖(副作用函数),通过

    track

    函数将其存储起来,存储依赖的数据结构要理解并记住


  • setter

    中通过

    trigger

    函数触发依赖的重新执行,更新视图(后续我们还要知道这一步会进行复杂的处理,不定期还会再写关于这方面的内容)

依赖就是一个函数,被称为副作用函数,简单理解一下就是:对数据进行依赖,数据变化就会产生副作用这是符合我们的思维模式的。就像:

我饿了,要吃饭。饿了就是状态变更,要吃饭就是我所体现出的副作用行为

  • 由于状态的获取会触发

    getter



    getter

    又会调用

    track

    函数存储函数和对象间的关系,因此在执行

    effect

    函数的时候要对当前的

    effect

    函数用变量保存起来,因为这样才能在

    track

    函数中访问到,才能维护依赖和状态间的关系。
  • 状态变更会触发

    setter

    ,在

    setter

    中会通过调用

    trigger

    函数触发源对象的依赖集合的执行,这样就能更新视图,实现响应式操作。

那么事不宜迟开启我们今天的内容



2. Ref函数


ref

函数也是用于创建响应式数据的API,与

reactive

不同的是

ref

函数可以接收普通类型的数据,尽管

ref

函数也仍然可以处理复杂类型的数据,不过这并不是一个好的选择。



1. 区别


  1. ref

    函数既可以创建原始基础类型的响应式数据也可以创建复杂类型的响应式数据

  2. reactive

    函数中对响应式的操作是重写

    proxy

    拦截器,而在

    ref

    中实现拦截操作的却与之不同

  3. ref

    函数创建的响应式的数据被处理为一个有着

    value

    属性的值,因此访问值需要

    state.value

    这样才能获取到值

  4. ref

    函数实际上返回的是一个新的对象,而

    reactive

    函数返回的其实是这个源对象的代理

记不住或者看蒙了不要紧,我们在后面的实现中,会一步步的对上面的内容进行解释



2. 理论准备

在此之前再来回忆下

ref

的使用

const age = ref(18)
const state = ref({ age: 18 })
console.log(age.value, state.value.age) // 18, 18

我们看到

ref

函数是可以对复杂类型做响应式的,这个原理在

官网-ref响应式核心

中 写出

如果将一个对象赋值给 ref,那么这个对象将通过 reactive() 转为具有深层次响应式的对象

其实就是说:如果处理的是个复杂类型的数据,那响应式的处理还是交给

reactive

函数

实现一个

ref

函数和

reactive

函数的思路并不相同,实际上

ref

函数的实现是和vue2相似的。

对你没看错,在

ref

函数中的响应式是依靠于

class

关键字定义的实例对象的

get



set

拦截器去拦截操作,那也不是像vue2那样使用的

defineProperty

啊?


class

关键字是ES6推出的,在我们的项目中会babel转义为es5代码的,那对象的get和set不就是在

defineProperty

中的操作吗

所以

ref

的实现你可以说是借助了

defineProperty

,这点要区分一下不能混。

That’s all 那我们来动手实现吧



3. 实现

function ref(value) {
     判断是否已经是一个ref了,如果是那就直接返回就好了
    if(isRef(value)) {
        return value
    }
     创建ref的实例对象
     因此ref函数返回的其实是一个有着value属性的实例对象
    return new RefImpl(value)
}

 其实就是判断传入的数据是否有这个属性标识
 下面的RefImpl类就有这个属性
function isRef(val) {
    return !!(val && val['__v_isRef'])
}

构造

RefImpl

类,我们响应式的核心就在这一模块

上面已经铺垫了,

ref

函数的实现要依赖于

get



set

class RefImpl {
     _value用来代理传入的值操作
    public _value
     _rawValue 用来存储原始值
    public _rawValue
     标明这个状态对象是一个ref
    public __v_isRef = true
    constructor(value) {
        this._rawValue = toRaw(value)
        this._value = toReactive(value)
    }
    
    get value() {
         这里要触发函数存储依赖
        return this._value
    }
    
    set value(newValue) {
        if(状态改变) {
             重新执行依赖
             修改代理的值
        }
    }
}

基础的逻辑就是这样的


  • _value

    用来做我们传入值的代理;

  • _rawValue

    用来存储原始值,在检查值是否修改的时候会用到

在继续补全之前先来了解两个函数:

toRaw



toReactive


  • toRaw

    :如果看过Vue的官网,那应该知道这个API,

    响应式 API:进阶 | Vue.js

    。简言之就是返回响应式对象的原始对象

  • toReactive

    :这并不是一个导出的函数,不过你翻看源码

    vuejs/core/packages/reactive

    差不多第400行的位置就能看到这个函数,

先来实现这两个函数,再继续实现

RefImpl



1.

toRaw

/**
* toRaw
*/
function toRaw(observed) {
     用来检查是否是一个响应式的对象
    cons raw = observed && observed["__v_raw"]
     如果是一个响应式的对象则继续递归该函数
     否则返回这个传入值即可
    reetur raw ? toRaw(raw) : observed
}
  1. 函数作用:返回一个数据的原始数据。

  2. 举个例子:

    1. 如果是个普通类型如字符串就直接返回这个字符串
    2. 如果是个普通对象也还是返回这个对象即可(因为并没有这个

      '__v_raw'

      属性)
    3. 如果是个响应式对象,那么

      raw

      变量就一定能拿到值,这个值就是响应式对象的原始值!

你可能会好奇这个

'__v_raw'

标识是什么,这是Vue3中用来标记响应式对象的标识,所以如果这个传入的数据拥有这个属性那么这不就意味着这是一个响应式的对象我们仍然需要递归拿到它的原始数据嘛!

OK!get it!!

  • 实际上这个标识在源码中并不是这样零零散散的,这是一个枚举:

    ReactiveFlags

    ,别担心,这并不影响你理解
  • const enum ReactiveFlags {
      SKIP = '__v_skip',
      IS_REACTIVE = '__v_isReactive',
      IS_READONLY = '__v_isReadonly',
      IS_SHALLOW = '__v_isShallow',
      RAW = '__v_raw'
    }
    



2.

toReactive

这个函数更简单!理论准备部分已经讲了:

ref

函数既可以处理普通类型也可以处理复杂类型,这个原理就是要判断传入的数据是否是一个对象,如果是对象那就使用

reactive

函数进行包裹,如果不是那就返回这个值即可。所以这个

toReactive

函数的目的就是判断是否是一个对象并进行转化的操作。

```
function toReactive(value) {
    return isObject(value) ? reactive(value) : value
}
```

好了!了解完上面的两个函数之后就可以继续进行下一步了。不过你肯定好奇为啥一定要实现这两个函数,别急这里就用上了。

  1. 存储_rawValue:

    toRaw



    RefImpl

    的构造函数中传入的这个

    value

    数据可能是个普通类型也可能是个复杂类型,也可能是个已经被代理的对象。但不论是什么类型,

    toRaw

    函数都能找到它的原始数据,然后将这个原始数据赋给

    _rawValue

    ,这样在后续的比对数据是否更改的时候就能使用这个

    _rawValue

    进行判别了

  2. 存储_value:

    toReactive

    在构造函数中调用

    toReactive

    函数,

    toReactive

    函数会返回一个

    proxy

    或者是原始的数据(如果没看懂,请回看一下

    toReactive

    函数实现的解释),然后将其赋给

    _value

    用作数据的代理,后续的操作就会直接使用

    _value



3. 依赖收集

和先前的一样,在

get

拦截器中调用

track

函数收集依赖函数

get value() {
     收集依赖
    track(this, 'get', 'value')
     返回代理的值
    return this._value
}    

实际上这里这样写是和最新版的源码是有出入的,在源码中函数功能的粒度要更细一些,比如收集依赖的时候会分为

track

函数和

trackEffects

函数。

调用的的收集函数也是有出入的,在

ref

源码中收集依赖调用的是

trackEffects

函数并传递的是一个通过调用

createDep

函数创建的

dep

集合,在这个

createEffects

函数中会对

dep

这个集合添加

activeEffect

也就是当前正在执行的

effect

,其实原理是一样的。最核心的区别不过就是源码是依靠

ReactiveEffect

这个类做的副作用依赖而我们是采用函数因为会更简单一些!

这里只是顺带提一嘴,不用纠结和源码的出入,咱们这么写就是最简就是最容易理解的!



4. 变更执行

显然的,状态变更触发依赖的重新执行要调用

trigger

函数,在哪里调呢?就是在

set

拦截器中。

set value(newVal) {
    if(hasChange(this._rawValue, newVal)) {
        this._rawValue = toRaw(newVal)
        this._value = toReactive(newVal)

        trigger(this, 'set', 'value')
    }
}

在构造函数中存储的原始值

_rawValue

在这里派上了用处,我们要用它来比对是否更改

不过在这里还是有些小问题:

  • 是否需要直接调用

    toReactive

    函数进行转换,以及存储

    _rawValue

    是否需要调用

    toRaw

    进行转换,在这里还需要额外的处理。
  • 需要对是否转换进行一个判定,源码中称它为

    useDirectValue

    直接翻译过来就是:使用直接的值吗,或者说成是否直接使用值,这个值就是你修改传入的值。然后基于这个

    useDirectValue

    进行判定

为什么需要转换的判定呢?

我猜一定有小伙伴会疑惑,就像我第一眼看到一样

因为有些时候传入的值就是一个普通的类型那你进行

toReactive

转换或者

toRaw

转换就没有意义了

还有的时候你传入的是浅层或者只读的响应式对象,那么也仍然是不需要处理(为什么呢?在下面的判定条件部分解释了)

还有的时候你传入的是一个普通的响应式对象,那就需要处理了,需要对响应式对象进行转化。

  1. 那判定的条件是什么呢?

    • 判断的条件就是判断这个新更改的值是否是一个浅层代理或者只读的响应式对象,如果是那就不需要进行转化,这是为啥呢?
    • 因为对于非浅层代理和非只读的响应式对象来说,我们需要对其进行深度观测,以便在对象的某个属性发生变化时能够触发响应式更新。而浅层响应式对象和只读响应式对象是特殊的响应式对象,并不需要深度观测也不需要响应式更新。

好了那优化一下上面的代码

set value(newVal) {
    const useDirectValue = 
        isShallow(newVal) || isReadonly(newVal)

     是否转换该值  
     如果不是shallow或者readonly那就有可能是个响应式对象或者其他的类型
     不管是什么就直接调用toRaw拿到它的原始值
    newVal = useDirectValue ? newVal : toRaw(newVal)

    if(hasChange(this._rawValue, newVal)) {
        this._rawValue = newVal
         进行是否转换的校验
        this._value = useDirectValue ? newVal : toReactive(newVal)

        trigger(this, 'set', 'value', newVal)
    }
}

 前面说过了这两个标识是什么,这是ReactiveFlags的枚举属性
 如果是浅层代理的对象或者只读代理的对象都分别会有属于自己的标识
function isShallow(value) {
    return !!(value && value['__v_isShallow'])
}

function isReadonly(value) {
    return !!(value && value['__v_isReadonly'])
}



4. 其他的Ref API

说完了

ref

函数的基本构造,我们来补充和

Ref

相关的API,分别是

shallowRef、toRef、toRefs



1. 理论准备


  1. shallowRef

    像前一章写过的

    shallowReactive

    一样,

    shallowRef

    也是一个浅层的代理,只对

    .value

    做出响应式处理

  2. toRef


    官网的解释

    :基于响应式对象上的一个属性,创建一个对应的

    ref

    响应式对象。这样创建的

    ref

    与其源属性保持同步:改变源属性的值将更新

    ref

    的值,反之亦然。

  3. toRefs


    官网的解释

    :将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的

    ref

    。每个单独的

    ref

    都是使用

    toRef()

    创建的。

我们用代码来解释要更为简单一些

 shallowRef
const state = shallowRef({ age: 18 })
state.value.age = 21  不更新,因为只能对value做出响应式因为这是一个浅层的代理
state.value = { age: 21 }  更新

 定义reactive响应式对象
const state = reactive({
    name: 'SG',
    age: 18
})

 toRef
const age = toRef(state, 'age')  对age的操作同样会映射到state上,与源属性是保持同步的
const name = toRef(state, 'name')

 toRefs
const { age, name } = toRefs(state)  这样要方便得多,这个函数是基于toRef实现的



2. 实现本体



1.

shallowRef


shallowRef

其实就是浅层代理,并不会对数据进行深层观测代理,看到这句话想没想到上面说的那个

useDirectValue

判定条件?那个不就是判断是否是一个浅层代理或者一个只读的属性的吗,如果它是一个

true

那就不会对值进行深度检测,那自然就是一个

shallowRef

在编写

shallowRef

的同时我也来重构一下上面的

ref

创建的代码,使其更像源码,顺便就把

shallowRef

的创建解释一下

function ref(value) {
     调用createRef函数,传入value,第二个参数是标明是否是一个shallow代理
     对于ref传入false即可
    return createRef(value, false)
}

 用于创建RefImpl实例对象,这就是ref响应式的核心
function createRef(value, shallow) {
    if(isRef(value)) {
        return value
    }

    return new RefImpl(value, shallow)
}

class RefImpl {
     _value用来代理传入的值操作
    public _value
     _rawValue 用来存储原始值
    public _rawValue
     标明这个状态对象是一个ref
    public __v_isRef = true
    constructor(value, __v_isShallow) {
         改动点一:
          使用__v_isShallow进行判断
          如果是个shallow那就没必要进行转换了
        this._rawValue = __v_isShallow ? value : toRaw(value)
        this._value = __v_isShallow ? value : toReactive(value)
    }

    get value() {
        track(this, 'get', 'value')
        return this._value
    }

    set value(newVal) {
         改动点二:
          如果是个shallow那就也不需要转换了
          因为并不需要深度检测与更新
        const useDirectValue = 
            this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)

        newVal = useDirectValue ? newVal : toRaw(newVal)
        if(hasChange(this._rawValue, newVal)) {
            this._rawValue = newVal
            this._value = useDirectValue ? newVal : toReactive(newVal)

            trigger(this, 'set', 'value', newVal)
        }
    }
}

有了

__v_isShallow

属性就可以用它去判断

useDirectValue

了,改动点在构造函数和set拦截器部分的注释部分

 shallowRef函数构造:
function shallowRef(value) {
     shallowRef是一个浅层代理,第二个参数标明这是一个shallow
    return createRef(value, true)
}


2.

toRef


toRef

函数的实现就略有不同了,但是明确

toRef

的作用:基于响应式对象上的属性创建

ref

并将更改同步映射到源对象上

直接引出实现:

ObjectRefImpl

,就不过多解释,代码解释就足矣了

function toRef(target, key) {
    const val = target[key]
     如果取得值是个ref那就直接返回就好了
     因为都已经是个ref响应式了还有啥必要性重新做ref嘛?
    return isRef(val) ? val : (new ObjectRefImpl(target, key))
}

 这个类实际上就是操作传递的target这个响应式对象的属性,
 因为在操作它的时候不论是读取还是set value 都会触发源响应式对象的拦截器
 那么就要考虑如何在修改通过toRef创建的ref对象的时候去同步源对象了
 这就利用了ObjectRefImpl这个实例对象的get和set拦截器
 在对toRef创建出的ref响应式对象进行get或者set的时候都会触发这个ObjectRefImpl的拦截器
 看下我们写的拦截器,都是操作的源响应式对象
 那么自然也会直接的触发了源响应式对象的拦截器了

class ObjectRefImpl {
  public readonly __v_isRef = true

  constructor(
    private readonly _object,
    private readonly _key
  ) {}

  get value() {
    return this._object[this._key]
  }

  set value(newVal) {
    this._object[this._key] = newVal
  }
}


3.

toRefs


toRefs

就简单多了,就是利用了

toRef

函数,如果你理解了

toRef

,那么

toRefs

其实就是一个循环的事情

function toRefs(target) {
     其实就是遍历这个传入对象的所有属性并调用toRef函数
    const res = isArray(target) ? (new Array(target.length)) : {}

    for(const k in target) {
        res[key] = toRef(target, key)
    }

    return res
}

这样就是

toRefs

函数的实现,其实就是循环调用

toRef

函数返回的都是一个

ref

对象它的值就是这个响应式对象的对应的属性值,因此这个res结果的每个属性都是一个

ref

对象,因此你解构取值也依然会存在响应式

所以如果再有人问你为什么

toRefs

函数解构出属性的时候仍然会保留响应式,你就告诉他因为

toRefs

函数对源对象的每个属性都创建了一个

ref

对象,因此你解构出来的是这个

ref

对象并不是源响应式对象的属性!(如果他还听不懂让他来看这篇🤣🤣🤣)




3. computed函数


响应式 API:核心 | Vue.js

如果你还没有使用过

computed

可以先看下官网的描述

先简单演示一下

computed

的使用

const state = reactive({ age: 18 })

 第一种方式
const aged = computed(() => {
    return state.age + "岁了~"
})
console.log(aged.value)  18岁了~

 第二种方式
const aged = computed({
    get: () => {
        .....
    }, 
    set: (v) => {
        ....
    }
})
console.log(aged.value)
aged.value = 21

可以看出

computed

的值也是需要通过

value

属性获取的,所以

computed

返回的也是一个

ref

对象

  • 如果是第一种的传入方式:传入一个

    getter

    ,那么这就是一个只读的

    computed
  • 如果是第二种的传入方式:传入一个有着

    get



    set

    属性的对象那么这就允许你做出更改



1. 特点

  1. 两种调用方式,会对值做出只读或者读写都可的限制
  2. 依靠于响应式状态,状态变更

    computed

    重新执行
  3. 如果依靠的状态没有改变且多次调用

    computed

    的变量,

    computed

    不会重复执行返回值来源于缓存



2. 理论准备

对于

computed

函数的创建和正常运行是会涉及到

effect

部分,如果有些遗忘或者还没看过上一篇的内容建议先看一看

通过实现最简reactive,学习Vue3响应式核心 – 掘金 (juejin.cn)

  • 针对

    computed

    要实现的点

    • 状态变更重新执行
    • 缓存上次执行的结果,多次调用返回缓存值

v3和v2中的

computed

函数的实现有很大的出入,这点知道就好并不需要去翻源码,因为我刚看了一遍源码中2和3的写法,我认为3比2要容易懂得多。如果感兴趣也可以去看看

vue2/computed

&

vue3/computed



3. 实现



1.

ComputedRefImpl


ref



RefImpl



toRef



ObjectRefImpl

,那

computed

也得有个处理的类呀!

它就叫

ComputedRefImpl

,不戳不戳!根据名字就很容易知道它是干啥的

看起来不难,所以就直接边写边解释吧

function computed(getterOrOptions) {
    let getter
    let setter

     如果是个函数,那就符合只读的调用方式
     那么他就是一个不可以进行更改的computed
    const onlyGetter = isFunction(getterOrOptions)

    if(onlyGetter) {
        getter = getterOrOptions
        setter = () => {
            console.warn("Write operation failed: computed value is readonly")
        }
    } else {
        getter = getterOrOptions.get
        setter = getterOrOptions.set
    }

     创建实例
    return new ComputedRefImpl(getter, setter, onlyGetter)
}
class ComputedRefImpl {
     存储computed函数返回的value值,其实就是effect函数返回值
    public _value
     存储副作用函数,副作用函数需要我们在构造函数中手动创建以达到响应式
    public effect
     标识这个computed的对象是一个ref
    public readonly __v_isRef = true
     是否只读这个属性需要依靠于computed函数的调用方式
    public readonly __v_isReadonly = false

    constructor(
        getter,
        public setter,
        isReadonly
    ) {
         在这里就需要创建副作用函数
         在所依赖的状态发生变化的时候触发这个副作用函数的重新执行
        this.effect = effect(getter, {
             lazy属性标明不立即执行该effect函数而是等待调用的时候才执行
            lazy: true,
            scheduler: () => {
                 scheduler你可以理解为调度器
                 我们在执行effect函数的时候触发调度器的执行
                 这样就能在状态变更同时也更新视图了
                 你可能对此还有疑问为什么要这样写,别急等下一起解释
                trigger(this, 'set', 'value')
            }
        })

         注明这个effect是一个computed的依赖,并且这个值同时指向这个computed对象
        this.effect.computed = this
         根据传递的参数确定是否为只读
        this['__v_isReadonly'] = isReadonly
    }

    get value() {
         重新执行函数获取最新值
        this._value = this.effect()
         储存依赖关系
        track(this, 'get', 'value')
        return this._value
    }

    set value(v) {
        this.setter(v)
    }
}

你可能觉得这么多个属性都有个啥子用啊!像

__v_isRef

,

__v_isReadonly

啥的感觉都没啥用啊!

其实不然,

ReactiveFlags

标识能够帮助

Vue

更好的管理响应式状态。举个例子在

ref

函数中我们在确定是否要对

setter

中的新值进行转换的时候构造出来了

useDirectValue

这个变量,它是根据对象本身是否是一个浅代理或者修改的值是否是一个

shallow

或者

readonly

来判断是否需要进行转换的。那么

shallow



readonly

怎么判断?不就是去看看这个新值上有没有

__v_isShallow



__v_isReadonly

属性吗!现在明白了吗?



2.

Scheduler

在构造函数创建

effect

副作用函数的时候我们有一个

scheduler

属性,我们挖了一个坑说如果没看懂等下一起说,那现在来填坑!

为什么要这样写?为什么要写在这里面?

明确

computed

函数如果调用的时候是一个函数传参那他就是一个不可更改的

computed

。那么对于不可修改的

computed

来说还能在

setter

中触发

trigger

吗?显然不行

其次

computed

的官方名称叫做计算属性,也就是说往往是去依赖某些状态,在那些状态变更的时候重新执行对应的依赖,那状态变更的时候就会执行

effect

,也就是

ComputedRefImpl



effect

属性,那你说

trigger

触发写在别的地方能行吗?显然不行,现在理解了不?


好了那继续进行下一步为什么要把

scheduler

单独拿出来当作一个小节?

首先上面的代码是可以作为

computed

正常运行的,但是你一试就会发现:不是说好在状态不变的时候读取缓存的值不重新执行

effect

的嘛!

这就是我们当前的不足,如何改进呢?

我们需要一个标志,标识所依赖的状态没有改变的时候就取出缓存值,在依赖改变的时候再将标识改为“改变” 然后重新执行依赖获取新值,这个标志就是

ComputedRefImpl

的一个属性叫做

_dirty

这个dirty意译就是脏,这个属性其实在很多类库的源码中都有它的身影,你可以简单理解一下,如果一个数据是脏的那就要换新的对不对!如果不是脏的也就是没有更改,那自然这个缓存的数据就可以继续作为值返回喽,好理解吧

因此我们重新写一下

ComputedRefImpl

class ComputedRefImpl {
    public _dirty = true
     只写主要逻辑部分
    .....
    get value() {
        if(this._dirty) {
             执行完就立即切换为false,这样后续的获取就一定会从缓存中取值不会重新执行依赖
            this._dirty = false
            this._value = this.effect()
            
             存储依赖关系
            track(this, 'get', 'value')
        }
        
        return this._value
    } 
    .....
}

好极了!这样以后无论数据变不变都不会再重新执行了!?🤔🤔

因为你需要在数据变化的时候重新将

_dirty

变为脏的,上面说过了就在

Scheduler

中去操作!好了再来一遍!!

class ComputedRefImpl {
     除了constructor之外都不需要变化因此就写这个,其他的就先省略掉
    constructor(
        getter,
        public readonly setter,
        isReadonly
    ){
        this.effect = effect(setter, {
            lazy: true,
            scheduler: () => {
                if(!this._dirty) {
                     如果是脏的
                    this._dirty = true
                     触发依赖的重新执行
                    trigger(this, 'get', 'value')
                }
            }
        })
    }
}

这样在

getter

中所依赖的响应式状态变更的时候就能通过执行

scheduler

来修改阈值并且触发依赖的重新执行

不过还是有点问题!在依赖变更执行

effect

的时候要执行

scheduler

,咱们之前实现的

trigger

重执行函数的时候好像没处理这个吧!

所以要简单的修改一下



3.

Trigger

函数修改

至于trigger函数是咋做的就不想再提了,如果忘记的可以去再看一下上一篇的trigger实现思路:

通过实现最简reactive,学习Vue3响应式核心 – 掘金

我就废话不多说直接修改

先来看看原先的

trigger

函数是什么样子的

 我们原先的依赖执行是这样的
function trigger(target, type, key, value, oldValue) {
    .....
     前面的内容就是为了获取对应的依赖集合,不是重点先省略掉了
    effects.forEach(effect => {
         调用执行
        effect()
    })
}

改动版本:

function trigger(target, type, key, value, oldValue) {
    .....
     前面的内容就是为了获取对应的依赖集合
    
     这个effects是依赖集合 Set
    triggerEffects(effects)
}

function triggerEffects(dep) {
    const effects = isArray(dep) ? dep : [...dep]
    
    for(const effect of effects) {
        if(effect.computed) {
             如果有这个属性那就是一个计算属性的依赖
             回看一下ComputedRefImpl的创建
             可以看到在构造函数中我们将自身(this)赋予了computed属性
            triggerEffect(effect)
        }
    }
    
    for(const effect of effects) {
        if(!effect.computed) {
             没有这个属性那就是一个普通的响应式依赖函数执行
            triggerEffect(effect)
        }
    }
}

function triggerEffect(effect) {
    if(effect.options.scheduler) {
         我们先前说的scheduler是位于effect的options里面的
         不过源码中并不是在options中的,而是作为effect它的直接属性
         不过没关系核心逻辑都是一样的
        effect.options.scheduler()
    } else {
         普通的effect直接执行就好了
        effect()
    }
}

不过就是新增了一个执行

effect.options.scheduler

的事情,又写

triggerEffects

又写

triggerEffect

的不会很麻烦吗?这其实是

fix

的一个

bug

我们能看到在

triggerEffects

中是让

computed

的依赖先执行的。等所有的

computed

依赖执行完毕之后再执行普通的副作用依赖,这就是

fix



bug

。我看了一下Evan You的提交记录是这样写的

附上与之相关联的

issue



github.com

看完你就明白了,我就不多赘述了



4. 完整代码


  1. Ref



    shallowRef
function ref(value) {
    return createRef(value, false)
}

function shallowRef(value) {
    return createRef(value, true)
}

function createRef(value, shallow) {
    if(isRef(value)) {
        return value
    }

    return new RefImpl(value, shallow)
}


class RefImpl {
    public _value
    public _rawValue
    public __v_isRef = true
    constructor(value, __v_isShallow) {
        this._rawValue = __v_isShallow ? value : toRaw(value)
        this._value = __v_isShallow ? value : toReactive(value)
    }

    get value() {
        track(this, 'get', 'value')
        return this._value
    }

    set value(newVal) {
        const useDirectValue = 
            this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)

        newVal = useDirectValue ? newVal : toRaw(newVal)
        if(hasChange(this._rawValue, newVal)) {
            this._rawValue = newVal
            this._value = useDirectValue ? newVal : toReactive(newVal)

            trigger(this, 'set', 'value', newVal)
        }
    }
}

  1. toRef



    toRefs
function toRef(target, key) {
    const val = target[key]

    return isRef(val) ? val : ObjectRefImpl(target, key)
}

function toRefs(target) {
    const res = isArray(target) ? (new Array(target.length)) : {}

    for(const k in target) {
        res[key] = toRef(target, key)
    }

    return res
}

class ObjectRefImpl {
    public readonly __v_isRef = true
    constructor(
        private readonly _object,
        private readonly _key
    ){}

    get value() {
        return this._object[this._key]
    }

    set value(newVal) {
        this._object[this._key] = newVal
    }
}

  1. Computed
class ComputedRefImpl {
    public _value
    public effect
    public _dirty
    public readonly __v_isRef = true
    public readonly __v_isReadonly = false

    constructor(
        getter,
        public setter,
        isReadonly
    ) {
        this.effect = effect(getter, {
            lazy: true,
            scheduler: () => {
                if(!this._dirty) {
                    this._dirty = true
                    trigger(this, 'set', 'value')   
                }
            }
        })
        this.effect.computed = this
        this['__v_isReadonly'] = isReadonly
    }

    get value() {
        if(this._dirty) {
            this._value = this.effect()
            this._dirty = false
            track(this, 'get', 'value')
        }
        return this._value
    }

    set value(v) {
        this.setter(v)
    }
}


 更换原先的trigger函数
function trigger(target, type, key, value, oldValue) {
    const depsMap = targetMap.get(target)
    if(!depMap) return 

    const effects = new Set()
    const add = (effectAdd) => {
        effectAdd.forEach(effect => effects.add(effect))
    }

    if(key === 'length' && isArray(target)) {
        depsMap.forEach((dep, key) => {
            if(key === 'length' && key >= value) {
                add(dep)
            }
        })
    } else {
        if(key !== undefined) {
            add(depsMap.get(key))
        }

        switch (type) {
            case 'add': {
                if (isArray(target) && isIntegerKey(key)) {
                    add(depsMap.get('length'))
                }
                break
            }
        }
    }

    triggerEffects(effects)
}

function triggerEffects(dep) {
    const effects = isArray(dep) ? dep : [...dep]

    for(const effect of effects) {
        if(effect.computed) {
            triggerEffect(effect)
        }
    }

    for(const effect of effects) {
        if(!effect.computed) {
            triggerEffect(effect)
        }
    }
}

function triggerEffect(effect) {
    if(effect.options.scheduler) {
        effect.options.scheduler()
    } else {
        effect()
    }
}



写在后面

在这一小节,完善了

ref



ref

相关的

shallowRef



toRef



toRefs

函数的创建,简单剖析了一下原理

此外还完善了

computed

函数的创建以及为了完善

computed

需要对

effect

部分的

trigger

函数做出一定的修改,与此同时还引出了一个fix的issue,这些都是推动技术不断完善的外驱力

补全了

ref

函数和

computed

函数的原理之后,

Vue3/core/reactivity

的核心原理部分就差不多结束了

结束了响应式核心之后我们接下来要迎接的就是

runtime

了,不过暂时时间不算充足,我一点点的写,也欢迎家银们点个关注以免到时候找不到我了哈哈哈哈😎❤️

帖子首发于我的Github:

都用过ref和computed,但你懂它的原理吗?

欢迎您为我的仓库点个star,有更多内容都会及时更新!

如果对你有帮助,欢迎点赞、讨论、收藏、勘误!!