提示:看到我 请让我滚去学习
1.什么是响应式数据(响应式数据做了什么)?
响应式数据是指在某些编程框架或库中,尤其是前端框架如Vue.js中,能够自动检测其变化并触发视图更新的数据。这意味着当数据发生变化时,与这些数据绑定的用户界面(UI)会自动更新,而无需手动触发更新操作。响应式数据的核心在于它能自动追踪依赖关系,并在数据改变时自动通知相关的观察者。简单来说就是数据发生改变会触发视图更新。
有如下示例:
我们想要实现效果如下,有一个product 对象,product.price改变时,product依赖也会改变,即打印console.log(total is ${total})显示40.
let product = { price: 5, quantity: 2 }
let total = product.price * product.quantity // 10 right?
product.price = 20
console.log(`total is ${total}`)
问题:
正如您在上面的代码中看到的,为了开始构建反应性,我们需要保存我们如何计算,以便当或发生变化total时我们可以重新运行它。pricequantity
解决方案:
首先,我们需要某种方式来告诉我们的应用程序,“存储我即将运行的代码(效果),我可能需要你在另一个时间运行它。”然后我们要运行代码,如果变量得到更新,price则quantity再次运行存储的代码。在es5中我们可以使用对象的属性访问器实现,在es6后,可以使用class来实现,那我们尝试使用class来实现属性的拦截:

let activeEffect = null
class Dep {
constructor(value) {
this._value = value
this.effectlist = new Set()
}
tigger() {
this.effectlist.forEach(effect => {
effect()
})
}
depend() {
if (activeEffect) {
this.effectlist.add(activeEffect)
}
}
get value () {
this.depend()
return this._value
}
set value (value) {
this._value = value
this.tigger()
}
}
const reactive = function (value) {
return new Dep (value)
}
let a = 111
const proxya = reactive(a)
function effect (fn) {
activeEffect = fn
fn()
activeEffect = null
}
effect(() => {
document.getElementById('app').textContent = proxya.value
})
以上是我们代理一个简单的基础类型,那我们怎么取代理对象呢。很容易想到,遍历对象的键然后用上述的方法代理每个键就可以,那么如何将Dep类实例和对应属性关联起来呢?在es5中我们可以使用Object.defineProperty来实现,让我们尝试一下:
let activeEffect = null
class Dep {
constructor() {
this.effectlist = new Set()
}
tigger() {
this.effectlist.forEach(effect => {
effect()
})
}
depend() {
if (activeEffect) {
this.effectlist.add(activeEffect)
}
}
}
const reactive = function (obj) {
Object.keys(obj).forEach(key => {
let dep = new Dep()
let value = obj[key]
Object.defineProperty(obj, key, {
get () {
dep.depend()
return value
},
set (newValue) {
value = newValue
dep.tigger()
}
})
})
return obj
}
let a = {name:'zhangsan0',age:18}
const proxya = reactive(a)
console.log("🚀 ~ proxya:", a)
function effect (fn) {
activeEffect = fn
fn()
activeEffect = null
}
effect(() => {
document.getElementById('app').textContent = proxya.name
})
这也是我们vue2实现响应式数据的方法,从上述实现方式不难看出,这种方法有一个弊端,即我们只是对初始对象上的属相进行拦截,如= {name:‘zhangsan0’,age:18}上的name和age属性进行了遍历和拦截,如果有新的属性加入,并不能够做到相应的响应式。这也是vue2中引入了$set-api的原因。但是这也许并不是一个好方法,在es6之后有了新浏览器api-Proxy的加入,我们获取有了更好的方法来代理我们的对象,实现响应式。
那好,让我们尝试使用Proxy来实现对象代理吧:
<html>
<body>
<div id="app">div>
body>
<script type="module">
let activeEffect = null
let targerMap = new WeakMap()
class Dep {
constructor() {
this.effectlist = new Set()
}
tigger () {
this.effectlist.forEach(effect => {
effect()
})
}
depend () {
if (activeEffect) {
this.effectlist.add(activeEffect)
}
}
}
const getDep = function (target, key) {
let depsMap = targerMap.get(target)
if (!depsMap) {
depsMap = new Map()
targerMap.set(target, depsMap)
}
let dep = depsMap.get(key)
if (!dep) {
dep = new Dep()
depsMap.set(key, dep)
}
return dep
}
let reactiveHandlers = {
get (target, key, receiver) {
const dep = getDep(target, key)
dep.depend()
return Reflect.get(target, key, receiver)
},
set (target, key, value, receiver) {
const dep = getDep(target, key)
const result = Reflect.set(target, key, value, receiver)
dep.tigger()
return result
}
}
const reactive = function (obj) {
return new Proxy(obj, reactiveHandlers)
}
let obj = { name: 'zhangsan0', age: 18 }
const proxya = reactive(obj)
function effect (fn) {
activeEffect = fn
fn()
activeEffect = null
}
effect(() => {
document.getElementById('app').textContent = proxya.name
})
setTimeout(() => {
proxya.name = 222
}, 1000);
script>
html>
new Proxy(target, handler) 是 JavaScript 中用于创建代理对象的构造函数,它允许你拦截和控制对目标对象的访问。target 参数是你想要代理的对象,handler 参数是一个对象,包含了一系列的陷阱(traps),这些陷阱定义了当执行特定操作时,代理对象应该如何反应。Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
下面是一些常见的陷阱及其作用:
Reflect 是 JavaScript 中的一个内置对象,它提供了一组静态方法来操作对象的属性、元属性以及执行其他与对象相关的操作。Reflect 的引入旨在提供一种更直观、更符合元编程原则的方式来处理对象,尤其是在与 Proxy 结合使用时,它能够更精确地控制和拦截对象的操作。
与 Proxy 对象的结合
Proxy 是一个可以定义基本操作(如获取属性、设置属性、枚举属性等)行为的对象。当创建一个 Proxy 对象时,可以指定一系列的“陷阱”(traps),这些陷阱会在执行特定操作时被调用,允许你自定义这些操作的行为。Reflect 方法在 Proxy 的陷阱中扮演着至关重要的角色,原因如下:
例如在代理对象使用属性访问器的时候当触发getremark属性的get陷阱时,收集依赖。
如果在proxy的get陷阱中直接返回target[key],其中的this.name的指向为obj原对象,当访问name属性时并不会触发proxyobj的get陷阱,所以当我们修改name值时不会触发watchEffect中方法执行
如果在proxy的get陷阱中直接返回receiver[key],那么可以触发name属性get陷阱,但是会死循环,而使用reflect可以避免这种情况。

