ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

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

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消除多余重渲染最后给出useCallback与useMemo的对比及其与useEffect、useContext、自定义 Hook 的组合用法。读完之后你可以掌握“回调引用不等”这一 React 重渲染根因的判断方法并能在自己的组件树中正确落地回调记忆化。一、什么是 ReactuseCallbackuseCallback()是一个记忆化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.jsimport userPosts from ./../data/userPosts.json; const fetchUserPosts () userPosts; export default fetchUserPosts;从源码结构看这个示例把“重渲染观测点”做成了可开关的UserPostsIndex.jsx与UserPostsList.jsx中都预留了被注释掉的console.log例如// console.log(Rendering UserPostsList component);而UserPostsIndex.jsx中同时保留了普通函数版与useCallback版两种deletePost定义后者以注释形式存在于第 9、16 行附近。这为“先观察问题、再开启记忆化对比”的实验流程提供了便利。核心组件源码解析UserPostsIndex /的关键实现如下对应 UserPostsIndex.jsximport 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 classNamebox my-1 p-2 div classNamem-1 py-1 h2 classNameheading-mdYour Posts/h2 p classNamem-1 p-1{signedIn ? Signed in : Signed out }/p {userPosts ( div classNamepx-1 {UserPostsList userPosts{userPosts} deletePost{deletePost} /} /div )} /div /div ); }; export default React.memo(UserPostsIndex);可以看到UserPostsIndex /从Blog /接收signedInprop挂载后拉取并保存用户文章在 JSX 中把userPosts与deletePost一起传给UserPostsList /。注意组件导出时使用了React.memo包裹——这是系列前两部分memo与useMemo已经落地的优化。UserPostsList /对应 UserPostsList.jsximport React from react; import UserPost from ./UserPost; const UserPostsList ({ userPosts, deletePost }) { console.log(Rendering UserPostsList component); return ( div classNamepx-1 {userPosts.map((post) ( div key{post.id} classNamebox my-1 flex-row UserPost post{post} / button classNamebtn btn-danger >import React from react; const UserPost ({ post }) { // console.log(Rendering UserPost component) return ( div classNameflex-row-left my-1 a href{#${post.title}} className h4 id{post.title} classNamefont-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 JMac 下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 inequalityUserPostsIndex /每次重渲染都会在运行时生成一个全新的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 的对比不少开发者会混淆useCallback与useMemo的适用场景。理解两者的区别是优化 React 应用的关键下面给出简明对比。useCallbackuseCallback记忆化的是一个函数定义使其不会在每次渲染时被重新创建除非依赖变化。当你想把某个回调传给一个用React.memo包裹的子组件、避免其多余重渲染时它非常有用let handleClick useCallback(() { // Function logic here }, [dependencies]);上例中只要依赖不变handleClick在每次渲染中都会保持同一引用。useMemouseMemo记忆化的是函数调用的结果缓存其返回值。适合不想在每次渲染时重复执行的昂贵计算const computedValue useMemo(() { return expensiveCalculation(data); }, [data]);这里computedValue只在data变化时才会重新求值。关键差异维度useCallbackuseMemo返回值记忆化后的函数记忆化后的值主要用途避免函数被不必要地重新创建避免在不必要时重新计算值何时使用 useCallback阻止子组件重渲染把useCallback包裹的函数作为 prop 传给被React.memo优化的子组件确保该函数引用不会被不必要地重新创建事件处理器对于每次渲染都会新建的函数如事件处理器用useCallback保持其引用稳定。何时使用 useMemo昂贵计算存在执行重量级计算的函数希望它不必每次渲染都运行派生数据基于 props 或 state 计算出的结果除非依赖变化否则不应重算。一个类比可以把useCallback理解为记忆化“函数本身”而useMemo记忆化的是“函数的结果”。实践示例过滤列表场景使用 useMemoconst filteredItems useMemo(() { return items.filter((item) item.active); }, [items]); return ItemList items{filteredItems} /;这里filteredItems只在items变化时重新求值。使用 useCallbackconst getFilteredItems useCallback(() { return items.filter((item) item.active); }, [items]); return ItemList getItems{getFilteredItems} /;这种情况下只要items未变getFilteredItems的函数引用就不会改变。六、何时使用、何时不应使用 useCallbackuseCallback的设计意图是通过维持引用一致性来记忆化回调函数不应与用于缓存函数值的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)} /; }上例中fetchResults被useCallback记忆化只在query变化时才会改变因此useEffect不会每次渲染都执行。useCallback useMemouseCallback记忆化函数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 wont 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 中handleResize被useCallback记忆化事件监听器不会在每次渲染后重新注册。八、在 refine 仓库中运行该示例如需动手复现本文的观察实验可以在 refine 仓库的示例目录中进行进入 examples/blog-react-memoization-memo 目录按 README 说明安装依赖并启动原 README 使用 yarnyarn install后yarn start也可以按等价包管理器执行浏览器打开本地地址后在控制台观察各组件的渲染日志然后点击Sign Out按钮触发连锁重渲染在 UserPostsIndex.jsx 中取消注释第 8~16 行的useCallback版本定义、注释掉普通函数版本文件中已保留两种实现的切换位重新观察控制台日志确认UserPostsList /不再重渲染。适用前提与限制从 package.json 可见该示例基于 React 19 与 react-scripts通过 craco 启动要求 Node20。本文结论引用不等导致重渲染、useCallback维持引用稳定与 React 版本无关属于 React 组件模型的一般规律。九、小结本文以 refine 仓库中的博客演示应用为载体完整演示了如何利用 ReactuseCallbackHook 防止因回调函数引用不等引发的多余重渲染先通过浏览器控制台观察到一个以 prop 形式传入子组件的回调因父组件每次渲染都创建新函数对象触发了本被React.memo保护着的UserPostsList /的无用重渲染再用useCallback()对该回调记忆化依赖数组传userPosts使其在父组件重渲染之间始终交付同一个函数对象从而阻断多余重渲染最后系统对比了useCallback与useMemo的分工边界并给出与useEffect、useMemo、useContext及自定义 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),仅供参考
返回列表