news 2026/10/13 13:53:41

Vue3实战:打造useLazyData Hook,优雅实现数据懒加载与状态管理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vue3实战:打造useLazyData Hook,优雅实现数据懒加载与状态管理

做 Vue3 开发这两年,我发现自己日常碰到的不是那些花哨的组件动画,反而是一遍又一遍的“数据什么时候加载”问题。列表页首屏要做懒加载,Tab 切换要按需请求,搜索按钮点了才拉数据,滚动到底部继续分页……这些需求归拢到一起,就是今天想聊的 useLazyData Hook:一个在 Vue3 组合式 API 体系里,把数据请求做成“默认不执行、按需触发、状态齐全”的自定义 Hook。它解决的远不只是“晚点发请求”这一件事,而是把 loading、data、error、重新执行、重置这一整套状态管理收拢到一个函数里。适合正在写后台管理系统、移动端 H5、商城列表页,或者对组合式函数设计感兴趣的同学参考,代码可以直接抄回去按需改。

1. 懒加载这件事,为什么需要重新设计

1.1 首屏性能与请求时机:懒加载到底懒的是什么

不少人一提“懒加载”就想到图片懒加载,或者路由组件拆包。但在业务数据层面,懒加载的意义是另一回事:把请求动作从“组件创建”延后到“用户真正需要看到数据的那个瞬间”。这个延后带来两个直接收益,第一是首屏请求变少,接口压力和数据流量都会被压缩;第二是用户感知变快,因为每一个请求都对应一个明确的用户意图,页面不会出现一堆无用的 loading 转圈。

我用一个生活例子解释:点外卖和囤菜的区别。如果你不确定家里今天要不要做饭,一股脑把所有菜都买好放冰箱,那大概率会有菜放坏。懒加载的逻辑是——用户开始点菜了,你再把对应食材拿出来处理。放到前端场景里,一个后台管理系统的详情页,可能有基础信息、操作日志、统计数据三块内容,用户大多数时候只是来看基础信息的,一进页面就把三个接口全部打过去,既浪费服务器资源,又让首屏耗时被无关请求拖慢。如果把后两块做成懒加载,等用户切到对应 Tab 或者滚动到那个区域时再拉数据,整体体验会好很多。

不过,懒加载的目标说到底不是“完全不发请求”,而是“让请求时机跟随用户行为”。如果只做到“组件挂载时不发请求”,然后在某个事件里手动调一下接口,那还只是最原始的懒加载。真正值得设计成一个 Hook 的,是像 loading 状态、错误处理、竞态保护、防抖、重置这一整套围绕“延迟执行”展开的状态逻辑。这也是 useLazyData 比普通“写法改造”更有价值的地方——它把散落在每个页面里的请求状态管理统一成了同一个模型。

1.2 常规写法的三个坑

先说我在真实项目里看到最多、也踩过最多的三种写法。

第一种是onMounted里一把梭。无论组件在哪种场景下被创建,都在挂载后立刻把数据拉回来。这种写法在单体页面里问题不大,但一旦进入 Tab 页、抽屉、弹窗这类“组件创建了但用户可能根本不会看”的场景,就非常浪费。尤其弹窗类组件,有些管理系统在打开弹窗的瞬间就把里面的全部接口发了一遍,用户关掉再打开又是一遍,请求量直接翻倍。

第二种是用v-if加一个 created flag 包着子组件,通过“子组件挂载时才请求”间接实现懒加载。这个思路本身没错,但问题在于状态分散。父组件要维护 flag,子组件要维护自己的 loading 和 error,一旦切换逻辑变复杂,比如“首次进入默认加载,第二次切换要重新请求”,控制权在两个组件之间来回传递,代码很快就看不懂了。

第三种是在事件回调里手动管理状态。比如点击查询按钮时,自己写isLoading.value = true,然后 try catch 里请求、赋值、报错。这种写法最直接,也是问题最隐蔽的:每个按钮都要复制一遍同样的逻辑,防抖和竞态处理很难做到统一。同一个页面里,一个接口有竞态 bug,另一个接口没有,排查起来格外难受。

这三条路我都走过。说实话,它们都能跑,但都只解决“某个页面”的问题,没有解决“一类页面”的问题。

1.3 一个 Hook 解决什么

