背景
首先用一个例子来解释什么是响应式。
- 设置变量
a为1; - 设置变量
b为a和2之和; - 当修改
a的值,b的值也会随之发生改变。
初步实现
let a = 1
let b = a + 2
a = 3 // b 为 3
a = 4 // b 为 3
上述代码显然无法满足例子中的第三点,即b随a的变化而变化。原因很简单,JS代码是从上往下执行的。a的赋值操作无法通知上一行的b = a + 2。 如果一定要希望b及时能够响应a的变化,可以在每次a的赋值操作之后紧跟b = a + 2
简单代码的改进
let a = 1
let b = undefined
b = a + 2
// ...
a = 3
b = a + 2
// ...
a = 4
b = a + 2
方便起见,这里将b = a + 2包在一个函数里。
let a = 1
let b = undefined
function foo() {
b = a + 2
}
foo()
// ...
a = 3
foo()
// ...
a = 4
foo()
但这里有一个问题,就是a的赋值操作必须手工紧跟这个foo函数的执行。 那么,是否有一个自动的方式来处理这个依赖关系呢?
Proxy对象
ES6里有一个Proxy,可以对对象属性的get/set操作进行拦截处理。 如果要将Proxy的这个功能运用到这里,需要将a包装成一个对象。
let a = 1
let b = undefined
let obj_a = {a} // 将a包装成对象
let proxy_a = new Proxy(obj_a, {
get(target, property) {
return target[property]
},
set(target, property, value) {
target[property] = value
foo() // 拦截set操作,执行foo函数
return true
}
})
function foo() {
b = proxy_a.a + 2
}
foo()
// ...
proxy_a.a = 3 // b 为 5
// ...
proxy_a.a = 4 // b 为 6
- 首先,将
a封装成目标对象obj_a,用Proxy对obj_a进行代理,返回的对象proxy_a为目标对象obj_a的代理对象; - 其次,因为对
obj_a的set操作进行拦截调用foo函数,因此proxy_a的属性赋值操作,会实时的运行foo函数,这样就实时更新b值。 - 只有对
proxy_a的属性赋值才会自动调用foo函数,如果是对obj_a的属性赋值,或者直接对a赋值,则不会自动调用foo函数。
为了简化后续分析,我们将上面的问题进行进一步抽象和简化:
- 因为这里重点关注的是响应式机制的实现,而
Proxy的输入又只能适用于对象,因此文章后面部分我们只考虑对象类型的参数(因为基本类型也可以通过简单封装,改造成对象)。 - 另外,
b存在的意义在于观察程序是否能够响应proxy_a.a的变化,因此用一个console.log(proxy_a.a)也能购代替b的作用,而且还少一个变量。
可以将代码改成如下:
let obj_a = {a}
let proxy_a = new Proxy(obj_a, {
get(target, property) {
return target[property]
},
set(target, property, value) {
target[property] = value
foo()
return true
}
})
functon foo() {
console.log('foo ', proxy_a.a)
}
只要在每次proxy_a.a赋值,看下console.log输出的值是否也有响应变化,就能判断响应式机制是否正确实现了。
多个类似的foo函数的情况
前面的例子中正好只有一个foo函数,且这个foo函数正好用到了proxy_a.a,因此我们在set操作的时候可以精准的知道需要调用的哪个函数和函数名。 但如果是如下情况呢?
let obj_a = {a:1}
let proxy_a = new Proxy(obj_a, {
get(target, property) {
return target[property]
},
set(target, property, value) {
target[property] = value
foo() // 这里调用的是foo,而不是foo1还foo2,原因是什么?
return true
}
})
function foo() {
console.log('foo ', proxy_a.a)
}
function foo1() {
console.log('foo1')
}
function foo2() {
console.log('foo2', proxy_a.a)
}
.....
这里有多个类似的foo函数,有些和proxy_a有关,有些又没有关系。如果我们想事前就实现一个通用的proxy_a,在定义proxy_a的set操作的时候,如何知道该调用哪个函数呢?
一个有效的实现:
- 首先运行每个
foo函数,运行过程中检查是否有用到proxy_a的get方法; - 如果有的话,就将当前
foo函数和当前的代理对象proxy_a的关系记录下来; - 后续如果有用到
proxy_a的set方法时,再从这个对应关系获得相关联的foo函数,并依次执行。
let obj_a ={a: 1}
let set = new Set() // 保存和proxy_a有关联的函数变量
let activeFunc = undefined // 于记录当前运行的是哪个函数变量
let foo = function() {
activeFunc = foo
console.log('foo =', proxy_a.a)
activeFunc = undefined
}
let foo1 = function() {
activeFunc = foo1
console.log('foo1')
activeFunc = undefined
}
let foo2 = function() {
activeFunc = foo2
console.log('foo2 =', proxy_a.a)
activeFunc = undefined
}
let proxy_a = new Proxy(obj_a, {
get(target, property) {
if(activeFunc) {
set.add(activeFunc)
}
return target[property]
},
set(target, property, value) {
for(let item of set) {
item()
}
target[property] = value
return true
}
})
foo()
foo1()
foo2()
//....
proxy_a.a = 3
// ...
proxy_a.a = 4
set保存和proxy_a有关联的函数变量;activeFunc用于记录当前运行的是哪个函数;- 如果当前运行
foo函数中,存在proxy_a.a,那么就会进入到get拦截器,将当前的函数变量添加到Set集合; - 运行完全部的
foo函数,Set集合保存了所有和proxy_a.a有关系的函数变量; - 后续在给
proxy_a.a赋值的时候,进入到set拦截器,将Set集合中保存的函数逐个执行。
总结
以上就是Vue3响应式实现的思路原型。我们总结下:
- Vue3的响应式处理的是对象,我们称为目标对象;如果不是对象,可以通过处理,封装成一个对象;
- 利用
Proxy,从目标对象生成一个代理对象,代理对象的属性读写操作可以进行拦截预处理; - 将响应式关联逻辑封装到函数中,且响应式逻辑中的目标对象须替换为代理对象;
- 程序运行的同时也同时运行上述封装的函数,并收集和代理对象
get操作有关的函数变量,保存到一个Set集合中; - 执行完毕之后,如有对代理对象的属性赋值操作,则会触发
Set集合中的函数变量依次执行,从而完成响应式流程。
3684

被折叠的 条评论
为什么被折叠?



