携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第11天,点击查看活动详情
响应性的基本原理
我们假设有两个变量a和b:
var a = 2;
var b = a*2;
这时候,两个变量不构成依赖关系,也就是说我们当给a赋其他值时,比如a=3此时b依然时初始值时的4,而不会随着a进行改变,因此我们改变方式,假设如果当a进行改变时,对b进行一次赋值,那么b就会随着a继续进行变化了,比如我们定义一个onAChange方法,当a的值发生变化时调用这个方法,b的值就会随之变化了:
function onAChange() {
b = a*2;
}
DefineProperty方法
那么,如何在每次对a赋值的时候调用这个方法来更新b呢,这时候只依靠变量就不行了,需要使用对象属性来完成这件事。Object.DefineProperty是一个可以修改对象属性的方法,具体描述可以看Mozilla官方文档,它可以修改一个对象的属性,同时也可以修改对象的getter和setter,于是借助这个方法我们就可以实现a变化的同时让b也随之变化:
var obj = {a:2}
var b = obj.a*2;
Object.defineProperty(obj,"a",{
set(nval) {
b = nval * 2;
}
})
obj.a = 4;
console.log(b);// 输出 8
obj.a = 8;
console.log(b);// 输出 16
依赖收集
虽然按照上面的方法我们实现了b和a的绑定,但是总不能每次跟a绑定的变量和方法都要跑到set方法里去写一遍,那么我们能不能每次在跟a相关的代码调用时自动将相关的方法保存下来,在每次对a进行赋值的时候调用这些方法,从而实现变量a的依赖收集呢?
答案当然是可以的,但是我们需要两个步骤:
- 将调用
a的方法保存下来 - 在
a进行赋值时调用这些方法
我们首先完成步骤1:
var obj = {a:1};
var aFunctionList = new Set();// 使用Set而非Array是为了去重,相同的方法再次调用时不用再次保存
var currentFun = null;// 用于临时保存当前方法以传递给getter
var currentA = obj.a;// 单独存取a值避免getter中出现递归而栈溢出
Object.defineProperty(obj,"a",{
set(nval) {
currentA = nval
if (!aFunctionList.size) return;
aFunctionList.forEach(fun => fun())
},
get() {
if (currentFun) {
aFunctionList.add(currentFun);
}
return currentA
}
});
// 定义一个方法来对a的依赖进行收集,参数即为需要收集的依赖方法
function collect(fun) {
currentFun = fun;// 临时保存方法
fun();// 执行方法时,如果方法中调用了obj.a就会触发a的get方法,就会将当前方法保存到列表中,实现了依赖收集
currentFun = null;
}
collect(() => console.log(`我是a,我现在是${obj.a}`)); // 输出:我是a,我现在是1
obj.a++; // 输出:我是a,我现在是2
var b;
collect(() => {
b = obj.a * 2;
console.log(`我是b,受a的影响我变成了${b}`)
}) // 输出:我是b,受a的影响我变成了4
obj.a++; // 输出:我是a,我现在是3 我是b,受a的影响我变成了6
进阶
上面的方式只是对某一个属性进行依赖收集,而且因为依赖列表全都为全局变量,很容易被污染而造成依赖异常,因此,我们做以下两个优化:
- 遍历对象的所有属性进行依赖收集
- 利用遍历中产生的闭包生成当前上下文中的永久变量,避免污染
var obj = {a:1}
var currentFun = null;
Object.keys(obj).forEach(item => {
// 这里形成闭包
var functionList = new Set();
var currentValue = obj[key];
Object.defineProperty(obj,"a",{
set(nval) {
currentValue = nval
if (!aFunctionList.size) return;
aFunctionList.forEach(fun => fun())
},
get() {
if (currentFun) {
aFunctionList.add(currentFun);
}
return currentValue
}
});
})
function collect(fun) {
currentFun = fun;
fun();
currentFun = null
}
var b;
collect(() => {
b = obj.a * 2
console.log(`受a的影响,b变成了${b}`);
}) // 受a的影响,b变成了4
obj.a = 8 //受a的影响,b变成了16
collect(() => {
c = obj.a + 10
console.log(`受a的影响,c变成了${c}`)
}) //受a的影响,c变成了18
obj.a = 10 //受a的影响,b变成了20 受a的影响,c变成了20