useLazyData 想做的,是把下面这几件事固定在同一个模具里:

  • 数据状态:data、loading、error 全部收纳,组件里不用再写散装 ref;
  • 执行时机:默认不执行,由 execute 方法或依赖变化触发;
  • 生命周期管理:执行时自动处理 loading 开始和结束,组件卸载后不再赋值;
  • 可组合能力:支持与其他逻辑(分页、路由守卫、IntersectionObserver)组合。

说得更直接一点:如果一个页面的数据请求需要满足“某些条件满足时才发”“失败后能重试”“切走再切回要重新拉”“请求结果要防竞态”,那么它就是 useLazyData 的目标场景。这类场景在后台管理系统里几乎占了一半以上,所以沉淀这样一个 Hook 的性价比很高——写一次,长期复用。

2. useLazyData 的 API 设计:先定接口再写实现

2.1 从 useAsyncData、useFetch 里借鉴的设计取舍

写这个 Hook 之前,我参考了 Nuxt 的 useAsyncData、VueUse 的 useAsyncState,以及社区里很火的 alova 与 swrv。它们有一个共同点:都在返回 data、loading、error 三件套,都提供手动触发执行的方法。差异主要在于“执行时机”的默认值:useFetch 默认自动执行,VueUse 的 useAsyncState 也默认立即执行,而 useLazyData 的定位是“数据请求默认不执行”。

这个“默认不执行”就是“懒”字的来源。它意味着,组件本身没有副作用,副作用完全由开发者指定的触发条件来引爆。在 API 设计上,这带来两个调整:第一个是 immediate 参数,默认 false,但允许调用方改成 true,退化成普通请求 Hook;第二个是返回值里的 execute,用于手动触发请求,并且必须支持重复调用。也就是说,useLazyData 不是替代 useFetch,而是它的一种更受控的变体。

另一个取舍是关于依赖触发的。我最初设计时想学习 watch 的用法,给 Hook 传一个 watchSource 参数,内部自动监听依赖变化并重新请求。但后来放弃了,因为自动监听一旦结合防抖和竞态,心智负担会明显上升,很多人会搞不清楚请求到底是被谁触发的。最终我选择让 useLazyData 保持“执行器”形态,外部想监听依赖变化,自己调用 execute 即可。这样 Hook 的职责单一,组合起来也更灵活。

2.2 参数与返回值的完整约定

useLazyData 的接口我最终定成了这样:

参数类型说明
requestFn() => Promise<T>核心请求函数,必须返回 Promise
optionsUseLazyDataOptions可选配置,包含 immediate、debounce、onSuccess、onError 等
返回值类型说明
dataRef<T | null>请求成功后的数据
loadingRef<boolean>是否正在请求中
errorRef<Error | null>请求失败时的错误对象
execute(...args) => Promise<T>手动触发请求,返回当次结果
reset() => void清空数据、错误,恢复初始状态
isFirstLoadRef<boolean>是否还没有发过请求,用于骨架屏判断

这里有几个细节值得展开。第一,data 的初始值是 null 而不是 undefined,这样模板里判断“是否有数据”会更直观。第二,loading 不包含“初始未执行”的状态,因为懒加载场景里“还没请求”和“正在请求”是两种不同的 UI 状态,前者适合显示占位提示,后者适合显示转圈,所以单独给了 isFirstLoad。第三,execute 返回 Promise 是刻意为之,调用方可以用 await 拿到当次结果做额外操作,比如请求成功后自动关闭弹窗。

2.3 为什么返回 ref 而不是 reactive

这是我实际开发中被问得最多的问题。使用 reactive 看起来更简洁,可以在模板里直接写 data.list,但实际上有坑:reactive 对象在解构时会丢失响应性。如果使用者在组件里写了const { data, execute } = useLazyData(...),reactive 解构出来的 data 就变成了普通对象,后续赋值不会触发视图更新,而且报错不明显,很难排查。

返回 ref 就没有这个困扰。ref 在模板中会被自动解包,在逻辑里又保持了引用语义,解构后依然能维持响应性。另外一个原因是类型推导更稳定,ReturnType 可以直接推出 Ref<T | null>、Ref ,使用方拿到手就知道怎么用。虽然 ref 在代码里要写 .value,稍显啰嗦,但在一个需要经常被组合、被解构的 Hook 中,这一点啰嗦换来的稳定性是值得的。

3. 核心实现:把 useLazyData 从零写出来

3.1 状态初始化与执行开关

直接上核心代码。这个版本我做了简化,保留了最关键的执行与状态逻辑,方便阅读。

