JavaScript前端超時非同步操作完美解決過程
目錄
- 如果一段程式碼久久不能執行完成,會怎麼樣?
- Axios 自帶超時處理
- 處理 fetch() 超時
- 萬物皆可超時
自從 ECMAScript 的 PromiseES2015和 async/awaitES2017特性發布以後,非同步在前端界已經成為特別常見的操作。非同步程式碼和同步程式碼在處理問題順序上會存在一些差別,編寫非同步程式碼需要擁有跟編寫同步程式碼不同的“意識”。
如果一段yzLXh程式碼久久不能執行完成,會怎麼樣?
如果這是同步程式碼,我們會看到一種叫做“無響應”的現象,或者通俗地說 —— “死掉了”;但是如果是一段非同步程式碼呢?可能我們等不到結果,但別的程式碼仍在繼續,就好像這件事情沒有發生一般。
當然事情並不是真的沒發生,只不過在不同的情況下會產生不同的現象。比如有載入動畫的頁面,看起來就是一直在載入;又比如應該進行資料更新的頁面,看不到資料變化;
再比如一個對話方塊,怎麼也關不掉 …… 這些現象我們統稱為 BUG。但也有一些時候,某個非同步操作過程並沒有“回顯”,它就默默地死在那裡,沒有人知道,待頁面重新整理之後,就連一點遺蹟都不會留下。
Axios 自帶超時處理
使用 Axios 進行 Web Api 呼叫就是一種常見的非同步操作過程。通常我們的程式碼會這樣寫:
try { const res = await axios.get(url,options); // TODO 正常進行後續業務 } catch(err) { // TODO 進行容錯處理,或者報錯 }
這段程式碼一般情況下都執行良好,直到有一天使用者抱怨說:怎麼等了半天沒反應?
然後開發者意識到,由於伺服器壓力增大,這個請求已經很難瞬時響應了。考慮到使用者的感受,加了一個 loading 動畫:
try {
showLoading();
const res = await axios.get(url,options);
// TODO 正常業務
} catch (err) {
// TODO 容錯處理
} finally {
hideLowww.cppcns.comading();
}
然而有一天,有使用者說:“我等了半個小時,居然一直在那轉圈圈!”於是開發者意識到,由於某種原因,請求被卡死了,這種情況下應該重發請求,或者直接報告給使用者 —— 嗯,得加個超時檢查。
幸運的是 Axios 確實可以處理超時,只需要在options
裡新增一個timeout: 3000
就能解決問題。如果超時,可以在catch
塊中檢測並處理:
try {...} catch (err) { if (err.isAxiosError && !err.response && err.request && err.message.startsWith("timeout")) { // 如果是 Axios 的 request 錯誤,並且訊息是延時訊息 // TODO 處理超時 } } finally {...}
Axios 沒問題了,如果用fetch()
呢?
處理 fetch() 超時
fetch()
自己不具備處理超時的能力,需要我們判斷超時後通過AbortController
來觸發“取消”請求操作。
如果需要中斷一個fetch()
操作,只需從一個AbortController
物件獲取signal
,並將這個訊號物件作為fetch()
的選項傳入。大概就是這樣:
const ac = new AbortController(); const { signal } = ac; fetch(url,{ signal }).then(res => { // TODO 處理業務 }); // 1 秒後取消 fetch 操作 setTimeout(() => ac.abort(),1000);
ac.abort()
會向signal
傳送訊號,觸發它的abort
事件,並將其.aborted
屬性置為true
。fetch()
內部處理會利用這些資訊中止掉請求。
上面這個示例演示瞭如何實現fetch()
操作的超時處理。如果使用await
的形式來處理,需要把setTimeout(...)
放在fetch(...)
之前:
const ac = new AbortController(); const { signal } = ac; setTimeout(() => ac.abort(),1000); const res = await fetch(url,{ signal }).catch(() => undefined);
為了避免使用try ... catch ...
來處理請求失敗,這裡在fetch()
後加了一個.catch(...)
在忽略錯誤的情況。如果發生錯誤,res
會被賦值為undefined
。實際的業務處理可能需要更合理的catch()
處理來讓res
包含可識別的錯誤資訊。
本來到這裡就可以結束了,但是對每一個fetch()
呼叫都寫這麼長一段程式碼,會顯得很繁瑣,不如封裝一下:
async function fetchWithTimeout(timeout,resoure,init = {}) { const ac = new AbortController(); const signal = ac.signal; setTimeout(() => ac.abort(),timeout); return fetch(resoure,{ ...init,signal }); }
沒問題了嗎?不,有問題。
如果我們在上述程式碼的setTimeout(...)
裡輸出一條資訊:
setTimeout(() => { console.log("It's timeout"); ac.abort(); },timeout);
並且在呼叫的給一個足夠的時間:
fetchWithTimeout(5000,url).then(res => console.log("success"));
我們會看到輸出success
,並在 5 秒後看到輸出It's timeout
。
對了,我們雖然為fetch(...)
處理了超時,但是並沒有在fetch(...)
成功的情況下幹掉timer
。作為一個思維縝密的程式設計師,怎麼能夠犯這樣的錯誤呢?幹掉他!
async function fetchWithTimeout(timeout,init = {}) { const ac = new AbortController(); const signal = ac.signal; const timer = setTimeout(() => { console.log("It's timeout"); return ac.abort(); },timeout); try { return await fetch(resoure,signal }); } finally { clearTimeout(timer); } }
完美!但問題還沒結束。
萬物皆可超時
Axios 和 fetch 都提供了中斷非同步操作的途徑,但對於一個不具備abort
能力的普通 Promise 來說,該怎麼辦?
對於這樣的 Promise,我只能說,讓他去吧,隨便他去幹到天荒地老 —— 反正我也沒辦法阻止。但生活總得繼續,我不能一直等啊!
這種情況下我們可以把setTimeout()
封裝成一個 Promise,然後使用Promise.race()
來實現“過時不候”:
race 是競速的意思,所以Promise.race()
的行為是不是很好理解?
function waitWithTimeout(promise,timeout,timeoutMessage = "timeout") { let timer; const timeoutPromise = new Promise((_,reject) => { timer = setTimeout(() => reject(timeoutMessage),timeout); }); return Promise.race([timeoutPromise,promise]) .finally(() => clearTimeout(timer)); // 別忘了清 timer }
可以寫一個 Timeout 來模擬看看效果:
(async () => { const business = new Promise(resolve => setTimeout(resolve,1000 * 10)); try { await waitWithTimeout(business,1000); console.log("[Success]"); } catch (err) { console.log("[Error]",err); // [Error] timeout }www.cppcns.com })();
以上就是前端超時非同步操作完美解決的詳細內容,更多關於解決前端超時的非同步操作的資料請關注我們其它相關文章!