提示:看到我 请让我滚去学习
文章目录
开篇
1.什么是响应式数据(响应式数据做了什么)?
响应式数据是指在某些编程框架或库中,尤其是前端框架如Vue.js中,能够自动检测其变化并触发视图更新的数据。这意味着当数据发生变化时,与这些数据绑定的用户界面(UI)会自动更新,而无需手动触发更新操作。响应式数据的核心在于它能自动追踪依赖关系,并在数据改变时自动通知相关的观察者。简单来说就是数据发生改变会触发视图更新。
一、我们自己怎么设计实现一个响应式数据(响应式数据原理)?
使用class属性访问器对简单数据进行拦截
有如下示例:
我们想要实现效果如下,有一个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
})
使用Object.defineProperty拦截对象属性
以上是我们代理一个简单的基础类型,那我们怎么取代理对象呢。很容易想到,遍历对象的键然后用上述的方法代理每个键就可以,那么如何将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代理对象
那好,让我们尝试使用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>
Proxy相关知识学习
什么是Proxy?
new Proxy(target, handler) 是 JavaScript 中用于创建代理对象的构造函数,它允许你拦截和控制对目标对象的访问。target 参数是你想要代理的对象,handler 参数是一个对象,包含了一系列的陷阱(traps),这些陷阱定义了当执行特定操作时,代理对象应该如何反应。Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
下面是一些常见的陷阱及其作用:
- get(target, prop, receiver):当访问代理对象的属性时触发。target 是被代理的对象,prop 是被访问的属性名,receiver 是访问者对象(通常是代理对象自身)。
- set(target, prop, value, receiver):当设置代理对象的属性时触发。返回一个布尔值,表示属性是否成功设置。
- has(target, prop):当使用 in 操作符检查属性是否存在时触发。
- deleteProperty(target, prop):当删除属性时触发,返回一个布尔值,表示删除是否成功。
- ownKeys(target):当使用 Object.keys(), for…in 循环,或者获取 Reflect.ownKeys() 时触发。
- getOwnPropertyDescriptor(target, prop):当获取属性描述符时触发。
- defineProperty(target, prop, descriptor):当使用 Object.defineProperty() 或 Object.defineProperties() 时触发。
- enumerate(target):当使用 for…of 循环(仅限于旧版浏览器)或 Object.keys() 时触发。
- getPrototypeOf(target):当获取对象的原型时触发。
- setPrototypeOf(target, proto):当设置对象的原型时触发。
- apply(target, thisArgument, argumentsList):当调用函数对象时触发,主要用于代理函数调用。
- construct(target, argumentsList, newTarget):当使用 new 关键字实例化对象时触发,主要用于代理构造函数。
代理对象通常用于创建数据绑定、元编程、访问控制、模拟等场景。在 Vue 3 中,new Proxy 用于实现响应式系统,通过拦截和控制数据访问来实现数据变化的追踪。
Reflect相关知识学习
什么是Reflect ?
Reflect 是 JavaScript 中的一个内置对象,它提供了一组静态方法来操作对象的属性、元属性以及执行其他与对象相关的操作。Reflect 的引入旨在提供一种更直观、更符合元编程原则的方式来处理对象,尤其是在与 Proxy 结合使用时,它能够更精确地控制和拦截对象的操作。
与 Proxy 对象的结合
Proxy 是一个可以定义基本操作(如获取属性、设置属性、枚举属性等)行为的对象。当创建一个 Proxy 对象时,可以指定一系列的“陷阱”(traps),这些陷阱会在执行特定操作时被调用,允许你自定义这些操作的行为。Reflect 方法在 Proxy 的陷阱中扮演着至关重要的角色,原因如下:
- 标准化操作:在 Proxy 的陷阱中使用 Reflect 方法,可以确保即使在自定义行为时,也遵循了JavaScript的标准操作逻辑。例如,在 get 陷阱中使用 Reflect.get(),确保了属性访问的逻辑与直接访问对象属性一致。
- 清晰的控制流:通过 Reflect 方法,可以清晰地看到哪些操作被拦截和修改,哪些保持原样。这提高了代码的可读性和可维护性。
- 避免直接操作对象的副作用:直接修改对象可能引起不可预知的副作用,而使用 Reflect 方法可以在不改变原始操作逻辑的基础上,安全地进行操作。
例如在代理对象使用属性访问器的时候当触发getremark属性的get陷阱时,收集依赖。
如果在proxy的get陷阱中直接返回target[key],其中的this.name的指向为obj原对象,当访问name属性时并不会触发proxyobj的get陷阱,所以当我们修改name值时不会触发watchEffect中方法执行
如果在proxy的get陷阱中直接返回receiver[key],那么可以触发name属性get陷阱,但是会死循环,而使用reflect可以避免这种情况。
WeakMap相关知识学习
在我们学习完Proxy后,我们还需要了解WeakMap,因为vue3中使用了WeakMap来存储代理对象和副作用函数。
什么是weakMap?
WeakMap 是 JavaScript 中的一种内置对象,它是一种特殊的映射数据结构。以下是关于 WeakMap 的一些关键点:
- 弱引用:
WeakMap 的键必须是对象,而值可以是任意类型。与普通的 Map 不同,WeakMap 对键的引用是弱引用。这意味着如果键对象不再被其他地方引用,垃圾回收机制会将其删除,即使 WeakMap 中还存在对该键的映射也会被自动清除。 - 非枚举属性:
WeakMap 对象的实例没有可枚举属性,因此不能通过 for…of 循环或 Object.keys()、Object.values() 或 Object.entries() 来遍历其成员。 - 方法:
set(key, value): 向 WeakMap 中添加一个键值对。
get(key): 根据给定的键获取对应的值。如果键不存在,返回 undefined。
has(key): 检查给定的键是否存在于 WeakMap 中。
delete(key): 删除指定键的键值对。 - 生命周期:
由于 WeakMap 中的键是弱引用,它们不会阻止垃圾回收。当键对象被销毁时,相应的条目也会从 WeakMap 中自动移除,这使得 WeakMap 适用于存储与对象相关的元数据,而不阻止对象的释放。 - 不可遍历:
由于隐私和性能原因,WeakMap 不支持迭代,没有 size 属性,也不能通过 keys(), values(), 或 entries() 方法获取其内容。 - 应用场景:
常用于缓存,特别是当需要缓存对象的元数据且不希望缓存阻止对象被垃圾回收时。
另一个常见用途是在闭包中存储私有变量,防止外部访问或意外保留引用。
示例
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代理一个object对象
前面我们使用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
上述实例其实也是在读取属性,响应式系统应该勘界一切读取操作,以便数据变化时能正确的像响应,下面列举了一个普通对象所有可能的读取操作:
- 访问属性:obj.foo -----get陷阱拦截
- 判断 对象或原型上是否存在给定的key :key in obj -----has陷阱拦截
- 使用for … in循环遍历对象: for (const key in obj) {} -----ownKeys陷阱拦截
proxy代理一个数组对象
在js中,数组只是一个特殊的对象,但对数组的操作与普通对象仍有不同,下面总结了数组元素或对象的‘读取’操作:
- 通过索引访问数组元素值:arr[0]
- 通过数组长度访问:arr.length
- 数组循环遍历 for…in
- 数组的原型方法,如concat/join/every/some/find/findIndex/include等,以及其他不改变原数组的原型方法
数组的‘设置’操作有哪些:
- 通过索引修改数组元素:arr[0]=3
- 修改数组长度: arr.length =0
- 数组的栈方法: push/pop/shitf/unshift
- 修改原数组的原型方法:splice/fill/sort等
我们要注意我们新增、删除数组元素时会隐式的修改数组长度,直接给数组长度赋值,也会印象数组内部元素,这在响应式系统中也要重做数组元素的副作用更新
例如:
let array=[1,2,3]
array.length=0
//array=[]
总结:
我们手写了一个简单的reactiveapi的实现,并且了解了es6中proxy、reflct、weakmap的相关知识 ,后一章我们将逐行解析vue3源码如何实现reactiveapi。
相关es6推荐大家学习阮大的工具书:
https://es6.ruanyifeng.com/