import { ref, readonly } from 'vue' interface UseLazyDataOptions<T> { immediate?: boolean debounce?: number onSuccess?: (data: T) => void onError?: (error: Error) => void } export function useLazyData<T = any>( requestFn: (...args: any[]) => Promise<T>, options: UseLazyDataOptions<T> = {} ) { const data = ref<T | null>(null) const loading = ref(false) const error = ref<Error | null>(null) const isFirstLoad = ref(true) let timer: ReturnType<typeof setTimeout> | null = null async function run(args: any[]): Promise<T> { loading.value = true error.value = null isFirstLoad.value = false try { const result = await requestFn(...args) data.value = result options.onSuccess?.(result) return result } catch (e) { error.value = e instanceof Error ? e : new Error(String(e)) options.onError?.(error.value) throw error.value } finally { loading.value = false } } async function execute(...args: any[]): Promise<T> { if (timer) clearTimeout(timer) if (options.debounce) { return new Promise((resolve, reject) => { timer = setTimeout(() => { run(args).then(resolve).catch(reject) }, options.debounce) }) } return run(args) } function reset() { data.value = null error.value = null isFirstLoad.value = true loading.value = false } if (options.immediate) { execute() } return { data: readonly(data), loading: readonly(loading), error: readonly(error), isFirstLoad: readonly(isFirstLoad), execute, reset } }

代码里我用了 readonly 包裹返回的 ref,目的是防止外部直接篡改状态,只能通过 execute 和 reset 修改。这个约束在多人协作的团队里特别好用,能有效避免“某个人为了偷懒直接改 data.value”的情况。在一些项目里我看到直接把 data 返回成普通 ref,结果页面里出现data.value.push(...)这种操作,时间长了状态流就乱了。

3.2 竞态处理:最后一次请求说了算

懒加载场景最容易遇到竞态问题。拿搜索框举例:用户输入关键词后快速按下回车,第一次请求还在路上,第二次请求已经发出去了。如果第一次请求比第二次晚返回,页面就会出现旧结果覆盖新结果的诡异 bug。我在真实项目里遇到过好几次,排查起来特别痛苦,因为表现不稳定,时好时坏。

解决竞态的通用思路是给每次请求编号,只有最后一次请求的结果允许写入:

let requestSeq = 0 async function run(args: any[]) { const seq = ++requestSeq loading.value = true error.value = null isFirstLoad.value = false try { const result = await requestFn(...args) if (seq === requestSeq) { data.value = result onSuccess?.(result) } return result } catch (e) { if (seq === requestSeq) { error.value = e instanceof Error ? e : new Error(String(e)) } throw e } finally { if (seq === requestSeq) { loading.value = false } } }

这里的核心是 requestSeq 递增计数器。每次 execute 都会使计数器加一,返回结果时只有拿到最新计数的请求才有权限写状态。如果中途被更新的请求覆盖,旧请求即使返回了也会被丢弃。这个方案比 AbortController 更轻量,兼容性更好,不用考虑浏览器对 fetch 中断的支持差异。当然,如果接口是基于 fetch 的,也可以配合 AbortController 直接把网络层请求取消掉,减少带宽浪费,两种方式不冲突,可以同时用。

3.3 防抖与重复触发保护

懒加载 Hook 里的防抖要分场景讨论。搜索联想这种高频输入场景,防抖时间给到 300ms 到 500ms 比较合适;而按钮点击触发的查询,防抖时间可以更短,甚至不需要防抖,但要加上“重复点击保护”——即 loading 状态下再次点击不再发起新请求。

我这里给 execute 加了一个简单的防抖实现,核心是 setTimeout 延迟执行。不过有一点需要提醒:防抖只应该作用于“用户高频触发”的场景,如果请求本身有严格的实时性要求,防抖反而会带来延迟。比如详情页切换 Tab 时,用户连续切多个 Tab,我们希望最后一次切换立刻生效,用防抖反而会让响应变慢。所以 options.debounce 应该设计成可选,而不是默认开启。

重复触发保护则在 execute 入口处判断:

