什么是响应,响应就是发生了某些事情,我们针对这些事情去做一些处理。在遇到天灾人祸时,有关部门会响应应急措施,组织营救。这就是响应。在程序里,前端向后端发送请求,后端需要根据这个请求针对性地响应数据,这也属于响应。在Vue和React中,响应式数据的变化会触发视图的更新,这个触发更新的动作是响应,所以这些数据被称作响应式数据。响应式数据是如何实现的呢?
1. defineProperty
数据的变化触发视图的更新,那我们就需要监视数据的变化,并在数据变化时做出反应,vue2中是依靠Object.defineProperty来完成的,defineProperty接收三个参数,第一个参数是需要监视的对象,第二个参数是需要监视的属性,第三个参数是属性描述符对象(关于属性,大家可以去看js权威指南第14章,这里简单讲一下,属性分为数据属性和访问器属性,他们分别有各自有4个配置,有两个配置一样,有两个配置互斥,我们需要把对象身上的某个属性变为响应式的,就需要配置这个属性为访问器属性,并设置getter和setter),如下:
const user = {
name: "小明",
age: 18,
}
Object.defineProperty(user, "age", {
// 数据属性的配置,只列举出来
// value: 23, 属性值
// writable: true, 是否可写,也就是是否可以改变user.age的值
// enumerable: true, 是否可枚举,也就是是否可以通过for in 遍历到这个age属性
// configurable: true, 是否可配置,若不可配置,则不可以删除此属性,也不能将writable从false变为true,不能改变enumerable,cibfugurable的值
// 访问器属性的配置,没有value与writable,替换为了get方法,set方法,人们喜欢称这两个方法为getter,setter
get() {
// 读取obj.age的值时,会调用getter,getter的返回值,即为obj.age的值
return Math.floor(Math.random() * 20) + 1;
},
set(newVal) {
// 设置user.age = xxx时,会调用setter,并将xxx作为参数传入setter
// user.age = newVal;
// 如果我们写了上面这行代码,把传进来的值直接赋值给user.gae,那就糟了,刚说过,设置user.gae的值,会触发setter,在setter里又设置了user.gae的值,这样会循环往复递归地触发setter,导致堆栈溢出报错。这里我们需要通过一个中间对象来代理user的属性
}
})
const user = {
name: "小明",
age: 18,
}
// 中间代理对象
const proxy = {
...user
}
Object.defineProperty(user, "age", {
get() {
// 读取user.age时,返回的是proxy.age的值
return proxy.age;
},
set(newVal) {
// 设置user.age时,设置的也是proxy.age的值
proxy.age = newVal;
}
})
我们假设在vue中,更新视图的一个方法叫做updateView,新旧虚拟dom对比,diff算法,将虚拟dom转为真实dom等所有操作都在这个updateView里,那我们的响应式数据就只需要在设置值时,也就是在setter里,调用一下这个updateView就实现了
Object.defineProperty(user, "age", {
// ...
set(newVal) {
// 更新视图
updateView();
proxy.age = newVal;
}
})
如果要把一个对象的所有属性都变为响应式的我们需要循环递归地调用defineProperty
const obj = {
a: 1,
child: {
b: 2,
}
}
const defReactive(obj) {
for(let key in obj) {
if(typeof obj[key] === 'object' && obj[key] !== null) {
defReactive(obj[key]);
} else {
Object.defineProperty(obj, key, {
get() {},
set(newVal) {},
})
}
}
}
在es6以后,设置getter与setter可以以一种非常简单的方式来设置,直接写在对象里或者类里
const obj = {
get a() {
return 123;
},
set a(newVal) {
console.log(newVal)
}
}
class Something {
get a() {
return 1;
}
set a(newVal) {
console.log(newVal);
}
}
console.log(obj.a); // 123
obj.a = 2; // 2
const some = new Something()
console.log(some.a); // 1
some.a = 3; // 3
这是一个非常简单的演示,vue2里面应该要比这要复杂得多,在触发getter时需要收集依赖,在响应式数据更新时也不是每更新一次就调用一次视图更新,而是将更新收集起来,异步地统一更新视图。但初入响应式原理,我们知道这么多就已经够了。
2、Reflect与Proxy
2.1 Reflect
vue2用的defineProperty,vue3迎来了Proxy代理类,Proxy是比defineProperty要强大得多的一个东西,在Proxy之前,我们需要了解另一个js的内置对象(Math就是一个内置对象),Reflect(反射),它身上有一组API用来映射对对象与函数的操作
什么叫做对对象的操作,例如删除对象身上的一个属性,delete obj.a
,遍历对象的key,遍历对象的value,设置getter,setter等,这些都是对对象的操作,ES6之后,出现了一个Reflect反射对象,把这些操作对象的操作映射到了这个Reflect对象身上,通过方法的形式来进行这些操作,例如删除obj.a属性,用Reflect来操作就是Reflect.deleteProperty(obj, 'a')
有什么用?
如果只是将一些对对象的操作通过方法放到Reflect对象身上的话,那用不用Refelect都行,但配合上Proxy,Reflect就有作用了,先不急着讲Proxy,我们先把Reflect身上的比较重要的API列举一下,完整的API可以去MDN上搜索Reflect
Reflect的API | Object对应的操作 | 他们之间的差异 |
---|---|---|
Reflect.defineProperty(obj, key, descriptor) |
Object.defineProperty(obj,key,descriptor) |
Reflect在成功时返回true,失败时报错 |
Reflect.deleteProperty(obj, key) |
delete obj[key] |
Reflect在成功时返回true,失败时返回false |
Reflect.get(obj, key, receiver) |
读取obj[key] | receiver可传可不传,区别就在于receiver,后面详讲 |
Reflect.set(obj, key, value, receiver) |
设置obj[key]=value | 区别在于receiver,同get |
Reflect.has(obj, key) |
同key in obj | |
Reflect.ownKeys(obj) |
等价于Object.getOwnPropertyNames()与Object.getOwnPropertySymbols()结果的组合 |
以上是我认为比较重要和常用的一些Reflect的api,还有一些跟属性描述符,获取原型等的api没有列举,之前有讲到getter和setter,读取属性值会触发getter,设置属性值会触发setter,读取属性值在Reflect里对应的API是Reflect.get,设置属性值在Reflect里对应的API是Reflect.set,通过Reflect的get和set同样会触发getter和setter,它俩没什么区别,区别在于Reflect的get和set方法传入了最后一个参数receiver,如果obj[key]这个key属性是一个访问器属性,还传入了receiver,那在obj[key]的getter和setter中会将this指向receiver,示例如下。
// 用es6的方式简单设置getter,setter
const obj = {
val: 1,
get a() {
return this.val;
},
set a(newVal) {
this.val = newVal;
}
}
const receiver = {
val: 2222,
}
// 不传receiver
console.log(Reflect.get(obj, 'a')); // 1
Reflect.set(obj, 'a', 123);
console.log(obj.val); // 123
// 传入receiver
console.log(Reflect.get(obj, 'a', receiver)); // 22222
Reflect.set(obj, 'a', 123456, receiver);
console.log(obj.val); // obj.val没有被改变,还是123
console.log(receiver.val); // 123456
到这里还是会感觉Reflect没啥用一样,先不急,我们再来看一下Proxy的用法
2.2 Proxy
Proxy是一个构造函数,通过new来初始化一个proxy实例,这里涉及到三个对象,proxy代理对象,目标对象,以及处理器对象。写法如下
const obj = {a: 1}
const proxy = new Proxy(obj, {
// ...
})
obj是目标对象,也就是原对象或被代理对象,proxy为代理对象,构造函数的第二个参数为处理器对象,重点就在于这个处理器对象。
处理器对象里面写什么呢,很神奇的是,处理器对象里面需要写一些方法,这些方法的名字与传参,跟Reflect反射对象身上的方法与传参一摸一样,那它俩有什么区别呢,区别就在于处理器对象帮我们捕获了对代理对象的操作,然后把控制权交给我们,例如gette和setter
const obj = {a: 1};
const proxy = new Proxy(obj, {
get(target, key, receiver) {
return target[key];
},
set(target, key, newVal, receiver) {
target[key] = newVal;
}
})
console.log(proxy.a); // 1;
proxy.a = 2;
console.log(obj.a); // 2;
对proxy的操作,也就是上面表格中间列对Object的操作会被捕获,对应到第一列Reflect的API,对应到处理器对象的方法,然后调用该方法,如果处理器对象身上没有对应的方法,则用第二列里原本的方式来操作原对象。这意味着我们可以完全修改操作对象的方式,例如将删除一个属性改为添加一个属性,判断一个属性是否在这个对象中始终返回true,当然实际不可能会这样做。
对于响应式数据来说,我们可以不用再像vue2一样遍历对象的所有属性,给属性添加Object.defineProperty来设置getter和setter了,我们可以通过proxy的处理器对象的get方法和set方法,拦截代理对象的属性读写,这等价于给原对象的所有属性都添加了一个getter和setter,可以不用再遍历对象的属性就实现响应式了。并且更为强大的是,不仅读写操作,还有对对象的in操作,delete操作等,都可以被处理器捕获,实现响应,这在Object.defineProperty中是无法实现的。下面是一个用proxy实现响应式的超简单示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="root"></div>
<button id="btn">按钮</button>
<script>
let count = 0;
const data = {
txt: 'hello world',
}
const proxy = new Proxy(data, {
set(target, key, newVal, receiver) {
target[key] = newVal;
document.querySelector('#root').textContent = newVal;
}
})
document.querySelector('#root').textContent = proxy.txt;
document.querySelector('#btn').addEventListener('click', () => {
proxy.txt = count++;
})
</script>
</body>
</html>
这里的document.querySelector(‘#root’).textContent = proxy.txt,意味着视图需要用到proxy.txt的值,proxy.txt值的改变需要再重新这段代码,这段代码在实际的vue3里会变为一个回调函数传入effect函数中,effect函数为副作用函数,在副作用函数里面所用到的所有响应式数据,会与传入的回调函数相绑定,也就是调用getter的时候会绑定对象的属性值与副作用,在修改属性时,会调用setter重新调用与此属性相关的所有副作用函数,vue3不仅捕获了getter,setter,还捕获了in,delete,for in等操作,所有读取的操作都会触发绑定依赖,所有修改的操作都会触发副作用函数,并且每次触发副作用函数都会将不必要的依赖清除,这大致是vue3的响应式原理。
其实最主要的还是getter和setter。
根据这也能猜到React的响应式原理大致是怎样的,useState()内部会绑定依赖,返回的set方法就相当于setter,调用set方法会更改数据,然后触发视图更新。我说的很简单,实际应该是很繁琐复杂吧。
我组长曾说过,再复杂的源码,底层无非也是一些if else,循环,等,只要用心去钻研,一定能看懂,翁恺老师也说过,c语言里的scanf也不过是一个函数调用,这个函数也不过是人写的,大家的头脑都差不多,别人能写出来,相信我们也一定能,假以时日,下足功夫,那些底层的源码,如何实现的,我们也一定都会知道的。