歡迎關注個人公衆號睿Talk
,獲取我最新的文章:
javascript
API請求的控制一直以來都是前端領域的熱點問題,市面上已經有不少優秀的開源項目可供使用。本文本着授人以漁的精神,拋開全部的工具函數,介紹各類場景下如何用最樸素的代碼解決實際問題。前端
在某些場景中,前端須要在短期內發送大量的網絡請求,同時又不能佔用太多的系統資源,這就要求對請求作併發控制了。這裏的請求既多是同一個接口,也多是多個接口,通常還要等全部接口都返回後再作統一的處理。爲了提升效率,咱們但願一個請求完成時立刻把位置空出來,接着發起新的請求。這裏咱們能夠綜合運用 Promise
的2個工具方法達到目的,分別是 race
和 all
。java
async function concurrentControl(poolLimit, requestPool) { // 存放全部請求返回的 promise const ret = []; // 正在執行的請求,用於控制併發 const executing = []; while (requestPool.length > 0) { const request = requestPool.shift(); const p = Promise.resolve().then(() => request()); ret.push(p); // p.then()返回一個新的 promise,表示當前請求的狀態 const e = p.then(() => executing.splice(executing.indexOf(e), 1)); executing.push(e); if (executing.length >= poolLimit) { await Promise.race(executing); } } return Promise.all(ret); }
其中這行代碼比較關鍵:const e = p.then(() => executing.splice(executing.indexOf(e), 1))
要正確的理解這行代碼,必須理解 promise
的如下特色:react
promise
,then 函數是同步執行代碼p
這個 promise
進行訂閱,相似於 dom
的 addEventListener
promise
resolve 後,纔會被 JS 引擎放在微任務隊列裏異步執行因此上面代碼真正的執行順序是:segmentfault
const e = p.then(fn); executing.push(e); // p resolve 後執行 fn () => executing.splice(executing.indexOf(e), 1)
下面是測試代碼,感興趣的能夠自行驗證。設計模式
let i = 0; function generateRequest() { const j = ++i; return function request() { return new Promise(resolve => { console.log(`r${j}...`); setTimeout(() => { resolve(`r${j}`); }, 1000 * j); }) } } const requestPool = [generateRequest(), generateRequest(), generateRequest(), generateRequest()]; async function main() { const results = await concurrentControl(2, requestPool); console.log(results); } main();
前面的實現裏用到的 async/await
是 ES7
的特性,用 ES6
也能實現相同的效果。數組
function concurrentControl(poolLimit, requestPool) { // 存放全部請求返回的 promise const ret = []; // 正在執行的請求,用於控制併發 const executing = []; function enqueue() { const request = requestPool.shift(); if (!request) { return Promise.resolve(); } const p = Promise.resolve().then(() => request()); ret.push(p); let r = Promise.resolve(); const e = p.then(() => executing.splice(executing.indexOf(e), 1)); executing.push(e); if (executing.length >= poolLimit) { r = Promise.race(executing); } return r.then(() => enqueue()); } return enqueue().then(() => Promise.all(ret)); }
這裏使用的是函數嵌套調用的方式,代碼實現起來沒有 async/await
的寫法簡潔,但有另一個好處,支持動態添加新的請求:promise
const requestPool = [generateRequest(), generateRequest(), generateRequest(), generateRequest()]; function main() { concurrentControl(2, requestPool).then(results => console.log(results)); // 動態添加新請求 requestPool.push(generateRequest()); }
從代碼能夠看出,requestPool 的請求完成前,咱們均可以動態往裏面添加新的請求,適合一些根據條件發起請求的場景。網絡
傳統的節流是控制請求發送的時機,而本文的提到的節流是經過發佈訂閱的設計模式,複用請求的結果,適用於在短期內發送多個相同請求的場景。代碼以下:併發
function generateRequest() { let ongoing = false; const listeners = []; return function request() { if (!ongoing) { ongoing = true return new Promise(resolve => { console.log('requesting...'); setTimeout(() => { const result = 'success'; resolve(result); ongoing = false; if (listeners.length <= 0) return; while (listeners.length > 0) { const listener = listeners.shift(); listener && listener.resolve(result); } }, 1000); }) } return new Promise((resolve, reject) => { listeners.push({ resolve, reject }) }) } }
這裏的關鍵點是若是有正在進行的請求,則新建一個 promise
,將 resolve
和 reject
存到 listeners 數組中,訂閱請求的結果。
測試代碼以下:
const request = generateRequest(); request().then(data => console.log(`invoke1 ${data}`)); request().then(data => console.log(`invoke2 ${data}`)); request().then(data => console.log(`invoke3 ${data}`));
取消請求有 2 種實現思路,先來看第一種。
經過設置一個 flag 來控制請求的有效性,下面結合 React Hooks
來進行講解。
useEffect(() => { // 有效性標識 let didCancel = false; const fetchData = async () => { const result = await getData(query); // 更新數據前判斷有效性 if (!didCancel) { setResult(result); } } fetchData(); return () => { // query 變動時設置數據失效 didCancel = true; } }, [query]);
在請求返回後,先判斷請求的有效性,若是無效了就忽略後續的操做。
上面的實現方式其實不是真正的取消,更貼切的說是丟棄。若是想實現真正的取消請求,就要用到 AbortController
API,示例代碼以下:
const controller = new AbortController(); const signal = controller.signal; setTimeout(() => controller.abort(), 5000); fetch(url, { signal }).then(response => { return response.text(); }).then(text => { console.log(text); }).catch(err => { if (err.name === 'AbortError') { console.log('Fetch aborted'); } else { console.error('Uh oh, an error!', err); } });
當調用 abort()
時,promise
會被 reject 掉,觸發一個名爲 AbortError
的 DOMException
。
像搜索框這種場景,須要在用戶邊輸入的時候邊提示搜索建議,這就須要短期內發送多個請求,並且前面發出的請求結果不能覆蓋後面的(網絡阻塞可能致使先發出的請求後返回)。能夠經過下面這種方式實現過時需求的淘汰。
// 請求序號 let seqenceId = 0; // 上一個有效請求的序號 let lastId = 0; function App() { const [query, setQuery] = useState('react'); const [result, setResult] = useState(); useEffect(() => { const fetchData = async () => { // 發起一個請求時,序號加 1 const curId = ++seqenceId; const result = await getData(query); // 只展現序號比上一個有效序號大的數據 if (curId > lastId) { setResult(result); lastId = curId; } else { console.log(`discard ${result}`); fetchData(); }, [query]); return ( ... ); }
這裏的關鍵點是比較請求返回時,請求的序號是否是比上一個有效請求大。若是不是,則說明一個後面發起的請求先響應了,當前的請求應該丟棄。
本文列舉了前端處理API請求時的幾個特殊場景,包括併發控制、節流、取消和淘汰,並根據每一個場景的特色總結出瞭解決方式,在保證數據有效性的同時提高了性能。