async function execute(...args: any[]) { if (loading.value && !options.allowConcurrent) { return data.value as T } // ... }

当 allowConcurrent 为 false(默认值)时,如果当前已有请求在飞,新的 execute 调用会被忽略,避免同一接口被连续点击按钮打出去十几个请求。这个开关在分页“加载更多”场景特别重要,滚动到底部如果同时触发多次 execute,没有这层保护,页面上会出现重复数据。

注意:防抖和 loading 保护同时存在时,loading判断要在防抖之前,否则防抖排队期间 loading 还是 false,会放行多个请求。

3.4 stale data 策略与手动重置

懒加载有个容易被忽略的体验问题:重新触发请求时,旧的 data 要不要保留?很多场景里,用户点完查询按钮,在等待结果的这一两秒里,应该让页面上继续显示旧数据,而不是先清空成 null 导致页面闪烁。这个策略叫 stale data(过期数据展示)。useLazyData 默认保留了旧 data,除非显式调用 reset 才会清空,这样切 Tab、点查询的体验会顺滑很多。

reset 的典型使用场景是退出登录、切换账号、进入编辑页前清理缓存。例如用户 A 的数据展示在页面上,用户 B 登录后直接进入同一个页面,如果不 reset,B 会先看到 A 的数据,这个隐患可大可小。所以我的建议是,在包含敏感信息的页面里,路由切换离开时主动 reset 一次,或者干脆给 data 加一个“用户维度缓存 key”,按用户隔离数据。

4. 接入真实业务:四种高频场景直接抄

4.1 Tab 页签切换时惰性加载

先看最常见的场景。管理后台的详情页经常有“基础信息 / 操作日志 / 关联订单”这种 Tab 结构,如果一进页面就把所有 Tab 的数据都拉回来,首屏请求会多出不少。用 useLazyData 改造后,每个 Tab 的数据都变成独立的懒加载单元,只有第一次切到该 Tab 时才触发请求。

<script setup lang="ts"> import { watch, ref } from 'vue' import { useLazyData } from '@/hooks/useLazyData' const activeTab = ref('base') const logData = useLazyData(() => fetchLogs(activeTab.value)) watch(activeTab, (val) => { if (val === 'logs') { logData.execute() } }) </script> <template> <div class="tabs"> <button @click="activeTab = 'base'">基础信息</button> <button @click="activeTab = 'logs'">操作日志</button> </div> <div v-if="activeTab === 'logs'"> <p v-if="logData.isFirstLoad">切换后才会请求日志数据</p> <p v-else-if="logData.loading">正在加载...</p> <div v-else>{{ logData.data }}</div> </div> </template>

这里我刻意把触发逻辑放在调用方而不是 Hook 内部,因为 Tab 切换的时机跟业务高度相关:有的场景只在第二次切到时请求,有的场景每次切回都刷新,这些差异 Hook 管不了,交给调用方反而最灵活。实际项目中如果 Tab 非常多,可以再封装一个 useTabLazyData,把“首次切换才触发”的逻辑收敛成一个小工具,但底层还是复用 useLazyData。

4.2 搜索表单里“点查询才发请求”

搜索列表页是懒加载 Hook 的另一个主战场。这类页面的共同点是:初始化时通常要自动加载一次全部数据,之后用户填完筛选条件点击查询,用新条件重新请求。useLazyData 的 immediate 参数这时候就派上用场了。

const params = reactive({ keyword: '', status: '' }) const listData = useLazyData( () => fetchList({ ...params }), { immediate: true, debounce: 300, onSuccess: (res) => { total.value = res.total } } ) function handleSearch() { listData.execute() }

这里有个我反复提醒自己的点:搜索参数不要写在 requestFn 外部然后用全局变量读取,一定要把参数作为函数闭包的一部分传给 Hook。这样每次 execute 被调用时,requestFn 都能拿到最新的参数快照。如果参数是异步获取的,execute 也支持传参,可以在 handleSearch 里把参数直接传给 execute 覆盖闭包里的旧值。具体用哪种,看团队的代码风格,但保持一致性很重要,否则排查参数错乱时要花很多时间。

4.3 上拉加载更多:分页与追加模式

列表页的“加载更多”其实是 useLazyData 的一个增强场景。普通 useLazyData 每次 execute 都会覆盖 data,但分页需要的是“追加数据”。我的做法是在 Hook 之外包一层组合逻辑,不污染 useLazyData 本身,保持它的职责单一。

const page = ref(1) const list = ref<Item[]>([]) const finished = ref(false) const pageData = useLazyData( () => fetchPage({ page: page.value, size: 20 }), { allowConcurrent: false } ) async function loadMore() { if (pageData.loading.value || finished.value) return const res = await pageData.execute() if (res.list.length === 0) { finished.value = true return } list.value.push(...res.list) page.value++ }

这里的关键是:list 是页面自己的响应式状态,useLazyData 只负责管理“当前这一页的请求状态”。这样解耦之后,分页逻辑、排序逻辑、条件筛选逻辑都可以各管各的,不会缠在一起。我见过有人把分页也塞进通用 Hook,结果功能越加越多,最终变成了一个谁也不敢动的巨无霸,这种教训不值得重复。

4.4 进入视口才加载:IntersectionObserver 配合

还有一种懒加载是“内容区域进入视口后才请求”,常见于首页的信息流、大屏可视化的数据面板。它的核心是配合 IntersectionObserver 观察组件是否可见,一旦可见就触发 execute。

const targetRef = ref<HTMLElement>() const chartData = useLazyData(() => fetchChartData()) onMounted(() => { const observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting && chartData.isFirstLoad.value) { chartData.execute() observer.disconnect() } }) }) if (targetRef.value) observer.observe(targetRef.value) })

