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