再谈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

相关推荐
GISer_Jing15 分钟前
React-Markdown详解
前端·react.js·前端框架
太阳花ˉ16 分钟前
React(九)React Hooks
前端·react.js
拉不动的猪1 小时前
vue与react的简单问答
前端·javascript·面试
污斑兔2 小时前
如何在CSS中创建从左上角到右下角的渐变边框
前端
星空寻流年2 小时前
css之定位学习
前端·css·学习
旭久2 小时前
react+antd封装一个可回车自定义option的select并且与某些内容相互禁用
前端·javascript·react.js
是纽扣也是烤奶3 小时前
关于React Redux
前端
阿丽塔~3 小时前
React 函数组件间怎么进行通信?
前端·javascript·react.js
冴羽3 小时前
SvelteKit 最新中文文档教程(17)—— 仅服务端模块和快照
前端·javascript·svelte