响应系统
前置学习
响应式数据与副作用函数
副作用: 是指在函数调用过程中对函数外部的状态进行修改的行为
// 全局变量
let val = 1
function effect() {
val = 2 // 修改全局变量,产生副作用
}
响应式数据(Reactive Data):是一种编程范式,其中数据模型会自动反映其变化,从而使得视图和数据之间保持同步 目前 不能实现 修改 obj.text 同步实现 body 变化
const obj = { text: 'hello world' }
function effect() {
// effect 函数的执行会读取 obj.text
document.body.innerText = obj.text
}
响应数据基本实现
响应数据对于上面代码来说,修改 obj 的值时,重新执行 effect
我们可以观察代码发现
- 当副作用函数 effect 执行时,会触发字段 obj.text 的
读取
操作; - 当修改 obj.text 的值时,会触发字段 obj.text 的
设置
操作。
实现思路:副作用读取值,将副作用放在一个“桶”里,修改对象值时,从桶里取出,然后执行副作用
在 ES2015之前,只能通过 Object.defineProperty 函数实现,这也是 Vue.js 2 所采用的方式。在 ES2015+ 中,我们可以使用代理对象 Proxy 来实现(vue3)
const bucket = new Set();
// 原始数据
const data = { text: 'hello world' };
// 对原始数据的代理
const obj = new Proxy(data, {
// 拦截读取操作
get(target, key) {
// 将副作用函数 effect 添加到存储副作用函数的桶中
bucket.add(effect);
// 返回属性值
return target[key];
},
// 拦截设置操作
set(target, key, newVal) {
// 设置属性值
target[key] = newVal;
// 把副作用函数从桶里取出并执行
bucket.forEach((fn) => fn());
// 返回 true 代表设置操作成功
return true;
},
});
// 副作用函数
function effect() {
document.body.innerText = obj.text;
}
// 执行副作用函数,触发读取
effect();
// 1 秒后修改响应式数据
setTimeout(() => {
obj.text = 'hello vue3';
}, 1000);
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Demo</title>
</head>
<body>
</body>
<script src="./effect.js"></script>
</html>
设计一个完善的响应系统
提供一个用来注册副作用函数的机制,解决硬编码 副作用函数名称
const bucket = new Set();
// 原始数据
const data = { text: 'hello world' };
// 对原始数据的代理
const obj = new Proxy(data, {
// 拦截读取操作
get(target, key) {
// 将副作用函数 effect 添加到存储副作用函数的桶中
if (activeEffect) {
bucket.add(activeEffect);
}
// 返回属性值
return target[key];
},
// 拦截设置操作
set(target, key, newVal) {
// 设置属性值
target[key] = newVal;
// 把副作用函数从桶里取出并执行
bucket.forEach((fn) => fn());
// 返回 true 代表设置操作成功
return true;
},
});
// 用一个全局变量存储被注册的副作用函数
let activeEffect;
// effect 函数用于注册副作用函数
function effect(fn) {
// 当调用 effect 注册副作用函数时,将副作用函数 fn 赋值给 activeEffect
activeEffect = fn;
// 执行副作用函数
fn();
}
// 一个匿名的副作用函数
effect(() => {
document.body.innerText = obj.text;
});
// 删除原来 副作用函数
// function effect() {
// document.body.innerText = obj.text;
// }
// 执行副作用函数,触发读取
// effect();
// 1 秒后修改响应式数据
setTimeout(() => {
obj.text = 'hello vue3';
}, 1000);
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Demo</title>
</head>
<body>
</body>
<script src="./1_effect.js"></script>
</html>
新问题,如果给响应数据添加一个不存在的值,依旧会触发
const bucket = new Set();
// 原始数据
const data = { text: 'hello world' };
// 对原始数据的代理
const obj = new Proxy(data, {
// 拦截读取操作
get(target, key) {
// 将副作用函数 effect 添加到存储副作用函数的桶中
if (activeEffect) {
bucket.add(activeEffect);
}
// 返回属性值
return target[key];
},
// 拦截设置操作
set(target, key, newVal) {
// 设置属性值
target[key] = newVal;
// 把副作用函数从桶里取出并执行
bucket.forEach((fn) => fn());
// 返回 true 代表设置操作成功
return true;
},
});
// 用一个全局变量存储被注册的副作用函数
let activeEffect;
// effect 函数用于注册副作用函数
function effect(fn) {
// 当调用 effect 注册副作用函数时,将副作用函数 fn 赋值给 activeEffect
activeEffect = fn;
// 执行副作用函数
fn();
}
// 一个匿名的副作用函数
effect(() => {
// 执行两次
console.log('effect执行');
document.body.innerText = obj.text;
});
// 1 秒后修改响应式数据
setTimeout(() => {
// 正常情况 修改 abc 应该不执行 effect
obj.abc = 'hello vue3';
}, 1000);
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Demo</title>
</head>
<body>
</body>
<script src="./2_run2_effect.js"></script>
</html>
为了解决这个问题,我们需要重新设计一个桶的数据结构,由于之前桶是 set 作为数据存储,没有保存副作用函数和操作目标之间的关系 先观察下面代码
effect(function effectFn() {
document.body.innerText = obj.text
})
- 被操作(读取)的对象 obj
- 被操作(读取)的对象属性 text
- 注册的副作用方法 effectFn
如果用 target 来表示一个代理对象所代理的原始对象,用 key 来表示被操作的字段名,用 effectFn 来表示被注册的副作用函数,那么可以为这三个角色建立如下关系
target
└── key
└── effectFn
// 存储副作用函数的桶 Set 改为 WeakMap
const bucket = new WeakMap();
// 原始数据
const data = { text: 'hello world' };
// 用一个全局变量存储被注册的副作用函数
let activeEffect;
// 对原始数据的代理
const obj = new Proxy(data, {
// 拦截读取操作
get(target, key) {
// 将副作用函数 activeEffect 添加到存储副作用函数的桶中
track(target, key);
// 返回属性值
return target[key];
},
// 拦截设置操作
set(target, key, newVal) {
// 设置属性值
target[key] = newVal;
// 把副作用函数从桶里取出并执行
trigger(target, key);
},
});
// 在 get 拦截函数内调用 track 函数追踪变化
function track(target, key) {
// 没有 activeEffect,直接 return
if (!activeEffect) {
return target[key];
}
// 根据 target 从“桶”中取得 depsMap,它也是一个 Map 类型:key --> effects
let depsMap = bucket.get(target);
// 如果不存在 depsMap,那么新建一个 Map 并与 target 关联
if (!depsMap) {
bucket.set(target, (depsMap = new Map()));
}
// 再根据 key 从 depsMap 中取得 deps,它是一个 Set 类型,
// 里面存储着所有与当前 key 相关联的副作用函数:effects
let deps = depsMap.get(key);
// 如果 deps 不存在,同样新建一个 Set 并与 key 关联
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
// 最后将当前激活的副作用函数添加到“桶”里
deps.add(activeEffect);
}
// 在 set 拦截函数内调用 trigger 函数触发变化
function trigger(target, key) {
// 根据 target 从桶中取得 depsMap,它是 key --> effects
const depsMap = bucket.get(target);
if (!depsMap) return;
// 根据 key 取得所有副作用函数 effects
const effects = depsMap.get(key);
// 执行副作用函数
effects && effects.forEach((fn) => fn());
}
// effect 函数用于注册副作用函数
function effect(fn) {
// 当调用 effect 注册副作用函数时,将副作用函数 fn 赋值给 activeEffect
activeEffect = fn;
// 执行副作用函数
fn();
}
// 一个匿名的副作用函数
effect(() => {
// 执行两次
console.log('effect执行');
document.body.innerText = obj.text;
});
// 1 秒后修改响应式数据
setTimeout(() => {
// 正常情况 修改 abc 应该不执行 effect
obj.text = 'hello vue3';
}, 1000);
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Demo</title>
</head>
<body>
</body>
<script src="./3_fix_run2_effect.js"></script>
</html>
其中 WeakMap 的键是原始对象 target,WeakMap 的值是一个 Map 实例,而 Map 的键是原始对象 target 的 key,Map 的值是一个由副作用函数组成的 Set。它们的关系如图
分支切换和 cleanup
明确分支切换的定义(值变化会导致执行代码分支变化)
const data = { ok: true, text: 'hello world' }
const obj = new Proxy(data, { /* ... */ })
effect(function effectFn() {
document.body.innerText = obj.ok ? obj.text : 'not'
})
联系示意图
data
└── ok
└── effectFn
└── text
└── effectFn
出现的问题:当修改 ok 为 false 的时候,再修改 text 应该不在影响系统,不应该再执行副作用函数,但目前还做不到
// 存储副作用函数的桶 Set 改为 WeakMap
const bucket = new WeakMap();
// 原始数据 变为两个属性
const data = { ok: true, text: 'hello world' };
// 用一个全局变量存储被注册的副作用函数
let activeEffect;
// 对原始数据的代理
const obj = new Proxy(data, {
// 拦截读取操作
get(target, key) {
// 将副作用函数 activeEffect 添加到存储副作用函数的桶中
track(target, key);
// 返回属性值
return target[key];
},
// 拦截设置操作
set(target, key, newVal) {
// 设置属性值
target[key] = newVal;
// 把副作用函数从桶里取出并执行
trigger(target, key);
},
});
// 在 get 拦截函数内调用 track 函数追踪变化
function track(target, key) {
// 没有 activeEffect,直接 return
if (!activeEffect) {
return target[key];
}
// 根据 target 从“桶”中取得 depsMap,它也是一个 Map 类型:key --> effects
let depsMap = bucket.get(target);
// 如果不存在 depsMap,那么新建一个 Map 并与 target 关联
if (!depsMap) {
bucket.set(target, (depsMap = new Map()));
}
// 再根据 key 从 depsMap 中取得 deps,它是一个 Set 类型,
// 里面存储着所有与当前 key 相关联的副作用函数:effects
let deps = depsMap.get(key);
// 如果 deps 不存在,同样新建一个 Set 并与 key 关联
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
// 最后将当前激活的副作用函数添加到“桶”里
deps.add(activeEffect);
}
// 在 set 拦截函数内调用 trigger 函数触发变化
function trigger(target, key) {
// 根据 target 从桶中取得 depsMap,它是 key --> effects
const depsMap = bucket.get(target);
if (!depsMap) return;
// 根据 key 取得所有副作用函数 effects
const effects = depsMap.get(key);
// 执行副作用函数
effects && effects.forEach((fn) => fn());
}
// effect 函数用于注册副作用函数
function effect(fn) {
// 当调用 effect 注册副作用函数时,将副作用函数 fn 赋值给 activeEffect
activeEffect = fn;
// 执行副作用函数
fn();
}
// 一个匿名的副作用函数
effect(() => {
console.log('effect执行');
// 改为条件判断
document.body.innerText = obj.ok ? obj.text : 'not';
});
// 1 秒后修改响应式数据
setTimeout(() => {
// 会正常触发
obj.ok = false;
}, 1000);
// 2 秒后修改响应式数据
setTimeout(() => {
// 正常情况 修改 text 应该不执行 effect 但还是执行了
obj.text = 'hello vue3';
}, 2000);
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Demo</title>
</head>
<body>
</body>
<script src="./4_branch_switch_effect.js"></script>
</html>
解决这个问题的思路很简单,每次副作用函数执行时,我们可以先把它从所有与之关联的依赖集合中删除 如果我们能做到每次副作用函数执行前,将其从相关联的依赖集合中移除,那么问题就迎刃而解了
// 存储副作用函数的桶 Set 改为 WeakMap
const bucket = new WeakMap();
// 原始数据 变为两个属性
const data = { ok: true, text: 'hello world' };
// 用一个全局变量存储被注册的副作用函数
let activeEffect;
// 对原始数据的代理
const obj = new Proxy(data, {
// 拦截读取操作
get(target, key) {
// 将副作用函数 activeEffect 添加到存储副作用函数的桶中
track(target, key);
// 返回属性值
return target[key];
},
// 拦截设置操作
set(target, key, newVal) {
// 设置属性值
target[key] = newVal;
// 把副作用函数从桶里取出并执行
trigger(target, key);
},
});
// 在 get 拦截函数内调用 track 函数追踪变化
function track(target, key) {
// 没有 activeEffect,直接 return
if (!activeEffect) {
return;
}
// 根据 target 从“桶”中取得 depsMap,它也是一个 Map 类型:key --> effects
let depsMap = bucket.get(target);
// 如果不存在 depsMap,那么新建一个 Map 并与 target 关联
if (!depsMap) {
bucket.set(target, (depsMap = new Map()));
}
// 再根据 key 从 depsMap 中取得 deps,它是一个 Set 类型,
// 里面存储着所有与当前 key 相关联的副作用函数:effects
let deps = depsMap.get(key);
// 如果 deps 不存在,同样新建一个 Set 并与 key 关联
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
// 最后将当前激活的副作用函数添加到“桶”里
deps.add(activeEffect);
// deps 就是一个与当前副作用函数存在联系的依赖集合
// 将其添加到 activeEffect.deps 数组中
activeEffect.deps.push(deps); // 新增
}
// 在 set 拦截函数内调用 trigger 函数触发变化
function trigger(target, key) {
// 根据 target 从桶中取得 depsMap,它是 key --> effects
const depsMap = bucket.get(target);
if (!depsMap) return;
// 根据 key 取得所有副作用函数 effects
const effects = depsMap.get(key);
// 执行副作用函数
// 使用一个新 set 避免死循环
const effectsToRun = new Set(effects); // 新增
effectsToRun.forEach((effectFn) => effectFn()); // 新增
// effects && effects.forEach((fn) => fn()); // 删除
}
function cleanup(effectFn) {
// 遍历 effectFn.deps 数组
for (let i = 0; i < effectFn.deps.length; i++) {
// deps 是依赖集合
const deps = effectFn.deps[i];
// 将 effectFn 从依赖集合中移除
deps.delete(effectFn);
}
// 最后需要重置 effectFn.deps 数组
effectFn.deps.length = 0;
}
function effect(fn) {
// activeEffect = fn;
// fn();
const effectFn = () => {
// 当 effectFn 执行时,将其设置为当前激活的副作用函数
activeEffect = effectFn;
// 调用 cleanup 函数完成清除工作
cleanup(effectFn); // 新增
fn();
};
// activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = [];
// 执行副作用函数
effectFn();
}
// 一个匿名的副作用函数
effect(() => {
console.log('effect执行');
// 改为条件判断
document.body.innerText = obj.ok ? obj.text : 'not';
});
// 1 秒后修改响应式数据
setTimeout(() => {
// 会正常触发
obj.ok = false;
}, 1000);
// 2 秒后修改响应式数据
setTimeout(() => {
// 正常情况 修改 text 应该不执行 effect 但还是执行了
obj.text = 'hello vue3';
}, 2000);
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Demo</title>
</head>
<body>
</body>
<script src="./5_branch_switch_effect.js"></script>
</html>
支持嵌套
默认不支持嵌套,主要由于多层嵌套会导致 全局参数 activeEffect
被覆盖,所以新加字段 effectStack
来存储
// 存储副作用函数的桶 Set 改为 WeakMap
const bucket = new WeakMap();
// 原始数据 变为两个属性
const data = { foo: true, bar: true };
// 用一个全局变量存储被注册的副作用函数
let activeEffect;
// effect 栈
const effectStack = []; // 新增
// 对原始数据的代理
const obj = new Proxy(data, {
// 拦截读取操作
get(target, key) {
// 将副作用函数 activeEffect 添加到存储副作用函数的桶中
track(target, key);
// 返回属性值
return target[key];
},
// 拦截设置操作
set(target, key, newVal) {
// 设置属性值
target[key] = newVal;
// 把副作用函数从桶里取出并执行
trigger(target, key);
},
});
// 在 get 拦截函数内调用 track 函数追踪变化
function track(target, key) {
// 没有 activeEffect,直接 return
if (!activeEffect) {
return;
}
// 根据 target 从“桶”中取得 depsMap,它也是一个 Map 类型:key --> effects
let depsMap = bucket.get(target);
// 如果不存在 depsMap,那么新建一个 Map 并与 target 关联
if (!depsMap) {
bucket.set(target, (depsMap = new Map()));
}
// 再根据 key 从 depsMap 中取得 deps,它是一个 Set 类型,
// 里面存储着所有与当前 key 相关联的副作用函数:effects
let deps = depsMap.get(key);
// 如果 deps 不存在,同样新建一个 Set 并与 key 关联
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
// 最后将当前激活的副作用函数添加到“桶”里
deps.add(activeEffect);
// deps 就是一个与当前副作用函数存在联系的依赖集合
// 将其添加到 activeEffect.deps 数组中
activeEffect.deps.push(deps); // 新增
}
// 在 set 拦截函数内调用 trigger 函数触发变化
function trigger(target, key) {
// 根据 target 从桶中取得 depsMap,它是 key --> effects
const depsMap = bucket.get(target);
if (!depsMap) return;
// 根据 key 取得所有副作用函数 effects
const effects = depsMap.get(key);
// 执行副作用函数
// 使用一个新 set 避免死循环
const effectsToRun = new Set(effects);
effectsToRun.forEach((effectFn) => effectFn());
}
function cleanup(effectFn) {
// 遍历 effectFn.deps 数组
for (let i = 0; i < effectFn.deps.length; i++) {
// deps 是依赖集合
const deps = effectFn.deps[i];
// 将 effectFn 从依赖集合中移除
deps.delete(effectFn);
}
// 最后需要重置 effectFn.deps 数组
effectFn.deps.length = 0;
}
function effect(fn) {
const effectFn = () => {
// 调用 cleanup 函数完成清除工作
cleanup(effectFn);
// 当 effectFn 执行时,将其设置为当前激活的副作用函数
activeEffect = effectFn;
// 用一个全局变量存储当前激活的 effect 函数
// 在调用副作用函数之前将当前副作用函数压入栈中
effectStack.push(effectFn); // 新增
fn();
// 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并把 activeEffect 还原为之前的值
effectStack.pop(); // 新增
activeEffect = effectStack[effectStack.length - 1]; // 新增
};
// activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = [];
// 执行副作用函数
effectFn();
}
// 全局变量
let temp1, temp2;
// effectFn1 嵌套了 effectFn2
effect(function effectFn1() {
console.log('effectFn1 执行');
effect(function effectFn2() {
console.log('effectFn2 执行');
// 在 effectFn2 中读取 obj.bar 属性
temp2 = obj.bar;
});
// 在 effectFn1 中读取 obj.foo 属性
temp1 = obj.foo;
});
// 1 秒后修改响应式数据
setTimeout(() => {
// 会正常触发
obj.bar = false;
}, 1000);
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Demo</title>
</head>
<body>
</body>
<script src="./6_嵌套_effect.js"></script>
</html>
避免无限循环
如果在副作用函数内修改响应数据,目前代码会死循环,原因就是 在读取 foo 的同时,还要修改 foo 导致不断循环
const data = { foo: 1 }
const obj = new Proxy(data, { /*...*/ })
effect(() => obj.foo++)
添加判断 修改运行的副作用和当时的不是一个才可以运行(如果嵌套还是有问题,需要遍历数组)
// 存储副作用函数的桶 Set 改为 WeakMap
const bucket = new WeakMap();
// 原始数据 变为两个属性
const data = { foo: 1, foo1: 1 };
// 用一个全局变量存储被注册的副作用函数
let activeEffect;
// effect 栈
const effectStack = [];
// 对原始数据的代理
const obj = new Proxy(data, {
// 拦截读取操作
get(target, key) {
// 将副作用函数 activeEffect 添加到存储副作用函数的桶中
track(target, key);
// 返回属性值
return target[key];
},
// 拦截设置操作
set(target, key, newVal) {
// 设置属性值
target[key] = newVal;
// 把副作用函数从桶里取出并执行
trigger(target, key);
},
});
// 在 get 拦截函数内调用 track 函数追踪变化
function track(target, key) {
// 没有 activeEffect,直接 return
if (!activeEffect) {
return;
}
// 根据 target 从“桶”中取得 depsMap,它也是一个 Map 类型:key --> effects
let depsMap = bucket.get(target);
// 如果不存在 depsMap,那么新建一个 Map 并与 target 关联
if (!depsMap) {
bucket.set(target, (depsMap = new Map()));
}
// 再根据 key 从 depsMap 中取得 deps,它是一个 Set 类型,
// 里面存储着所有与当前 key 相关联的副作用函数:effects
let deps = depsMap.get(key);
// 如果 deps 不存在,同样新建一个 Set 并与 key 关联
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
// 最后将当前激活的副作用函数添加到“桶”里
deps.add(activeEffect);
// deps 就是一个与当前副作用函数存在联系的依赖集合
// 将其添加到 activeEffect.deps 数组中
activeEffect.deps.push(deps); // 新增
}
// 在 set 拦截函数内调用 trigger 函数触发变化
function trigger(target, key) {
// 根据 target 从桶中取得 depsMap,它是 key --> effects
const depsMap = bucket.get(target);
if (!depsMap) return;
// 根据 key 取得所有副作用函数 effects
const effects = depsMap.get(key);
// 执行副作用函数
// 使用一个新 set 避免死循环
const effectsToRun = new Set(effects);
effectsToRun.forEach((effectFn) => {
effectFn !== activeEffect &&
effectStack.every((effectFn) => effectFn !== activeEffect) &&
effectFn();
});
}
function cleanup(effectFn) {
// 遍历 effectFn.deps 数组
for (let i = 0; i < effectFn.deps.length; i++) {
// deps 是依赖集合
const deps = effectFn.deps[i];
// 将 effectFn 从依赖集合中移除
deps.delete(effectFn);
}
// 最后需要重置 effectFn.deps 数组
effectFn.deps.length = 0;
}
function effect(fn) {
const effectFn = () => {
// 调用 cleanup 函数完成清除工作
cleanup(effectFn);
// 当 effectFn 执行时,将其设置为当前激活的副作用函数
activeEffect = effectFn;
// 用一个全局变量存储当前激活的 effect 函数
// 在调用副作用函数之前将当前副作用函数压入栈中
effectStack.push(effectFn); // 新增
fn();
// 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并把 activeEffect 还原为之前的值
effectStack.pop(); // 新增
activeEffect = effectStack[effectStack.length - 1]; // 新增
};
// activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = [];
// 执行副作用函数
effectFn();
}
effect(function effectFn1() {
console.log('effectFn1 执行');
obj.foo++;
effect(function effectFn2() {
console.log('effectFn2 执行');
obj.foo1++;
});
});
// 1 秒后修改响应式数据
setTimeout(() => {
obj.foo++;
}, 1000);
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Demo</title>
</head>
<body>
</body>
<script src="./7_循环_effect.js"></script>
</html>
执行调度
决定副作用函数执行的时机、次数以及方式
// 存储副作用函数的桶 Set 改为 WeakMap
const bucket = new WeakMap();
// 原始数据 变为两个属性
const data = { foo: 1, foo1: 1 };
// 用一个全局变量存储被注册的副作用函数
let activeEffect;
// effect 栈
const effectStack = [];
// 对原始数据的代理
const obj = new Proxy(data, {
// 拦截读取操作
get(target, key) {
// 将副作用函数 activeEffect 添加到存储副作用函数的桶中
track(target, key);
// 返回属性值
return target[key];
},
// 拦截设置操作
set(target, key, newVal) {
// 设置属性值
target[key] = newVal;
// 把副作用函数从桶里取出并执行
trigger(target, key);
},
});
// 在 get 拦截函数内调用 track 函数追踪变化
function track(target, key) {
// 没有 activeEffect,直接 return
if (!activeEffect) {
return;
}
// 根据 target 从“桶”中取得 depsMap,它也是一个 Map 类型:key --> effects
let depsMap = bucket.get(target);
// 如果不存在 depsMap,那么新建一个 Map 并与 target 关联
if (!depsMap) {
bucket.set(target, (depsMap = new Map()));
}
// 再根据 key 从 depsMap 中取得 deps,它是一个 Set 类型,
// 里面存储着所有与当前 key 相关联的副作用函数:effects
let deps = depsMap.get(key);
// 如果 deps 不存在,同样新建一个 Set 并与 key 关联
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
// 最后将当前激活的副作用函数添加到“桶”里
deps.add(activeEffect);
// deps 就是一个与当前副作用函数存在联系的依赖集合
// 将其添加到 activeEffect.deps 数组中
activeEffect.deps.push(deps); // 新增
}
// 在 set 拦截函数内调用 trigger 函数触发变化
function trigger(target, key) {
// 根据 target 从桶中取得 depsMap,它是 key --> effects
const depsMap = bucket.get(target);
if (!depsMap) return;
// 根据 key 取得所有副作用函数 effects
const effects = depsMap.get(key);
// 执行副作用函数
// 使用一个新 set 避免死循环
const effectsToRun = new Set(effects);
effectsToRun.forEach((effectFn) => {
// effectFn !== activeEffect &&
// effectStack.every((effectFn) => effectFn !== activeEffect) &&
// effectFn();
if (
effectFn !== activeEffect &&
effectStack.every((effectFn) => effectFn !== activeEffect)
) {
// 如果一个副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递
if (effectFn.options.scheduler) {
// 新增
effectFn.options.scheduler(effectFn); // 新增
} else {
// 否则直接执行副作用函数(之前的默认行为)
effectFn(); // 新增
}
}
});
}
function cleanup(effectFn) {
// 遍历 effectFn.deps 数组
for (let i = 0; i < effectFn.deps.length; i++) {
// deps 是依赖集合
const deps = effectFn.deps[i];
// 将 effectFn 从依赖集合中移除
deps.delete(effectFn);
}
// 最后需要重置 effectFn.deps 数组
effectFn.deps.length = 0;
}
// 添加配置参数 options
function effect(fn, options = {}) {
const effectFn = () => {
// 调用 cleanup 函数完成清除工作
cleanup(effectFn);
// 当 effectFn 执行时,将其设置为当前激活的副作用函数
activeEffect = effectFn;
// 用一个全局变量存储当前激活的 effect 函数
// 在调用副作用函数之前将当前副作用函数压入栈中
effectStack.push(effectFn); // 新增
fn();
// 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并把 activeEffect 还原为之前的值
effectStack.pop(); // 新增
activeEffect = effectStack[effectStack.length - 1]; // 新增
};
// 将 options 挂载到 effectFn 上
effectFn.options = options;
// activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = [];
// 执行副作用函数
effectFn();
}
// 定义一个任务队列
const jobQueue = new Set();
// 使用 Promise.resolve() 创建一个 promise 实例,我们用它将一个任务添加到微任务队列
const p = Promise.resolve();
// 一个标志代表是否正在刷新队列
let isFlushing = false;
function flushJob() {
// 如果队列正在刷新,则什么都不做
if (isFlushing) return;
// 设置为 true,代表正在刷新
isFlushing = true;
// 在微任务队列中刷新 jobQueue 队列
p.then(() => {
jobQueue.forEach((job) => job());
}).finally(() => {
// 结束后重置 isFlushing
isFlushing = false;
});
}
effect(
() => {
console.log(obj.foo);
},
{
scheduler(fn) {
// 每次调度时,将副作用函数添加到 jobQueue 队列中
jobQueue.add(fn);
// 调用 flushJob 刷新队列
flushJob();
},
}
);
obj.foo++;
obj.foo++;
// effect(
// function effectFn1() {
// console.log('effectFn1 执行');
// console.log(obj.foo);
// }, // options
// {
// // 调度器 scheduler 是一个函数
// scheduler(fn) {
// // 将副作用函数放到宏任务队列中执行
// setTimeout(fn);
// },
// }
// );
// obj.foo++;
// console.log('完成');
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Demo</title>
</head>
<body>
</body>
<script src="./8_调度_effect.js"></script>
</html>