第十四章 vue3中ref的原理,ref功能实现

117 阅读4分钟

vue3中ref的原理,ref功能实现

其实ref的实现也是很简单的,和reactive的思路是差不多的,但是要注意的点还是有的

1 ref 一般都针对单个值 怎么才能知道他被get 或 set 2 reactive 中的proxy又只是针对对象的,所以实现起来肯定还是不太一样的

解决方法:

通过一个对象进行包裹 对象就由RefImpl这个类来的 然后可以给类里加个value的 get 或 set,这样就可以监听到这个值什么时候被get和set了,就可以进行依赖的收集和触发

首先先给出测试用例:ref.spec.ts

import { effect, trigger } from "../effect"
import { ref } from "../ref"

describe('ref',()=>{
    it('happy path',()=>{
        const a = ref(1)
        expect(a.value).toBe(1)
    })

    it('should be reactive',()=>{
        const a = ref(1)
        let dummy
        let calls = 0
        effect(()=>{
            calls++
            dummy = a.value
        })
        expect(calls).toBe(1)
        expect(dummy).toBe(1)
        a.value = 2
        expect(calls).toBe(2)
        expect(dummy).toBe(2)
        // same value should not trigger
        a.value = 2
        expect(calls).toBe(2)
        expect(dummy).toBe(2)


    })
    it('should make nested properties reactive',()=>{
        const a = ref({
            count:1
        })
        let dummy
        effect(()=>{
            dummy = a.value.count
        })
        expect(dummy).toBe(1)
        a.value.count = 2
        expect(dummy).toBe(2)
    })
})

由于案例分成3部分,我们就先从happy path通过开始,happy path的通过就比较简单,只要实现get方法就可以通过了:

1 导出ref方法

2 由上面分析,我们要监听单个值,那我们就要通过对象包裹的方式去对value进行一个监听,所以ref函数中就要导出RefImpl实例

3 RefImpl中创建私有变量_value,get value时把_value变量return出去就可以实现了

代码截图如下

image.png

image.png

这时候happy path也就通过了

接着我们实现should be reactive测试用例,大致浏览实现功能如下

1 对ref类的set功能的追加 (就是给value私有变量进行一个更新)

2 对ref的依赖收集和触发进行实现

2.1 ref是一个单值的依赖收集和触发,是可以对reactive中的依赖收集和触发的代码做一个复用的
2.2 RefImpl中需要一个dep来对依赖进行收集
2.3 抽离effect.ts中track和trigger中收集和触发部分,分别为trackEffects函数和triggerEffects函数并导出给ref.ts使用

3 对重复设置相同值的条件进行判断: 使用Object.is对this._value和newValue的值进行判断,当对象改变的时候再去做set的操作和依赖触发的流程

想着内容也不算多,就直接把should make nested properties reactive的测试案例也一起分析了,再一块附上代码截图,这块测试大致要实现功能就是: 让ref支持传入对象,并且具有响应式

那这时候大家就会想到了,让对象响应式不就是之前写的reactive吗,直接导入进来使用就可以了,那其实也确实是这样的

这块我觉得直接截图,然后在截图中解释代码块内容会比较简单一些:

image.png

然后你以为这就结束了吗,其实才刚开始,你发现你包装完后,测试并没有完全通过,那是因为之前的对象对比就出现问题了

image.png

解决问题图片解释:

image.png

其他需要注意的还有优化:

image.png

image.png

接下来就出涉及到修改的几个代码

ref.ts:

import { hasChange, isObject } from "../shared";
import { trackEffects, triggerEffects } from "./effect";
import { reactive } from "./reactive";

// ref 一般都针对单个值 怎么才能知道他被get 或 set
// reactive 中的proxy又只是针对对象的
// 通过一个对象进行包裹 对象就由RefImpl这个类来的 然后可以给类里加个value的 get 或 set

class RefImpl {
    private _value: any;
    public dep
    private _rawValue: any;

    constructor(value){
        // 如果是对象 要用reactive进行一层包装
        this._rawValue = value
        this._value = convert(value)
        this.dep = new Set()
    }

    get value(){
        trackEffects(this.dep)
        return this._value
    }

    set value(newValue){
        // hasChange
        // 对比的时候 object的对比 但是this._value已经变成了proxy
        if(hasChange(this._rawValue,newValue)) {
            // 设置完值再去做依赖触发
            this._rawValue = newValue
            this._value = convert(newValue)
            triggerEffects(this.dep)
        }
    }
}

function convert(value){
    return isObject(value) ? reactive(value) : value
}

export function ref(value){
    return new RefImpl(value)
}

effect.ts:

import { extend } from "../shared"

let activeEffect
let shouldTrack

class ReactiveEffect {
    private _fn: any
    deps=[]
    active=true
    onStop?:()=>void

    constructor(fn,public scheduler?){
        this._fn = fn
    }

    run(){

        if(!this.active){
            return this._fn()
        }

        // 依赖收集之前去给激活的effect赋值
        activeEffect = this

        shouldTrack = true
        const res = this._fn()

        shouldTrack = false
        return res


    }

    stop(){
        if(this.active){
            cleanupEffect(this)
            if(this.onStop){
                this.onStop()
            }
            this.active = false
        }
    }
}

function cleanupEffect(effect){
    effect.deps.forEach((dep:any) => {
        dep.delete(effect)
    })
    effect.deps.length=0
}

const targetMap = new Map()
export function track(target,key){

    if(!isTracking()) return

    // target -> key -> dep
     let depsMap = targetMap.get(target)
     if(!depsMap){
        depsMap = new Map()
        // 没有depsMap时要加进去
        targetMap.set(target,depsMap)
     }

    let dep = depsMap.get(key)
    if(!dep){
        dep = new Set()
        depsMap.set(key,dep)
    }

    trackEffects(dep)
}

export function trackEffects(dep){
    if(!isTracking()) return

    if(dep.has(activeEffect)) return

    dep.add(activeEffect)
    // 这里需要反向收集一下dep
    activeEffect.deps.push(dep)
}

function isTracking() {
    return shouldTrack && activeEffect !== undefined
    // if(!activeEffect) return
    // if(!shouldTrack) return
}

export function trigger(target,key){
    const depsMap = targetMap.get(target)
    const dep = depsMap.get(key)
    triggerEffects(dep)
}

export function triggerEffects(dep){
    for (const effect of dep) {
        if(effect.scheduler){
            effect.scheduler()
        } else {
            effect.run()
        }
    }
}

export function effect(fn,options:any = {}){
    const _effect = new ReactiveEffect(fn,options.scheduler)

    extend(_effect,options)

    _effect.run()

    const runner:any = _effect.run.bind(_effect)

    runner.effect = _effect

    return runner
}

export function stop(runner) {
    runner.effect.stop()
}

index.ts

export const extend = Object.assign

export const isObject = (value) => {
    return value !== null && typeof value === 'object'
}

export const hasChange = (value,newVal) => {
    return !Object.is(value,newVal)
}