手撸vue3核心源码——响应式原理(ref)

简介: 手撸vue3核心源码——响应式原理(ref)

实现ref

我们知道读取ref对象的值需要.value, 因此我们来写一个单元测试来简单实现以下它的功能


it("happy path", () => {
        let obj = ref(1)
        expect(obj.value).toBe(1)
    })

我们要读取到obj身上的value属性即可,读属性就想到了get操作,同reactive,我们也定义一个类来实现方法


class RefImpl {
    private _value
    constructor(value) {
        this._value = value
    }
    get value() {
        return this._value
    }
    /*
    set value(){
    }*/
}
export function ref(value) {
    return new RefImpl(value)
}

当我们触发obj.value时,会调用ref对象身上的get方法,此时返回的数值也就是传进来的值,就实现了.value能拿到ref对象的值

编辑

image.png

触发依赖以及收集依赖

基本数据类型

触发依赖与收集依赖,同样是get时触发track, set时触发trigger,先来实现一下case

it('it should be effect', () => {
        let dummy
        let obj = ref(1)
        let count = 0
        effect(() => {
            count++
            dummy = obj.value
        })
        expect(count).toBe(1)
        expect(dummy).toBe(1)
        obj.value = 2
        expect(count).toBe(2)
        expect(dummy).toBe(2)
    })

这里的功能是,effect监听ref对象的变化,当我们修改obj.value时,dummy同意跟着变化,用count来监听effect触发的次数,这个后面细说,先埋个伏笔

收集依赖

对于ref与reactive的对比,ref他只有一个value属性,因此它的依赖性只有一个set, 我们知道依赖性存在于dep中,因此我们可以将依赖性收集在dep中,set value时我们再执行即可,遵循这个思路就很清晰了


export function track(target, key) {
    let depMap = targetMap.get(target)
    //解决初始化不存在的问题
    if (!depMap) {
        depMap = new Map()
        targetMap.set(target, depMap)
    }
    let dep = depMap.get(key)
    //解决初始化不存在的问题
    if (!dep) {
        dep = new Set()
        depMap.set(key, dep)
    }
    // 这里我们用下面的isTrack来优化
    /*如果没有依赖项,就直接返回
    if (!activeEffect) return;
    //如果不要收集依赖,这里直接返回
    if (!shouldTrack) return;*/
    if (!isTrack()) return
    //收集依赖, 我们需要拿到fn 如果依赖中存在activeEffect 就直接return 不再收集了
    if (dep.has(activeEffect)) return
    dep.add(activeEffect)
    //这里我们将
    activeEffect.deps.push(dep)
}

观察一下我们的track函数,我们通过key value的形式取到的dep 而我们的ref直接就定义一个dep就可,所以我们可以执行长注释下面这一段逻辑,因此我们将该部分提炼出来

export function trackEffect(dep) {
    if (!isTrack()) return
    //收集依赖, 我们需要拿到fn 如果依赖中存在activeEffect 就直接return 不再收集了
    if (dep.has(activeEffect)) return
    dep.add(activeEffect)
    //这里我们将
    activeEffect.deps.push(dep)
}


这里写的也就是如果不需要收集依赖以及已经存在依赖,就不需要再收集了,最后收集的依赖存在dep中,因此我们的get value操作就可以这样

get value() {
        if (isTrack()) {
            trackEffect(this.dep)
        }
        return this._value
    }

这里的isTrack是我们之前写的,判断依赖项是否存在,以及是否应该收集依赖的操作


export function isTrack() {
    return shouldTrack && activeEffect
}

因为如果依赖项是个undefined或者不应该收集依赖,那么我们就不需要走到收集依赖

到这里我们收集依赖的任务也完成了,接下来实现触发依赖


触发依赖

我们下来看一下trigger函数,我们是先找到dep ,然后依次执行dep里的东西,我们因为已经有dep了,所有可以跳过dep的获取过程了


