再谈Vue3响应式

鉴于之前看的手写Vue3源码约等于没看懂,写的博客也是一坨💩,我再来写一次加深理解...

什么是响应式系统?

比如,下面这段代码:

JavaScript 复制代码
let a = 10
let b = 20
let c = a * b
console.log(c)
b = 30
console.log(c)

我们的意图是为了让b发生变动时,控制台输出也发生改变,但是很显然上述代码做不到。我们想要实现的效果就是响应式系统意图解决的痛点。在数据发生变化时,所有使用了该数据的位置都会发生更新(也就是更新副作用),这一过程是自动的,也就避免了大量的冗余代码。

变量名解释

此处先介绍一下后面会用到的变量名和它们的简单释义,方便理解后续代码。

  • effect:关于要某个数据的副作用函数,通过再次调用来实现数据更新
  • track:用于收集这些副作用的函数
  • trigger:用于触发这些副作用的函数
  • dep:也就是依赖,每个变量对应一个dep,用于存储多个副作用函数(Set类型,可以去重)
  • depsMap:用于存储dep,键值对形式,变量对应各自的dep(Map类型)
  • targetMap:用于存储depsMap,每个响应式对象对应一个depsMapWeakMap类型,键为对象)
  • reactive:Vue中的响应式类型,用于包裹对象类型数据为响应式数据
  • activeEffect:用于存储当前正要执行的effect副作用函数
  • ref:Vue中的响应式类型,用于包裹基本类型数据为响应式数据

逐步构建

JavaScript 复制代码
const depsMap = new Map() 
function track(key) { 
    let dep = depsMap.get(key) 
    if (!dep) { 
        depsMap.set(key, (dep = new Set())) 
    } 
    dep.add(effect) 
} 
function trigger(key) { 
    let dep = depsMap.get(key) 
    if (dep) { 
        dep.forEach(effect => { effect() }) 
    } 
}

上方的代码中实现了简易的tracktrigger函数,可以窥见响应式的雏形。

首先定义了一个depsMapMap类型,用于存储<key-dep>

后续定义了track函数,实现了根据key(属性)来从depsMap中查找依赖,若取到值,则说明先前已经对该属性进行了追踪,后续只需要继续向该属性对应的dep中添加effect就可以;否则,创建新的dep来存储副作用函数,并将其与对应的key一同存入depsMap

之后又定义了trigger函数,实现了根据keydepsMap中查找对应的依赖,如果找到则说明先前已经对该属性进行依赖收集,接下来遍历并执行dep中存储的副作用函数就可以实现数据更新。

那么问题来了,目前实现的只是对于一个对象里的多个属性能实现响应式,如果有很多个对象怎么办?不能每个对象都写一遍吧😦???

没错,当然不行,这样的轮子显然不够好用,那么我们需要另一个能存储对象和对应的depsMap的,类似<object-depsMap>形式的变量。一般来讲,会很自然地想到使用Map但是实际上这样行不通,因为Map的键只能使用字符串或者Symbol类型,此时WeakMap是更好的选择。

因为它的键只能使用对象类型!!这你受的了吗,简直和我们的需求天生一对

那么很自然的就催生了targetMap用于存储<object-depsMap>,实现了对于不同对象收集它们各自属性的依赖(准确的讲,键应该是reactive类型的对象)。

有了targetMap,我们可以对代码做如下改进:

JavaScript 复制代码
const targerMap = new WeakMap() 
function track(target,key) { 
    let depsMap = targetMap.get(target)
    if (!depsMap) {
        depsMap = targetMap.set(target,(depsMap = new Map()))
    }
    let dep = depsMap.get(key) 
    if (!dep) { 
        depsMap.set(key, (dep = new Set())) 
    } 
    dep.add(effect) 
} 
function trigger(target,key) { 
    const depsMap = targetMap.get(target) 
    if (!depsMap) return 
    let dep = depsMap.get(key)
    if (dep) {
        dep.forEach(effect => { effect() })
    }
}

也就是说,我们的逻辑是先根据target确定响应式对象,然后在该对象中寻找key属性,之后进行依赖收集或者更新依赖。

可是新的问题又出现了,总不能™一直手动调用tracktrigger吧?

