前端JS發起的請求能暫停嗎?
在討論前端JS發起的請求是否能暫停時,需要明確兩個概念:什么狀態可以被認為是“暫停”?以及什么是JS發起的請求?
如何定義暫停?
暫停指的是臨時停止一個已經開始但尚未完成的過程。這意味著這個過程可以在某個時間點被中斷,并在另一個時間點恢復。
什么是請求?
首先,讓我們介紹一下TCP/IP網絡模型。網絡模型從上到下分為應用層、傳輸層、網絡層和網絡接口層。
上圖表示,每次網絡傳輸,應用數據都需要通過網絡模型逐層打包,然后發送到目的地,就像寄包裹一樣。要寄送的物品首先被包裝并登記其大小,然后放入箱子并登記目的地,最后裝上運輸工具送到目的地。
請求的概念可以理解為客戶端通過多次數據網絡傳輸將完整數據發送到服務器,而服務器為特定請求返回的數據可以稱為響應。
理論上,應用層協議可以通過標記數據包序列號來實現暫停機制。然而,TCP協議不支持這一點。TCP協議的數據傳輸是面向流的,數據被視為連續的字節流。客戶端發送的數據將被分成多個獨立傳輸的TCP段。無法直接控制每個TCP段的傳輸,因此無法實現暫停請求或響應的功能。
回答問題
如果請求指的是網絡模型中的傳輸,那么自然是不可能暫停的。
考慮到使用場景——由JS發起的請求。因此,可以認為這里的問題指的是在JS運行時發起的XMLHttpRequest或fetch請求。由于請求已經發出,問題自然變成響應是否可以暫停。
我們都知道,上傳大文件分片和下載大文件本質上是定義分片順序,按順序請求,可以通過中斷和記錄中斷點來實現暫停和恢復。然而,單個請求并沒有這樣的環境。
使用JS實現“假暫停”機制
雖然我們無法真正實現暫停請求,但我們可以模擬一個假暫停功能。在前端業務場景中,數據在接收到后不會立即顯示在客戶端。前端開發人員需要先處理這些數據,然后再渲染到界面上。如果我們在發起請求前添加一個控制器,并且在請求返回時該控制器處于暫停狀態,則不處理數據。相反,等待控制器恢復后再處理數據。這樣我們是否就達到了目標呢?讓我們嘗試實現它。
如果我們使用 fetch 發起請求,可以設計一個控制器 Promise,并結合請求使用 Promise.all 封裝。當 fetch 完成時,檢查控制器是否處于暫停狀態;如果沒有暫停,直接resolve 控制器并同時 resolve 和拋出 Promise.all。
function _request () {
return new Promise<number>((res) => setTimeout(() => {
res(123)
}, 3000))
}
// 原本想用 "class extends Promise" 實現。
// 問題在于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()
controller.resolveWhenResume = controller.isPause
return data
})
const result = Promise.all([controlRequest, controller.promise])
.then(data => data[0])
result.finally(() => controller.resolve())
(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 }
}
使用方法
我們可以將調用 _request 替換為調用 requestWithPauseControl(_request) ,并通過返回的pause和 resume 方法控制暫停和恢復。
const result = requestWithPauseControl(_request).then((data) => {
console.log(data)
})
if (Math.random() > 0.5) { result.pause() }
setTimeout(() => {
result.resume()
}, 4000)
執行原理
在流程設計上,步驟如下:設計一個控制器,發起請求,在接收到響應后,檢查控制器的狀態。如果控制器不處于“暫停”狀態,則正常返回數據;如果控制器處于“暫停”狀態,則將控制器設置為一旦調用resume方法就返回數據的狀態。
在代碼中,使用 Promise.all 將控制器 Promise 綁定。如果控制器處于暫停狀態,Promise.all 不會被釋放。然后對應地暴露 pause 方法和 resume 方法供外部使用。
最后
有些人可能誤以為網絡請求和響應根本無法暫停。我在文章開頭特別提到了數據傳輸相關的內容,并加了一句“理論上,應用層協議可以通過標記數據包序列號來實現暫停機制”。這意味著,如果你修改HTTP或設計自己的應用層協議(例如socket、vmess等協議),只要雙方支持該協議,就可以實現請求或響應的暫停。這不會影響TCP連接,但實現暫停機制需要綜合考慮場景和TCP策略,以確保更好的可靠性。
例如,提供一種控制消息類型來控制傳輸暫停,需要標記所有數據包的序列號。當需要暫停時,發送帶有序列號的暫停消息到接收端。接收端收到暫停消息后,將已收到的數據包標記塊返回給發送端(類似分片上傳機制)。