文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Vue2剥丝抽茧-响应式系统之分支切换

2024-12-02 04:11

关注
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 中移除掉。

问题

总结下来我们需要做两件事情。

去重

去重的话有两种方案:

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();
}
}

重置

同样是两个方案:

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 对象呢?

来源:windliang内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