也就是说我们的目标是在effect函数中获取(get)或者修改(set)响应式数据时,需要自动调用track或者trigger来实现依赖收集和依赖更新,那么就需要拦截GETSET方法,来判断究竟调用track还是trigger

Vue2中,官方使用的是Object.defineProperty()来实现对于对象的代理,从而实现拦截的效果。

而在Vue3中,官方使用的是ES6的新特性,Proxy & Reflect,它们相较于前者更加简洁高效。

Vue3不支持IE,别踏🐎的用IE了,给👴爬)

现在我们先来了解一下ProxyReflect

Proxy

对象代理 ,简单来讲它就是隔在真实对象和用户之间的一个管家 。用户的代码对于对象的操作无非是获取和更改 两种,它们现在都要由这个管家 经手,而管家 可以在经手的过程中搞点事情,这就给了我们实现自动执行tracktrigger的机会。

它的大致语法如下:

JavaScript 复制代码
const obj = { a:10,b:20 }
let ProxyObj = new Proxy(obj,{
    get(target,key,receiver?) {...}
    set(target,key,value,receiver?) {...}
})
Proxy.a === obj.a
Proxy.b = 30
obj.b = 30 // 操作的结果等价

targetkeyvalue都好理解,但这个receiver需要解释一下,它是一个可选参数,通常就是Proxy对象本身,用于确保this指向正确,在get方法中保证上下文一致性。这里说实话不太好理解,举个例子:

JavaScript 复制代码
const obj = {
    a:10,
    b:2,
    alias:function() { 
        return this.a 
    }
}
let ProxyObj = new Proxy(obj,{
    get(target,key,receiver?) {...}
    set(target,key,value,receiver?) {...}
})
ProxyObj.alias()

这种情况就需要receiver来保证this指向正确

Reflect

专门用来配合Proxy ,方法与 Proxy 对象的方法一一对应,只要是 Proxy 对象的拦截方法,就能在 Reflect 对象上找到对应的方法。这使得 Proxy 对象可以方便地调用对应的 Reflect 方法,完成默认行为,作为修改行为的基础。

大致语法如下:

JavaScript 复制代码
Reflect.set(target, Key, value[, receiver])
// receiver是可选参数

了解了它们的语法就可以开始搞事情了🤤

JavaScript 复制代码
let product = {
    price: 5,
    quantity: 2
};
let proxiedProduct = new Proxy(product, {
    get(target, key, receiver) {
        console.log('Get was called with key ='+ key);
        return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
        console.log('Set was called with key ='+ key +'and value ='+ value);
        return Reflect.set(target, key, value, receiver);
    }
});
proxiedProduct.quantity = 4;
console.log(proxiedProduct.quantity);

好了,现在我们用代理实现了对于获取和修改操作的拦截,之后就是思考一下把tracktrigger函数放在哪里的问题了

so,我们可以做如下改进:

JavaScript 复制代码
let product = {
    price: 5,
    quantity: 2
};
let proxiedProduct = new Proxy(product, {
    get(target, key, receiver) {
        let result = Reflect.get(target, key, receiver);
        track(target,key)
        return result
    },
    set(target, key, value, receiver) {
        let oldVal = target.key
        if(oldVal != value) trigger(target,key)
        return Reflect.set(target, key, value, receiver);
        // 注意,Refelct.set()返回Boolean值
    }
});
proxiedProduct.quantity = 4;
console.log(proxiedProduct.quantity);

现在已经实现了响应式,但是我们还是想要它更接近Vue3的源码,所以我们稍稍封装一下:

JavaScript 复制代码
function reactive(target) {
    const handler = {
        get(target, key, receiver) {
            let result = Reflect.get(target, key, receiver);
            track(target,key)
            return result
        },
        set(target, key, value, receiver) {
            let oldVal = target.key
            if(oldVal != value) trigger(target,key)
            return Reflect.set(target, key, value, receiver);
            // 注意,Refelct.set()返回Boolean值
        }
    }
    return new Proxy(target,handler)
}
const obj = { a:10,b:20 }
let product = reactive(obj)

