普通视图

发现新文章,点击刷新页面。
今天 — 2026年2月14日首页

React 性能优化双子星:深入、全面解析 useMemo 与 useCallback

作者 AAA阿giao
2026年2月14日 14:51

引言

在现代 React 应用开发中,随着组件逻辑日益复杂、状态管理愈发庞大,性能问题逐渐成为开发者绕不开的话题。幸运的是,React 提供了两个强大而精巧的 Hooks —— useMemouseCallback,它们如同“缓存魔法”,帮助我们在不牺牲可读性的前提下,显著提升应用性能。

本文将结合完整代码示例,逐行解析、对比说明、深入原理,带你彻底掌握 useMemouseCallback 的使用场景、工作机制、常见误区以及最佳实践。文章内容力求全面、准确、生动有趣,并严格保留原始代码一字不变,确保你既能理解理论,又能直接复用实战。


一、为什么需要 useMemo 和 useCallback?

1.1 React 函数组件的“重运行”特性

在 React 中,每当组件的状态(state)或 props 发生变化时,整个函数组件会重新执行一遍。这意味着:

  • 所有变量都会重新声明;
  • 所有函数都会重新定义;
  • 所有计算逻辑都会重新跑一次。

这本身是 React 响应式更新机制的核心,但也会带来不必要的开销

💡 关键洞察
“组件函数重新运行” ≠ “DOM 重新渲染”。
React 会通过 Virtual DOM diff 算法决定是否真正更新 DOM。
昂贵的计算子组件的无谓重渲染,仍可能拖慢应用。


二、useMemo:为“昂贵计算”穿上缓存外衣

2.1 什么是“昂贵计算”?

看这段代码:

// 昂贵的计算
function slowSum(n) {
  console.log('计算中...')
  let sum = 0
  for(let i = 0; i < n*10000; i++){
    sum += i
  }
  return sum
}

这个 slowSum 函数执行了 n * 10000 次循环!如果 n=100,就是一百万次加法。在每次组件重渲染时都调用它,用户界面可能会卡顿。

2.2 不用 useMemo 的后果

假设我们这样写:

const result = slowSum(num); // ❌ 每次渲染都重新计算!

那么,即使你只是点击了 count + 1 按钮(与 num 无关),slowSum 依然会被执行!因为整个 App 函数重新运行了。

2.3 useMemo 如何拯救性能?

React 提供 useMemo记忆(memoize)计算结果

const result = useMemo(() => {
  return slowSlow(num)
}, [num])

工作原理

  • 第一次渲染:执行函数,缓存结果。

  • 后续渲染:检查依赖项 [num] 是否变化。

    • 如果 num 没变 → 直接返回缓存值,不执行函数体
    • 如果 num 变了 → 重新执行函数,更新缓存。

2.4 完整上下文中的 useMemo 使用

export default function App(){
  const [num, setNum] = useState(0)
  const [count, setCount] = useState(0)
  const [keyword, setKeyword] = useState('')
  const list = ['apple','banana', 'orange', 'pear']

  // ✅ 仅当 keyword 改变时才重新过滤
  const filterList = useMemo(() => {
    return list.filter(item => item.includes(keyword))
  }, [keyword])

  // ✅ 仅当 num 改变时才重新计算 slowSum
  const result = useMemo(() => {
    return slowSum(num)
  }, [num])

  return (
    <div>
      <p>结果: {result}</p>
      <button onClick={() => setNum(num + 1)}>num + 1</button>

      <input type="text" value={keyword} onChange={(e) => setKeyword(e.target.value)} />
      <p>count: {count}</p>
      <button onClick={() => setCount(count + 1)}>count + 1</button>
      {
        filterList.map(item => (
          <li key={item}>{item}</li> 
        ))
      }
    </div>
  )
}

🔍 重点观察

  • 点击 “count + 1” 时:

    • slowSum 不会执行(因为 num 没变);
    • filterList 不会重新计算(因为 keyword 没变);
    • 控制台不会打印 “计算中...” 或隐含的 “filter执行”。
  • 这就是 useMemo 带来的精准缓存

