别让副作用“爆炸”你的代码!——一次CR后的深度反思

前情提要

笔者下午在code review一个新手的代码时 发现了一个几乎每个新手都会踩的坑:

他把一个表单校验的副作用函数直接放在了业务核心 reducer 的 action 里

不过情有可原 部门的新手入门一般是vue 上手门槛不高 对reducer side-effect之类的理解并不深

笔者有过一段react的开发经验 所以写下了这篇文章 旨在帮助新手朋友们构建更优雅的代码—至少,不能是屎山

什么是副作用(Side Effect)?

笔者刚接触react的时候 也关于这个问题疑虑了好久

useEffect这个hook让我困惑:

这个东西 究竟要不要放到useEffect里 为什么

然后 我就看到了react官方这么写到

“An Effect lets you keep your component synchronized with some external system (like a chat service).”

useEffect – React

这句话说明了useEffect 的作用是让组件与外部系统保持同步,而这些外部系统的交互就是副作用的体现。

你可能还是觉得“副作用”这个词有点抽象,别急,先看看常见的副作用操作:

  • 数据获取:例如,使用 fetchaxios 从服务器获取数据。
  • 订阅和取消订阅:例如,订阅 WebSocket 或事件总线,并在组件卸载时取消订阅。
  • 手动操作 DOM:例如,使用 document.getElementByIdref 直接操作 DOM 元素。
  • 设置定时器:例如,使用 setTimeoutsetInterval
  • 日志记录:例如,使用 console.log 或发送日志到服务器。

看到这里,你对副作用的直观认识应该已经有了雏形。接下来我们来看一下更准确的定义:


副作用指的是函数在执行过程中,除了返回结果之外,还对外部状态产生影响的行为。这些影响包括但不限于:

  • 修改全局变量:函数内部改变了全局变量的值。
  • 进行 I/O 操作:如读取或写入文件、网络请求等。
  • 操作外部设备:如打印机、数据库等。
  • 抛出异常:函数在执行过程中抛出了异常,影响了程序的控制流。

这些副作用使得函数的行为不仅仅取决于输入参数,还依赖于外部状态,从而增加了程序的复杂性。

与之相对的,我们把完全不产生副作用的函数称为纯函数(Pure Function)。纯函数始终对同样的输入产生同样的输出,不依赖也不影响外部状态——这也是函数式编程中的核心理念之一。


写到这里,相信你已经对副作用的本质有了更清晰的理解。

为什么要分离副作用?

副作用混进主逻辑的常见后果

  • 代码将变得难以阅读,如同线缠在一起一般,加大团队协作难度
  • 测试 debug的成本也会急剧变高 再次加大开发成本
  • 多次触发副作用可以会导致多次触发、数据不一样
  • 存在潜在的性能隐患

分离副作用的好处

  • 内部逻辑与“对外逻辑”分明 方便协作者阅读
  • 便于单元测试
  • 以react的useEffect hook为例 分离副作用可以精准控制副作用的触发时机 方便性能调度和优化

小结

说了那么多,其实分离副作用的核心就是让代码更加优雅 同时减少开销

既然要分离,我们该如何优雅地处理副作用

副作用分离实战:React 与 Vue 框架指南

React框架下的实践

在 React 里,副作用的分离几乎是“官方强制”,尤其是函数组件+Hooks 流派。

1. 典型做法:用 useEffect “圈养”副作用

React 推荐所有副作用操作都写进 useEffect(或 useLayoutEffect)里,让组件的“主逻辑”(渲染&交互)变成纯函数,副作用归副作用、逻辑归逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

import { useEffect, useState } from 'react';

function UserProfile({ userId }) {
const [user, setUser] = useState(null);

useEffect(() => {
// 副作用:数据请求
fetch(`/api/user/${userId}`)
.then(res => res.json())
.then(data => setUser(data));
}, [userId]); // 依赖数组,精准控制副作用触发

return <div>{user ? user.name : 'Loading...'}</div>;
}

Tips:

  1. useEffect 里的内容就是副作用。
  2. 依赖项 [userId],保证副作用只在需要的时候执行。
  3. 组件的“渲染逻辑”天然保持纯粹。
  4. 注:实际项目中通常不建议直接在 useEffect 里裸写 fetch,推荐用封装好的请求库或 api 方法,便于统一错误处理和请求管理
  5. 注:useEffect 里的代码在渲染流程之后异步执行,不会阻塞渲染

Vue框架下的实践

通过 setup()onMountedonUnmountedwatch 等 API,我们可以更细粒度地管理副作用,让逻辑解耦更加彻底:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { ref, watch, onMounted, onUnmounted } from 'vue';

const username = ref('');

// 副作用1:生命周期内执行(如事件监听)
onMounted(() => {
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
});

// 副作用2:监听数据变化(如自动校验)
watch(username, async (newVal) => {
if (newVal) {
// 比如自动发请求校验用户名
const exists = await checkUsername(newVal);
if (exists) {
// 弹窗、提示等都属于副作用
ElMessage.error('用户名已存在');
}
}
});

function handleResize() {
// 处理窗口变化
}

return { username };

Tips:

  • onMounted/onUnmounted 负责“和外部世界打交道”(如订阅/解绑)。
  • watch 负责数据响应后的副作用操作(如自动请求、弹窗提示)。
  • 主渲染逻辑和副作用逻辑泾渭分明,组件更易维护
  • 很多同学一入门 Vue 就会用 watch 和生命周期钩子,但真正高级的分离副作用,是把副作用封装成 composable、精确控制依赖、善用副作用清理机制。只有这样,才能写出可维护、易扩展、无脏副作用的组件代码。

小结

副作用分离,是每一个现代前端开发者都绕不开的话题。刚入门时,我们可能只是在文档指导下“会用”生命周期和 watch,能把代码跑起来就算成功;但只有真正理解副作用的本质、学会分离与管理,才能让你的项目从“能跑”变得“优雅可维护”,甚至经得住时间和团队协作的考验。

无论你用 React 还是 Vue,记住这一点:主逻辑越“纯”,副作用越“规矩”,你的代码就越不容易变成屎山

当你把副作用都圈进 useEffect、watch、composable 等“指定区域”,主逻辑清晰,副作用有序——这就是成熟开发者的工程素养。

就写到这里 希望你有所收获。


别让副作用“爆炸”你的代码!——一次CR后的深度反思
https://imjh.xyz/2025/05/19/side-effect/
Author
blues
Posted on
May 19, 2025
Licensed under