上一篇文章中,我们实现了基本的 effect,支持嵌套和 runner。但真实的响应式系统还需要处理更复杂的情况:分支切换导致的无效依赖、调度器控制执行时机、停止 effect 回收资源等,本文将深入这些高级特性。
前言:从分支切换的问题说起
javascript
const state = reactive({ ok: true, text: 'hello' });
effect(() => {
console.log(state.ok ? state.text : 'not ok');
});
上述代码中,当 state.ok 为 true 时,effect 依赖了 ok 和 text ; 当 state.ok 为 false 时,effect 只依赖了 ok ,此时 text 的依赖应该被清理掉!
这就是分支切换需要解决的问题。
分支切换与cleanup
什么是分支切换?
以 前言 中的代码为例,effect 内部存在一个三元表达式:state.ok ? state.text : 'not ok' ,根据字段 state.ok 值的不同,代码执行的分支会随之变化,执行不同的代码分支,这就是所谓的 分支切换。
分支切换带来的问题
分支切换会产生遗留的副作用函数,上述例子中,当字段 state.ok 值为 false 时,此时 state.text 并不会被读取使用,但仍然会被依赖收集,这就产生了遗留的副作用,这显然是不合理的。
cleanup的作用
为了解决分支切换的问题,我们需要使用 cleanup 函数进行处理。该函数接收副作用函数作为参数,遍历副作用函数中的依赖集合,然后将该副作用从依赖集合中移除。这样,就可以避免副作用函数产生遗留问题了:
javascript
function cleanup(effect) {
const { deps } = effect;
if (deps.length) {
console.log(` [cleanup] 清除 ${effect.name} 的 ${deps.length} 个旧依赖`);
deps.forEach(dep => dep.delete(effect));
deps.length = 0;
}
}
cleanup 的执行时机
每次 effect 执行之前,都会先调用 cleanup 函数,清理所有旧的依赖关系。以此确保依赖关系始终是最新的,避免无效更新:
javascript
run() {
// 先清理所有旧依赖
cleanup(this);
// 设置为当前effect
activeEffect = this;
effectStack.push(this);
console.log(` [run] ${this.name} 执行`);
try {
// 执行fn,重新收集依赖
this.fn();
} finally {
// 恢复activeEffect
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
}
}
cleanup 工作流程
javascript
effect.run()
↓
cleanup(effect) → 移除所有旧依赖
↓
执行fn() → 重新收集依赖
↓
完成执行
scheduler调度器
可调度性
可调度性是响应式系统中非常重要的特性,当触发更新重新执行副作用函数时,我们能决定副作用函数执行的时机、次数以及方式等,这就是可调度性。
为什么需要调度器?
没有调度器的时候,可能会产生一些新的问题:
- 频繁修改数据会导致effect执行多次,造成性能浪费
- 有时需要控制effect的执行时机(如异步更新)
- 需要批量处理更新,减少重复计算
如以下示例:
javascript
function demonstrateNoScheduler() {
const state = reactive({ count: 0 });
effect(() => {
console.log(` effect执行: count = ${state.count}`);
});
console.log('连续修改3次数据:');
state.count = 1;
state.count = 2;
state.count = 3;
console.log(' effect被执行了3次,可能是不必要的');
}
上述代码中,由于连续更改 state.count 的值,可能会导致 effect 被多次重复执行,这在很大程度上是不必要的。
通过 scheduler 调度器,可以自定义 effect 的执行策略,很好的解决上述问题。
调度器的基本实现
javascript
class EffectWithScheduler {
constructor(fn, scheduler) {
this.fn = fn;
this.scheduler = scheduler;
this.deps = [];
this.active = true;
}
run() {
if (!this.active) {
return this.fn();
}
cleanup(this);
activeEffect = this;
effectStack.push(this);
try {
const result = this.fn();
return result;
} finally {
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
}
}
// 触发更新时调用
trigger() {
if (this.scheduler) {
// 如果有调度器,交给调度器处理
this.scheduler(this);
} else {
// 否则立即执行
this.run();
}
}
}
调度器的应用场景
- 异步更新(Vue的nextTick),可以将多次同步更新合并为一次异步更新
- 使用 requestAnimationFrame 控制动画帧更新时机
- 控制高频更新的执行频率,防抖/节流
- 可以限制只有在特定条件下才执行 effect
懒执行effect(lazy)
为什么需要懒执行?
在有些场景下,我们并不希望 effect 副作用函数立即执行,而是希望它在需要的时候才执行,此处我们就可以通过 lazy 属性,即懒执行来实现。
懒执行场景
- 计算属性(computed)------ 只在被访问时才计算
- 需要手动控制的 effect
- 条件性执行的副作用
- 性能优化 ------ 避免不必要的初始化计算
懒执行的实现
javascript
class LazyEffect {
constructor(fn, options = {}) {
this.fn = fn;
this.lazy = options.lazy || false;
this.scheduler = options.scheduler || null;
this.deps = [];
this.active = true;
}
run() {
if (!this.active) {
return this.fn();
}
cleanup(this);
activeEffect = this;
effectStack.push(this);
try {
const result = this.fn();
return result;
} finally {
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
}
}
trigger() {
if (this.scheduler) {
this.scheduler(this);
} else {
this.run();
}
}
}
停止effect(stop方法)
stop 的作用
- 组件卸载时,停止响应式依赖,避免内存泄漏
- 用户可以手动停止不需要的副作用
- 临时禁用某个响应式关系
stop 的实现
javascript
class StoppableEffect {
constructor(fn, options = {}) {
this.fn = fn;
this.scheduler = options.scheduler || null;
this.onStop = options.onStop || null; // 停止时的回调
this.deps = [];
this.active = true; // 是否活跃
}
run() {
if (!this.active) {
// 如果不活跃,只执行函数,不收集依赖
return this.fn();
}
cleanup(this);
activeEffect = this;
effectStack.push(this);
try {
const result = this.fn();
return result;
} finally {
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
}
}
stop() {
if (this.active) {
console.log(' [stop] 停止effect');
cleanup(this); // 清除所有依赖
this.active = false;
// 调用停止回调
if (this.onStop) {
this.onStop();
}
}
}
trigger() {
if (this.scheduler) {
this.scheduler(this);
} else if (this.active) {
this.run();
}
}
}
完整的ReactiveEffect类源码
javascript
// 全局变量
const targetMap = new WeakMap();
let activeEffect = null;
const effectStack = [];
// 清理函数
function cleanup(effect) {
const { deps } = effect;
if (deps.length) {
console.log(` [cleanup] 清除 ${effect.name || 'anonymous'} 的 ${deps.length} 个依赖`);
deps.forEach(dep => dep.delete(effect));
deps.length = 0;
}
}
// 完整的ReactiveEffect类
class ReactiveEffect {
constructor(fn, options = {}) {
this.fn = fn;
this.scheduler = options.scheduler || null;
this.onStop = options.onStop || null;
this.onTrack = options.onTrack || null;
this.onTrigger = options.onTrigger || null;
this.deps = [];
this.active = true;
this.name = fn.name || 'anonymous';
}
run() {
if (!this.active) {
return this.fn();
}
// 清理旧依赖
cleanup(this);
// 入栈
effectStack.push(this);
activeEffect = this;
// 调试钩子
if (this.onTrack) {
// 实际会传入更详细的信息
}
console.log(` [run] 开始执行 ${this.name}`);
try {
const result = this.fn();
console.log(` [run] ${this.name} 执行完成`);
return result;
} finally {
// 出栈
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
}
}
stop() {
if (this.active) {
console.log(` [stop] 停止 ${this.name}`);
cleanup(this);
this.active = false;
if (this.onStop) {
this.onStop();
}
}
}
// 触发更新(由响应式系统调用)
trigger() {
if (!this.active) return;
if (this.scheduler) {
console.log(` [scheduler] ${this.name} 被调度`);
this.scheduler(this);
} else {
this.run();
}
}
}
// 依赖收集
function track(target, key) {
if (!activeEffect) return;
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = new Set();
depsMap.set(key, dep);
}
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
activeEffect.deps.push(dep);
if (activeEffect.onTrack) {
activeEffect.onTrack({
effect: activeEffect,
target,
key,
type: 'get'
});
}
console.log(` [track] ${activeEffect.name} 依赖了 ${String(key)}`);
}
}
// 触发更新
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (!dep) return;
console.log(` [trigger] ${String(key)} 变化,触发 ${dep.size} 个effect`);
// 复制一份,避免遍历时修改Set
const effects = new Set(dep);
effects.forEach(effect => {
if (effect !== activeEffect) {
effect.trigger();
}
});
}
// 创建响应式对象
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key);
return target[key];
},
set(target, key, value) {
target[key] = value;
trigger(target, key);
return true;
}
});
}
// 主effect函数
function effect(fn, options = {}) {
const _effect = new ReactiveEffect(fn, options);
if (!options.lazy) {
_effect.run();
}
const runner = _effect.run.bind(_effect);
runner.effect = _effect;
return runner;
}
结语
本篇文章主要介绍了 effect 副作用函数的高级特性,掌握这些特性,我们不仅能更好地理解 Vue3 的工作原理,还能在遇到性能问题时,知道如何优化 effect 的执行策略,甚至在需要时实现自己的响应式系统。对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!