2.5 关于 includes 和 filter 的小贴士

  • "apple".includes("") 确实返回 true(空字符串是任何字符串的子串);
  • list.filter(...) 返回的是一个新数组,即使结果为空(如 []),它也是一个新的引用

⚠️ 正因如此,如果不使用 useMemo,每次渲染都会生成一个新数组引用,可能导致依赖该数组的子组件误判为 props 变化而重渲染!


三、useCallback:为“回调函数”打造稳定身份

3.1 问题起源:函数是“新”的!

在 JavaScript 中,每次函数定义都会创建一个新对象

// 每次 App 重运行,handleClick 都是一个全新函数!
const handleClick = () => { console.log('click') }

即使函数体完全一样,handleClick !== previousHandleClick

3.2 子组件为何“无辜重渲染”?

看这段代码:

const Child = memo(({count, handleClick}) => {
  console.log('child重新渲染')
  return (
    <div onClick={handleClick}>
      <h1>子组件 count: {count}</h1>
    </div>
  )
})
  • memo 的作用:浅比较 props,若没变则跳过渲染。
  • 但每次父组件重渲染,handleClick 都是新函数 → props 引用变了 → memo 失效 → 子组件重渲染!

即使你只改了 numChild 也会重渲染,尽管它只关心 count

3.3 useCallback 的解决方案

useCallback 本质上是 useMemo 的语法糖,专用于缓存函数

const handleClick = useCallback(() => {
  console.log('click')
}, [count])

效果

  • 只要 count 不变,handleClick 的引用就保持不变;
  • Child 的 props 引用未变 → memo 生效 → 跳过重渲染

3.4 完整 useCallback 示例

import {
  useState,
  memo,
  useCallback
} from 'react'

const Child = memo(({count, handleClick}) => {
  console.log('child重新渲染')
  return (
    <div onClick={handleClick}>
      <h1>子组件 count: {count}</h1>
    </div>
  )
})

export default function App(){
  const [count, setCount] = useState(0)
  const [num, setNum] = useState(0)

  // ✅ 缓存函数,依赖 count
  const handleClick = useCallback(() => {
    console.log('click')
  }, [count])
  
  return (
    <div>
      <p>count: {count}</p>
      <button onClick={() => setCount(count + 1)}>count + 1</button>
      <p>num: {num}</p>
      <button onClick={() => setNum(num + 1)}>num + 1</button>
      <Child count={count} handleClick={handleClick} />
    </div>
  )
}

🔍 行为验证

  • 点击 “num + 1”:Child 不会打印 “child重新渲染”;
  • 点击 “count + 1”:Child 重渲染(因为 counthandleClick 都变了);
  • 如果 handleClick 不依赖 count(依赖项为 []),则只有 count 变化时 Child 才重渲染。

四、useMemo vs useCallback:一张表说清区别

特性 useMemo useCallback
用途 缓存任意值(数字、数组、对象等) 缓存函数
本质 useMemo(fn, deps) useMemo(() => fn, deps) 的简写
典型场景 昂贵计算、过滤/映射大数组、创建复杂对象 传递给 memo 子组件的事件处理器
返回值 函数执行的结果 函数本身
错误用法 用于无副作用的纯计算 用于依赖外部变量但未声明依赖

💡 记住
useCallback(fn, deps)useMemo(() => fn, deps)


五、常见误区与最佳实践

❌ 误区1:到处使用 useMemo/useCallback

  • 不要为了“可能的优化”而滥用

  • 缓存本身也有开销(存储、比较依赖项)。

  • 只在以下情况使用

    • 计算确实昂贵(如大数据处理);
    • 导致子组件无谓重渲染(配合 memo);
    • 作为 props 传递给已优化的子组件。

❌ 误区2:依赖项遗漏

const handleClick = useCallback(() => {
  console.log(count) // 依赖 count
}, []) // ❌ 错误!应该写 [count]

这会导致函数捕获旧的 count(闭包陷阱)。

✅ 正确做法:所有外部变量都必须出现在依赖数组中

✅ 最佳实践

  1. 先写逻辑,再优化:不要过早优化。
  2. 配合 React DevTools Profiler:定位真实性能瓶颈。
  3. useMemo 用于值,useCallback 用于函数
  4. 依赖项要完整且精确:使用 ESLint 插件 eslint-plugin-react-hooks 自动检查。

六、总结:性能优化的哲学

useMemouseCallback 并非银弹,而是 React 赋予我们的精细控制权。它们让我们能够:

  • 隔离变化:让无关状态的更新不影响其他部分;
  • 减少冗余:避免重复计算和渲染;
  • 提升用户体验:使应用更流畅、响应更快。

正:

“count 和 keyword 不相关”
“某一个数据改变,只想让相关的子组件重新渲染”

这正是 React 性能优化的核心思想:局部更新,全局协调


附:完整代码地址

源码地址:react/memo/memo/src/App.jsx · Zou/lesson_zp - 码云 - 开源中国

🎉 掌握 useMemouseCallback,你已经迈入 React 性能优化的高手之列!
下次遇到“为什么子组件总在乱渲染?”或“计算太慢怎么办?”,你就知道答案了。

Happy coding! 🚀

昨天以前首页

React 与 Vue 的 CSS 模块化深度实战指南:从原理到实践,彻底告别样式“打架”

作者 AAA阿giao
2026年2月6日 19:35

引言

在前端开发的日常中,我们常常会遇到一个令人抓狂的问题:为什么我只改了一个组件的样式,结果整个页面都乱了?

这背后的根本原因,就是 CSS 的全局作用域特性。默认情况下,所有 .button.header.txt 这样的类名在整个 HTML 文档中都是共享的——你在一个地方定义了 .txt { color: red; },另一个组件用了同样的类名,也会被染红!

为了解决这个问题,现代前端框架如 ReactVue 都提供了强大的 CSS 模块化(Scoped Styling) 能力。它们虽然思路不同,但目标一致:让每个组件的样式只作用于自己,互不干扰

本文将带你深入剖析 React 与 Vue 是如何实现 CSS 模块化的,并逐行解读真实代码,确保你不仅“会用”,更“懂原理”。全文内容详尽、结构清晰,适合初学者入门,也适合进阶开发者查漏补缺。


一、问题的根源:CSS 为何“容易打架”?

CSS(层叠样式表)的设计初衷是全局生效。这意味着:

  • 类名没有作用域;
  • 后加载的样式可能覆盖前面的;
  • 相同类名在不同组件中会互相污染。

比如:

.txt {
  color: red;
}

如果你在两个不同的组件里都用了 <div class="txt">,那么它们都会变成红色——即使你只想让其中一个变红。

这就是我们需要“模块化”的根本原因


二、React 的 CSS 模块化方案

React 社区推崇“显式优于隐式”的哲学,因此它提供了多种模块化方案。我们重点讲解两种:styled-components(CSS-in-JS)CSS Modules(原生 CSS 模块化)

2.1 方案一:styled-components —— 样式即组件

以下正是使用 styled-components 的典型示例:

import {
  useState 
} from 'react';
import styled from 'styled-components';  // 样式组件 

// 样式组件
const  Button = styled.button`
background:${props => props.primary?'blue': 'white'};
color:${props => props.primary?'white': 'blue'};
border: 1px solid blue;
padding: 8px 16px;
border-radius: 4px;
`
console.log(Button);

function App() {
  return (
    <>
      <Button>默认按钮</Button>
      <Button primary>主要按钮</Button>
    </>
  )
}

export default App

