简介:react-hooks-async是一套面向React开发者的自定义Hooks库,用于解决JavaScript Promise无法直接中止或取消的问题。它基于AbortController实现异步任务的可终止性,并提供useAsyncTask、useAsyncRun等核心Hook,以及组合全部、竞态、顺序执行等高级用法,可满足请求取消、竞态控制、进度展示等常见前端场景。资源包含完整源码、类型声明、示例工程与测试用例,共92个文件,以JS、TS、TSX源码为主,另有JSON配置、HTML演示、Markdown文档和WASM示例等,压缩包仅295KB,轻量易读。已有315人学习。通过该资源可快速掌握React Hooks异步处理模式,也可为封装Axios、WASM等异步任务提供参考模板。目录结构涵盖dist、src、examples、__tests__等模块,源码与示例对应清晰,便于按需阅读和二次开发。
1. 从一次“点太快就白屏”的故障说起:React 异步函数到底缺什么
某次给内部后台加列表页,用户反馈“把查询按钮连点几下,页面偶尔卡死,数据还是旧的那份”。查了半天,问题不是接口慢,而是没有做“请求取消”。前一个慢请求回来时,把后一个请求的 setState 覆盖了,网络和时间全部错乱。这种问题在 React 里并不稀有,因为useEffect天生的执行时机和异步回调的完成时机完全对不上,而官方 hooks 并没有直接提供“可终止、可组合”的异步能力。react-hooks-async就是补这个缺的那批自定义 hooks:它围绕AbortController把每次异步请求变成“能取消、能组合、能安全 setState”的受控单元,专治搜索列表、弹窗连续提交、图表频繁刷新这类容易竞态的场景。适合被异步状态折磨过,并且想在 React 里拿回“请求生命周期控制权”的开发者。
2. 项目定位与 API 概览:三个核心 hook 解决响应式异步的三类问题
2.1 三个核心 hooks:useAsync / useAsyncCallback / useAsyncAbortable
这套库本质是“把异步函数变成 React 状态机”的封装集合。先看它的定位:当你决定不使用 Redux Toolkit Query、React Query 这类重型数据方案,又不想每次写五六个useState去管理pending / data / error时,它就是中间层。核心导出的 hooks 有三个,职责边界非常清晰。
| Hook 名称 | 核心职责 | 典型返回结构 | 适用场景 |
|---|---|---|---|
useAsync | 组件渲染后自动发起异步请求 | { data, loading, error, abort } | 页面初始加载、依赖 props 变化自动重新请求 |
useAsyncCallback | 由事件触发异步请求 | { data, loading, error, execute, abort } | 点击按钮、提交表单、分页切换 |
useAsyncAbortable | 每次执行自动生成新的AbortController | { data, loading, error, execute, abort, signal } | 渲染时自动持有可取消信号,依赖变化即取消旧请求 |
先说一个容易混淆的点:useAsync和useAsyncAbortable看起来都是“自动请求”,但前者偏向于“只关心数据最终回来”,后者把每一次请求的signal暴露给你,让你可以把取消信号透传给子组件、fetch、甚至 axios 的signal配置。我通常把useAsyncAbortable理解为“需要更精细控制取消时机”的版本,需要手动把 signal 传给异步函数;useAsync适合那种“请求发出去就不管,setState 时给我安全保护”的默认场景。
从使用者的角度,useAsync的参数一般是这样的:
const { data, loading, error, abort } = useAsync( async (signal) => { const res = await fetch('/api/list', { signal }); return res.json(); }, [page] // 依赖数组,page 变化时自动重新执行 );逻辑说明:useAsync接收两个参数,第一个是返回 Promise 的异步函数,第二个是依赖数组,依赖变化时 hook 会在内部触发重新执行。上面代码里把signal传给fetch,当依赖变化触发下一次执行时,前一次请求的signal会被abort(),浏览器层面会直接取消这次网络请求。这就是“可终止性”落在代码里的第一步。
参数说明:signal类型是AbortSignal | null,首次渲染时为 null,第二次开始才有值;deps数组参与内部useEffect的依赖收集,必须传引用稳定或者基础类型的值,否则会陷入重复请求的死循环。我用的时候会刻意把signal放在第一位参数,保证它在promiseFn内部一定可用。
useAsyncCallback则不接受依赖数组,它把触发权交给你:
const { execute, loading, data } = useAsyncCallback( async (userId, { signal }) => { return await api.getUser(userId, { signal }); } ); // 事件里手动触发 <button onClick={() => execute(userId)}>查询</button>逻辑说明:execute返回 Promise,可以在 await 后拿到结果;内部同样维护 loading 状态,连续点击时前一次执行会被标记为 stale,最后写 state 时会做“是否还是最新一次”的判断,避免旧请求覆盖新数据。
这两个 hook 的关系理解之后,第三层useAsyncAbortable就很好说:它把signal从配置项变成返回项,你可以在父组件里把 signal 往下传给子组件的fetch,甚至给axios.get(url, { signal })。这种能力在“不同子组件各自发请求,但需要由父组件统一取消”的场景里尤其有用。
2.2 源码包结构与模块边界:一个 hooks 库该怎么组织
拿到源码包先看目录,熟悉结构比急着跑 demo 更重要。react-hooks-async 的源码组织方式是典型的“hooks 单一职责拆包”:
src/ ├── useAsync.ts # 自动感知依赖的异步状态 hook ├── useAsyncCallback.ts # 事件驱动的异步状态 hook ├── useAsyncAbortable.ts # 暴露 signal 的可终止 hook ├── useAbortController.ts # AbortController 生命周期管理内部 hook ├── createAbortableFn.ts # 核心:把 promiseFn 与 signal 绑定的工具 ├── useTrackedPromise.ts # 内部:竞态保护与状态更新分发 ├── types.ts # PromiseFn / AsyncResult 等类型 ├── index.ts # 统一导出 └── __tests__/ ├── useAsync.test.tsx └── useAsyncAbortable.test.tsx模块职责可以这么理解:useAbortController.ts管“控制器什么时候创建、什么时候销毁”;createAbortableFn.ts管“把用户的异步函数变成能接收 signal 的形式”;useTrackedPromise.ts管“每次执行生成一个 id,setState 前先判断 id 是否还是最新的”,这就是整个库最关键的竞态保护层。
很多开发者自己写自定义 hook 时习惯把“状态管理、请求触发、取消逻辑”全混在同一个函数里,导致后来加一个取消功能就要改三处。这个库的拆分方式是:状态管理在useTrackedPromise,控制器生命周期在useAbortController,两者通过组合方式叠加成useAsyncAbortable。任何一个 hook 都可以独立测试,这是它可组合性的架构基础。
我自己的感受是:读这个项目时,不要只把它当“工具库”,它更值得学习的是“如何把一个通用逻辑切成多个可注入的小 hook”。比如useAbortController你完全可以单独拿出来放在自己的项目里,解决“组件卸载时自动 abort 所有未完成请求”这件事。
2.3 最小可运行示例:带取消的搜索联想请求
先用最典型的“搜索框输入 → 请求 → 展示”来跑通。这个例子里搜索关键词每次变化都会触发新的请求,但用户输入很快时,旧请求必须立即取消,否则结果会错乱。
import { useAsyncAbortable } from 'react-hooks-async'; function SearchBox() { const [keyword, setKeyword] = useState(''); const { data, loading, execute, abort } = useAsyncAbortable( async (kw, { signal }) => { const res = await fetch(`/api/search?q=${kw}`, { signal }); return res.json(); } ); return ( <div> <input value={keyword} onChange={(e) => { setKeyword(e.target.value); if (e.target.value) { execute(e.target.value); } else { abort(); } }} /> {loading && <span>loading...</span>} <ul> {(data ?? []).map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> </div> ); }逻辑说明:这里用useAsyncAbortable而不是useAsync,是因为我们需要在 input 的onChange里主动控制“要不要取消”。execute接收一个参数kw,它会把这个参数透传给异步函数的第一个形参;第二个形参自动获得当前这次执行的{ signal }。当用户清空输入框时调用abort(),正在进行的请求在 fetch 层面被取消,不会再有 setState 行为。
参数说明:异步函数签名里的{ signal }不是随意写的,它是useAsyncAbortable内部createAbortableFn按约定注入的。只要你的 promiseFn 声明了两个参数,第二个参数解构出来的就是{ signal }对象。这里的execute返回一个 Promise 对象,可以在事件中 await 它并处理返回值。每次调用execute时,hook 内部都会先abort()上一次请求,再创建一个新的AbortController,确保同一时间只有一个请求处于活动状态。
跑通这个 demo 后,你就掌握了这套 hooks 的核心用法:需要自动触发用useAsync,需要手动触发用useAsyncCallback,需要“手动触发 + 主动取消 + 拿到 signal”用useAsyncAbortable。剩下的问题就是它内部到底怎么做到“旧请求被取消后不更新状态”的。
3. 核心实现拆解:一个请求从触发到被取消的完整链路
3.1 AbortController 如何进入 React:从创建到清理的完整生命周期
要理解这个库,必须先看懂AbortController在 React 组件里的生命周期:组件挂载时不创建,第一次执行异步函数时创建,下一次执行或组件卸载时销毁。看起来简单,但把“销毁”和“setState 不执行”绑在一起,才是精髓。
内部useAbortController的伪代码逻辑长这样:
function useAbortController() { const controllerRef = useRef<AbortController | null>(null); const getAbortController = useCallback(() => { if (!controllerRef.current) { controllerRef.current = new AbortController(); } return controllerRef.current; }, []); const abortController = useCallback(() => { if (controllerRef.current) { controllerRef.current.abort(); controllerRef.current = null; } }, []); useEffect(() => { return () => { if (controllerRef.current) { controllerRef.current.abort(); } }; }, []); return { getSignal: () => getAbortController().signal, abortController, }; }逻辑说明:getAbortController采用懒创建策略,第一次调用时才实例化AbortController,避免组件渲染阶段创建不必要的对象;abortController在 abort 后把controllerRef.current置为 null,这样下一次调用会重新创建新的控制器。组件卸载时的useEffectcleanup 是关键防线:只要组件卸载时还有未完成的请求,就立刻abort(),从源头阻止后续 setState。
参数说明:getSignal返回当前的AbortSignal,这个信号需要传给 fetch 或 axios;abortController是主动取消入口。这里有一个值得注意的设计:abort 之后引用重置为 null,意味这个 controller 不会被复用。AbortController的 abort 操作是终态的,一旦执行,signal 永远处于 aborted 状态,后续 fetch 遇到它都会直接拒绝,所以“abort 后重建”比“abort 后复用”更安全。
3.2 可组合性:多个异步请求如何串联成一个状态
可组合性是这个库不同于普通“请求 hooks”的核心卖点。它允许你传入一个“异步函数”,这个函数内部可以自由组合多个 Promise,比如先拿配置、再拿用户数据、最后拿列表,它只会给你一次状态变化。
const { data, loading } = useAsync(async (signal) => { const config = await getConfig(signal); const user = await getUser(config.userId, signal); const list = await getList(user.permission, signal); return { user, list }; }, []);逻辑说明:getConfig、getUser、getList三个请求按顺序执行,但它们的 signal 都来自同一个AbortController。这意味着外部调用一次abort(),三个阶段的请求全部被取消,不需要每个阶段单独维护取消逻辑。这就是“可组合性”的一种体现——不是把多个 hook 拼在一起,而是把多次 Promise 组合进一个 hook 的状态机里。
如果换成Promise.all并发组合,一样成立:
const { data } = useAsync(async (signal) => { const [menu, user] = await Promise.all([ getMenu(signal), getUserInfo(signal), ]); return { menu, user }; }, []);这里更适合解释组合与取消的关系:Promise.all里任何一个 fetch 因为 signal 被 abort,整个组合 Promise 都会 reject,hook 内部捕获到后会把它当作“主动取消”而不是“错误”,因此不会触发error状态更新。
参数说明:signal 在组合场景必须逐层透传,这也是最容易遗漏的地方。如果getMenu内部只用fetch('/menu')而拒绝接受 signal,那么取消信号就无法透传到 fetch 层,abort 后网络请求实际仍在继续。我在开发时有个固定习惯:给项目里的统一请求封装函数强制第二个参数接收signal,拿不到就报 lint 警告。
3.3 竞态保护:id 标记法如何避免旧请求覆盖新状态
整个库状态更新的安全夹层在useTrackedPromise,负责“保证只有最新一次执行的结果能 setState”。这个机制很多人会误以为是“取消后不再 setState”,其实更准确的说法是“被标记为过期的那次执行,即使它回调了,也不碰 state”。
function useTrackedPromise(asyncFn) { const currentRef = useRef(0); const trackedRun = useCallback( async (...args) => { const runId = ++currentRef.current; setLoading(true); try { const result = await asyncFn(...args); if (runId === currentRef.current) { setData(result); } } catch (e) { if (runId === currentRef.current) { setError(e); } } finally { if (runId === currentRef.current) { setLoading(false); } } return result; }, [asyncFn] ); return { trackedRun, setData, setError }; }逻辑说明:每次调用trackedRun时,自增一个runId并存入 ref。异步函数完成后,先判断runId是否等于 ref 里最新的值;只有相等才能 setState。如果用户在第一次请求未返回时又触发了第二次,第二次的runId会成为最新值,第一次请求回来时runId !== currentRef.current,直接跳过 setState。这就是“stale 丢弃”机制,它不依赖 AbortController 是否生效,即使请求无法真正取消(比如用了不可取消的定时器、Socket),也仍然能保护 React 状态不被旧数据覆盖。
参数说明:asyncFn是用户传入的 promise 函数;trackedRun执行时透传所有参数。这段代码没有处理 abort 信号的区分,所以useAsyncAbortable会在它外面再包一层:先获取新的 signal,再把 signal 传给asyncFn,并组合 AbortController 的 abort 事件,确保“取消后即使 Promise resolve 了也无法通过 id 校验”。
到这里,可终止性和可组合性的实现路径基本清晰:useAbortController负责信号的创建与销毁,createAbortableFn把用户异步函数变成能接收 signal 的函数,useTrackedPromise负责竞态保护。三者组合,才有了对外看起来简洁的useAsync系列 hooks。
4. 把项目跑起来:从空目录到可取消请求的完整 Demo
4.1 初始化工程并安装依赖
这个库不依赖任何运行时框架的特殊能力,所以用 Vite 建一个标准 React + TypeScript 工程就够了。命令如下:
npm create vite@latest async-demo -- --template react-ts cd async-demo npm install npm install react-hooks-async npm run dev命令说明:--template react-ts会生成 React + TypeScript 脚手架;安装react-hooks-async时不需要额外配置,它内部依赖 react 的 hooks API,不引入第三方运行时。跑完npm run dev后,浏览器打开终端提示的本地地址,就能开始改代码了。
如果网络源安装较慢,我一般会切换镜像源重试,这是唯一一个可能出现的环境问题。安装完成后打开src/App.tsx,把默认内容整个替换成上面 2.3 节里的搜索框示例,保存后页面应该能正常渲染输入框。
4.2 用假的慢接口验证取消行为
真实环境很难控制接口返回速度,我习惯用 Promise + setTimeout 模拟一个 3 秒后返回的“慢请求”,专门验证取消是否生效。
import { useAsyncAbortable } from 'react-hooks-async'; const fakeRequest = (keyword: string, { signal }: { signal?: AbortSignal }) => new Promise((resolve, reject) => { const timer = setTimeout(() => { resolve([ { id: 1, name: `${keyword}-结果1` }, { id: 2, name: `${keyword}-结果2` }, ]); }, 3000); // 关键:监听 signal 的 abort 事件,手动清除定时器并 reject signal?.addEventListener('abort', () => { clearTimeout(timer); reject(new DOMException('Aborted', 'AbortError')); }); }); function App() { const { data, loading, execute, abort } = useAsyncAbortable( fakeRequest, [] ); return ( <div> <input placeholder="输入关键词触发请求" onChange={(e) => e.target.value && execute(e.target.value)} /> <button onClick={abort}>取消</button> {loading && <p>请求中,3 秒后返回...</p>} <pre>{JSON.stringify(data, null, 2)}</pre> </div> ); }逻辑说明:fakeRequest的核心是监听signal的abort事件,如果 signal 被 abort,就clearTimeout并且 reject 一个AbortError。这一步很重要,因为 Promise 本身没有取消机制,取消只能靠“监听 signal 后手动执行清理动作”。如果省略这个监听,就算 signal 被 abort,setTimeout3 秒后依然会 resolve,数据仍会进入后续流程。
参数说明:fakeRequest的函数签名和useAsyncAbortable要求的异步函数格式严格匹配:第一个参数是业务参数,第二个参数是{ signal }对象。执行execute('hello')时,'hello' 会成为第一个参数传递进去。返回的数据结构是数组,所以渲染时可以直接用JSON.stringify查看。
4.3 浏览器里验证三个关键行为
跑起来之后,重点不是看界面,而是验证三个关键行为。第一:输入关键词后立即点击取消按钮,3 秒后控制台不会出现任何 resolve 日志,说明 abort 事件确实切断了定时器的后续流程。第二:连续输入两个关键词,比如先输入 a 再输入 b,3 秒后最终显示的数据中,name里的关键词必须是 b,不能出现 a 的结果,这验证的就是竞态保护。第三:在请求未返回时直接刷新页面,观察控制台,不能出现 React 的 “Can't perform a React state update on an unmounted component” 警告。
验证过程中可以打开浏览器开发者工具的 Network 面板。如果请求是通过 fetch 发出的,abort 后对应请求会在网络面板里显示为 canceled,这是一种非常直观的反馈。如果接口是用 XMLHttpRequest 封装的,abort 后会触发onabort事件,同样能观测到。
我一般会在验证完这三个行为后,再故意把代码里fakeRequest的signal.addEventListener('abort')注释掉,跑一遍对比——这时即使取消按钮被点击,请求还是会把结果 set 进来,但因为有useTrackedPromise的 id 竞态保护,不会出现报错,只是数据会“迟到”。这个实验能帮你理解两个保护层的区别:abort 负责停止底层异步任务,id 校验负责阻断过期回调的 setState,两者缺一不可。
5. 踩坑与排查:接入 react-hooks-async 后最容易翻车的 5 个细节
5.1 现象:点击取消后,控制台仍然有“在卸载组件上调用 setState”的警告
原因:useAsync内部虽然有竞态保护,但它只能保证“当前最新一次执行”的 setState 生效,如果组件卸载时机恰好发生在请求 pending 阶段,而异步操作没有被 signal 监听(比如用户传入的是一个不会被取消的 Promise),那么卸载后 setState 依然会被调用。这个警告不一定会崩溃,但会影响排查真正问题的视线。
解决:使用useAsyncAbortable并在异步函数第一行通过signal.addEventListener('abort', () => { throw ... })或者像 4.2 节示例里那样手动清理资源。从根本上保证“请求 pending 期间,组件卸载时 abort 会自动触发并中断 Promise”。如果你的异步操作无法被 AbortSignal 监听,那就自己维护一个mountedRef,在 useEffect cleanup 里置为 false,setState 前先判断。
5.2 现象:abort 后再次 execute,请求始终处于已取消状态,永远不发出
原因:AbortController.abort()是终态操作,调用后它的signal.aborted永远为 true。如果代码里复用了同一个 controller 实例并再次把它传给 fetch,fetch 会直接以 “Aborted” 结束。有的封装把 controller 存在 ref 里且未在 abort 后置空,就会出现“取消一次后整个页面再也无法请求”的问题。
解决:检查你写的 controller 是否在 abort 后被置为 null。参考 3.1 节里的abortController实现,abort 后必须controllerRef.current = null,下一次请求时重新 new 一个。这也是这个库在useAbortController里刻意做“懒创建 + 销毁”的原因:确保每次请求都拿到全新的 signal。
5.3 现象:Promise 组合请求中只取消了最外层,子请求还在继续
原因:useAsync把 signal 传给异步函数后,内部如果调用了getConfig()、getUser()但这两个函数内部没有接收 signal,则取消信号被“吞掉”。组件看起来已经没有 loading 了,网络面板里那个子请求却还在 pending,浪费连接数。
解决:给项目所有请求封装函数增加第二个参数signal?: AbortSignal,并在 fetch、axios 里都挂上。我自己的工程标准是:所有 api 方法签名统一为(params, signal) => Promise<T>,哪怕是 GET 请求也不例外。这样无论嵌套多深,signal 都能透过参数逐层传递到最底层的网络层。
5.4 现象:React StrictMode 环境下请求发了两遍
原因:React 18 起 StrictMode 在开发环境下会刻意让useEffect执行两遍(挂载 → 清理 → 挂载),目的是帮你暴露忘记清理的副作用。useAsync内部依赖useEffect触发请求,因此也会在挂载阶段发两次请求。第一次的效果被清理时 abort,第二次正常接收结果,属于“预期行为”,但如果后端没有幂等性,可能导致重复数据插入。
解决:如果后端接口不是幂等的,尽量不在开发环境使用 StrictMode,或者在网络层做请求去重。这个库本身不背这个锅,它是严格遵守 React 生命周期语义的:清理阶段 abort 第一次请求,第二次请求是新的 controller,行为完全符合预期。排查时注意看 abort 是否真的在第一次请求上触发,如果两次请求都成功且没有 cancel,说明你的 watch effect 依赖传得有问题。
5.5 现象:execute 返回的 Promise 在 abort 后一直不 resolve
原因:内部实现中,abort 触发时是执行 controller.abort(),并不是像useAsyncCallback那样 reject 掉execute的 promise。如果你在事件里写了await execute()并希望在 abort 后收到一个明确结果,你会发现 await 永远挂起。这是“主动取消”和“请求失败”两种语义的差异,很多初次使用者会在这里卡住。
解决:把 abort 理解成“不再关心执行结果”,而不是“让执行失败”。如果你确实需要在外层 await 并感知取消,就不能依赖 execute 的 Promise,应自己在异步函数里通过try/catch捕获 AbortError 后返回特殊标记值。例如:{ canceled: true },execute 外层再判断这个标记。
6. 扩展思路:给 useAsync 做一层“缓存 + 竞态丢弃”的增强封装
6.1 封装目标与参数设计
跑通这个库不代表能直接放进生产项目,生产环境还有一个高频需求:短时间内相同参数的请求不要重复发出,直接从缓存返回。与其每次业务组件里写一堆缓存判断,不如基于useAsync包一层自定义 hook,命名为useCachedAsync。参数设计如下:
| 参数 | 类型 | 作用 |
|---|---|---|
promiseFn | (signal) => Promise<T> | 基础的数据请求函数 |
deps | any[] | 依赖数组,变化时重新请求 |
cacheKey | string | 缓存键,相同键走缓存 |
cacheTime | number | 缓存有效期,单位 ms,默认 5000 |
6.2 核心实现:缓存表 + stale 丢弃
import { useAsyncAbortable } from 'react-hooks-async'; // 模块级缓存表,相同 cacheKey 复用数据 const cacheStore = new Map<string, { data: unknown; expire: number }>(); export function useCachedAsync<T>( promiseFn: (signal: AbortSignal) => Promise<T>, cacheKey: string, deps: any[], cacheTime = 5000 ) { const { data, loading, error, execute } = useAsyncAbortable( async (signal) => { const cached = cacheStore.get(cacheKey); if (cached && Date.now() < cached.expire) { return cached.data as T; } const result = await promiseFn(signal); cacheStore.set(cacheKey, { data: result, expire: Date.now() + cacheTime, }); return result; }, deps ); return { data, loading, error, execute }; }逻辑说明:cacheStore使用模块级Map存储已完成的请求结果,缓存键由调用方传入。在异步函数内部,先检查缓存是否存在且未过期,命中则直接返回缓存数据,不再发起网络请求;未命中则调用原始 promiseFn,拿到数据后在写入缓存。signal照常传递,因此取消逻辑依然有效——如果请求被 abort,缓存不会写入。
参数说明:cacheTime默认 5000 毫秒,超出后重新请求;deps变化时 hook 会重新执行,但缓存的命中判断会优先于请求执行,所以相同数据不会重复拉取。注意这里的deps需要包含cacheKey的依赖因素,否则 key 变了但请求没重跑,会导致数据和 key 不匹配。
6.3 验证方法
验证这个封装是否可靠,我会采用两层验证。第一层是单元测试:用 jest + @testing-library/react 渲染组件,第一次执行后 mock 请求函数被调用 1 次,第二次用相同 cacheKey 渲染,mock 请求函数仍只被调用 1 次;等待超过 cacheTime 后再渲染,请求函数调用次数应变成 2。第二层是手动验证:在 React DevTools 里直接修改组件 props 触发重新渲染,观察 Network 面板没有发出重复请求。
这个 “缓存 + 竞态丢弃” 的组合,就是 react-hooks-async 可组合性的实际应用:你可以用它当一个基础异步电池,自己拼装出带缓存、带重试、带轮询的各种业务定制版本。从那以后我每次接触新 hooks 库,都会先强制走一遍“最小用例 → 读实现 → 包一层增强”的流程,读别人的源码不是为了照抄,而是为了让自己的封装更有底气。希望帮到你。
本文还有配套的精品资源,点击获取