从零实现一个响应式状态管理

概念

简单地说,响应式状态(state reactivity)是指当程序状态发生改变时,比如说某个变量的值发生了变化,就自动执行某些指定的操作。

这个功能主要分成两个部分:

  1. 当一个变量发生改变时,它能发出通知。

  2. 收集变量的依赖函数,即哪些函数对这个变量的变化是感兴趣的,把它们收集起来,变量改变时通知它们。

Dep Class

getter/setter

由于不存在变量“赋值钩子”这种东西,我们没法直接监听变量的修改情况,所以需要先把变量变成对象,用 gettersetter 来代替读取和赋值操作。

class Dep {
    constructor(value) {
        this._value = value;
    }
    get value() {
        return this._value;
    }
    set value(val) {
        this._value = val;
    }
}
  • Dep 的实例是一个响应式的对象,目前它只提供了两个“钩子”(getter 和 setter),让我们可以在读值和赋值的时候进行某些操作。

notify

接下来我们来实现第一部分的功能,当变量值改变时,发出通知。

class Dep {
    constructor() {
        this.subscribers = [];
    }
    notify() {
        this.subscribers.forEach(fn => fn());
    }
    set value(val) {
        this._value = val;
        this.notify;
    }
}
  • subscribers 里面存放着当前变量的依赖函数。

  • notify,通知各个依赖函数,在 setter 钩子里触发 notify,执行各个依赖函数。

depend

然后是收集依赖函数的工作。很简单,在 value 的 getter “钩子”里收集就行。

唯一的问题是,depend 中的依赖函数 func 是从哪里传进来的,this.depend() 是在 get value 中调用的,但 getter 也不能传参数呀。

class Dep {
    constructor() {
        this.subscribers = new Set();
    }
    depend() {
        this.subscriber.add(func);
    }
    get value() {
        this.depend();
        return this._value;
    }
}

这一点比较 tricky。在 get value 执行之前,执行流是在某个依赖函数 funcA 的内部,这时我们用一个全局变量把 funcA 存起来,接着在执行 depend 的时候读取那个全局变量就能得到 funcA 了。

const count = new Dep(1);

let activeUpdate = null;

function update() {
    activeUpdate = update;
    console.log(count.value);
}

update();
activeUpdate = null;

setTimeout(() => (count.value = 2), 2000);
  • activeUpdate,存放当前依赖函数的全局变量。

  • update,变量 count 的一个依赖函数

但是我们不能在每个依赖函数中都写上这句 activeUpdate = update; 吧。

上面代码需要重构一下,用一个 autorun 函数来完成依赖函数收集的功能,这样就不需要修改依赖函数本身的逻辑了。

const count = new Dep(1);

let activeUpdate = null;
function autorun(update) {
    activeUpdate = update;
    update();
    activeUpdate = null;
}

function update() {
    console.log(count.value);
}
autorun(update);

到这里就差不多了,我们已经实现了对一个原始值变量的监听,不过我们在用 Vue 的时候,监听的是 data 对象的所有属性。

对于对象属性,JS 有提供一个 Object.defineProperty API,可以直接将这个属性改成 getter 和 setter。

function observe(obj) {
    Object.keys(obj).forEach(key => {
        let internalValue = obj[key];
        const dep = new Dep();
        Object.defineProperty(obj, key, {
            get() {
                dep.depend();
                return internalValue;
            },
            set(newVal) {
                internalValue = newVal;
                dep.notify();
            },
        });
    });
}

class Dep {
    constructor() {
        this.subscribers = new Set();
    }
    depend() {
        activeUpdate && this.subscribers.add(activeUpdate);
    }
    notify() {
        this.subscribers.forEach(func => func());
    }
}
  • observe 函数把一个对象的所有属性都变成响应式的。

  • Dep 就剩下 dependnotify 两个功能了。

完整代码

完整代码

PS. 如果有人对完整代码中的 autorun 函数有疑问的话,可以看下这里,注意,个人理解而已。

Last updated