简单地说,响应式状态(state reactivity)是指当程序状态发生改变时,比如说某个变量的值发生了变化,就自动执行某些指定的操作。
这个功能主要分成两个部分:
- 当一个变量发生改变时,它能发出通知。
- 收集变量的依赖函数,即哪些函数对这个变量的变化是感兴趣的,把它们收集起来,变量改变时通知它们。
由于不存在变量“赋值钩子”这种东西,我们没法直接监听变量的修改情况,所以需要先把变量变成对象,用 getter
和 setter
来代替读取和赋值操作。
class Dep {
constructor(value) {
this._value = value;
}
get value() {
return this._value;
}
set value(val) {
this._value = val;
}
}
Dep
的实例是一个响应式的对象,目前它只提供了两个“钩子”(getter 和 setter),让我们可以在读值和赋值的时候进行某些操作。
接下来我们来实现第一部分的功能,当变量值改变时,发出通知。
class Dep {
constructor() {
this.subscribers = [];
}
notify() {
this.subscribers.forEach(fn => fn());
}
set value(val) {
this._value = val;
this.notify;
}
}
subscribers
里面存放着当前变量的依赖函数。notify
,通知各个依赖函数,在 setter 钩子里触发notify
,执行各个依赖函数。
然后是收集依赖函数的工作。很简单,在 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
就剩下depend
和notify
两个功能了。
PS. 如果有人对完整代码中的 autorun 函数有疑问的话,可以看下这里,注意,个人理解而已。