🧑💻 寫在開頭
點贊 + 收藏 === 學會🤣🤣🤣
Axios.CancelToken
axios
物件有一個屬性叫CancelToken
,該屬性提供了中斷已經發出去的請求的方式。具體使用方式有兩種:
方式一:執行器模式
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> <script> const CancelTokenFunc = axios.CancelToken; let cancel; // 傳送請求 axios .get("https://jsonplaceholder.typicode.com/todos/1", { cancelToken: new CancelTokenFunc(function executor(c) { // 將 cancel 函式賦值給外部變數 cancel = c; }), }) .catch((error) => { console.log(error.message); }); // 取消請求 setTimeout(() => { cancel("Operation canceled by the user."); }, 1000); </script>
在第4行中,我們先獲取一箇中斷建構函式CancelTokenFunc
,我們在第10行中用這個建構函式new
出一個例項賦值給get
請求的引數cancelToken
欄位。
在呼叫CancelTokenFunc
建構函式new
出一個例項的時候,我們傳入了一個執行器函式,該執行器會接受一個引數,這個引數就是用來控制中斷請求的取消函式,接著我們把該引數函式賦值給外部變數,這樣就可以在外部需要的時候執行中斷請求的操作。
執行上述程式碼,將瀏覽器調整成低速3G模式後,執行結果如下:
並在控制檯中輸入瞭如下資訊:
Operation canceled by the user.
方式二:令牌模式
// 建立一個 CancelToken 源 const CancelTokenFunc = axios.CancelToken; const { token, cancel } = CancelTokenFunc.source(); // 傳送請求 axios .get("https://jsonplaceholder.typicode.com/todos/1", { cancelToken: token, }) .catch((error) => { console.log(error.message); }); // 取消請求 setTimeout(() => { cancel("Operation canceled by the user."); }, 1000);
在第3行程式碼中,用CancelTokenFunc
的source
方法生成一個取消令牌源,並從取消令牌源中解構出token
和cancel
欄位,然後在GET
請求中將取消令牌源的token
傳遞給cancelToken
,接著在外部呼叫請求令牌源的cancel
方法來取消請求。
執行結果和上面那種方式一樣,就不再贅述了。
相比於方式一的執行器模式,方式二的令牌模式更簡單易懂,另外需要注意一下,每次呼叫CancelTokenFunc.source()
生成的令牌源是不一樣的。
AbortController
AbortController
是一個Web API,用於控制和管理可中止的非同步操作,例如 fetch
請求、DOM
操作。接下來我們看看怎麼用AbortController
來中止請求。
<!DOCTYPE html> <html> <head> <title>中斷請求demo</title> </head> <body> <script> // 建立一個 AbortController 訊號源 const controller = new AbortController(); const { signal } = controller; // 傳送請求 fetch("https://jsonplaceholder.typicode.com/todos/1", { signal, }).catch((error) => { console.log(error); }); // 取消請求 setTimeout(() => { controller.abort("Operation canceled by the user."); }, 1000); </script> </body> </html>
在第9行中,我們建立了一個AbortController
訊號源,在fetch
請求的時候傳遞一個訊號給請求的signal
引數,之後便可以在請求的外部透過呼叫訊號源的abort
方法來取消請求。
這個API的用法其實和Axios.CancelToken
的令牌模式一樣,但是該API會有相容性問題,需要透過引入yet-another-abortcontroller-polyfill
或者abortcontroller-polyfill
來解決。
令牌中斷請求原理
中斷請求的原理其實很簡單,只要監聽到呼叫取消函式,就執行xhr.abort()
(其中,xhr
是XMLHttpRequest
的例項)中斷請求即可,值得探究的是令牌中斷請求的原理,也就是token
和cancel
之間的對映關係是怎麼建立的。
首先我們需要模擬下請求取消的過程,其程式碼如下:
function fetchData(url, options = {}) { const { cancelToken } = options; return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('GET', url); // 監聽請求狀態變化,處理請求的常規邏輯 xhr.onreadystatechange = () => { if (xhr.readyState === 4) { if (xhr.status >= 200 && xhr.status < 300) { resolve(xhr.responseText); } } }; // 監聽取消請求 if (cancelToken) { // ... 需要在外界呼叫cancel請求的時候,呼叫xhr.abort()方法中止請求, // 並在這裡呼叫reject函式將Promise物件的狀態改成rejected } xhr.send(); }); } fetchData("https://jsonplaceholder.typicode.com/todos/1").then((res) => { console.log(res); });
上述程式碼中,我們在fetchData
中返回一個Promise
物件,並在Promise
物件新建一個原生的XMLHttpRequest
物件。
其中的關鍵程式碼,在於監聽取消請求這個判斷裡。
在監聽取消請求這個判斷中,我們只有一個cancelToken
屬性,這個屬性需要在外界執行cancel
時呼叫xhr.abort()
來中止已經發出去的請求,同時將fetchData
內的Promise
物件的狀態改成Rejected
。
因此,cancelToken
需要攜帶一個回撥屬性,在外界執行cancel
方法時觸發回撥。
自然而然的,我們就想到,能否給cancelToken
掛載一個Promise
例項的屬性,然後將這個Promise
屬性的resolved
方法傳遞給cancel
,這樣,當執行cancel
函式的時候,其實就是執行resolve()
,從而改變Promise
例項的狀態,我們就能在Promise
例項的then
方法中執行需要的操作。
也就是說,監聽取消請求需要被設計成這樣:
function fetchData(url, options = {}) { const { cancelToken } = options; return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('GET', url); // 監聽請求狀態變化,處理請求的常規邏輯 // 其他程式碼 // 監聽取消請求 if (cancelToken) { // 需要在外界呼叫cancel請求的時候,呼叫xhr.abort()方法中止請求 // 並呼叫reject函式將Promise物件的狀態改成rejected cancelToken.promise.then((msg) => { xhr.abort(); reject(new Error(msg)); }) }。 xhr.send(); }); }
其中,cancelToken.promise
是一個Promise
例項的屬性。
現在,我們繼續設計建構函式CancelToken
的實現,這個函式需要有一個source
方法,該方法返回兩個屬性,一個是token
,一個是cancel
函式,其中token
應該有一個promise
屬性,該屬性是一個Promise
例項,該例項的resolved
方法將傳遞給cancel
函式。
function CancelToken() {} CancelToken.source = function () { let cancel; const token = { promise: new Promise((resolve) => {cancel = resolve}) }; return { cancel, token, }; };
上述程式碼裡,我們將token
宣告為物件,並在第5行中給token
新增一個promise
屬性,該屬性是一個Promise
例項,並且將Promise
例項的resolve
方法傳遞給了cancel
變數,這樣,當呼叫執行cancel()
的時候,就是在執行resolve()
,token
的promise
屬性就能觸發then
回撥函式。
這樣,我們就實現了令牌中斷請求的要求,並將cancel和token關聯起來了。到這裡,我們就明白每一次呼叫source
方法生成的cancel
和token
為啥能一一對應了。
執行器模式原理
CancelToken
不僅支援令牌中斷模式,還支援執行器中斷模式,而執行器模式是需要透過CancelToken
的建構函式實現。
該建構函式的實現有三個細節需要注意:
- 首先,該建構函式同樣需要給例項物件掛載一個
promise
屬性,該屬性是一個Promise
例項。這樣才能支援在token.promise.then
回撥裡執行取消操作。 - 其次,需要接受一個執行器函式作為入參,
- 最後,作為入參的執行器,它本身也有入參,它的入參是一個方法,在這個方法呼叫的時候,執行
promise
屬性的resolve
方法,這樣才能觸發toekn.promise.then
回撥。
帶著上面三個細節,我們來嘗試實現CancelToken
建構函式:
function CancelToken(executor) { let resolvePromise; this.promise = new Promise((resolve) => { resolvePromise = resolve;}); executor(function c() { resolvePromise(); }) }
上述程式碼中,我們依照三個細節,來一一解讀下:
- 對於第一個細節,我們在第3行程式碼中,我們在
this
上掛載了promise
屬性,該屬性是一個Promise物件,同時,為了達到在外部觸發該Promise物件的狀態變更,我們將其resolve
方法儲存給了外部變數resolvePromise
。 - 對於第二個細節,我們在第1行宣告建構函式的時候就宣告瞭
executor
入參。 - 對於第三個細節,我們在第5行中,在執行器呼叫的時候傳入一個函式作為入參,同時在函式內部執行
resolvePromise()
觸發this.promise
狀態變更。
這樣,我們就實現了簡單的CancelToken
的建構函式。
兩個模式結合
接下來我們將執行器模式結合令牌中斷模式的程式碼一起看下:
function CancelToken(executor) { let resolvePromise; this.promise = new Promise((resolve) => { resolvePromise = resolve;}); executor(function c() { resolvePromise(); }) } CancelToken.source = function () { let cancel; const token = { promise: new Promise((resolve) => {cancel = resolve}) }; return { cancel, token, }; };
結合令牌中斷模式和執行器中斷模式的程式碼一起看後,我們發現,第3行中給this.promise
賦值了一個Promies
例項,第11行中token
需要的promise
屬性,也是一個Promise
例項,因此,這兩個能最佳化一下:
function CancelToken(executor) { let resolvePromise; this.promise = new Promise((resolve) => { resolvePromise = resolve;}); executor(function c() { resolvePromise(); }) } CancelToken.source = function () { let cancel; const token = new CancelToken(function executor(c) { cancel = c; }); return { cancel, token, }; };
上述程式碼中,我們修改了第11行程式碼,給token
賦值為CancelToken
例項物件,並在例項化的時候傳入一個執行器函式executor
,該執行器函式接受一個引數c
,並將c
賦值給了外部變數cancel
屬性,這樣,執行cancel
的流程就變成下面這樣:
- 呼叫執行第15行返回的
cancel()
函式。 cancel
函式來自於第11行中executor
的入參c
。- 第11行中的入參
c
來自於第5行執行executor
時的賦值。 - 最終,執行
cancel()
的時候,就會執行第6行中的resolvePromise()
方法,從而改變promise
屬性的狀態,觸發then
回撥函式。
測試手寫版CancelToken
接下來,使用我們實現的CancelToken
來試試取消網路請求,
方式一:執行器模式示例如下:
<script> function CancelToken(executor) { let resolvePromise; this.promise = new Promise((resolve) => { resolvePromise = resolve; }); executor(function c() { resolvePromise(); }); } CancelToken.source = function () { let cancel; const token = new CancelToken(function executor(c) { cancel = c; }); return { cancel, token, }; }; function fetchData(url, options = {}) { const { cancelToken } = options; return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open("GET", url); // 監聽請求狀態變化,處理請求的常規邏輯 xhr.onreadystatechange = () => { if (xhr.readyState === 4) { if (xhr.status >= 200 && xhr.status < 300) { resolve(xhr.responseText); } } }; // 監聽取消請求 if (cancelToken) { cancelToken.promise.then((msg) => { xhr.abort(); reject(`Request cancelled: ${msg}`); }); } xhr.send(); }); } let cancel; fetchData("https://jsonplaceholder.typicode.com/todos/1", { cancelToken: new CancelToken((c) => { cancel = c; }), }).catch((e) => { console.log(e); }); setTimeout(() => { cancel("取消請求"); }, 500); </script>
將網速調整成慢速3G後執行後效果如下:
控制檯列印的結果,有個undefined
方式二:令牌模式示例如下:
<script> function CancelToken(executor) { // ... } CancelToken.source = function () { // ... }; function fetchData(url, options = {}) { // ... } const { token, cancel } = CancelToken.source(); fetchData("https://jsonplaceholder.typicode.com/todos/1", { cancelToken: token, }).catch((e) => { console.log(e); }); setTimeout(() => { cancel("取消請求"); }, 500); </script>
執行結果同執行器模式,這裡就不截圖了。
最佳化
我們手寫版的CancelToken已經實現了基本的功能,也就是取消請求,但是有個問題,那就是呼叫cancel("取消請求")
裡,引數沒有傳遞到給cancelToken.promise.then
回撥函式,所以列印出來的結果裡有個undefined
。因此,我們需要稍微最佳化下CancelToken
,補齊引數的傳遞。
最佳化的方式也很簡單,取消函式cancel
的入參,會透過形參賦值的方式傳遞給c
的入參,因此我們只需要拿c
的入參給resolve
就行了。具體如下:
function CancelToken(executor) { let resolvePromise; this.promise = new Promise((resolve) => { resolvePromise = resolve; }); executor(function c(msg) { resolvePromise(msg); // 這裡將cancel的入參傳遞給resolve }); }
這樣,就完成了引數的傳遞。
還有一點需要注意,那就是cancel
可能會被多次呼叫,我們需要在第二次之後的呼叫直接結束。這裡我們就可以在第一次呼叫cancel
的時候用傳入的引數做個標記,有引數則代表已經呼叫過cancel
,後續再呼叫cancel
時直接返回,這樣就能防止多次呼叫。
function CancelToken(executor) { let resolvePromise; this.promise = new Promise((resolve) => { resolvePromise = resolve; }); const token = this; executor(function c(msg) { if (token.reason) { return; // 如果已經有了reason,說明之前呼叫過cancel,後續再次呼叫直接接結束 } token.reason = msg || 'cancel request'; resolvePromise(token.reason); // 這裡將cancel的入參傳遞給resolve }); }
上述程式碼中,我們在executor
的外部,也就是第7行先儲存this指向為token
,然後在第9行中判斷是token
是否存在取消原因欄位reason
,有的話,說明之前已經呼叫過cancel
了,這時再次呼叫cancel
就是重複執行cancel
方法,我們可以直接retuen
從而避免重複取消請求。
在第12行中,我們給token.reason
賦了一個預設值cancel request
,因為第一次呼叫cancel
時有可能沒傳參。
這樣,我們就完成了CancelToken
的手寫版最佳化,完整程式碼如下:
function CancelToken(executor) { let resolvePromise; this.promise = new Promise((resolve) => { resolvePromise = resolve; }); const token = this; executor(function c(msg) { if (token.reason) { return; } token.reason = msg || 'cancel request'; resolvePromise(token.reason); }); } CancelToken.source = function () { let cancel; const token = new CancelToken(function executor(c) { cancel = c; }); return { cancel, token, }; };
本文轉載於:https://juejin.cn/post/7395446371031728169
如果對您有所幫助,歡迎您點個關注,我會定時更新技術文件,大家一起討論學習,一起進步。