一、响应式数据与副作用函数
副作用函数:
-
effect函数执行时, 但除了effect函数之外的任何函数都可以读取或者设置body的文本内容
javascriptfunction effect() { document.body.innerText = "hello vue3" }
-
effect函数修改了全局变量, 但除了effect函数之外的任何函数都可以读取或者设置该变量
javascriptvar a = 1 function effect() { a = 2; }
响应式数据:
javascript
const obj = { text: "hello world" };
function effect() {
document.body.innerText = obj.text
}
obj.text = "hello vue3"
如果修改obj.text的值能够触发effect函数的执行, 那么obj就是响应式数据
二、响应式数据的基本实现
关键点: 属性的读取和设置
-
当副作用函数effect执行的时候, 会触发字段obj.text的读取操作
(当读取操作发生时, 将副作用函数收集到桶中)
-
当修改obj.text的值时, 会触发字段obj.text的设置操作
(当设置操作发生时,从桶中出副作用函数并执行)
javascript
const bucket = new Set();
const data = {text: "hello world"};
const obj = new Proxy(data, {
get(target, p) {
bucket.add(effect);
return target[p];
},
set(target, p, value) {
target[p] = value;
bucket.forEach(fn => fn());
return true;
}
})
function effect() {
console.log("执行了")
document.body.innerText = obj.text;
}
effect();
setTimeout(() => {
obj.text = "hello vue3"
}, 3 * 1000);
三、设计一个完善的响应系统
1、注册副作用函数
2、副作用函数与被操作字段之间建立明确的而联系(target, key, effectFn)
01 target
02 └── text1
03 └── effectFn
04 └── text2
05 └── effectFn
01 target1
02 └── text1
03 └── effectFn1
04 target2
05 └── text2
06 └── effectFn2
javascript
// 注册副作用函数
let activeEffect
function effect(fn) {
activeEffect = fn;
fn()
activeEffect = "";
}
// 桶,储存代理对象属性的相关事件
const bucket = new WeakMap();
// 跟踪函数
function track(target, key) {
if(!activeEffect) return;
let depsMap = bucket.get(target);
if(!depsMap) bucket.set(target, depsMap = new Map());
let deps = depsMap.get(key);
if(!deps) depsMap.set(key, deps = new Set())
deps.add(activeEffect);
}
// 触发函数
function trigger(target, key) {
let depsMap = bucket.get(target);
if(!depsMap) return;
let effects = depsMap.get(key);
effects && effects.forEach(fn => fn(key));
}
// 代理配置
let proxyOption = {
get(target, key, receiver) {
track(target, key);
return target[key];
},
set(target, key, newVal) {
target[key] = newVal;
trigger(target, key);
}
}
// 测试示例
const data = {text1: "hello world", text2: "hello vue3"}
const obj = new Proxy(data, proxyOption)
effect(() => console.log(obj.text1));
effect(() => console.log(obj.text2,));
setTimeout(() =>{
obj.text1 = "text1";
obj.text2 = "text2";
}, 1000)
ps: weakMap与map之间的区别
- 键的引用类型 :
map
可以使用任何类型的值作为键, 包括基本类型和引用类型, 而weakMap
只能使用引用类型作为键值, 不能使用基本类型 - 垃圾回收机制 :
map
中的键值对在不被引用时不会被自动删除, 除非手动删除, 而weakMap
的键是弱引用, 不会组织垃圾回收器回收键对象 - 迭代和大小 :
map
具有size属性, 可以轻松获取其键值对的数量, 并且可以使用forEach、keys、values、entries进行遍历, 而weakMap
没有size属性, 也没有内置的迭代器方法, 因此无法直接获取其大小或进行迭代 - 性能和内存占用 : 由于
weakMap
的键是弱引用,不会阻止垃圾回收器回收键对象, 这意味着weakMap
在某些情况下可能比map
更高效, 因为它不会导致内容泄露, 然后由于垃圾回收的开销,weakMap
的性能可能会稍微降低
四 、分支切换和cleanup
示例
javascript
01 const data = { ok: true, text: 'hello world' }
02 const obj = new Proxy(data, { /* ... */ })
03
04 effect(function effectFn() {
05 document.body.innerText = obj.ok ? obj.text : 'not'
06 })
此时的副作用函数与响应式数据之间的联系
arduino
01 data
02 └── ok
03 └── effectFn
04 └── text
05 └── effectFn
当obj.ok设置false时候, 理想情况下, 副作用函数effect不应该被字段obj.text所对应的依赖集合收集, 建立的联系如下
kotlin
01 data
02 └── ok
03 └── effectFn
**解决方案:**每次副作用函数执行时, 可以将该副作用从所有与之关联的依赖集合中删除, 当副作用函数执行完成之后, 会重新建立联系, 但在新的联系不会包含遗留的副作用函数
代码设计:
effectFn.deps
: 用来储存所有与该副作用函数相关联的依赖集合
cleanup
: 副作用函数执行前进行依赖集合的清除
代码执行顺序: 影响数据发生改变→trigger函数执行, 遍历副作用函数(该副作用函数拷贝了原数据, 原数据的删除与修改跟当前遍历无关)→副作用函数执行时cleanup删除 , 后执行fn进行副作用函数的收集
javascript
// 注册副作用函数
let activeEffect
function effect(fn) {
const effectFn = () => {
cleanup(effectFn);
activeEffect = effectFn;
fn()
activeEffect = "";
}
effectFn.deps = [];
effectFn();
}
// 副作用函数依赖合集的删除
function cleanup(effectFn) {
for(let i = 0; i < effectFn.deps.length; i++){
let deps = effectFn.deps[i];
deps.delete(effectFn);
}
effectFn.deps.length = 0;
}
// 桶,储存代理对象属性的相关事件
const bucket = new WeakMap();
// 跟踪函数
function track(target, key) {
if(!activeEffect) return;
let depsMap = bucket.get(target);
if(!depsMap) bucket.set(target, depsMap = new Map());
let deps = depsMap.get(key);
if(!deps) depsMap.set(key, deps = new Set())
deps.add(activeEffect);
activeEffect.deps.push(deps);
}
// 触发函数
function trigger(target, key) {
let depsMap = bucket.get(target);
if(!depsMap) return;
let effects = depsMap.get(key);
let effectsToRun = new Set();
effects && effects.forEach(item => {
effectsToRun.add(item);
})
effectsToRun.forEach(fn => fn(key));
}
// 代理配置
let proxyOption = {
get(target, key, receiver) {
track(target, key);
return target[key];
},
set(target, key, newVal) {
target[key] = newVal;
trigger(target, key);
}
}
// 测试示例
const data = { ok: true, text: "hello vue3"}
const obj = new Proxy(data, proxyOption)
effect(() => {
document.body.innerText = obj.ok ? obj.text : "测试数据"
console.log("执行了多少次")
})
setTimeout(() =>{
obj.ok = false;
}, 1000)
setTimeout(() =>{
obj.text = "hello world"
}, 2000)
知识点 : 如果不对trigger函数中的effects进行拷贝, 会触发报错, 程序循环执行, 原因:
javascript
let set = new Set([1]);
set.forEach(item => {
set.delete(1);
set.add(1);
console.log(item);
})
语言规范有说明: 在调用forEach遍历set集合时, 如果一个值已经被访问过了, 但该值被删除并重新添加到集合, 如果此时forEach遍历没有结束, 那么该值会重新被访问, 因为上面代码会无限执行。
五、嵌套的effect和effect栈
5.1、业务上: 组件嵌套
javascript
01 // Bar 组件
02 const Bar = {
03 render() { /* ... */ },
04 }
05 // Foo 组件渲染了 Bar 组件
06 const Foo = {
07 render() {
08 return <Bar /> // jsx 语法
09 },
10 }
相当于
01 effect(() => {
02 Foo.render()
03 // 嵌套
04 effect(() => {
05 Bar.render()
06 })
07 })
5.2、不符合的测试例子:
javascript
const data = { foo: true, bar: true}
const obj = new Proxy(data, proxyOption)
effect(() => {
console.log("执行了effect1");
effect(() => {
console.log("执行了effect2")
console.log(obj.foo)
})
console.log(obj.bar)
})
setTimeout(() =>{
obj.bar = false;
}, 1000)
结果:
arduino
执行了effect1
执行了effect2 true
true
与期望不符的原因:
javascript
function effect(fn) {
const effectFn = () => {
cleanup(effectFn);
activeEffect = effectFn;
fn()
activeEffect = "";
}
effectFn.deps = [];
effectFn();
}
- 执行effect1, activeEffect为effect1
- 执行effect2,activeEffect为effect2
- effect2执行完成, 回到effect1中, 此时activeEffect为空
- effect1访问了obj.bar, 这时候需要跟踪bar的关联事件, 但是activeEffect为空,没有事件可关联
- 更改obj.bar, 没有事件可以触发,故控制台没有输出
5.3、修改后的代码
javascript
let activeEffect // 注册副作用函数
let effectStack = []; // 副作用函数栈
function effect(fn) {
const effectFn = () => {
cleanup(effectFn);
activeEffect = effectFn;
effectStack.push(effectFn);
fn()
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
}
effectFn.deps = [];
effectFn();
}
// 副作用函数依赖合集的删除
function cleanup(effectFn) {
for(let i = 0; i < effectFn.deps.length; i++){
let deps = effectFn.deps[i];
deps.delete(effectFn);
}
effectFn.deps.length = 0;
}
// 桶,储存代理对象属性的相关事件
const bucket = new WeakMap();
// 跟踪函数
function track(target, key) {
if(!activeEffect) return;
let depsMap = bucket.get(target);
if(!depsMap) bucket.set(target, depsMap = new Map());
let deps = depsMap.get(key);
if(!deps) depsMap.set(key, deps = new Set())
deps.add(activeEffect);
activeEffect.deps.push(deps);
}
// 触发函数
function trigger(target, key) {
let depsMap = bucket.get(target);
if(!depsMap) return;
let effects = depsMap.get(key);
let effectsToRun = new Set();
effects && effects.forEach(item => {
effectsToRun.add(item);
})
effectsToRun.forEach(fn => fn(key));
}
// 代理属性
let proxyOption = {
get(target, key, receiver) {
track(target, key);
return target[key];
},
set(target, key, newVal) {
target[key] = newVal;
trigger(target, key);
}
}
// 测试示例
const data = { foo: true, bar: true}
const obj = new Proxy(data, proxyOption)
effect(() => {
console.log("执行了effect1");
effect(() => {
console.log("执行了effect2")
console.log(obj.foo)
})
console.log(obj.bar)
})
setTimeout(() =>{
obj.bar = false;
}, 1000)
执行结果:
arduino
执行了effect1
执行了effect2 true
true
执行了effect1
执行了effect2 true
false
六、避免无限递归循环
6.1、不符合的测试例子
ini
// 测试示例
const data = { i: 1}
const obj = new Proxy(data, proxyOption)
effect(() => {
obj.i ++ // 相当于obj.i = obj.i + 1;
})
结果:
arduino
Uncaught RangeError: Maximum call stack size exceeded
与期望不符合的原因:
- obj.i + 1, 读取了obj中i属性, 此时将副作用函数effect收集到'桶'中,
- obj.i = obj.i + 1, 设置了obj中i属性, 此时触发了'桶'中的副作用的函数的执行
- 读取, 收集
- 设置, 触发
- 循环往复
解决方法 :
如果trigger触发执行的副作用函数和当前正则执行的副作用函数相同, 则不触发执行
5.3、修改后的代码
javascript
let activeEffect // 注册副作用函数
let effectStack = []; // 副作用函数栈
function effect(fn) {
const effectFn = () => {
cleanup(effectFn);
activeEffect = effectFn;
effectStack.push(effectFn);
fn()
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
}
effectFn.deps = [];
effectFn();
}
// 副作用函数依赖合集的删除
function cleanup(effectFn) {
for(let i = 0; i < effectFn.deps.length; i++){
let deps = effectFn.deps[i];
deps.delete(effectFn);
}
effectFn.deps.length = 0;
}
// 桶,储存代理对象属性的相关事件
const bucket = new WeakMap();
// 跟踪函数
function track(target, key) {
if(!activeEffect) return;
let depsMap = bucket.get(target);
if(!depsMap) bucket.set(target, depsMap = new Map());
let deps = depsMap.get(key);
if(!deps) depsMap.set(key, deps = new Set())
deps.add(activeEffect);
activeEffect.deps.push(deps);
}
// 触发函数
function trigger(target, key) {
let depsMap = bucket.get(target);
if(!depsMap) return;
let effects = depsMap.get(key);
let effectsToRun = new Set();
effects && effects.forEach(effectFn => {
// 如果trigger触发执行的副作用函数和当前正则执行的副作用函数相同, 则不触发执行
if(effectFn !== activeEffect){
effectsToRun.add(effectFn);
}
})
effectsToRun.forEach(fn => fn(key));
}
// 代理配置
let proxyOption = {
get(target, key, receiver) {
track(target, key);
return target[key];
},
set(target, key, newVal) {
target[key] = newVal;
trigger(target, key);
}
}
// 测试示例
const data = { i: 1}
const obj = new Proxy(data, proxyOption)
effect(() => {
obj.i ++
})
七、调度执行
可调度性: 当trigger动作触发副作用函数重新执行时, 有能力决定副作用函数执行的时机、次数以及方式
示例:
javascript
const data = { foo: 1 }
const obj = new Proxy(data, proxyOption)
effect(() => {
console.log(obj.foo)
})
obj.foo++;
console.log('结束了')
结果:
1
2
结束了
如果期望输出下面结果
1
结束了
2
修改:
为effect函数设计一个选项参数options, 允许用户指定调度器
javascript
effect(() => {
console.log(obj.foo)
}, {
scheduler(fn){
Promise.resolve().then(() => {
fn();
})
}
})
effect函数内部需要把options挂载到对应的副作用函数上
javascript
let activeEffect // 当前副作用函数
let effectStack = []; // 副作用函数栈
function effect(fn, options) {
const effectFn = () => {
cleanup(effectFn);
// 当调用effect注册副作用函数时,将副作用函数赋值给activeEffect
activeEffect = effectFn;
// 调用副作用函数之前将副作用函数压入栈
effectStack.push(effectFn);
fn()
// 在当前副作用函数执行完成之后, 将当前副作用函弹出栈,并将activeEffect还原为之前的值
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
}
// 将options挂载到effectFn上
effectFn.options = options;
// 用来所有与该副作用函数相关的依赖集合
effectFn.deps = [];
effectFn();
}
trigger函数中触发副作用函数重新执行时, 就可以直接调用用户传递的调度器函数, 从而把控制权交给用户
javascript
function trigger(target, key) {
let depsMap = bucket.get(target);
if(!depsMap) return;
let effects = depsMap.get(key);
let effectsToRun = new Set();
effects && effects.forEach(effectFn => {
if(effectFn !== activeEffect){
effectsToRun.add(effectFn);
}
})
effectsToRun.forEach(effectFn => {
if(effectFn.options.scheduler){
effectFn.options.scheduler(effectFn);
} else {
effectFn();
}
});
}
整体代码
javascript
let activeEffect // 当前副作用函数
let effectStack = []; // 副作用函数栈
function effect(fn, options = {}) {
const effectFn = () => {
cleanup(effectFn);
// 当调用effect注册副作用函数时,将副作用函数赋值给activeEffect
activeEffect = effectFn;
// 调用副作用函数之前将副作用函数压入栈
effectStack.push(effectFn);
fn()
// 在当前副作用函数执行完成之后, 将当前副作用函弹出栈,并将activeEffect还原为之前的值
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
}
// 将options挂载到effectFn上
effectFn.options = options;
// 用来所有与该副作用函数相关的依赖集合
effectFn.deps = [];
effectFn();
}
// 副作用函数依赖合集的删除
function cleanup(effectFn) {
for(let i = 0; i < effectFn.deps.length; i++){
let deps = effectFn.deps[i];
deps.delete(effectFn);
}
effectFn.deps.length = 0;
}
// 桶,储存代理对象属性的相关事件
const bucket = new WeakMap();
// 跟踪函数
function track(target, key) {
if(!activeEffect) return;
let depsMap = bucket.get(target);
if(!depsMap) bucket.set(target, depsMap = new Map());
let deps = depsMap.get(key);
if(!deps) depsMap.set(key, deps = new Set())
deps.add(activeEffect);
activeEffect.deps.push(deps);
}
// 触发函数
function trigger(target, key) {
let depsMap = bucket.get(target);
if(!depsMap) return;
let effects = depsMap.get(key);
let effectsToRun = new Set();
effects && effects.forEach(effectFn => {
if(effectFn !== activeEffect){
effectsToRun.add(effectFn);
}
})
effectsToRun.forEach(effectFn => {
if(effectFn.options.scheduler){
effectFn.options.scheduler(effectFn);
} else {
effectFn();
}
});
}
// 代理配置
let proxyOption = {
get(target, key, receiver) {
track(target, key);
return target[key];
},
set(target, key, newVal) {
target[key] = newVal;
trigger(target, key);
}
}
测试示例1: 调换事件执行顺序
javascript
const data = { foo: 1 }
const obj = new Proxy(data, proxyOption)
effect(() => {
console.log(obj.foo)
}, {
scheduler(fn) {
Promise.resolve().then(() => {
fn();
})
}
})
obj.foo++;
console.log('结束了')
// 执行结果:
1
结束了
2
测试示例2 : 多次触发属性修改, 只执行一次副作用函数, 对应到vue中多次修改响应式数据, 但只会触发一次template的更新
javascript
const data = { foo: 1 }
const obj = new Proxy(data, proxyOption)
// 定义一个任务队列
const jobQueue = new Set();
// 一个标志代表是否刷新队列
let isFlushing = false;
function flushJob() {
// 如果队列正在刷新, 则什么都不做
if(isFlushing) return;
// 设置为true, 代表正在刷新
isFlushing = true;
// 在微任务中执行jobQueue队列
Promise.resolve().then(() => {
jobQueue.forEach(fn => fn());
}).finally(() => {
// 重置isFlushing
isFlushing = false;
})
}
effect(() => {
console.log(obj.foo)
}, {
scheduler(fn) {
jobQueue.add(fn);
flushJob();
}
})
obj.foo++;
obj.foo++;
// 执行结果:
1
3
八、计算属性computed与lazy
computed: 不调用不执行
, 计算得到值
, 值缓存, 相关属性不更改使用旧值
,相关属性更改触发值的更新
-
不调用不执行: 在options中添加lazy属性, 返回effectFn让用户手动执行
inilet activeEffect; let effectStack = []; function effect(fn, options = {}) { const effectFn = () => { cleanup(effectFn); activeEffect = effectFn; effectStack.push(effectFn); fn() effectStack.pop(); activeEffect = effectStack[effectStack.length - 1]; } effectFn.options = options; effectFn.deps = []; if(!options.lazy){ effectFn(); } return effectFn; }
-
计算得到值:
inilet activeEffect let effectStack = []; function effect(fn, options = {}) { const effectFn = () => { cleanup(effectFn); activeEffect = effectFn; effectStack.push(effectFn); let res = fn() effectStack.pop(); activeEffect = effectStack[effectStack.length - 1]; return res; } effectFn.options = options; effectFn.deps = []; if(!options.lazy){ effectFn(); } return effectFn; }
-
此时可以得到基本的computed方法
javascriptfunction computed(getter) { let effectFn = effect(getter, {lazy: true}); let obj; obj = { get value() { return effectFn(); } } return obj; } // 测试示例 const data = { foo: 1, bar: 2 } const obj = new Proxy(data, proxyOption) let sum = computed(() => obj.foo + obj.bar); console.log(sum.value); // 执行结果: 3
-
值缓存, 相关属性不更改使用旧值
inifunction computed(getter) { let effectFn = effect(getter, { lazy: true, scheduler() { dirty = true; //相关属性发生更改的时候,重新进行值的计算 // 按照上面的逻辑, 惯性思维上应该执行effectFn(), 属性发生修改, 则触发相关副作用函数, // 但是computed中只有调用到computed的值才会触发计算 // 所以这里不要执行effectFn, 而是交由 obj { get value () { 这里执行effectFn } } } }); let obj; let value; let dirty = true; obj = { get value() { if(dirty) { value = effectFn(); dirty =false; } return value } } return obj; } // 测试示例 const data = { foo: 1, bar: 2 } const obj = new Proxy(data, proxyOption) let sum = computed(() => { console.log("执行computed方法") return obj.foo + obj.bar; }); console.log(sum.value); console.log(sum.value); obj.foo = 2; console.log(sum.value); // 执行结果 执行computed方法 3 3 执行computed方法 4
-
相关属性更改触发值的更新
下面的例子是不会触发副作用函数, 原因这个computed返回的值不是代理对象, 没有该属性进行读取储存,更改触发的配置
iniconst data = { foo: 1, bar: 2 } const obj = new Proxy(data, proxyOption) let sum = computed(() => { return obj.foo + obj.bar; }); effect(() => { console.log(sum.value) }) obj.foo = 4; // 执行结果 3
故需要手动进行属性的跟踪和触发
inifunction computed(getter) { let effectFn = effect(getter, { lazy: true, scheduler() { dirty = true; trigger(obj, "value"); // 进行属性的触发 } }); let obj; let value; let dirty = true; obj = { get value() { if(dirty) { track(obj, "value"); // 获取值进行属性的跟踪 value = effectFn(); dirty = false; } return value } } return obj; }
手动建立了下面的联系关系
此时再执行一遍测试例子
iniconst data = { foo: 1, bar: 2 } const obj = new Proxy(data, proxyOption) let sum = computed(() => { return obj.foo + obj.bar; }); effect(() => { console.log(sum.value) }) obj.foo = 4; // 执行结果 3 4
九、watch的实现原理
watch: 就是观测一个响应式数据, 当数据发生变化时通知并执行响应的回调函数
javascript
watch(obj, () => {
console.log("数据遍历")
})
obj.foo ++;
本质上利用了effect以及options.scheduler的选项, 如果option中存在scheduler中选项, 当响应式数据发生变化时, 会触发scheduler调度函数的执行, 而非直接触发副作用函数的执行,下面是watch的实现
ini
function traverse(value, seen = new Set()) {
if(typeof value !== "object" || value === null || seen.has(value)) return value;
// 将数据添加到seen中, 代表遍历读取过了,避免循环引用引起的死循环
seen.add(value)
// 这里只考虑到了对象, 没有考虑到数组等结构体
for(let key in value) {
traverse(value[key], seen)
}
return value;
}
function watch(source, cb) {
let getter;
if(typeof source === "function") {
getter = source;
} else {
// 如果改为getter = traverse(source), 那么一开始就已经遍历obj下面的所有属性, 返回一个obj对象,
// 后面effect中调用() => getter的时候, 访问的只是obj对象, 所有当对obj的对象进行设置的时候, 不起作用
getter = () => traverse(source);
}
let newValue, oldValue;
let effectFn = effect(() => getter(), {
lazy: true,
scheduler() {
newValue = effectFn();
cb(newValue, oldValue)
oldValue = newValue;
}
})
oldValue = effectFn();
}
// 测试示例
const data = { foo: 1}
const obj = new Proxy(data, proxyOption)
watch(() => obj.foo, (newValue, oldValue) => {
console.log("foo发生了变更", newValue, oldValue);
});
obj.foo++;
十、立即执行的watch与回调执行时机
-
立即执行的回调函数, 其实就是立即执行cb
scssfunction traverse(value, seen = new Set()) { if(typeof value !== "object" || value === null || seen.has(value)) return value; // 将数据添加到seen中, 代表遍历读取过了,避免循环引用引起的死循环 seen.add(value) // 这里只考虑到了对象, 没有考虑到数组等结构体 for(let key in value) { traverse(value[key], seen) } return value; } function watch(source, cb, options = {}) { let getter; if(typeof source === "function") { getter = source; } else { // 如果改为getter = traverse(source), 那么一开始就已经遍历obj下面的所有属性, 返回一个obj对象, // 后面effect中调用() => getter的时候, 访问的只是obj对象, 所有当对obj的对象进行设置的时候, 不起作用 getter = () => traverse(source); } let newValue, oldValue; const job = () => { newValue = effectFn(); cb(newValue, oldValue) oldValue = newValue; } let effectFn = effect(() => getter(), { lazy: true, scheduler() { job(); } }) if(options.immediate) { job(); } else { oldValue = effectFn(); } } // 测试示例 const data = { foo: 1} const obj = new Proxy(data, proxyOption) watch(() => obj.foo, (newValue, oldValue) => { console.log("foo发生了变更", newValue, oldValue); }, { immediate: true, }); obj.foo++;
-
回调函数的执行时机
watch方法options选项存在flush选项, 用于指定调度函数的执行时间
- pre: 默认值, 组件更新前执行
- post:代表调度函数需要将副作用函数放到一个微任务队列中, 等待DOM更新结束轴再执行
- async: 下面的直接执行job() ,本质上相当于'async'的效果, 即同步执行
inifunction watch(source, cb, options = {}) { let getter; if(typeof source === "function") { getter = source; } else { // 如果改为getter = traverse(source), 那么一开始就已经遍历obj下面的所有属性, 返回一个obj对象, // 后面effect中调用() => getter的时候, 访问的只是obj对象, 所有当对obj的对象进行设置的时候, 不起作用 getter = () => traverse(source); } let newValue, oldValue; const job = () => { newValue = effectFn(); cb(newValue, oldValue) oldValue = newValue; } let effectFn = effect(() => getter(), { lazy: true, scheduler() { if(options.flush === "post") { Promise.resolve().then(() => { job(); }) } else { job(); } } }) if(options.immediate) { job(); } else { oldValue = effectFn(); } }
十一、过期的副作用
在下面的例子中
ini
let finalData
watch(obj, async () => {
const res = await featch("/path/to/request");
finalData = res;
})
可能存在下面的情况, 书中称之为竞态问题
 {
let getter;
if(typeof source === "function") {
getter = source;
} else {
// 如果改为getter = traverse(source), 那么一开始就已经遍历obj下面的所有属性, 返回一个obj对象,
// 后面effect中调用() => getter的时候, 访问的只是obj对象, 所有当对obj的对象进行设置的时候, 不起作用
getter = () => traverse(source);
}
let newValue, oldValue;
let cleanUp;
function onInvalidate(cb) {
cleanUp = cb;
}
const job = () => {
newValue = effectFn();
if(cleanUp) {
cleanUp();
}
cb(newValue, oldValue, onInvalidate)
oldValue = newValue;
}
let effectFn = effect(() => getter(), {
lazy: true,
scheduler() {
if(options.flush === "post") {
Promise.resolve().then(() => {
job();
})
} else {
job();
}
}
})
if(options.immediate) {
job();
} else {
oldValue = effectFn();
}
}
// 测试示例
const data = { foo: 1}
const obj = new Proxy(data, proxyOption)
watch(() => obj.foo, async (newValue, oldValue, onInvalidate) => {
let expired = false;
onInvalidate(() => {
expired = true;
})
const res = await getFetchData()
if(!expired) {
console.log(res)
}
}, {
immediate: true,
flush: "post"
});
obj.foo++;
function getFetchData() {
const time = Math.random() * 1000 + 1000;
return new Promise(resolve => {
setTimeout(() => {
console.log("fetch", time)
resolve(time);
}, time)
})
}
思路:
1、立刻执行的时候, 执行第一次回调函数, 调用onInvalidate注册了cleanup,然后发送请求, 等待接口的返回
2、此时执行了obj.foo++, 触发了cleanup的执行 ,第一次回调函数的expired被设置为了true, 跟着第二次回调函数的执行, 调用onInvalidate注册了cleanup, 然后发送请求, 等待接口的返回
第一种情况
3、第一次请求返回数据,expired为true, 不会触发console.log
4、第二次请求返回数据,expired为false, 触发console.log
第一次情况
第二种情况
3、第二次请求返回数据,注册的cleanup函数没有被调用过, expired为true, 触发console.log
4、第一次请求返回数据,expired为false, 不会触发console.log
代码归总:
javascript
let activeEffect // 当前副作用函数
let effectStack = []; // 副作用函数栈
function effect(fn, options = {}) {
const effectFn = () => {
cleanup(effectFn);
// 当调用effect注册副作用函数时,将副作用函数赋值给activeEffect
activeEffect = effectFn;
// 调用副作用函数之前将副作用函数压入栈
effectStack.push(effectFn);
let res = fn()
// 在当前副作用函数执行完成之后, 将当前副作用函弹出栈,并将activeEffect还原为之前的值
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
return res;
}
// 将options挂载到effectFn上
effectFn.options = options;
// 用来所有与该副作用函数相关的依赖集合
effectFn.deps = [];
if(!options.lazy){
effectFn();
}
return effectFn;
}
// 副作用函数依赖合集的删除
function cleanup(effectFn) {
for(let i = 0; i < effectFn.deps.length; i++){
let deps = effectFn.deps[i];
deps.delete(effectFn);
}
effectFn.deps.length = 0;
}
// 桶,储存代理对象属性的相关事件
const bucket = new WeakMap();
// 跟踪函数
function track(target, key) {
if(!activeEffect) return;
let depsMap = bucket.get(target);
if(!depsMap) bucket.set(target, depsMap = new Map());
let deps = depsMap.get(key);
if(!deps) depsMap.set(key, deps = new Set())
deps.add(activeEffect);
activeEffect.deps.push(deps);
}
// 触发函数
function trigger(target, key) {
let depsMap = bucket.get(target);
if(!depsMap) return;
let effects = depsMap.get(key);
let effectsToRun = new Set();
effects && effects.forEach(effectFn => {
if(effectFn !== activeEffect){
effectsToRun.add(effectFn);
}
})
effectsToRun.forEach(effectFn => {
if(effectFn.options.scheduler){
effectFn.options.scheduler(effectFn);
} else {
effectFn();
}
});
}
// 代理配置
let proxyOption = {
get(target, key, receiver) {
track(target, key);
return target[key];
},
set(target, key, newVal) {
target[key] = newVal;
trigger(target, key);
}
}
function computed(getter) {
let effectFn = effect(getter, {
lazy: true,
scheduler() {
dirty = true;
trigger(obj, "value")
}
});
let obj;
let value;
let dirty = true;
obj = {
get value() {
if(dirty) {
track(obj, "value");
value = effectFn();
dirty = false;
}
return value
}
}
return obj;
}
function traverse(value, seen = new Set()) {
if(typeof value !== "object" || value === null || seen.has(value)) return value;
// 将数据添加到seen中, 代表遍历读取过了,避免循环引用引起的死循环
seen.add(value)
// 这里只考虑到了对象, 没有考虑到数组等结构体
for(let key in value) {
traverse(value[key], seen)
}
return value;
}
function watch(source, cb, options = {}) {
let getter;
if(typeof source === "function") {
getter = source;
} else {
// 如果改为getter = traverse(source), 那么一开始就已经遍历obj下面的所有属性, 返回一个obj对象,
// 后面effect中调用() => getter的时候, 访问的只是obj对象, 所有当对obj的对象进行设置的时候, 不起作用
getter = () => traverse(source);
}
let newValue, oldValue;
let cleanUp;
function onInvalidate(cb) {
cleanUp = cb;
}
const job = () => {
newValue = effectFn();
if(cleanUp) {
cleanUp();
}
cb(newValue, oldValue, onInvalidate)
oldValue = newValue;
}
let effectFn = effect(() => getter(), {
lazy: true,
scheduler() {
if(options.flush === "post") {
Promise.resolve().then(() => {
job();
})
} else {
job();
}
}
})
if(options.immediate) {
job();
} else {
oldValue = effectFn();
}
}
// 测试示例
const data = { foo: 1}
const obj = new Proxy(data, proxyOption)
watch(() => obj.foo, async (newValue, oldValue, onInvalidate) => {
let expired = false;
onInvalidate(() => {
expired = true;
})
const res = await getFetchData()
if(!expired) {
console.log(res)
}
}, {
immediate: true,
flush: "post"
});
obj.foo++;
function getFetchData() {
const time = Math.random() * 1000 + 1000;
return new Promise(resolve => {
setTimeout(() => {
console.log("fetch", time)
resolve(time);
}, time)
})
}