Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
HardenSG opened this issue Apr 3, 2023 · 0 comments
Open

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

HardenSG opened this issue Apr 3, 2023 · 0 comments

Comments

@HardenSG
Copy link
Owner

HardenSG commented Apr 3, 2023

What's up man!

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

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

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

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

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

1. 复盘

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

如果没看,可以去看一下哈:通过实现最简reactive,学习Vue3响应式核心 - 掘金

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

  • reactive函数要接收复杂类型,对于普通类型则会直接返回。
  • reactive函数会返回一个proxy实例对象,与此同时会对这个proxy进行缓存。
  • 在创建proxy的时候会对所代理的源对象创建gettersetter的拦截器,在进行获取或者修改的操作的时候就会触发拦截器。
  • getter拦截器中要收集状态的依赖(副作用函数),通过track函数将其存储起来,存储依赖的数据结构要理解并记住
  • setter中通过trigger函数触发依赖的重新执行,更新视图(后续我们还要知道这一步会进行复杂的处理,不定期还会再写关于这方面的内容)

依赖就是一个函数,被称为副作用函数,简单理解一下就是:对数据进行依赖,数据变化就会产生副作用这是符合我们的思维模式的。就像:我饿了,要吃饭。饿了就是状态变更,要吃饭就是我所体现出的副作用行为

  • 由于状态的获取会触发gettergetter又会调用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关键字定义的实例对象的getset拦截器去拦截操作,那也不是像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函数的实现要依赖于getset

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用来存储原始值,在检查值是否修改的时候会用到

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

  • 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
  • 如果是第二种的传入方式:传入一个有着getset属性的对象那么这就允许你做出更改

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

refRefImpltoRefObjectRefImpl,那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来判断是否需要进行转换的。那么shallowreadonly怎么判断?不就是去看看这个新值上有没有__v_isShallow__v_isReadonly属性吗!现在明白了吗?

2. Scheduler

在构造函数创建effect副作用函数的时候我们有一个scheduler属性,我们挖了一个坑说如果没看懂等下一起说,那现在来填坑!

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

明确computed函数如果调用的时候是一个函数传参那他就是一个不可更改的computed。那么对于不可修改的computed来说还能在setter中触发trigger吗?显然不行

其次computed的官方名称叫做计算属性,也就是说往往是去依赖某些状态,在那些状态变更的时候重新执行对应的依赖,那状态变更的时候就会执行effect,也就是ComputedRefImpleffect属性,那你说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依赖执行完毕之后再执行普通的副作用依赖,这就是fixbug。我看了一下Evan You的提交记录是这样写的

附上与之相关联的issuegithub.com 看完你就明白了,我就不多赘述了

4. 完整代码

  1. RefshallowRef
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. toReftoRefs
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()
    }
}

写在后面

在这一小节,完善了refref相关的shallowReftoReftoRefs函数的创建,简单剖析了一下原理

此外还完善了computed函数的创建以及为了完善computed需要对effect部分的trigger函数做出一定的修改,与此同时还引出了fix的一个issue,这些都是推动技术不断完善的外驱力

补全了ref函数和computed函数的原理之后,Vue3/core/reactivity的核心原理部分就差不多结束了

结束了响应式核心之后我们接下来要迎接的就是runtime了,不过暂时时间不算充足,我一点点的写,也欢迎家银们点个关注以免到时候找不到我了哈哈哈哈😎❤️

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant