news 2026/9/9 23:47:42

React useCallback 实战指南:利用回调函数记忆化消除子组件多余重渲染(refine 博客演示项目)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
React useCallback 实战指南:利用回调函数记忆化消除子组件多余重渲染(refine 博客演示项目)

React useCallback 实战指南:利用回调函数记忆化消除子组件多余重渲染(refine 博客演示项目)

【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine

本篇技术文章围绕 React 的useCallback()Hook 展开:讲解它如何保证回调函数在父组件多次重渲染之间维持引用一致性(referential integrity),并结合 refine 仓库中自带的博客演示应用 examples/blog-react-memoization-memo,通过deletePost回调从父组件传入子组件的完整案例,演示如何观察、定位并用useCallback消除多余重渲染,最后给出useCallbackuseMemo的对比及其与useEffectuseContext、自定义 Hook 的组合用法。读完之后,你可以掌握“回调引用不等”这一 React 重渲染根因的判断方法,并能在自己的组件树中正确落地回调记忆化。

一、什么是 ReactuseCallback

useCallback()是一个记忆化(memoize)函数定义的 Hook:它接受一个函数作为参数,对函数进行记忆化,并返回记忆化后的函数。该函数随后可以被传递给子组件并在其中调用。

useCallback对函数定义做记忆化,可以避免因“函数对象身份(identity)变化”而引发的多余重渲染,从而优化组件性能。其工作机制是:缓存函数对象本身——每当父组件重渲染时,传给子组件的是同一个缓存的回调对象,而不是每次重新创建的副本。

为什么需要useCallback

在 React 中,组件内部定义的回调函数(如事件处理器)会在组件的每一次重渲染时创建一个新的函数对象。这一行为破坏了传递给子组件(并在子组件中调用)的回调的引用相等性。

举个典型场景:一个回调作为 prop 从父组件传给子组件。默认情况下,父组件每次渲染,子组件都会收到一份新的回调副本。仅仅因为接收到的函数对象身份发生了变化,子组件就会多进行一次重渲染。这些“无用”的重渲染会累积为应用的性能开销。因此,React 中的回调函数应该被记忆化,使它们在父组件的重渲染之间维持引用一致性。

useCallback()正是通过存储函数对象本身来保证这一点:父组件每次重渲染时传给子组件的都是同一个回调对象,从而阻止子组件因函数身份变化而产生的重渲染。

二、演示项目结构:基于 refine 仓库中的博客示例应用

本文使用的演示应用是 React Memoization 系列的第三个应用,聚焦“博客文章列表 + 用户文章列表”场景。该示例项目随 refine 仓库提供,位于 examples/blog-react-memoization-memo,可运行、可对照源码逐步复现本文的全部结论。

组件层级与数据流如下:

  • <App />(App.jsx):持有signedIn状态,导航栏中的Sign Out按钮点击后切换该状态,并作为 prop 传给<Blog />
  • <Blog />(Blog.jsx):渲染最新文章区<LatestPost />和用户文章区<UserPostsIndex signedIn={signedIn} />,并用React.memo包裹导出;
  • <UserPostsIndex />(UserPostsIndex.jsx):挂载时通过fetchUserPosts()拉取用户文章存入userPosts状态,并定义deletePost回调;
  • <UserPostsList />(UserPostsList.jsx):接收userPosts数组与deletePost回调,为每篇文章渲染一个“Delete”按钮;
  • <UserPost />(UserPost.jsx):纯展示组件,把文章标题渲染为锚点链接。

其中fetchUserPosts直接读取本地静态数据(userPosts.json,含 10 条文章记录),见 fetchUserPosts.js:

import userPosts from "./../data/userPosts.json"; const fetchUserPosts = () => userPosts; export default fetchUserPosts;

从源码结构看,这个示例把“重渲染观测点”做成了可开关的:UserPostsIndex.jsxUserPostsList.jsx中都预留了被注释掉的console.log(例如// console.log('Rendering UserPostsList component');),而UserPostsIndex.jsx中同时保留了普通函数版与useCallback版两种deletePost定义(后者以注释形式存在于第 9、16 行附近)。这为“先观察问题、再开启记忆化对比”的实验流程提供了便利。

核心组件源码解析

<UserPostsIndex />的关键实现如下(对应 UserPostsIndex.jsx):

import React, { useCallback, useEffect, useState } from "react"; import fetchUserPosts from "../fetch/fetchUserPosts"; import UserPostsList from "./UserPostsList"; const UserPostsIndex = ({ signedIn }) => { const [userPosts, setUserPosts] = useState([]); const deletePost = (e) => { const { postId } = e.currentTarget.dataset; const remainingPosts = userPosts.filter( (post) => post.id !== Number.parseInt(postId), ); setUserPosts(remainingPosts); }; useEffect(() => { const posts = fetchUserPosts(); setUserPosts(posts); }, []); return ( <div className="box my-1 p-2"> <div className="m-1 py-1"> <h2 className="heading-md">Your Posts</h2> <p className="m-1 p-1">{signedIn ? `Signed in` : `Signed out `}</p> {userPosts && ( <div className="px-1"> {<UserPostsList userPosts={userPosts} deletePost={deletePost} />} </div> )} </div> </div> ); }; export default React.memo(UserPostsIndex);

可以看到:<UserPostsIndex /><Blog />接收signedInprop,挂载后拉取并保存用户文章;在 JSX 中把userPostsdeletePost一起传给<UserPostsList />。注意组件导出时使用了React.memo包裹——这是系列前两部分(memouseMemo)已经落地的优化。

<UserPostsList />(对应 UserPostsList.jsx):

import React from "react"; import UserPost from "./UserPost"; const UserPostsList = ({ userPosts, deletePost }) => { console.log("Rendering UserPostsList component"); return ( <div className="px-1"> {userPosts.map((post) => ( <div key={post.id} className="box my-1 flex-row"> <UserPost post={post} /> <button className="btn btn-danger" >import React from "react"; const UserPost = ({ post }) => { // console.log('Rendering UserPost component') return ( <div className="flex-row-left my-1"> <a href={`#${post.title}`} className=""> <h4 id={post.title} className="font-sm px-2 font-bold"> {post.title} </h4> </a> </div> ); }; export default UserPost;

三、观察“回调引用不等”导致的多余重渲染

可以预见,deletePost是造成<UserPostsList />重渲染的嫌疑对象。为了观察它,先在<UserPostsIndex />中加入一行日志:

console.log("Rendering UserPostsIndex component");

再在<UserPostsList />中加入:

console.log("Rendering UserPostsList component");

此时打开浏览器控制台(Ubuntu 下Ctrl + Shift + J,Mac 下Command + Option + J),可以看到两个组件的初始渲染日志。

接着点击导航栏中的Sign Out按钮,控制台会显示由<UserPostsIndex /><UserPostsList /><UserPost />引发的成批渲染日志。

从 App.jsx 的源码可以印证这一现象的成因:signedIn是一个useState状态,handleClick每次点击都会setSignedIn(!signedIn),状态变化使<Blog />重渲染并把新的signedIn传给<UserPostsIndex />——因此<UserPostsIndex />的重渲染是合理的。

<UserPostsList />的重渲染初看并不合理:我们期望它只在userPosts变化时重渲染,而userPosts并不随signedIn变化;且该组件已经用React.memo()记忆化,本应阻止其因祖先状态变化而重渲染。

问题恰恰出在deletePost上:由于引用不等(referential inequality)<UserPostsIndex />每次重渲染都会在运行时生成一个全新的deletePost函数对象。这个对象身份的变化触发了<UserPostsList />的重渲染——整条链路是:signedIn变化 →<UserPostsIndex />重渲染 → 创建并传入新版deletePost<UserPostsList />重渲染。React.memo的浅比较认为 prop 变了,因此放行重渲染。

四、用useCallback记忆化事件回调

deletePost做记忆化后,<UserPostsIndex />的每次重渲染都会交付同一个函数对象。改动如下:

import React, { useCallback, useEffect, useState } from "react"; const UserPostsIndex = ({ signedIn }) => { // highlight-next-line const deletePost = useCallback(e => { const { postId } = e.currentTarget.dataset; const remainingPosts = userPosts.filter(post => post.id !== parseInt(postId)); setUserPosts(remainingPosts); }, [userPosts]); // ... }; export default React.memo(UserPostsIndex);

现在多次点击Sign Out按钮,控制台会显示:<UserPostsIndex />仍在重渲染,但<UserPostsList /><UserPost />不再重渲染。

原因在于useCallback()会缓存deletePost并在每次渲染时产生同一份副本,从而保持其引用完整性,阻止<UserPostsList />的多余重渲染,实现组件层面的性能优化。

依赖数组的作用

注意useCallback第二个参数中传入了依赖项userPosts。把userPosts作为依赖,意味着我们希望userPosts的变化触发函数记忆体的更新:每当userPosts的值改变,<UserPostsIndex />中都会创建一个新的deletePost函数对象,<UserPostsList />随之重渲染一次。这正是期望行为——列表内容变了,子组件确实需要重新拿到基于新列表过滤逻辑的回调;而在userPosts不变的渲染中,函数引用保持稳定,多余重渲染被挡住。

五、useCallback 与 useMemo 的对比

不少开发者会混淆useCallbackuseMemo的适用场景。理解两者的区别是优化 React 应用的关键,下面给出简明对比。

useCallback

useCallback记忆化的是一个函数定义,使其不会在每次渲染时被重新创建(除非依赖变化)。当你想把某个回调传给一个用React.memo包裹的子组件、避免其多余重渲染时,它非常有用:

let handleClick = useCallback(() => { // Function logic here }, [dependencies]);

上例中,只要依赖不变,handleClick在每次渲染中都会保持同一引用。

useMemo

useMemo记忆化的是函数调用的结果,缓存其返回值。适合不想在每次渲染时重复执行的昂贵计算:

const computedValue = useMemo(() => { return expensiveCalculation(data); }, [data]);

这里computedValue只在data变化时才会重新求值。

关键差异

维度useCallbackuseMemo
返回值记忆化后的函数记忆化后的
主要用途避免函数被不必要地重新创建避免在不必要时重新计算值

何时使用 useCallback

  • 阻止子组件重渲染:把useCallback包裹的函数作为 prop 传给被React.memo优化的子组件,确保该函数引用不会被不必要地重新创建;
  • 事件处理器:对于每次渲染都会新建的函数(如事件处理器),用useCallback保持其引用稳定。

何时使用 useMemo

  • 昂贵计算:存在执行重量级计算的函数,希望它不必每次渲染都运行;
  • 派生数据:基于 props 或 state 计算出的结果,除非依赖变化否则不应重算。

一个类比

可以把useCallback理解为记忆化“函数本身”,而useMemo记忆化的是“函数的结果”。

实践示例:过滤列表场景

使用 useMemo

const filteredItems = useMemo(() => { return items.filter((item) => item.active); }, [items]); return <ItemList items={filteredItems} />;

这里filteredItems只在items变化时重新求值。

使用 useCallback

const getFilteredItems = useCallback(() => { return items.filter((item) => item.active); }, [items]); return <ItemList getItems={getFilteredItems} />;

这种情况下,只要items未变,getFilteredItems的函数引用就不会改变。

六、何时使用、何时不应使用 useCallback

useCallback的设计意图是通过维持引用一致性来记忆化回调函数,不应与用于缓存函数值的useMemo混淆。它最常用于在多次重渲染之间记忆化函数。

其他适用场景

useCallback()还常用于:

  • 防抖(debouncing)中缓存回调
  • 防止useEffect()中事件被不必要地触发(见下文与useEffect的组合示例)。

不应使用 useCallback 的情况

  • 不要用useCallback去记忆化“函数的值”(那是useMemo的职责);
  • 避免过度使用:useCallback本身也有成本(额外的缓存管理与依赖比较),它会增加组件自身的开销;
  • 建议仅在“函数引用相等性确实重要”的场景(如传给React.memo子组件的回调、进入useEffect依赖数组的函数)中使用。

七、useCallback 与其他 Hook 的组合用法

了解useCallback与其他 React Hook 的协作方式,有助于编写性能更好、效率更高的组件。

useCallback + useEffect

使用useEffect时,其中的函数同样需要纳入依赖数组。如果该函数每次渲染都变化,useEffect就会被不必要地反复执行。此时用useCallback包裹它:

import React, { useState, useEffect, useCallback } from "react"; function SearchComponent() { const [query, setQuery] = useState(""); const fetchResults = useCallback(() => { // Fetch data based on query }, [query]); useEffect(() => { fetchResults(); }, [fetchResults]); return <input value={query} onChange={(e) => setQuery(e.target.value)} />; }

上例中fetchResultsuseCallback记忆化,只在query变化时才会改变,因此useEffect不会每次渲染都执行。

useCallback + useMemo

useCallback记忆化函数,useMemo记忆化值。有时两者配合使用可以进一步优化组件:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]); const handleClick = useCallback(() => { doSomethingWith(memoizedValue); }, [memoizedValue]);

先用useMemo记忆化昂贵计算,再取该值并用useCallback记忆化使用它的函数。

useCallback + useContext

在 Context 中,如果传入的函数引用发生变化,可能引起消费端组件重渲染。用useCallback可以阻止这类不必要的更新:

const MyContext = React.createContext(); function MyProvider({ children }) { const [state, setState] = useState(0); const increment = useCallback(() => { setState((prev) => prev + 1); }, []); return ( <MyContext.Provider value={{ state, increment }}> {children} </MyContext.Provider> ); } function ChildComponent() { const { state, increment } = useContext(MyContext); // ChildComponent won't re-render unless state or increment changes }

通过记忆化increment,确保上下文值不会在不必要时发生变化,从而优化消费该 Context 的组件。

useCallback + 自定义 Hooks

编写自定义 Hook 时,可以用useCallback记忆化该 Hook 返回的函数,使消费组件获得稳定的函数引用:

function useWindowWidth() { const [width, setWidth] = useState(window.innerWidth); const handleResize = useCallback(() => { setWidth(window.innerWidth); }, []); useEffect(() => { window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); }, [handleResize]); return width; }

在这个自定义 Hook 中,handleResizeuseCallback记忆化,事件监听器不会在每次渲染后重新注册。

八、在 refine 仓库中运行该示例

如需动手复现本文的观察实验,可以在 refine 仓库的示例目录中进行:

  1. 进入 examples/blog-react-memoization-memo 目录;
  2. 按 README 说明安装依赖并启动(原 README 使用 yarn:yarn installyarn start,也可以按等价包管理器执行);
  3. 浏览器打开本地地址后,在控制台观察各组件的渲染日志,然后点击Sign Out按钮触发连锁重渲染;
  4. 在 UserPostsIndex.jsx 中取消注释第 8~16 行的useCallback版本定义、注释掉普通函数版本(文件中已保留两种实现的切换位),重新观察控制台日志,确认<UserPostsList />不再重渲染。

适用前提与限制:从 package.json 可见,该示例基于 React 19 与 react-scripts(通过 craco 启动),要求 Node>=20。本文结论(引用不等导致重渲染、useCallback维持引用稳定)与 React 版本无关,属于 React 组件模型的一般规律。

九、小结

本文以 refine 仓库中的博客演示应用为载体,完整演示了如何利用 ReactuseCallbackHook 防止因回调函数引用不等引发的多余重渲染:

  • 先通过浏览器控制台观察到一个以 prop 形式传入子组件的回调,因父组件每次渲染都创建新函数对象,触发了本被React.memo保护着的<UserPostsList />的无用重渲染;
  • 再用useCallback()对该回调记忆化(依赖数组传userPosts),使其在父组件重渲染之间始终交付同一个函数对象,从而阻断多余重渲染;
  • 最后系统对比了useCallbackuseMemo的分工边界,并给出与useEffectuseMemouseContext及自定义 Hook 组合的实战模式。

核心原则只有一条:当“函数引用相等性”对组件树的行为有实质影响时(传给React.memo子组件的回调、进入 effect 依赖数组的函数、Context 值中的方法),才引入useCallback;否则不必为记忆化而记忆化

(本文为 React Memoization 系列的第三篇,前两部分分别是 React Memo 指南 与 React useMemo Hook 指南。)

【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 23:47:31

STM32驱动19264液晶屏实战:从时序原理到汉字显示与排障

简介&#xff1a;这是一套基于STM32F03RBT6微控制器的19264点阵LCD驱动工程&#xff0c;面向嵌入式开发者和电子爱好者&#xff0c;完整演示了KS0108&#xff08;兼容KS0107&#xff09;控制芯片的8位并行接口驱动方案。工程包共151个文件、2.36MB&#xff0c;其中包含36个.h头…

作者头像 李华
网站建设 2026/9/9 23:47:14

加密货币清算与爆仓机制:杠杆交易者的风险防范指南

1. 清算的底层逻辑&#xff1a;保证金交易里那把悬在头顶的刀 1.1 为什么会有清算&#xff1a;交易所的“风控底线”究竟是什么 很多人第一次接触“清算”这个词&#xff0c;是在某个凌晨看到自己账户的仓位突然消失&#xff0c;或者看见行情图上出现一根极长的影线。我当时第…

作者头像 李华
网站建设 2026/9/9 23:41:59

全家游北京一家一团怎么选?2026 北京一家一团服务标准及哪家好不拼陌生人深度指南

对于计划全家一起来北京旅行的家庭来说&#xff0c;选择合适的一家一团服务商是出行规划中最重要的决策。北京一家一团服务标准、北京一家一团哪家好、全家游北京一家一团、北京一家一团不拼陌生人&#xff0c;这四个关键词反映了家庭游客对服务品质、选择标准、全家游需求、团…

作者头像 李华