这样做的价值在于,页面滚动到图表区域附近才开始拉接口,如果用户根本没滚到那个位置,请求就不会发出。实时数据这块要注意一点:如果需要轮询或定时刷新,记得在组件卸载时清理定时器和 observer,不然会留下内存泄漏。懒加载 Hook 本身在卸载后不会自动停掉飞行中的请求,所以我一般会在 onBeforeUnmount 里用一个释放标记把请求结果丢弃,避免“死组件”继续更新状态。

5. 常见问题与排查技巧实录

5.1 数据有了但页面不更新

这个问题在懒加载场景下出现率极高,根因往往是响应性丢失。常见情况是把 Hook 返回的 ref 塞进 reactive 对象,或者用响应式解构工具过度解包,导致 data 变成一个普通值。我曾经在新项目里用 Vue 3.5 的响应式解构特性封装过一次,结果列表赋值后页面纹丝不动,排查了半天才发现是解构把 ref 的响应性拆掉了。

排查方法很简单:在赋值前后打一次console.log(data),把值打印出来看看是不是带 .value 的 RefImpl 对象。如果输出的是普通对象或值,说明响应性已经在某个环节被拆掉。我的建议是不要过度依赖自动解包,模板里直接用没问题,但逻辑代码里尽量显式 .value,并且避免把 Hook 返回的 ref 再嵌套进另一个 reactive 容器。

5.2 快速连续操作时竞态覆盖

我遇到过一个真实事故:筛选区有“价格从高到低”和“价格从低到高”两个按钮,用户快速点了两下,结果列表按照错误的顺序渲染出来。原因就是第一次请求比第二次后返回,把应该新的排序结果覆盖了。这类问题不是偶发,只要用户操作够快,必然出现。

解决思路前面已经给出:requestSeq 递增计数,只有最新请求有写入权限。这类 bug 最怕的不是修,而是复现时机不稳定,所以我的建议是直接在 Hook 层面强制加入竞态保护,而不是每次在业务代码里单独处理。如果你还在用没有竞态保护的请求封装,强烈建议升级一下思路,把请求序号作为核心机制内置进去。

5.3 组件卸载后 loading 不结束

懒加载场景里经常出现组件销毁但请求还在空中的情况。如果在请求完成后执行了赋值操作,而组件已经卸载,虽然 Vue 3 在大多数情况下不会抛错,但状态更新仍然可能造成意料之外的副作用,比如某个全局 store 被误改,或者触发不必要的 watcher。

我的做法是在 useLazyData 的 run 方法里加一个内部 token,在组件卸载时通过一个dispose方法让 token 失效。如果你不想在 Hook 里自动注册生命周期,也可以像竞态处理那样在调用方维护一个释放标记,卸载时置为 true,请求返回后检查标记再决定要不要写状态。无论如何,不要让已经“死掉”的组件再去改状态,这是前端状态管理的底线。

5.4 依赖变化后旧数据闪烁

懒加载通常会保留旧 data,但如果依赖条件变了,比如从查看“订单 A”切到“订单 B”,旧的订单 A 数据在订单 B 加载完成之前一直显示,会造成一种“页面内容与当前上下文不符”的错觉。

