Excel函数实战:10个核心函数提升数据处理效率
2026/11/3 14:56:09
测试反馈了一个诡异的问题:
iPhone 上操作审批页面一段时间后,整个页面"卡死"了
- 按钮点不动
- 输入框无法输入
- 下拉框打不开
- 切换页面也没反应
- 只能杀掉小程序重新进入
正常状态: 卡死状态: ┌─────────────────────┐ ┌─────────────────────┐ │ [选择预算科目 ▼] │ │ ┌─────────────────┐│ │ │ │ │ Loading... ││ ← 透明遮罩 │ [提交申请] │ ───→ │ │ (已消失) ││ 阻挡所有点击 │ │ │ └─────────────────┘│ │ 可以正常操作 │ │ 无法进行任何操作 │ └─────────────────────┘ └─────────────────────┘这个问题很难复现,测试也说不清具体是哪个操作导致的,只说"用着用着就卡了"。
页面"卡死"但没有报错,可能的原因:
搜索代码中所有showLoading调用,发现了问题:
// 问题代码:打开预算科目选择器constopenBudgetSelector=async()=>{uni.showLoading({title:'加载中',mask:true// 关键:开启全屏遮罩})try{constres=awaitapi.getBudgetSubjects()// 如果这里超时或无响应...budgetList.value=res.data showBudgetPopup.value=true}catch(e){uni.showToast({title:'加载失败',icon:'none'})}uni.hideLoading()// 问题:如果请求一直 pending,这行永远不会执行!}用户点击"选择预算科目" ↓ uni.showLoading({ mask: true }) ← 创建全屏透明遮罩 ↓ await api.getBudgetSubjects() ← 接口请求 ↓ ┌───────────────────────────────────────┐ │ 情况1:接口正常返回 │ │ → hideLoading() 执行 │ │ → 遮罩消失,一切正常 │ ├───────────────────────────────────────┤ │ 情况2:接口超时/无响应 │ │ → Promise 一直 pending │ │ → hideLoading() 永远不执行 │ │ → 遮罩永远存在,页面"卡死" │ └───────────────────────────────────────┘uni.showLoading({ mask: true })的mask参数会创建一个全屏透明遮罩,阻止用户点击页面上的任何元素。这本是为了防止用户在加载过程中重复操作,但如果hideLoading()没有被调用,遮罩就会一直存在。
try{constres=awaitapi.getBudgetSubjects()// ...}catch(e){uni.showToast({title:'加载失败',icon:'none'})}uni.hideLoading()// 在 try-catch 外部问题在于:接口无响应不等于接口报错。
constopenBudgetSelector=async()=>{uni.showLoading({title:'加载中',mask:true})try{constres=awaitapi.getBudgetSubjects()budgetList.value=res.data showBudgetPopup.value=true}catch(e){uni.showToast({title:'加载失败',icon:'none'})}finally{// finally 无论成功失败都会执行uni.hideLoading()}}但这还不够,如果接口一直 pending,finally 也不会执行。
使用Promise.race实现超时兜底:
// 超时工具函数constwithTimeout=(promise,ms,timeoutError='请求超时')=>{consttimeout=newPromise((_,reject)=>{setTimeout(()=>reject(newError(timeoutError)),ms)})returnPromise.race([promise,timeout])}// 使用超时机制constopenBudgetSelector=async()=>{uni.showLoading({title:'加载中',mask:true})try{// 8秒超时constres=awaitwithTimeout(api.getBudgetSubjects(),8000,'加载超时,请重试')budgetList.value=res.data showBudgetPopup.value=true}catch(e){uni.showToast({title:e.message||'加载失败',icon:'none'})}finally{uni.hideLoading()}}原理:
Promise.race([接口请求, 8秒超时]) ↓ ┌─────────────────────────────────────┐ │ 谁先完成就返回谁的结果 │ │ │ │ • 接口在8秒内返回 → 正常处理 │ │ • 8秒内未返回 → 抛出超时错误 │ │ │ │ 无论哪种情况,finally 都会执行 │ └─────────────────────────────────────┘用户可能在请求过程中离开页面,需要在onUnload中清理:
import{ref,onUnmounted}from'vue'import{onUnload}from'@dcloudio/uni-app'// 记录是否有 loadingconstisLoading=ref(false)constopenBudgetSelector=async()=>{isLoading.value=trueuni.showLoading({title:'加载中',mask:true})try{constres=awaitwithTimeout(api.getBudgetSubjects(),8000)budgetList.value=res.data showBudgetPopup.value=true}catch(e){uni.showToast({title:e.message||'加载失败',icon:'none'})}finally{isLoading.value=falseuni.hideLoading()}}// 页面卸载时强制关闭 loadingonUnload(()=>{if(isLoading.value){uni.hideLoading()}})为了避免每个页面都重复处理,可以封装一个通用的 Hook:
// hooks/useLoading.tsimport{ref}from'vue'import{onUnload}from'@dcloudio/uni-app'interfaceLoadingOptions{title?:stringmask?:booleantimeout?:number}exportconstuseLoading=()=>{constisLoading=ref(false)// 页面卸载时自动清理onUnload(()=>{if(isLoading.value){uni.hideLoading()isLoading.value=false}})/** * 执行带 Loading 的异步操作 */constwithLoading=async<T>(asyncFn:()=>Promise<T>,options:LoadingOptions={}):Promise<T>=>{const{title='加载中',mask=true,timeout=15000}=options isLoading.value=trueuni.showLoading({title,mask})try{// 超时处理consttimeoutPromise=newPromise<never>((_,reject)=>{setTimeout(()=>reject(newError('请求超时')),timeout)})constresult=awaitPromise.race([asyncFn(),timeoutPromise])returnresult}finally{isLoading.value=falseuni.hideLoading()}}/** * 手动控制 Loading */constshowLoading=(title='加载中',mask=true)=>{isLoading.value=trueuni.showLoading({title,mask})}consthideLoading=()=>{if(isLoading.value){isLoading.value=falseuni.hideLoading()}}return{isLoading,withLoading,showLoading,hideLoading}}<script setup> import { useLoading } from '@/hooks/useLoading' const { withLoading } = useLoading() // 方式一:包装异步函数(推荐) const loadBudgetList = async () => { try { const res = await withLoading( () => api.getBudgetSubjects(), { title: '加载预算科目...', timeout: 8000 } ) budgetList.value = res.data } catch (e) { uni.showToast({ title: e.message, icon: 'none' }) } } // 方式二:手动控制 const { showLoading, hideLoading } = useLoading() const handleSubmit = async () => { showLoading('提交中...') try { await api.submitForm(form) uni.showToast({ title: '提交成功' }) } catch (e) { uni.showToast({ title: '提交失败', icon: 'none' }) } finally { hideLoading() } } </script>// ❌ 问题:第一个 hideLoading 会关闭所有 loadingconstloadData=async()=>{loadList()// showLoadingloadStats()// showLoading// 第一个完成时调用 hideLoading,第二个还在加载但 loading 已消失}// ✅ 解决:使用计数器letloadingCount=0constshowLoading=()=>{loadingCount++if(loadingCount===1){uni.showLoading({title:'加载中',mask:true})}}consthideLoading=()=>{loadingCount--if(loadingCount<=0){loadingCount=0uni.hideLoading()}}// ❌ 问题:showToast 会自动关闭 showLoadinguni.showLoading({title:'加载中'})// ... 某处代码uni.showToast({title:'提示信息'})// 这会关闭 loading!// ✅ 解决:使用自定义 Toast 组件,或等 loading 结束再 toast// ❌ 问题:跳转页面后 loading 可能残留consthandleClick=async()=>{uni.showLoading({mask:true})awaitapi.doSomething()uni.navigateTo({url:'/pages/next'})// 跳走了,hideLoading 没执行}// ✅ 解决:跳转前关闭consthandleClick=async()=>{uni.showLoading({mask:true})try{awaitapi.doSomething()uni.hideLoading()// 先关闭uni.navigateTo({url:'/pages/next'})}finally{uni.hideLoading()// 兜底}}┌─────────────────────────────────────────────────────────────┐ │ Loading 最佳实践 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 1. 始终使用 try-finally 确保 hideLoading 执行 │ │ │ │ 2. 为异步操作添加超时机制(推荐 8-15 秒) │ │ │ │ 3. 在 onUnload 中清理可能残留的 Loading │ │ │ │ 4. 多个并发请求时使用计数器管理 │ │ │ │ 5. 封装通用 Hook,避免每个页面重复处理 │ │ │ │ 6. 页面跳转前确保关闭 Loading │ │ │ └─────────────────────────────────────────────────────────────┘// 标准的 Loading + 异步操作模板constdoAsyncAction=async()=>{uni.showLoading({title:'处理中',mask:true})try{// 带超时的异步操作constresult=awaitPromise.race([actualAsyncOperation(),newPromise((_,reject)=>setTimeout(()=>reject(newError('操作超时')),10000))])// 成功处理returnresult}catch(error){// 错误处理uni.showToast({title:error.message||'操作失败',icon:'none'})throwerror}finally{// 必定执行uni.hideLoading()}}问题根因:uni.showLoading({ mask: true })创建的全屏遮罩,在接口无响应时不会自动关闭,导致页面"卡死"
核心解决方案:
finally确保hideLoading()执行Promise.race添加超时机制onUnload中清理残留 Loading最佳实践:
记忆口诀:
showLoading 要配 finally,超时兜底不能忘,页面卸载要清理
这个问题虽然简单,但影响很大(页面完全无法操作),而且不容易复现和定位。通过规范的异步操作处理模式,可以从根本上避免这类问题。