它是如何工作的?

  • styled.button 创建了一个新的 React 组件,内部是一个 <button> 元素;
  • 所有写在反引号中的 CSS 会被注入到 <style> 标签中;
  • 关键点:每个 styled 组件都会生成一个唯一的类名(如 sc-abc123-def456 ,确保样式不会冲突;
  • 通过 props 实现动态样式(如 primary 控制颜色);
  • console.log(Button) 会输出一个 React 组件函数,说明它本质是 JS 对象。

浏览器实际渲染效果(简化版):

<style>
.sc-abc123-def456 {
  background: white;
  color: blue;
  border: 1px solid blue;
  padding: 8px 16px;
  border-radius: 4px;
}
.sc-abc123-xyz789 {
  background: blue;
  color: white;
}
</style>

<button class="sc-abc123-def456">默认按钮</button>
<button class="sc-abc123-xyz789">主要按钮</button>

💡 优点:样式与逻辑紧密耦合,支持动态主题、媒体查询、嵌套等;
缺点:运行时注入样式,略微增加 bundle 体积;不适合大型静态样式库。


2.2 方案二:CSS Modules —— 原生 CSS 的模块化革命

以下内容详细描述了 CSS Modules 的机制:

  • 文件名后面添加 .module.css
  • 类名会被编译为 AnotherButton_button__12345
  • 通过 import styles from './Button.module.css' 导入
  • JSX 中使用 {styles.button} 引用

示例:创建一个模块化 CSS 文件

/* Button.module.css */
.button {
  background-color: blue;
  color: white;
  padding: 10px 20px;
  border-radius: 4px;
}

在 React 组件中使用

import styles from './Button.module.css';

function Button({ children }) {
  return <button className={styles.button}>{children}</button>;
}

构建时发生了什么?

假设你的文件路径是 src/components/Button.module.css,构建工具(如 Webpack 或 Vite)会在打包时:

  1. .button 重命名为类似 Button_button__abc123 的唯一字符串;

  2. 生成一个 JavaScript 对象:

    // 编译后的 styles 对象
    const styles = {
      button: "Button_button__abc123"
    };
    
  3. 注入对应的 CSS 到页面中。

优势总结:

  • 完全隔离:每个类名全局唯一,零冲突;
  • 类型安全:配合 TypeScript 可获得自动补全和错误检查;
  • 性能优秀:无运行时开销,纯静态 CSS;
  • 可组合:支持 composes 复用样式(见下文)。

进阶技巧:样式复用(composes

/* base.module.css */
.baseBtn {
  padding: 8px 16px;
  border-radius: 4px;
}

/* Button.module.css */
.primary {
  composes: baseBtn from './base.module.css';
  background: blue;
  color: white;
}

这样,.primary 自动继承了 .baseBtn 的所有样式。


三、Vue 的 CSS 模块化方案:scoped 属性

相比 React 的“显式导入”,Vue 的方案更加“隐形而优雅”——只需在 <style> 标签上加一个 scoped 属性。

以下 Vue 代码完美展示了这一点:

<script setup>
import HelloWorld from './components/HelloWorld.vue'
</script>

<template>
<div>
  <h1 class="txt">Hello txt</h1>
  <h1 class="txt2">Hello txt2</h1>
  <HelloWorld />
</div>
</template>

<style scoped>
.button {
  background-color: blue;
  color: white;
  padding: 10px 20px;
}
.txt {
  color: red;
  background-color: orange;
}
.txt2 {
  color: pink;
}
</style>

以及子组件 HelloWorld.vue

<script setup>
</script>

<template>
  <div>
    <h1 class="txt">你好</h1>
    <h1 class="txt2">你好2</h1>
    该子组件中无样式内容,跟随父组件的样式
    如果子组件中需要自定义样式,需要使用scoped属性
    此时,子组件中的样式只作用于当前组件,不会影响到其他组件
    如果不加scoped属性,子组件中的样式会影响到其他组件
  </div>
</template>

<style scoped>
.txt {
  color: blue;
  background-color: green;
  font-size: 30px;
}
.txt2 {
  color: orange;
}
</style>

scoped 是如何实现隔离的?

Vue 在编译阶段会:

  1. 为当前组件生成一个唯一的 hash,例如 data-v-f3f3ec42
  2. 给组件内所有根元素(或指定元素)添加该属性;
  3. 重写 <style scoped> 中的选择器,加上属性限制。

编译后效果(简化):

父组件样式

.txt[data-v-f3f3ec42] { color: red; }
.txt2[data-v-f3f3ec42] { color: pink; }

子组件样式

.txt[data-v-7ba5bd90] { color: blue; }
.txt2[data-v-7ba5bd90] { color: orange; }

HTML 渲染结果

<div data-v-f3f3ec42>
  <h1 class="txt" data-v-f3f3ec42>Hello txt</h1>
  <h1 class="txt2" data-v-f3f3ec42>Hello txt2</h1>
  <div data-v-7ba5bd90>
    <h1 class="txt" data-v-7ba5bd90>你好</h1>
    <h1 class="txt2" data-v-7ba5bd90>你好2</h1>
  </div>
</div>

结果:尽管类名相同,但因为 data-v-xxx 不同,样式完全隔离!

注意事项:深度选择器

如果你希望父组件的样式能影响子组件(比如定制第三方 UI 库),可以使用 :deep()

<style scoped>
.parent :deep(.child) {
  color: purple;
}
</style>

📌 Vue 2 中使用 /deep/::v-deep,Vue 3 推荐使用 :deep()


四、React vs Vue:CSS 模块化对比全景图

维度 React (CSS Modules) React (styled-components) Vue (scoped)
实现方式 类名哈希化 动态生成唯一类名 + 注入 <style> 属性选择器 ([data-v-xxx])
样式位置 独立 .module.css 文件 写在 JS/TSX 中 写在 .vue 单文件组件内
类名可读性 开发时需 styles.xxx,运行时为哈希 开发时直观,运行时为哈希 开发和运行时均为原始类名
作用域强度 ⭐⭐⭐⭐⭐(绝对隔离) ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐(依赖属性,可被绕过)
动态样式 需结合 JS 条件拼接 原生支持 props 需绑定动态 class
TypeScript 支持 完美(自动类型推导) 良好 有限
学习成本 中等(需理解模块导入) 低(直观) 极低(加个 scoped 即可)
适用场景 大型项目、团队协作、静态样式多 快速原型、动态主题、UI 库开发 中小型项目、快速开发、Vue 生态

五、为什么需要 CSS 模块化?—— 真实痛点解析

场景 1:多人协作项目

想象一个 10 人团队同时开发一个后台系统。A 写了 .card { padding: 10px; },B 也写了 .card { margin: 20px; }。如果不模块化,最终 .card 会同时有 padding 和 margin,甚至可能因加载顺序导致样式错乱。

模块化后:A 的 .card 变成 PageA_card__abc,B 的变成 PageB_card__def,互不影响。

场景 2:开源组件库

如果你发布一个 React 组件库,使用普通 CSS,用户很容易因为类名冲突导致样式异常。而使用 CSS Modules 或 styled-components,就能保证“开箱即用,零污染”。

场景 3:微前端架构

在微前端中,多个子应用共存于同一页面。若都使用全局 CSS,冲突几乎是必然的。模块化是微前端样式的安全基石


六、最佳实践建议

React 项目推荐

  • 中小型项目:优先使用 styled-components,开发体验极佳;
  • 大型企业级应用:采用 CSS Modules + TypeScript,兼顾性能与可维护性;
  • 避免:直接使用全局 CSS(除非是 reset/normalize)。

Vue 项目推荐

  • 默认开启 scoped:所有组件样式都加上 scoped
  • 全局样式单独管理:如 assets/styles/global.css,用于 reset、变量、通用类;
  • 慎用深度选择器:仅在必要时(如覆盖 Element Plus 样式)使用 :deep()

七、结语:选择适合你的“样式盔甲”

  • React 的 CSS Modules 像一套精密的“锁链铠甲”——每一块甲片(类名)都有唯一编号,严丝合缝,坚不可摧;
  • styled-components 则像一件“魔法斗篷”——样式随组件而生,动态变幻,灵活自如;
  • Vue 的 scoped 更像一层“隐形护盾”——你看不见它,但它默默守护着你的样式不被污染。

🎯 记住:技术没有绝对优劣,只有是否适合当前项目。
但无论你选择哪一种,请坚持一致性——团队统一规范,才是长期可维护的关键。

现在,回看开头那个“按钮莫名变蓝”的问题,你已经有能力彻底解决它了!

❌
❌