export function trigger(target, key) {
    const depMap = targetMap.get(target)
    const dep = depMap.get(key)
    for (const effect of dep) {
        if (effect.scheduler) {
            effect.scheduler()
        } else {
            effect.run()
        }
    }


因此将ref触发依赖的逻辑抽离出来


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


class RefImpl {
    private _value
    public dep
    constructor(value) {
        this._value = value
        this.dep = new Set()
    }
    get value() {
        if (isTrack()) {
            trackEffect(this.dep)
        }
        return this._value
    }
    set value(newValue) {
        this._value = newValue
        tiggerEffect(this.dep)
    }
}

这样我们也就实现了依赖的触发


引用数据类型

当我们的ref是一个对象时,那么它的底层会调用reactive方法来包裹住,

我们就可以在传入值的时候做个处理,先实现一下case


it("ref Object should be a reactive", () => {
        let obj = ref({ age: 123 })
        let dummy
        let count = 0
        effect(() => {
            count++
            dummy = obj.value.age
        })
        expect(count).toBe(1)
        expect(dummy).toBe(123)
        obj.value.age = 222
        expect(count).toBe(2)
        expect(dummy).toBe(222)
    })

顺着思路来实现一下

class RefImpl {
    private _value
    public dep
    constructor(value) {
        //做对象类型检测
        this._value = isObject(value) ? reactive(value) : value
        this.dep = new Set()
    }
    get value() {
        trackRefValue(this.dep)
        return this._value
    }
    set value(newValue) {
        if (!isHasChanged(this._value, newValue)) return
        this._value = newValue
        tiggerEffect(this.dep)
    }
}


export function isObject(res) {
    return res !== null && typeof res === 'object'
}

这样就能够通过单测了


优化代码

上面我们提到有count来记录effect触发的次数,这里我们来实现一个功能


it('it should be effect', () => {
        let dummy
        let obj = ref(1)
        let count = 0
        effect(() => {
            count++
            dummy = obj.value
        })
        expect(count).toBe(1)
        expect(dummy).toBe(1)
        obj.value = 2
        expect(count).toBe(2)
        expect(dummy).toBe(2)
        obj.value = 2
        expect(count).toBe(2)
        expect(dummy).toBe(2)
    })

即当我们的obj.value更新后的值与他开始一样的话,我们就不让他做依赖的触发,即跳过这次的更新操作,这样可以极大的节省性能

Object.is

Object.is它用于比较两个值是否相等,和其他比较运算符(如 和 )相比, 有一些独特的特性和用途

1.精确的相等性比较: 使用严格相等比较()的规则来确定两个值是否相等。与 操作符相比,它不会进行隐式的类型转换

Object.is(1, "1"); // false
Object.is(NaN, NaN); // true

2.处理特殊值: 可以处理一些特殊值的比较,例如NaN与0

Object.is(NaN, NaN); // true
Object.is(-0, +0); // false


Object.is(0, -0); // false
0 === -0; // true

总之, 该方法是在进行值的比较时有用的工具,它提供了更加精确的相等性比较,并且可以处理一些特殊的值。但需要注意的是,由于 在处理非基本类型值时会进行引用比较,因此对于引用类型的对象,它并不常用,通常使用 运算符来进行对引用的比较

因此这里我们新旧值对比就可以用该方法


set value(newValue) {
        if (Object.is(newValue, this._value)) {
            return
        }
        this._value = newValue
        tiggerEffect(this.dep)
    }

这样就可以对一些不变的值进行优化了

注意点

这里我们注意一下,当我们对比两个对象类型时,我们的value已经进行Proxy代理了,所以会有问题,我i们可以用个变量来保存一下之前没有代理的普通value, 然后对比这俩就可以


class RefImpl {
    private _value
    private _raw
    public dep
    constructor(value) {
        //保存没有代理前的value
        this._raw = value
        //做对象类型检测
        this._value = isObject(value) ? reactive(value) : value
        this.dep = new Set()
    }
    get value() {
        trackRefValue(this.dep)
        return this._value
    }
    set value(newValue) {
        if (!isHasChanged(this._raw, newValue)) return
        this._raw = newValue
        this._value = newValue
        tiggerEffect(this.dep)
    }
}

这样就可以对比最原始的两个数了

相关文章
|
15天前
|
JavaScript 前端开发 开发者
Vue 3中的Proxy
【10月更文挑战第23天】Vue 3中的`Proxy`为响应式系统带来了更强大、更灵活的功能,解决了Vue 2中响应式系统的一些局限性,同时在性能方面也有一定的提升,为开发者提供了更好的开发体验和性能保障。
35 7
|
16天前
|
前端开发 数据库
芋道框架审批流如何实现(Cloud+Vue3)
芋道框架审批流如何实现(Cloud+Vue3)
38 3
|
15天前
|
JavaScript 数据管理 Java
在 Vue 3 中使用 Proxy 实现数据双向绑定的性能如何?
【10月更文挑战第23天】Vue 3中使用Proxy实现数据双向绑定在多个方面都带来了性能的提升,从更高效的响应式追踪、更好的初始化性能、对数组操作的优化到更优的内存管理等,使得Vue 3在处理复杂的应用场景和大量数据时能够更加高效和稳定地运行。
36 1
|
15天前
|
JavaScript 开发者
在 Vue 3 中使用 Proxy 实现数据的双向绑定
【10月更文挑战第23天】Vue 3利用 `Proxy` 实现了数据的双向绑定,无论是使用内置的指令如 `v-model`,还是通过自定义事件或自定义指令,都能够方便地实现数据与视图之间的双向交互,满足不同场景下的开发需求。
37 1
|
18天前
|
前端开发 JavaScript
简记 Vue3(一)—— setup、ref、reactive、toRefs、toRef
简记 Vue3(一)—— setup、ref、reactive、toRefs、toRef
|
5天前
|
JavaScript 前端开发
如何在 Vue 项目中配置 Tree Shaking?
通过以上针对 Webpack 或 Rollup 的配置方法,就可以在 Vue 项目中有效地启用 Tree Shaking,从而优化项目的打包体积,提高项目的性能和加载速度。在实际配置过程中,需要根据项目的具体情况和需求,对配置进行适当的调整和优化。
|
6天前
|
存储 缓存 JavaScript
在 Vue 中使用 computed 和 watch 时,性能问题探讨
本文探讨了在 Vue.js 中使用 computed 计算属性和 watch 监听器时可能遇到的性能问题,并提供了优化建议,帮助开发者提高应用性能。
|
6天前
|
存储 缓存 JavaScript
如何在大型 Vue 应用中有效地管理计算属性和侦听器
在大型 Vue 应用中,合理管理计算属性和侦听器是优化性能和维护性的关键。本文介绍了如何通过模块化、状态管理和避免冗余计算等方法,有效提升应用的响应性和可维护性。
|
6天前
|
存储 缓存 JavaScript
Vue 中 computed 和 watch 的差异
Vue 中的 `computed` 和 `watch` 都用于处理数据变化,但使用场景不同。`computed` 用于计算属性,依赖于其他数据自动更新;`watch` 用于监听数据变化,执行异步或复杂操作。
|
5天前
|
JavaScript 前端开发 UED
vue学习第二章
欢迎来到我的博客!我是一名自学了2年半前端的大一学生,熟悉JavaScript与Vue,目前正在向全栈方向发展。如果你从我的博客中有所收获,欢迎关注我,我将持续更新更多优质文章。你的支持是我最大的动力!🎉🎉🎉