欧吼,牛🍺,有模有样了,现在我们捋一捋到底是怎么个事儿:

  • 获取reactive响应式对象的属性时,因为它是Proxy代理过的对象,所以这时候拦截器会被触发,也就是get函数会被调用

    JavaScript 复制代码
    get(target, key, receiver) {
        let result = Reflect.get(target, key, receiver);
        track(target,key)
        return result
    }

    进入get函数以后,会首先取到该属性的值,然后调用track方法,走我们之前讲过的那套流程,从targetMap中寻找target对应的depsMap,没有则创建新的,有则在depsMap中寻找key对应的dep,没有则创建,有则将副作用函数effect放进dep,实现依赖收集

  • 修改reactive响应式对象的属性时,也会触发拦截器,此时调用set函数

    JavaScript 复制代码
    set(target, key, value, receiver) {
        let oldVal = target.key
        if(oldVal != value) trigger(target,key)
        return Reflect.set(target, key, value, receiver);
        // 注意,Refelct.set()返回Boolean值
    }

    进入set函数后,会先取到原属性值oldVal,之后比较新旧属性值,如果相同就不会更新,否则调用trigger函数,从targetMap中寻找target对应的depsMap,然后在depsMap中寻找key对应的dep,并遍历执行所有的副作用函数,实现数据更新

不过现在仍然有一些问题,对于effect,我们不想每次都手动调用一次,并且每次调用都会执行track

那么仿照Vue3的做法,我们引入activeEffect变量:

JavaScript 复制代码
const targerMap = new WeakMap() 
let activeEffect = null // 初始值置空
function effect(eff){
    activeEffect = eff // 赋值为当前的副作用函数
    activeEffect() // 执行当前的副作用函数
    activeEffect = null // 还原为空
}
function track(target,key) { 
    if (activeEffect) { // 确保了只有在activeEffect非空时才执行下面的逻辑
        let depsMap = targetMap.get(target)
        if (!depsMap) {
            depsMap = targetMap.set(target,(depsMap = new Map()))
        }
        let dep = depsMap.get(key) 
        if (!dep) { 
            depsMap.set(key, (dep = new Set())) 
        } 
        dep.add(effect) 
    }
} 
function trigger(target,key) { 
    const depsMap = targetMap.get(target) 
    if (!depsMap) return 
    let dep = depsMap.get(key)
    if (dep) {
        dep.forEach(effect => { effect() })
    }
}

现在实现了自动调用副作用函数,只需要把要执行的副作用函数作为回调函数传递给effect函数就可以实现这一点,免去了手动调用,并且在收集依赖的环节中,避免了每次都执行track中的逻辑,而是仅在有副作用函数时才执行,提升了效率

现在reactive搞的差不多了,我们来看看ref

刚刚写完reactive,我们很顺理成章的就能搓出来一个ref🤌

JavaScript 复制代码
function ref(initialVal) {
    return reactive({ value:initialVal })
}

竟然好像没什么毛病🤔

但是!!Vue可不是这么写的(要是我能写出来我能在这码字??)

看下面⬇️

JavaScript 复制代码
function ref(raw) {
    const r = {
        get value() {
            track(r, 'value');
            return raw;
        },
        set value(newVal) {
            if(newVal != raw) { // 避免死循环
                raw = newVal;
                trigger(r, 'value');
           }
        }
    };
    return r;
}

当然了,这也是极简版本,只是大致展示了一下核心原理。对于获取和更新ref来说,是比较直观的,但是如何在首次定义ref类型变量时,如:

const myRef = ref(5)

让myRef等于5?这是一个稍显晦涩难懂的点,下面我们来解释一下

注意⚠️,此处使用了闭包的机制

rawref 函数的参数,属于 ref 函数作用域内的变量。r 对象中的 get valueset value 方法虽然定义在 ref 函数内部,但它们可以访问和操作 raw 变量。当 ref 函数执行完毕后,由于 r 对象中的这两个方法仍然引用着 raw 变量,所以 raw 变量不会被销毁,而是被保留在内存中。

由此才实现了首次定义时初始值的保留,相当巧妙的做法😲

好了,本篇博客就到此为止了,应该是目前写的最长的一次,希望对您有帮助(其实我知道没人看👀)

PS:视频课参考 Vue-Mastery

相关推荐
崔庆才丨静觅3 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60614 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了4 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅4 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅4 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅5 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment5 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅5 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊5 小时前
jwt介绍
前端
爱敲代码的小鱼5 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax