【JS】1792- 前端中 JS 發(fā)起的請求可以暫停嗎?

之前在沸點看到一個哥們提出一個問題。
這個問題非常有意思,我一看到就想了很多可以回復(fù)的答案,但是評論區(qū)太窄,就直接開一篇文章來寫了。
審題
JS 發(fā)起的請求可以暫停嗎?這一句話當(dāng)中有兩個概念需要明確,一是什么樣的狀態(tài)才能稱之為 暫停?二是 JS 發(fā)起的請求 是什么?
怎么樣才算暫停?
暫停 全稱暫時停止,在已開始未結(jié)束的過程中臨時停止可以稱之為暫停,意味著這個過程可以在某個時間點截斷然后在另一個時間點重新續(xù)上。
請求應(yīng)該是什么?
這里得先介紹一下 TCP/IP 網(wǎng)絡(luò)模型, 網(wǎng)絡(luò)模型自上而下分為 應(yīng)用層、傳輸層、網(wǎng)絡(luò)層和網(wǎng)絡(luò)接口層。
上圖表示的意思是,每次網(wǎng)絡(luò)傳輸,應(yīng)用數(shù)據(jù)在發(fā)送至目標(biāo)前都需要通過網(wǎng)絡(luò)模型一層一層的包裝,就像寄快遞一樣,把要寄的物品先打包好登記一下大小,再裝在盒子里登記一下目的地,然后再裝到車上,最后送往目的地。
請求(Request) 這個概念就可以理解為客戶端通過若干次數(shù)據(jù)網(wǎng)絡(luò)傳輸,將單份數(shù)據(jù)完整發(fā)給服務(wù)端的行為,而針對某次請求服務(wù)端往客戶端發(fā)送的答復(fù)數(shù)據(jù)則可以稱之為 響應(yīng)(Response)。
理論上應(yīng)用層的協(xié)議可以通過類似于標(biāo)記數(shù)據(jù)包序列號等等一系列手段來實現(xiàn)暫停機制。但是 TCP 協(xié)議并不支持,TCP 協(xié)議的數(shù)據(jù)傳輸是流式的,數(shù)據(jù)被視為一連串的字節(jié)流。客戶端發(fā)送的數(shù)據(jù)會被拆分成多個 TCP 段(TCP segments),而這些段在網(wǎng)絡(luò)中是獨立傳輸?shù)模瑹o法直接控制每個 TCP 段的傳輸,因此也無法實現(xiàn)暫停請求或者暫停響應(yīng)的功能。
解答提問
如果請求是指網(wǎng)絡(luò)模型中的一次請求傳輸,那理所當(dāng)然是不可能暫停的。
來看看提問者的使用場景 —— JS 發(fā)起的請求,那么可以認(rèn)為問題當(dāng)中的請求,應(yīng)該是指在 JS 運行時中發(fā)起的 XMLHttpRequest 或者是 fetch 請求,而請求既然已經(jīng)發(fā)起,那問的自然就是 響應(yīng)是否能夠被暫停 。
我們都知道像大文件分片上傳、以及分片下載之類的功能本質(zhì)上是將分片順序定好之后按順序請求,然后就可以通過中斷順序并記錄中斷點來實現(xiàn)暫停重傳的機制,而單個請求并不具備這樣的環(huán)境。
用 JS 實現(xiàn) ”假暫停” 機制
雖然不能真正意義上實現(xiàn)暫停請求,但是我們其實可以模擬一個 假暫停 的功能,在前端的業(yè)務(wù)場景上,數(shù)據(jù)不是收到就可以直接打在客戶臉上的(什么光速打擊),前端開發(fā)者需要對這些數(shù)據(jù)進行處理之后渲染在界面上,如果我們能在請求發(fā)起之前增加一個控制器,在請求回來時,如果控制器為暫停狀態(tài)則不處理數(shù)據(jù),等待控制器恢復(fù)后再進行處理,是不是也能到達到目的?讓我們試著實現(xiàn)一下。
假如我們使用 fetch 來請求。我們可以設(shè)計一個控制器 Promise 和請求放在一起用 Promise.all 包裹,當(dāng) fetch 完成時判斷這個控制器的暫停狀態(tài),如果沒有被暫停,則控制器也直接 resolve,同時整個 Promise.all 也 resolve 拋出。
function _request () {
return new Promise<number>((res) => setTimeout(() => {
res(123)
}, 3000))
}
// 原本想使用 class extends Promise 來實現(xiàn)
// 結(jié)果一直出現(xiàn)這個問題 https://github.com/nodejs/node/issues/13678
function createPauseControllerPromise () {
const result = {
isPause: false,
resolveWhenResume: false,
resolve (value?: any) {},
pause () {
this.isPause = true
},
resume () {
if (!this.isPause) return
this.isPause = false
if (this.resolveWhenResume) {
this.resolve()
}
},
promise: Promise.resolve()
}
const promise = new Promise<void>((res) => {
result.resolve = res
})
result.promise = promise
return result
}
function requestWithPauseControl <T extends () => Promise<any>>(request: T) {
const controller = createPauseControllerPromise()
const controlRequest = request().then((data) => {
if (!controller.isPause) controller.resolve()
return data
}).finally(() => {
controller.resolveWhenResume = true
})
const result = Promise.all([controlRequest, controller.promise]).then(data => {
controller.resolve()
return data[0]
});
(result as any).pause = controller.pause.bind(controller);
(result as any).resume = controller.resume.bind(controller);
return result as ReturnType<T> & { pause: () => void, resume: () => void }
}
用法
我們可以通過調(diào)用 requestWithPauseControl(_request) 來替代調(diào)用 _request 使用,通過返回的 pause 和 resume 方法控制暫停和繼續(xù)。
const result = requestWithPauseControl(_request).then((data) => {
console.log(data)
})
if (Math.random() > 0.5) { result.pause() }
setTimeout(() => {
result.resume()
}, 4000)
補充
有些同學(xué)錯誤的認(rèn)為網(wǎng)絡(luò)請求和響應(yīng)是絕對不可以暫停的,我特意在文章前面提到了有關(guān)數(shù)據(jù)傳輸?shù)膬?nèi)容,并且掛了一句“理論上應(yīng)用層的協(xié)議可以通過類似于標(biāo)記數(shù)據(jù)包序列號等等一系列手段來實現(xiàn)暫停機制”,這句話的意思是,如果你魔改 HTTP 或者自己設(shè)計實現(xiàn)一個應(yīng)用層協(xié)議(例如像 socket、vmess 這些協(xié)議),只要雙端支持該協(xié)議,是可以實現(xiàn)請求暫停或者響應(yīng)暫停的,而且這不會影響到 TCP 連接,但是實現(xiàn)暫停機制需要對各種場景和 TCP 策略兜底才能有較好的可靠性。
例如,提供一類控制報文用于控制傳輸暫停,首先需要對所有數(shù)據(jù)包的序列號標(biāo)記順序,當(dāng)需要暫停時,發(fā)送該序列號的暫停報文給接收端,接收端收到暫停報文就將已接收數(shù)據(jù)包的塊標(biāo)記返回給發(fā)送端等等(這和分片上傳機制一樣)。
最后
以上就是本篇文章分享的全部內(nèi)容了。
這里是 Xekin(/zi:kin/)。喜歡的掘友們可以點贊關(guān)注點個收藏~
最近摸魚時間比較多,寫了一些奇奇怪怪有用但又不是特別有用的工具,不過還是非常有意思的,之后會一一寫文章分享出來,感謝各位支持。
關(guān)于本文
作者:xekin
https://juejin.cn/post/7260742402397863992
回復(fù)“加群”,一起學(xué)習(xí)進步
