
如何解決異步請求的競态問題
疫情期間 大家帶好口罩 ^ . ^
免責聲明
作者學藝不精又懶的要死,本文如有錯誤 概不負責 歡迎指出 随緣改正
部分圖檔可能挂了,強烈建議還是去 git 看吧,這破乎沒法玩了
如何解決異步請求的競态問題github.com
引子
這大概是所有前端在實際工作中都要解決的問題吧。。。
在現在的互動場景中,搜尋框裡的實時下拉提示,地圖縮放時的資料更新等等。。
隻要你多次觸發同一個動作 多次調用了同一個接口,你就要考慮時序的問題
這次所讨論的内容并不是給請求加
loading
的判斷啊,或者節流啊防抖啊,或者
async/await
啊之類的
因為在下面的場景中這些并不能解決問題。。
請注意,我們的接口響應時間是随機的,而且我們要得到最新的結果,也就是最後一個請求得到的響應
場景描述
現在頁面上有一個輸入框,随着使用者的輸入會不斷發出異步請求,取回後端傳回的結果渲染在頁面上。
但是蛋疼的是,接口的響應時間并不确定,也就是說,有可能先請求的後傳回,後請求的卻先傳回了。
如果不作處理,這會導緻前端渲染的結果錯誤(不是最後一個請求傳回的結果)
那麼。。如何才能保證頁面正确的渲染呢?
重制
我們可以先簡單重制一下,領會一下精神,代碼如下,效果如圖
參見連結 https://github.com/YuArtian/blog/blob/master/assets/%E5%A6%82%E4%BD%95%E8%A7%A3%E5%86%B3%E5%BC%82%E6%AD%A5%E8%AF%B7%E6%B1%82%E7%9A%84%E7%AB%9E%E6%80%81%E9%97%AE%E9%A2%98/abort_0.html">abort_0.html
以上
可以看到我們實際想渲染的是第四個請求的結果,但是卻被第三的請求的結果後來居上了,導緻了顯示錯誤
簡單粗暴的解決方式
最簡單的想法就是記一下數(其實已經記了)。。。比較一下是不是最後一個就好了
參見連結 https://github.com/YuArtian/blog/blob/master/assets/%E5%A6%82%E4%BD%95%E8%A7%A3%E5%86%B3%E5%BC%82%E6%AD%A5%E8%AF%B7%E6%B1%82%E7%9A%84%E7%AB%9E%E6%80%81%E9%97%AE%E9%A2%98/abort_1.html">abort_1.html
OK,這樣看起來已經闊以了
但是這樣簡單的方法在實際使用中并不友善,你總是需要想辦法傳回一個計數,然後做比較判斷。。每個請求還都要重寫一遍。。
鑒于現代的異步請求基本上都會使用
Promise
,接下來我們就介紹一種結合了
Promise
和
XHR.abort()
的船新方法
XHR.abort() 和 "終止"請求
HttpR
XML
t 提供的 abort 方法 可以用來将
eques
的
XMLHttpRequest
readyState
置為 0。這樣就可以視為請求被 "終止" 了 ^ . ^
但是請注意,這隻是前端視角上的"終止",實際上請求還是會到達伺服器的(後面我們會證明這一點)。
從 http 原理來講,也沒可能會有所謂的終止請求的。簡單設想一下,前端發出一個删除資料的請求,正常的請求流程中,請求被伺服器接受,後端操作資料庫,删掉資料,然後把結果響應給前端。在這中間前端如果真的能在響應到達之前終止請求的話,那删掉的資料怎麼辦呢。。。 -,-
所有的終止方法都隻能是在到達前端之後不做處理而已,這樣在使用者看來就是被"終止"了。
真實場景搭建
為了能真實的測試我們的請求和響應的情況,我們就真的寫一個随機響應的
node
服務出來吧 ~
建立一個
app.js
, 代碼如下
參見連結 https://github.com/YuArtian/blog/blob/master/assets/%E5%A6%82%E4%BD%95%E8%A7%A3%E5%86%B3%E5%BC%82%E6%AD%A5%E8%AF%B7%E6%B1%82%E7%9A%84%E7%AB%9E%E6%80%81%E9%97%AE%E9%A2%98/app.js" >app.js
同樣的,我們的前端請求也要寫一個真實的
現代的異步請求離不開
Promise
,即使你用
XHR
也建議用
Promise
包裝
實際上,這樣的封裝隻有一次,整個項目都會使用這個封裝好的
xhrAdapter
參見連結 https://github.com/YuArtian/blog/blob/master/assets/%E5%A6%82%E4%BD%95%E8%A7%A3%E5%86%B3%E5%BC%82%E6%AD%A5%E8%AF%B7%E6%B1%82%E7%9A%84%E7%AB%9E%E6%80%81%E9%97%AE%E9%A2%98/abort_2.html">abort_2.html
接下來,測試一下是否能重制我們要的場景
可以看到,請求按順序發出了,但是響應是不定時的。現在舞台搭好了。
Promise 和 XHR.abort()
之前,我們解決問題的思路是給請求計數,通過對比來判斷出最後一個請求
現在,換一種思路。在連續的請求過程中,每當我發出一個請求,我就将之前正在
pending
的請求的
Promise
reject
掉,并且該請求的
XHR
對象執行
abort()
之前的請求 如果已經有響應的不用管它,我們目前的請求的結果會覆寫它的
這樣就能確定最後的響應是正确的了
那麼問題來了,怎樣才能記錄之前的請求,還要能在适當的時機執行對它的一系列操作呢?
這個問題,其實
Promise
自己就是答案。仔細想一想,隻要
Promise
的狀态改變了,就會在
.then
或者
.catch
中執行我們之前寫好的回調函數,而且利用這個回調函數,剛好可以用來儲存之前的請求。簡直完美
這樣的話,就需要我們自己生成一個
Promise
并把它的
.then
回調關聯到我們的
xhrAdapter
中,回調函數中會儲存當時的
XHR
請求對象和其包裝
Promise
的
reject
方法,有了這兩個對象就可以達到我們的目的了。
那下面我們就來具體實作一下這個想法
首先,總不能每次都寫一遍生成
Promise
的代碼。這裡就構造一個名為
CancelToken
的類用來生成
Promise
,為了防止多次執行取消操作,也對取消請求操作進行記錄, 需要同樣的構造一個
Cancel
的類
這樣的話其實也有一個小問題,就是這個兩個類和具體送出請求的方法,也就是
handleInput
肯定是不在一起的了,那如何才能在外部(
handleInput
中)控制一個
Promise
的狀态呢?
具體的答案我們還是先來看一下代碼就知道了
然後,還要對
xhrAdapter
進行改造。為了不具有侵入性,這裡讀取參數中的
cancelToken
配置,有這個參數的請求才進入控制
參見連結 https://github.com/YuArtian/blog/blob/master/assets/%E5%A6%82%E4%BD%95%E8%A7%A3%E5%86%B3%E5%BC%82%E6%AD%A5%E8%AF%B7%E6%B1%82%E7%9A%84%E7%AB%9E%E6%80%81%E9%97%AE%E9%A2%98/abort_3.html">abort_3.html
實際使用的時候,方法如下
實際效果:
看來效果還是闊以的,而且寫法高端了很多
實際上,上面的實作就是
Axios
的源碼的簡陋版本
相關文檔 Cancellation部分
Axios
源碼位置 Cancel部分,XHR部分
Axios
fetch 和 AbortController
然而并不是所有的請求都是使用
XHR
的,使用
fetch
的并不少見。用了
fetch
的,可以使用
AbortController
來阻止請求。
AbortController abort 來自MDN
abortable-fetch
下面是用
fetch
和
AbortController
結合使用的例子
參見連結:https://github.com/YuArtian/blog/blob/master/assets/%E5%A6%82%E4%BD%95%E8%A7%A3%E5%86%B3%E5%BC%82%E6%AD%A5%E8%AF%B7%E6%B1%82%E7%9A%84%E7%AB%9E%E6%80%81%E9%97%AE%E9%A2%98/abort_4.html">abort_4.html
實際效果:
尾聲
至此,我們已經處理了兩種前端常見的請求方式
XHR
和
fetch
的競态問題。當然也隻是這種一種場景下的競态問題,前端會有更多複雜的異步問題需要面對。是以才會有
RxJS
等解決異步問題的庫出現。
以後也許會去深入研究更為複雜的異步問題 ^ . ^