小龙色盲色弱检测
103.04M · 2026-02-26
我们先看看这段代码,你认为 Vue 应该如何工作?
const state = reactive({ count: 0 });
// 这里访问了 state.count,Vue 需要记录这个依赖
function render() {
return `<div>${state.count}</div>`;
}
// 这里修改了 state.count,Vue 需要触发更新
state.count++;
问题来了:Vue 如何知道 render() 依赖于 state.count ?又如何知道当 count 变化时需要重新执行什么?
这就是副作用追踪系统要解决的核心问题。
在计算机科学中,副作用是指在函数执行过程中,除了返回值之外,对外部环境产生的任何影响,即产生了副作用,如下面代码所示:
function updateDOM() {
document.body.innerHTML = 'Hello'; // 副作用:修改 DOM
}
当 updateDOM 方法执行时,它会设置 body 的文本内容,但除了 updateDOM 方法外,还有很多方法都可以读取或设置 body 的文本内容。也就是说,updateDOM 方法的执行,会直接或间接影响其他函数的执行,此时 updateDOM 方法产生了副作用。
实际开发中,副作用很容易产生,我们可以看看以下几个示例:
let total = 0;
function addToTotal(value) {
total += value; // 副作用:修改了外部变量 total
}
function showMessage(msg) {
console.log(msg); // 副作用:控制台输出
}
function fetchData() {
fetch('/api/data'); // 副作用:发起网络请求
}
我们以前言中的代码为例,如果没有副作用追踪,Vue 怎么知道需要重新渲染呢?不追踪副作用,我们无法知道 render() 函数依赖于 count 。
所以,Vue 需要追踪哪些函数(副作用)访问了哪些响应式数据,这样当数据变化时,就能自动重新执行这些函数。
副作用追踪让声明式成为可能:我们只需要关心要如何显示数据;至于数据是如何更新的,我们其实并不关心:
<template>
<div>{{ count }}</div>
<div>{{ doubleCount }}</div>
</template>
<script setup>
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
</script>
// 假设我们有这样的响应式对象
const obj = reactive({
foo: 1,
bar: 2
});
// 定义一些副作用
function effect1() {
console.log('effect1:', obj.foo);
}
function effect2() {
console.log('effect2:', obj.bar);
}
function effect3() {
console.log('effect3:', obj.foo + obj.bar);
}
// 我们需要建立这样的依赖关系图:
const dependencyGraph = {
'obj.foo': ['effect1', 'effect3'],
'obj.bar': ['effect2', 'effect3']
};
// 当 obj.foo 变化时,自动执行 effect1 和 effect3
// 当 obj.bar 变化时,自动执行 effect2 和 effect3
如果没有依赖追踪,只能暴力更新:
function withoutTracking() {
// 每次数据变化都重新渲染整个应用
// 性能极差!
fullRender();
}
有了依赖追踪,可以实现精准更新:
function withTracking() {
// 只重新执行依赖于变化数据的副作用
// 性能优秀!
const deps = getDeps(changeProperty);
deps.forEach(effect => effect());
}
effect 是 Vue3 响应式系统的核心函数,它的作用是自动追踪函数内部对响应式数据的访问,并在这些数据变化时重新执行函数。其概念模型如下:
function effect(fn) {
// 1. 记录当前正在执行的 effect
// 2. 执行 fn,期间所有对响应式数据的访问都会被记录
// 3. 建立依赖关系:响应式数据 → 这个 effect
// 4. 当数据变化时,重新执行 fn
}
effect 是整个 Vue3 中,响应式系统的基石:
在 Vue3 底层基础中,effect 可以:
reactiverefeffect 。在 Vue3 中层应用中,基于 effect 可以:
computedwatchrender在 Vue3 上层暴露中,可以
// 1. 依赖存储:使用 WeakMap 存储对象 → 属性 → effect 的映射
const targetMap = new WeakMap();
// 2. 当前激活的 effect
let activeEffect = null;
// 3. effect 函数实现
function effect(fn) {
const effectFn = () => {
try {
activeEffect = effectFn; // 设置当前激活的 effect
console.log(` [effect] 执行 ${fn.name || 'anonymous'}`);
fn(); // 执行原始函数,期间会触发 track
} finally {
activeEffect = null; // 重置激活的 effect
}
};
effectFn(); // 立即执行一次,完成依赖收集
return effectFn;
}
function withoutEffectV1() {
const watchers = [];
class Watcher {
constructor(updateFn) {
this.updateFn = updateFn;
watchers.push(this);
}
update() {
this.updateFn();
}
}
// 数据变化时,手动通知所有 watcher
function notifyAll() {
watchers.forEach(w => w.update());
}
// 使用
const w1 = new Watcher(() => console.log('更新视图1'));
const w2 = new Watcher(() => console.log('更新视图2'));
console.log('数据变化,手动通知:');
notifyAll();
}
缺点:无法精确知道哪个 watcher 依赖哪个数据
function withoutEffectV2() {
const events = {};
function on(event, callback) {
if (!events[event]) events[event] = [];
events[event].push(callback);
}
function emit(event, data) {
events[event]?.forEach(cb => cb(data));
}
// 使用
on('countChange', (newVal) => {
console.log('count 变化了:', newVal);
});
console.log('数据变化,手动触发:');
emit('countChange', 1);
}
缺点:需要手动维护事件名,容易出错
function withoutEffectV4() {
let data = { count: 0 };
let dirty = true;
let cachedValue = null;
function computed(getter) {
return {
get value() {
if (dirty) {
cachedValue = getter();
dirty = false;
}
return cachedValue;
},
setDirty() {
dirty = true;
}
};
}
const doubleCount = computed(() => data.count * 2);
console.log('第一次访问:', doubleCount.value); // 计算
console.log('第二次访问:', doubleCount.value); // 缓存
data.count = 2;
doubleCount.setDirty(); // 手动标记为脏
console.log('数据变化后:', doubleCount.value); // 重新计算
}
缺点:需要手动标记脏数据
本文简单介绍了 Vue3 中的副作用函数 effect,对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!