副作用的概念与effect基础:Vue3响应式系统的核心

当我们修改一个响应式数据时,Vue 是如何知道要更新哪些地方的?这一切都始于 effect 函数。理解副作用和 effect,是掌握 Vue3 响应式系统的钥匙。

前言:一个看似简单的问题

我们先看看这段代码,你认为 Vue 应该如何工作?

javascript 复制代码
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 变化时需要重新执行什么?

这就是副作用追踪系统要解决的核心问题。

什么是副作用

副作用的定义

在计算机科学中,副作用是指在函数执行过程中,除了返回值之外,对外部环境产生的任何影响,即产生了副作用,如下面代码所示:

示例1:修改 DOM

javascript 复制代码
function updateDOM() {
    document.body.innerHTML = 'Hello'; // 副作用:修改 DOM
}

当 updateDOM 方法执行时,它会设置 body 的文本内容,但除了 updateDOM 方法外,还有很多方法都可以读取或设置 body 的文本内容。也就是说,updateDOM 方法的执行,会直接或间接影响其他函数的执行,此时 updateDOM 方法产生了副作用。

实际开发中,副作用很容易产生,我们可以看看以下几个示例:

示例2:修改外部变量

javascript 复制代码
let total = 0;
function addToTotal(value) {
    total += value; // 副作用:修改了外部变量 total
}

示例3:控制台输出

javascript 复制代码
function showMessage(msg) {
    console.log(msg); // 副作用:控制台输出
}

示例4:网络请求

javascript 复制代码
function fetchData() {
    fetch('/api/data'); // 副作用:发起网络请求
}

有一个与副作用函数对应的概念:纯函数。纯函数是指,当前函数操作不会产生任何副作用,不会改变外部任何状态。

副作用的特征

  • 修改函数外部的变量或状态
  • 执行输入/输出操作(控制台、网络、文件)
  • 修改文档对象模型
  • 抛出异常(改变了程序的控制流)
  • 产生随机数(结果不可预测)

Vue 的上下文中,最常见的副作用

  • 渲染 DOM
  • 更新响应式数据
  • 执行 watch 回调
  • 计算 computed 属性
  • 调用生命周期钩子

为什么副作用对 Vue 如此重要?

我们以前言中的代码为例,如果没有副作用追踪,Vue 怎么知道需要重新渲染呢?不追踪副作用,我们无法知道 render() 函数依赖于 count

所以,Vue 需要追踪哪些函数(副作用)访问了哪些响应式数据,这样当数据变化时,就能自动重新执行这些函数。

Vue为什么需要追踪副作用

声明式渲染的需求

副作用追踪让声明式成为可能:我们只需要关心要如何显示数据;至于数据是如何更新的,我们其实并不关心:

javascript 复制代码
<template>
    <div>{{ count }}</div>
    <div>{{ doubleCount }}</div>
</template>

<script setup>
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
</script>

自动追踪依赖

javascript 复制代码
// 假设我们有这样的响应式对象
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

精细化的更新控制

如果没有依赖追踪,只能暴力更新:

javascript 复制代码
function withoutTracking() {
    // 每次数据变化都重新渲染整个应用
    // 性能极差!
    fullRender();
}

有了依赖追踪,可以实现精准更新:

javascript 复制代码
function withTracking() {
    // 只重新执行依赖于变化数据的副作用
    // 性能优秀!
    const deps = getDeps(changeProperty);
    deps.forEach(effect => effect());
}

effect 函数的定位与作用

effect 是什么?

effect 是 Vue3 响应式系统的核心函数,它的作用是自动追踪函数内部对响应式数据的访问,并在这些数据变化时重新执行函数。其概念模型如下:

javascript 复制代码
function effect(fn) {
    // 1. 记录当前正在执行的 effect
    // 2. 执行 fn,期间所有对响应式数据的访问都会被记录
    // 3. 建立依赖关系:响应式数据 → 这个 effect
    // 4. 当数据变化时,重新执行 fn
}

effect 在 Vue3 中的定位

effect 是整个 Vue3 中,响应式系统的基石:

在 Vue3 底层基础中,effect 可以:

  • 创建响应式对象 reactive
  • 创建响应式引用 ref
  • 实现副作用追踪系统 effect

在 Vue3 中层应用中,基于 effect 可以:

  • 实现计算属性 computed
  • 实现监听器 watch
  • 实现渲染函数 render

在 Vue3 上层暴露中,可以

  • 组合式 API:CompositionAPI
  • 选项式 API:OptionsAPI

手写实现简易版 effect

effect 系统的工作流程

  1. 调用 effect(fn)
  2. 设置 activeEffect = fn
  3. 执行 fn,期间访问响应式数据
  4. 在 get 拦截器中调用 track,将 activeEffect 添加到依赖集合
  5. fn 执行完毕,重置 activeEffect = null
  6. 当数据变化时,在 set 拦截器中调用 trigger
  7. 从依赖集合中取出所有 effect,依次执行

基础版本:activeEffect 模式

javascript 复制代码
// 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;
}

思考题:如果没有 effect 系统,响应式如何实现?

方案1:手动依赖管理(Vue 1.x 的做法)

javascript 复制代码
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 依赖哪个数据

方案2:发布订阅模式(全局事件总线)

javascript 复制代码
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);
}

缺点:需要手动维护事件名,容易出错

方案3:计算属性风格(类似 Vue 2 的 computed)

javascript 复制代码
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); // 重新计算
}

缺点:需要手动标记脏数据

上述方案的共同问题

  1. 无法自动建立依赖关系:需要开发者手动声明依赖,容易遗漏或多余
  2. 更新粒度粗:无法精确知道哪个数据依赖哪个 effect,往往需要全量更新
  3. 内存泄漏风险:手动管理订阅时容易忘记取消,导致内存泄漏
  4. 扩展性差:添加新功能需要修改现有代码,违反开闭原则
  5. 测试困难:依赖关系分散在代码各处,难以单元测试

effect 系统的优势

  1. 自动依赖收集:执行 effect 时自动追踪访问的属性,建立精确依赖关系
  2. 精确更新:数据变化时只重新执行真正依赖它的 effect,避免无效计算
  3. 生命周期管理:effect 可以自动清理,避免内存泄漏
  4. 可组合性:effect 可以嵌套,支持复杂的依赖关系
  5. 测试友好:副作用集中管理,便于测试和调试

结语

本文简单介绍了 Vue3 中的副作用函数 effect,对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!

相关推荐
天蓝色的鱼鱼1 小时前
Vite 8:从“混动”到“纯电”,构建性能提升10倍+
前端·vite
dreams_dream1 小时前
XSS类型
前端·xss
张3蜂1 小时前
Vue.js-知识体系
前端·javascript·vue.js
Cache技术分享2 小时前
333. Java Stream API - 按年份找出合作最多的作者对:避免 Optional.orElseThrow() 的风险
前端·后端
用户600071819102 小时前
【翻译】元素与 Children 属性
前端·react.js
Mintopia2 小时前
又快又好的前端界面软件是怎么做出来的
前端
青青家的小灰灰2 小时前
深入解析 React 中的 useEffect:副作用管理的艺术与科学
前端·react.js
wuhen_n2 小时前
effect函数的完整实现与追踪:深入Vue3响应式核心
前端·javascript·vue.js
Never_Satisfied2 小时前
在JavaScript / HTML中,img标签loading lazy加载时机详解
开发语言·javascript·html