在我们学习完Proxy后,我们还需要了解WeakMap,因为vue3中使用了WeakMap来存储代理对象和副作用函数。
WeakMap 是 JavaScript 中的一种内置对象,它是一种特殊的映射数据结构。以下是关于 WeakMap 的一些关键点:
let map = new WeakMap();
let obj = {};
map.set(obj, 'Hello, World!');
console.log(map.get(obj)); // 输出 "Hello, World!"
console.log(map.has(obj)); // 输出 true
// 当 obj 不再被引用时,垃圾回收器可能会回收 obj,从而导致 map 中的条目消失
obj = null;
console.log(map.get(obj)); // 输出 undefined
//请注意,由于弱引用的特性,尝试在 obj 被清除后访问 WeakMap 中的条目将无法得到预期结果。
前面我们使用proxy的get去拦截一个对象属性的读取操作,但在响应式系统中,读取是一个宽泛的概念,例如使用in操作符查看对象上是否含有某个键,也是属于‘读取’操作。
const obj =reactive({ name: 'bobo', price: 20, age: 18, mi: 'big' })
effect(() => {
console.log('effect', 'foo' in obj)
})
obj.foo=11
delete obj.foo;
//effect false
//effect true
//effect false
effect(() => {
console.log('1111')
for (const key in obj) {
}
})
delete obj.name;
obj.sex = '女'
//1111
//1111
//1111
上述实例其实也是在读取属性,响应式系统应该勘界一切读取操作,以便数据变化时能正确的像响应,下面列举了一个普通对象所有可能的读取操作:
在js中,数组只是一个特殊的对象,但对数组的操作与普通对象仍有不同,下面总结了数组元素或对象的‘读取’操作:
数组的‘设置’操作有哪些:
我们要注意我们新增、删除数组元素时会隐式的修改数组长度,直接给数组长度赋值,也会印象数组内部元素,这在响应式系统中也要重做数组元素的副作用更新
例如:
let array=[1,2,3]
array.length=0
//array=[]
总结:
我们手写了一个简单的reactiveapi的实现,并且了解了es6中proxy、reflct、weakmap的相关知识 ,后一章我们将逐行解析vue3源码如何实现reactiveapi。
相关es6推荐大家学习阮大的工具书:
https://es6.ruanyifeng.com/