场景
我们考虑一下下边的代码会输出什么。
import { observe } from "./reactive";
import Watcher from "./watcher";
const data = {
text: "hello, world",
ok: true,
};
observe(data);
const updateComponent = () => {
console.log("收到", data.ok ? data.text : "not");
};
new Watcher(updateComponent); // updateComponent 执行一次函数,输出 hello, world
data.ok = false; // updateComponent 执行一次函数,输出 not
data.text = "hello, liang"; // updateComponent 会执行吗?
我们来一步一步理清:
observer(data)
拦截了 中 和 的 ,并且各自初始化了一个 实例,用来保存依赖它们的 对象。data
text
ok
get、set
Dep
Watcher
new Watcher(updateComponent)
这一步会执行 函数,执行过程中用到的所有对象属性,会将 收集到相应对象属性中的 中。updateComponent
Watcher
Dep
当然这里的 其实是同一个,所以用了指向的箭头。Watcher
data.ok = false
这一步会触发 ,从而执行 中所有的 ,此时就会执行一次 。set
Dep
Watcher
updateComponent
执行 就会重新读取 中的属性,触发 ,然后继续收集 。updateComponent
data
get
Watcher
重新执行 函数 的时候:updateComponent
const updateComponent = () => {
console.log("收到", data.ok ? data.text : "not");
};
因为 的值变为 ,所以就不会触发 的 , 的 就不会变化了。data.ok
false
data.text
get
text
Dep
而 会继续执行,触发 收集 ,但由于我们 中使用的是数组,此时收集到的两个 其实是同一个,这里是有问题,会导致 重复执行,一会儿我们来解决下。data.ok
get
Watcher
Dep
Wacher
updateComponent
data.text = "hello, liang"
执行这句的时候,会触发 的 ,所以会执行一次 。但从代码来看 函数中由于 为 , 对输出没有任何影响,这次执行其实是没有必要的。text
set
updateComponent
updateComponent
data.ok
false
data.text
之所以执行了,是因为第一次执行 读取了 从而收集了 ,第二次执行 的时候, 虽然没有读到,但之前的 也没有清除掉,所以这一次改变 的时候 依旧会执行。updateComponent
data.text
Watcher
updateComponent
data.text
Watcher
data.text
updateComponent
所以我们需要的就是当重新执行 的时候,如果 已经不依赖于某个 了,我们需要将当前 从该 中移除掉。updateComponent
Watcher
Dep
Watcher
Dep
问题
总结下来我们需要做两件事情。
- 去重, 中不要重复收集 。
Dep
Watcher
- 重置,如果该属性对 中的 已经没有影响了(换句话就是, 中的 已经不会读取到该属性了 ),就将该 从该属性的 中删除。
Dep
Wacher
Watcher
updateComponent
Watcher
Dep
去重
去重的话有两种方案:
Dep
中的 数组换为 。subs
Set
- 每个 对象引入 , 对象中记录所有的 的 ,下次重新收集依赖的时候,如果 的 已经存在,就不再收集该 了。
Dep
id
Watcher
Dep
id
Dep
id
Watcher
Vue2
源码中采用的是方案 这里我们实现下:2
Dep
类的话只需要引入 即可。id
let uid = 0;
export default class Dep {
static target; //当前在执行的函数
subs; // 依赖的函数
id; // Dep 对象标识
constructor() {
this.id = uid++;
this.subs = []; // 保存所有需要执行的函数
}
addSub(sub) {
this.subs.push(sub);
}
depend() {
if (Dep.target) {
// 委托给 Dep.target 去调用 addSub
Dep.target.addDep(this);
}
}
notify() {
for (let i = 0, l = this.subs.length; i < l; i++) {
this.subs[i].update();
}
}
}
Dep.target = null; // 静态变量,全局唯一
在 中,我们引入 来记录所有的 。Watcher
this.depIds
id
import Dep from "./dep";
export default class Watcher {
constructor(Fn) {
this.getter = Fn;
this.depIds = new Set(); // 拥有 has 函数可以判断是否存在某个 id
this.get();
}
get() {
Dep.target = this; // 保存包装了当前正在执行的函数的 Watcher
let value;
try {
value = this.getter.call();
} catch (e) {
throw e;
} finally {
this.cleanupDeps();
}
return value;
}
addDep(dep) {
const id = dep.id;
if (!this.depIds.has(id)) {
dep.addSub(this);
}
}
update() {
this.run();
}
run() {
this.get();
}
}
重置
同样是两个方案:
- 全量式移除,保存 所影响的所有 对象,当重新收集 的前,把当前 从记录中的所有 对象中移除。
Watcher
Dep
Watcher
Watcher
Dep
- 增量式移除,重新收集依赖时,用一个新的变量记录所有的 对象,之后再和旧的 对象列表比对,如果新的中没有,旧的中有,就将当前 从该 对象中移除。
Dep
Dep
Watcher
Dep
Vue2
中采用的是方案 ,这里也实现下。2
首先是 类,我们需要提供一个 方法。Dep
removeSub
import { remove } from "./util";
let uid = 0;
export default class Dep {
static target; //当前在执行的函数
subs; // 依赖的函数
id; // Dep 对象标识
constructor() {
this.id = uid++;
this.subs = []; // 保存所有需要执行的函数
}
addSub(sub) {
this.subs.push(sub);
}
removeSub(sub) {
remove(this.subs, sub);
}
depend() {
if (Dep.target) {
// 委托给 Dep.target 去调用 addSub
Dep.target.addDep(this);
}
}
notify() {
for (let i = 0, l = this.subs.length; i < l; i++) {
this.subs[i].update();
}
}
}
Dep.target = null; // 静态变量,全局唯一
然后是 类,我们引入 来保存所有的旧 对象,引入 来保存所有的新 对象。Watcher
this.deps
Dep
this.newDeps
Dep
import Dep from "./dep";
export default class Watcher {
constructor(Fn) {
this.getter = Fn;
this.depIds = new Set(); // 拥有 has 函数可以判断是否存在某个 id
this.deps = [];
this.newDeps = []; // 记录新一次的依赖
this.newDepIds = new Set();
this.get();
}
get() {
Dep.target = this; // 保存包装了当前正在执行的函数的 Watcher
let value;
try {
value = this.getter.call();
} catch (e) {
throw e;
} finally {
this.cleanupDeps();
}
return value;
}
addDep(dep) {
const id = dep.id;
// 新的依赖已经存在的话,同样不需要继续保存
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id);
this.newDeps.push(dep);
if (!this.depIds.has(id)) {
dep.addSub(this);
}
}
}
cleanupDeps() {
let i = this.deps.length;
// 比对新旧列表,找到旧列表里有,但新列表里没有,来移除相应 Watcher
while (i--) {
const dep = this.deps[i];
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this);
}
}
// 新的列表赋值给旧的,新的列表清空
let tmp = this.depIds;
this.depIds = this.newDepIds;
this.newDepIds = tmp;
this.newDepIds.clear();
tmp = this.deps;
this.deps = this.newDeps;
this.newDeps = tmp;
this.newDeps.length = 0;
}
update() {
this.run();
}
run() {
this.get();
}
}
测试
回到开头的代码
import { observe } from "./reactive";
import Watcher from "./watcher";
const data = {
text: "hello, world",
ok: true,
};
observe(data);
const updateComponent = () => {
console.log("收到", data.ok ? data.text : "not");
};
new Watcher(updateComponent); // updateComponent 执行一次函数,输出 hello, world
data.ok = false; // updateComponent 执行一次函数,输出 not
data.text = "hello, liang"; // updateComponent 会执行吗?
此时 修改的话就不会再执行 了,因为第二次执行的时候,我们把 中 里的 清除了。data.text
updateComponent
data.text
Dep
Watcher
总结
今天这个主要就是对响应式系统的一点优化,避免不必要的重新执行。所做的事情就是重新调用函数的时候,把已经没有关联的 去除。Watcher
不知道看到这里大家有没有一个疑问,我是一直没想到说服我的点,欢迎一起交流:
在解决去重问题上,我们是引入了 ,但如果直接用 其实就可以。在 类中是用 来存 ,用数组来存 对象,为什么不直接用 来存 对象呢?id
set
Watcher
Set
id
Dep
Set
Dep
到此这篇关于Vue2 响应式系统之分支切换的文章就介绍到这了,更多相关Vue2分支切换内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!