Vue2响应性和依赖收集——vue2源码探究(1)

100 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 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官方文档,它可以修改一个对象的属性,同时也可以修改对象的gettersetter,于是借助这个方法我们就可以实现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

依赖收集

虽然按照上面的方法我们实现了ba的绑定,但是总不能每次跟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