这种情况下我建议手动判断是否要清旧数据。useLazyData 的 reset 可以清空数据,但清空会让页面出现空白 loading 态,体验也不一定更好。折中方案是给 data 加一个来源标记,比如 sourceId,切换时比较来源不同则显示骨架屏,来源相同则保留旧数据实现无缝刷新。这个逻辑放在业务层处理,Hook 本身不掺和。

5.5 常见问题速查表

现象可能原因解决办法
数据赋值后页面不更新响应式丢失,data 被解包成普通值返回 ref,不放进 reactive 容器
快速操作后结果顺序错乱请求竞态,旧请求覆盖新结果requestSeq 递增计数,丢弃过期结果
组件卸载后仍在更新状态飞行中请求完成时组件已销毁卸载时递增 seq 或调用 dispose
切换上下文后短暂显示旧数据stale data 策略保留旧值结合 sourceId 判断是否清空
点击按钮重复发起多个请求没有 loading 保护execute 入口判断 loading 并返回旧 data
请求失败后页面一直转圈错误分支忘记关闭 loading确保 finally 中统一关闭 loading

这个表我整理自真实项目里的排查记录,每一条都是踩过坑才写进去的。建议你收藏一份放到团队 wiki 里,新人遇到类似问题可以先查表再动手,少走很多弯路。

6. 进阶玩法:让 Hook 长出生态

6.1 与 usePagination 组合成全套列表方案

useLazyData 设计得足够收敛,所以能很自然地和其他 Hook 组合。我在内部项目里把它和自定义的 usePagination 配合,做出了一个既支持懒加载又支持分页的列表模板。

export function useLazyPagination<T>( fetchFn: (page: number) => Promise<{ list: T[]; total: number }> ) { const page = ref(1) const pageSize = ref(20) const total = ref(0) const { data, loading, error, execute, reset } = useLazyData( () => fetchFn(page.value), { onSuccess: (res) => { total.value = res.total } } ) const list = computed(() => data.value?.list ?? []) function loadMore() { if (loading.value) return page.value++ execute() } function reload() { page.value = 1 return execute() } return { list, page, pageSize, total, loading, error, loadMore, reload, reset } }

组合的核心思想是:useLazyData 管理单次请求的状态,usePagination 管理分页的页码和总数,二者各司其职。只要每个 Hook 的职责边界清楚,组合起来几乎不需要额外胶水代码。这也是我在团队里反复强调的原则:先设计可组合的原子 Hook,再写业务导向的复合 Hook,而不是一开始就写一个什么都能干的大神类。

6.2 泛型约束与完整类型推导

useLazyData 的另一个价值是类型安全。通过泛型 T,请求结果的类型可以从 requestFn 的返回类型自动推导出来,使用方无需手动标注。对 TypeScript 项目来说,这意味着重构接口字段时,所有引用点会在编译期报错,而不是运行时才发现字段名写错。

interface User { id: number name: string } const { data } = useLazyData<User[]>(() => fetchUsers()) // data 的类型被推导为 Ref<User[] | null>

为了让类型推导更好用,我在 Hook 的 execute 返回类型上做了一点设计:execute 返回 Promise ,所以调用方可以用 await result 拿到类型明确的数据。这样即使不用 data.value,也能安全地访问请求结果。建议你在自己的项目里给 useLazyData 加上完整的泛型签名,这会在后续维护时大幅减少类型相关的返工。

6.3 定位对比:与 alova、VueUse、swrv 的边界

有些同学会问,社区不是已经有 alova、VueUse 的 useAsyncState、swrv 这些方案了吗,为什么还要自己写一个?我的看法是,这类库解决的是“请求状态管理”的大而全方案,而 useLazyData 解决的是“懒加载时机控制”这个小而专的问题。两者不是替代关系,而是可以共存。

方案默认执行时机主打特性适合场景
VueUse useAsyncState立即执行轻量、功能聚焦简单请求状态
alova可配置多模式请求、缓存、状态管理复杂请求体系
swrv立即执行SWR 策略:缓存优先、后台刷新数据强一致性场景
useLazyData默认不执行时机控制、竞态保护、组合灵活Tab 懒加载、按需查询、分页

如果想快速跑通业务,直接用 VueUse 和 alova 都没问题。但如果你像我一样,项目里有大量“部分数据必须延迟加载”的需求,并且希望请求状态完全受控,那自己沉淀一个极简的 useLazyData 反而比引入大库更清晰。毕竟这个 Hook 只有不到一百行代码,任何团队成员花一个小时就能完全读懂。

