重构与前端代码:重构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. 知识体系:重构决策树

下面这张图,是我做前端重构时脑子里始终绷着的一根弦。你可以把它当作决策参考。

前端重构决策树 函数/组件是否过长? 是 → 检查是否超过20行/200行 函数 → 提取子函数/纯函数化 组件 → 容器/展示分离 条件逻辑 → 策略模式/对象映射 副作用 → 隔离到最外层调用 逻辑复用 → 提取自定义Hook UI复用 → 拆分为原子组件 目标:每个函数/组件只做一件事

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行

每个文件都变得清晰可测。而且,useSearchuseSearchHistory这两个Hook,现在可以被其他组件复用了。

重构后的效果:总行数从250行变成了165行,但可维护性提升了不止一倍。后来产品经理要求增加「搜索联想」功能,我只改了useSearch一个文件,20分钟搞定。

5. 避坑指南:重构中的常见陷阱

  • 过度抽象:我见过有人把两行代码也提取成函数。别这样。抽象是为了消除重复,不是为了追求「干净」的形式。
  • 一次性改太多:重构要小步快跑。我曾经一次性重构了三个组件,结果一个bug查了两天。现在每次只改一个函数或一个组件,改完就跑测试。
  • 忽略TypeScript:如果你用TS,重构时一定要先定义好接口。类型就是最好的文档。我习惯先写类型,再写实现。
  • 忘记性能:提取组件时要注意,别把useMemouseCallback搞丢了。我踩过这个坑——拆完组件后页面渲染次数翻了三倍。

我曾经犯过的错:重构一个表格组件时,我把所有列配置都提取到了外部JSON里。看起来很优雅对吧?结果产品要求加一列自定义操作按钮,我得改JSON结构、改渲染逻辑、改类型定义……三个地方。从那以后我明白了:抽象要适度,别为了「优雅」牺牲了「灵活」。

前端重构,说到底是个平衡的艺术。拆得太碎,管理成本高;拆得太粗,维护成本高。我的建议是:先从最痛的地方下手——那些你每次改需求都要翻半天的函数和组件,就是最好的重构起点。

公众号:蓝海资料掘金营,微信deep3321