重构与前端代码:重构JavaScript/TypeScript函数与组件
前端代码的重构,说实话,比后端要「扎眼」得多。
为什么?因为UI是直接面向用户的。一个烂函数可能只是慢一点,但一个烂组件,用户一眼就能看出「这页面怎么这么卡」或者「这交互怎么这么别扭」。我这些年重构过的前端项目,少说也有几十个了。每次打开那些动辄上千行的「上帝组件」,我都得深吸一口气。
今天我们就聊聊,怎么把那些又臭又长的JS/TS函数和组件,拆得干净利落。
核心原则:函数只做一件事,组件只管一块UI。超过20行的函数,超过200行的组件,都是重构的信号。
1. 函数重构:从「面条代码」到「乐高积木」
我见过最夸张的一个函数,足足400行。里面混杂着数据校验、DOM操作、API调用、状态更新、甚至还有动画控制。这种函数,改一个bug能引出三个新bug。
1.1 提取条件逻辑
条件判断是函数变长的头号元凶。你想想看,一个if里面套着另一个if,再套个三元表达式……读代码的人不疯才怪。
// 重构前:一个函数里塞了三种逻辑
function processOrder(order) {
if (order.status === 'pending') {
if (order.amount > 1000) {
// 大额订单需要审批
approveLargeOrder(order);
} else {
// 普通订单直接处理
processNormalOrder(order);
}
} else if (order.status === 'shipped') {
// 已发货订单更新物流
updateLogistics(order);
}
}
我个人习惯,只要看到嵌套超过两层的if,立刻提取成独立函数。
// 重构后:每个条件分支都是独立函数
function processOrder(order) {
const handlers = {
'pending': handlePendingOrder,
'shipped': handleShippedOrder,
'cancelled': handleCancelledOrder
};
const handler = handlers[order.status];
if (handler) handler(order);
}
function handlePendingOrder(order) {
order.amount > 1000 ? approveLargeOrder(order) : processNormalOrder(order);
}
function handleShippedOrder(order) {
updateLogistics(order);
}
我的经验:用对象映射替代switch-case或if-else链,代码可读性直接提升一个档次。我在重构一个电商后台时,用这个模式把300行的订单处理函数砍到了60行。
1.2 消除副作用
纯函数是重构的终极目标。但前端不可避免有副作用——发请求、改DOM、写localStorage。怎么办?把副作用隔离到最外层。
// 重构前:副作用和计算混在一起
function calculateTotal(items) {
let total = 0;
items.forEach(item => {
total += item.price * item.quantity;
// 这里居然改了外部变量!
window.totalItems += item.quantity;
});
return total;
}
// 重构后:纯函数只做计算
function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
// 副作用交给调用方处理
function updateUI(items) {
const total = calculateTotal(items);
document.getElementById('total').textContent = total;
}
避坑指南:我曾经在一个支付模块里,发现计算函数偷偷修改了全局汇率变量。结果A页面调用后,B页面的价格全乱了。排查了整整一个下午。从那以后,我要求团队所有计算函数必须是无副作用的纯函数。
2. 组件重构:从「上帝组件」到「原子组件」
组件重构的核心,说白了就是「分」。一个组件如果同时负责数据获取、状态管理、UI渲染、事件处理,那它注定是个灾难。
2.1 容器组件与展示组件分离
这是React社区的老生常谈了,但真正做到的项目并不多。我见过太多组件,直接在JSX里写fetch请求。
// 重构前:组件既管数据又管展示
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setUser);
}, [userId]);
return (
<div>
<h2>{user?.name}</h2>
<p>{user?.email}</p>
</div>
);
}
// 重构后:数据逻辑和UI分离
function UserProfileContainer({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setUser);
}, [userId]);
return user ? <UserProfileView user={user} /> : <Loading />;
}
function UserProfileView({ user }) {
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
这么做的好处:展示组件可以轻松复用、测试、甚至迁移到其他框架。容器组件只关心数据流,UI怎么画它不管。
2.2 提取可复用逻辑到自定义Hook
React Hooks的出现,让逻辑复用变得前所未有的简单。我建议你把所有与UI无关的逻辑都提取到Hook里。
// 重构前:每个组件都重复写分页逻辑
function ProductList() {
const [page, setPage] = useState(1);
const [products, setProducts] = useState([]);
useEffect(() => {
fetch(`/api/products?page=${page}`)
.then(res => res.json())
.then(setProducts);
}, [page]);
// ... 渲染逻辑
}
// 重构后:分页逻辑提取为usePagination
function usePagination(apiUrl) {
const [page, setPage] = useState(1);
const [data, setData] = useState([]);
useEffect(() => {
fetch(`${apiUrl}?page=${page}`)
.then(res => res.json())
.then(setData);
}, [page, apiUrl]);
return { data, page, setPage };
}
function ProductList() {
const { data: products, page, setPage } = usePagination('/api/products');
// 只关心渲染
}
我的习惯:一个组件里如果出现两次以上的useEffect,或者useState超过三个,我就会考虑提取Hook。这不是硬性规定,但经验告诉我,超过这个阈值,组件的复杂度就开始失控了。
3. 知识体系:重构决策树
下面这张图,是我做前端重构时脑子里始终绷着的一根弦。你可以把它当作决策参考。
4. 实战案例:重构一个搜索组件
来看一个真实的例子。这是我去年重构一个后台管理系统时遇到的搜索组件。原始版本大概250行,包含了输入框、下拉筛选、自动补全、历史记录、防抖、缓存……所有东西都塞在一个文件里。
// 重构前:一个组件包罗万象
function SearchBox() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [history, setHistory] = useState([]);
const [loading, setLoading] = useState(false);
// 防抖逻辑
useEffect(() => {
const timer = setTimeout(() => {
if (query) {
setLoading(true);
fetch(`/api/search?q=${query}`)
.then(res => res.json())
.then(data => {
setResults(data);
setLoading(false);
});
}
}, 300);
return () => clearTimeout(timer);
}, [query]);
// 历史记录逻辑
useEffect(() => {
const saved = localStorage.getItem('searchHistory');
if (saved) setHistory(JSON.parse(saved));
}, []);
// ... 还有渲染逻辑、事件处理、样式计算
}
重构后,我把它拆成了四个文件:
| 文件 | 职责 | 行数 |
|---|---|---|
useSearch.js |
搜索逻辑、防抖、API调用 | 35行 |
useSearchHistory.js |
历史记录读写、localStorage缓存 | 25行 |
SearchInput.jsx |
输入框UI、自动补全下拉 | 60行 |
SearchResults.jsx |
结果列表渲染、空状态、加载状态 | 45行 |
每个文件都变得清晰可测。而且,useSearch和useSearchHistory这两个Hook,现在可以被其他组件复用了。
重构后的效果:总行数从250行变成了165行,但可维护性提升了不止一倍。后来产品经理要求增加「搜索联想」功能,我只改了useSearch一个文件,20分钟搞定。
5. 避坑指南:重构中的常见陷阱
- 过度抽象:我见过有人把两行代码也提取成函数。别这样。抽象是为了消除重复,不是为了追求「干净」的形式。
- 一次性改太多:重构要小步快跑。我曾经一次性重构了三个组件,结果一个bug查了两天。现在每次只改一个函数或一个组件,改完就跑测试。
- 忽略TypeScript:如果你用TS,重构时一定要先定义好接口。类型就是最好的文档。我习惯先写类型,再写实现。
- 忘记性能:提取组件时要注意,别把
useMemo和useCallback搞丢了。我踩过这个坑——拆完组件后页面渲染次数翻了三倍。
我曾经犯过的错:重构一个表格组件时,我把所有列配置都提取到了外部JSON里。看起来很优雅对吧?结果产品要求加一列自定义操作按钮,我得改JSON结构、改渲染逻辑、改类型定义……三个地方。从那以后我明白了:抽象要适度,别为了「优雅」牺牲了「灵活」。
前端重构,说到底是个平衡的艺术。拆得太碎,管理成本高;拆得太粗,维护成本高。我的建议是:先从最痛的地方下手——那些你每次改需求都要翻半天的函数和组件,就是最好的重构起点。