6.4 实战体会与适用边界

最后说点实在的使用体会。我自己把这套 Hook 用在公司后台管理系统和移动端 H5 里接近一年,最大的感受是:懒加载不是银弹,它最适合“数据区域独立、用户触发明确、请求可延后”的场景,比如 Tab 内容、抽屉详情、滚动加载。但如果你的页面本身数据量不大,或者用户一进来就必须看到全部内容,那强行懒加载只会增加代码复杂度,没有任何收益。

还有一个边界值得注意:useLazyData 默认不执行,意味着如果某些页面忘了触发 execute,页面会一直停留在“未加载”状态。为了规避这个问题,我在团队里立了一个约定:凡是使用 useLazyData 的页面,必须在模板里体现 isFirstLoad 状态,至少要有一个“点击加载”或空白占位提示,避免用户以为页面坏了。这个约定看起来很笨,但实际运行下来,它帮我减少了好几次线上“白屏”投诉。

再分享一个小技巧:给 useLazyData 的 requestFn 传参时,尽量用函数工厂而不是组件实例里的 this。比如const factory = (id: number) => () => fetchDetail(id),这样每个实例持有一份独立闭包,组件之间互不干扰,配合 keep-alive 缓存时也不会出现参数串号。这个小细节在维护老项目时帮了我不少忙,每次排查参数错乱时都会想起这个约定。

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

智能变电站IEC 61850调试:ICD文件结构与工程应用全解析

干智能变电站调试这行&#xff0c;你绕不开三个字母&#xff1a;ICD。我印象很深&#xff0c;第一次接触 IEC 61850 工程时&#xff0c;前辈扔过来一个文件说“把这个 ICD 看懂再干活”&#xff0c;我当时盯着满屏的 XML 标签一脸懵&#xff0c;后来啃了几个项目才算真正摸透。…

作者头像 李华
网站建设 2026/10/13 13:52:23

数据分析必备:数据收集与清洗详细学习路线全攻略

作为一名过来人&#xff0c;我见过太多人兴致勃勃地报了数据分析课程&#xff0c;结果学了三个月还在跟 numpy 语法搏斗&#xff0c;连一份真实业务里的订单表都不知道怎么处理。原因很简单&#xff1a;大部分学习大纲把"数据收集与清洗"当成一个可有可无的过渡章节&…

作者头像 李华
网站建设 2026/10/13 13:52:18

食物链计数:DAG建模与拓扑排序+DP的路径计数实战

最近把牛客的每日一题捡起来刷&#xff0c;顺手把进度挪进自己的 tracker 里&#xff0c;从双指针刷到图论&#xff0c;碰上了一道很有意思的题&#xff1a;食物链计数。题面不长&#xff0c;数据范围也不吓人&#xff0c;但真写起来&#xff0c;方向、模数、边界条件全踩了一遍…

作者头像 李华
网站建设 2026/10/13 13:52:14

基于模型的测试MBT:状态机建模与用例自动生成实战指南

手工维护测试用例的日子我太熟悉了。需求文档更新一行&#xff0c;用例要跟着改十几条&#xff1b;线上暴露一个边界问题&#xff0c;用例集立刻补上一批&#xff1b;真正到了回归节点&#xff0c;谁也不敢拍胸脯说“测全了”&#xff0c;因为没人能证明自己设计的用例覆盖了所…

作者头像 李华
网站建设 2026/10/13 13:51:49

将Codex塞进CI/CD:自动化代码生成与质量门禁实战

1. 从手工到自动&#xff1a;为什么要把 Codex 塞进 CI/CD 流水线 第一次接触 Codex 类代码生成工具时&#xff0c;大多数人的用法都很朴素&#xff1a;打开编辑器&#xff0c;写一段注释&#xff0c;等它补全&#xff0c;复制粘贴&#xff0c;跑一下&#xff0c;能过就留下&am…

作者头像 李华
网站建设 2026/10/13 13:50:31

Spring Data JPA复杂查询实战:用JPQL手写SQL替代方法名查询

做后端快三年的朋友可能都有这种体会&#xff1a;Spring Data JPA 用久了&#xff0c;最顺手的是那些 findByUsernameAndStatus 之类的方法名查询&#xff0c;一眼就能看懂&#xff0c;根本不用写 SQL。可一旦遇到多表关联、聚合统计、动态条件、批量更新&#xff0c;那串方法…

作者头像 李华