新聞中心

大家在平時的開發(fā)過程中估計不會經(jīng)常碰到需要主動取消一個 Fetch 請求的需求,所以一部分同學可能對這一塊知識不是很了解。沒有關(guān)系,看完這篇文章你就能夠掌握關(guān)于如何終止一個 Fetch 請求或者一個 Promise 的全部技能了。那我們趕快開始吧~
這篇文章比我預(yù)期要花費的時間和精力還要多,所以文章比較長,大家現(xiàn)在沒時間瀏覽的可以先收藏起來,以后慢慢看。如果覺得這篇文章不錯的話,也可以幫忙點個贊,轉(zhuǎn)發(fā)支持一下。
使用 AbortController 終止 Fetch 請求
在 fetch 之前,我們請求后端的資源使用的方式是通過 XMLHttpRequest 這個構(gòu)造函數(shù),創(chuàng)建一個 xhr 對象,然后通過這個 xhr 對象進行請求的發(fā)送以及接收。
const xhr = new XMLHttpRequest();
xhr.addEventListener('load', function (e) {
console.log(this.responseText);
});
xhr.open('GET', 'https://jsonplaceholder.typicode.com/todos/1');
xhr.send();
這個 xhr 上也存在一個 abort 方法用來進行請求的終止操作。但是需要注意的是,這個 abort 的執(zhí)行過程是比較模糊的。 我們不清楚 abort 在什么時候可以不進行或終止對應(yīng)的網(wǎng)絡(luò)請求,又或者如果在調(diào)用 abort 方法和獲取到請求的資源之間存在競爭條件的時候會發(fā)生什么。我們可以通過簡單的代碼來實踐一下:
// ... 省略掉上面的代碼
setTimeout(() => {
xhr.abort();
}, 10);
通過添加一個延時,然后取消掉對應(yīng)的請求;在控制臺可以看到,有時請求已經(jīng)獲取到結(jié)果了,但是卻沒有打印出對應(yīng)的結(jié)果;有時請求沒有獲取到對應(yīng)的結(jié)果,但是查看對應(yīng)的網(wǎng)絡(luò)的狀態(tài)卻是成功的。所以這里面有很多的不確定性,跟我們的感覺是比較模糊的。 等到 fetch 出來的時候,大家就在討論關(guān)于如何正確,清楚地取消一個 fetch 請求。最早的討論可以看這里 Aborting a fetch #27 ,那已經(jīng)是7年前(2015年)的事情了,可以看到當時的討論還是比較激烈的。大家感興趣的話可以看看當時大家都主要關(guān)注的是哪些特性。
最終,新的規(guī)范 出來了,通過 AbortController 和 AbortSignal 我們可以方便,快捷,清楚地終止一個 fetch 請求。要注意的是,這個規(guī)范是一個 DOM 層面的規(guī)范,不是 JavaScript 語言層面的規(guī)范?,F(xiàn)在絕大多數(shù)的瀏覽器環(huán)境和新版本的 Node.js 環(huán)境也都支持這個特性了。關(guān)于 AbortController 的兼容性,大家可以參考這里 AbortController#browser_compatibility
下面文章中的代碼例子基本上都可以直接復(fù)制粘貼到控制臺運行的,所以感興趣的同學閱讀到對應(yīng)的部分可以直接打開瀏覽器的控制臺去運行一下,然后看看對應(yīng)的結(jié)果。加深一下自己對相關(guān)知識點的記憶。
終止正在進行中的單個請
我們先通過一段代碼來給大家展示一下如何實現(xiàn)這個功能
const ac = new AbortController();
const { signal } = ac;
const resourceUrl = 'https://jsonplaceholder.typicode.com/todos/1';
fetch(resourceUrl, { signal })
.then(response => response.json())
.then(json => console.log(json))
.catch(err => {
// 不同瀏覽器的返回結(jié)果不同
console.log(err);
});
// 可以立即終止請求,或者設(shè)置一個定時器
// ac.abort();
setTimeout(() => {
ac.abort();
}, 10);
大家感興趣的話也可以把上面的代碼復(fù)制粘貼到瀏覽器的控制臺運行一下,上面代碼的運行結(jié)果如下所示:
可以看到控制臺的 Console 的輸出是:DOMException: The user aborted a request.對應(yīng)的 Network 展示的是一個取消狀態(tài)的請求。這說明我們剛才發(fā)送的請求被終止取消掉了。能夠在一些特定的情況下主動地取消相關(guān)的請求對我們應(yīng)用來說是很重要的,這能夠減少我們用戶的流量使用以及我們應(yīng)用的內(nèi)存使用。
AbortController 的深入剖析
接下來我們來講解一下上面的代碼,第一行通過 AbortController 創(chuàng)建了一個 AbortController 類型的實例 ac,這個實例上有一個 abort 方法和一個 AbortSignal 類型的 signal 實例。然后我們通過 fetch 方法去請求一個資源路徑,傳遞給 fetch 的選項把 ac 的 signal 對象傳遞進去。fetch 方法如果獲取到了資源就會把資源打印到控制臺,如果網(wǎng)絡(luò)發(fā)生了問題,就會捕獲異常,然后把異常打印到控制臺。最后,通過一個 setTimeout 延時,調(diào)用 ac 的 abort 方法終止 fetch 請求 。
fetch 的 options 選項允許我們傳遞一個 signal 對象;**fetch 的內(nèi)部會監(jiān)測這個對象的狀態(tài),如果這個對象的狀態(tài)從未終止的狀態(tài)變?yōu)榻K止的狀態(tài)的話,并且 fetch 請求還在進行中的話,fetch 請求就會立即失敗。其對應(yīng)的 Promise 的狀態(tài)就會變?yōu)?Rejected**。
如何改變 signal 的狀態(tài)呢?我們可以通過調(diào)用 ac 的 abort 方法去改變 signal 的狀態(tài)。一旦我們調(diào)用了 ac.abort() 那么與之關(guān)聯(lián)的 signal 的狀態(tài)會立刻從起始狀態(tài)(非終止狀態(tài))轉(zhuǎn)變?yōu)榻K止狀態(tài)。
我們上面只是簡單地使用了 signal 對象,這個對象是 AbortSignal 類的實例,對于 AbortSignal 我們下面會做深入的講解,這里暫時只需要知道 signal 可以作為一個信號對象傳遞給 fetch 方法,可以用來終止 fetch 的繼續(xù)進行。 另外,在不同的瀏覽器中打印的結(jié)果可能略有不同,這個跟不同瀏覽器的內(nèi)部實現(xiàn)有關(guān)系。比如在 Firefox 中的結(jié)果如下:
在 Safari 中的結(jié)果如下:
當然如果我們沒有終止 fetch 請求的話,控制臺的打印將會是:
image.png
另外大家如果需要一些模擬的數(shù)據(jù)接口的話可以試試 JSONPlaceholder ,還是很方便使用的。
批量取消多個 fetch 請求
值得注意的是,我們的 signal 對象可以同時傳遞給多個請求,在需要的情況下可以同時取消多個請求;我們來看看如何進行這樣的操作。代碼如下所示:
const ac = new AbortController();
const { signal } = ac;
const resourcePrefix = 'https://jsonplaceholder.typicode.com/todos/';
function todoRequest (id, { signal } = {}) {
return fetch(`${resourcePrefix}${id}`, { signal })
.then(response => response.json())
.then(json => console.log(json))
.catch(e => console.log(e));
}
todoRequest(1, { signal });
todoRequest(2, { signal });
todoRequest(3, { signal });
// 同時終止多個請求
ac.abort();
運行代碼后可以在控制臺看到如下結(jié)果:
如果我們需要同時對多個請求進行終止操作的的話,使用上面這種方式非常簡單方便。
如果我們想自定義終止請求的原因的話,可以直接在 abort 方法里傳遞我們想要的原因,這個參數(shù)可以是任何 JavaScript 類型的值。傳遞的終止的原因會被 signal 接收到,然后放在它的 reason 屬性中。這個我們下面會講到。
AbortController 的相關(guān)屬性和方法
image.png
詳細介紹 AbortSignal
AbortSignal 的屬性和方法
AbortSignal 接口繼承自 EventTarget ,所以 EventTarget 對應(yīng)的屬性和方法,AbortSignal 都繼承下來了。當然還有一些自己特有的方法和屬性,我們下面會一一講解到的。需要注意的是,AbortSignal 部分屬性有兼容性問題,具體的兼容性大家可以參考這里 AbortSignal#browser_compatibility 。
靜態(tài)方法 abort 和 timeout
這兩個方法是 AbortSignal 類上的靜態(tài)方法,用來創(chuàng)造 AbortSignal 實例。其中 abort 用來創(chuàng)造一個已經(jīng)被終止的信號對象。我們來看下面的例子:
// ... 省略 todoRequest 函數(shù)的定義
// Safari 暫時不支持, Firefox 和 Chrome 支持
// abort 可以傳遞終止的原因
const abortedAS = AbortSignal.abort();
// 再發(fā)送之前信號終止,請求不會被發(fā)送
todoRequest(1, { signal: abortedAS });
console.warn(abortedAS);
運行代碼,控制臺的輸出結(jié)果如下:
image.png
對應(yīng)的請求甚至都沒有發(fā)送出去
image.png
我們也可以給 abort 方法傳遞終止的原因,比如是一個對象:
// ...
const abortedAS = AbortSignal.abort({
type: 'USER_ABORT_ACTION',
msg: '用戶終止了操作'
});
// ...
那么輸出的結(jié)果就如下圖所示:
image.png
signal 的 reason 屬性就變成了我們自定義的值。
同樣的,大家看到 timeout 應(yīng)該很容易想到是創(chuàng)造一個多少毫秒后會被終止的 signal 對象。代碼如下:
// ... 省略部分代碼
const timeoutAS = AbortSignal.timeout(10);
todoRequest(1, { signal: timeoutAS }).then(() => {
console.warn(timeoutAS);
});
console.log(timeoutAS);
代碼的運行結(jié)果如下:
image.png
可以看到我們打印了兩次 timeoutAS,第一次是立即打印的,第二次是等到請求被終止后打印的。可以看到第一打印的時候,timeoutAS 的狀態(tài)還是沒有被終止的狀態(tài)。當請求被終止后,第二次打印的結(jié)果表明 timeoutAS 這個時候已經(jīng)被終止了,并且 reason 屬性的值表明了這次請求被終止是因為超時的原因。
屬性 aborted 和 reason
AbortSignal 實例有兩個屬性;一個是 aborted 表示當前信號對象的狀態(tài)是否是終止的狀態(tài),false 是起始狀態(tài),表示信號沒有被終止,true 表示信號對象已經(jīng)被終止了。
reason 屬性可以是任何的 JavaScript 類型的值,如果我們在調(diào)用 abort 方法的時候沒有傳遞終止信號的原因,那么就會使用默認的原因。默認的原因有兩種,一種是通過 abort 方法終止信號對象,并且沒有傳遞終止的原因,那么這個時候 reason 的默認值就是: DOMException: signal is aborted without reason;如果是通過 timeout 方法終止信號對象,那么這個時候的默認原因就是:DOMException: signal timed out。如果我們主動傳遞了終止的原因,那么對應(yīng)的 reason 的值就是我們傳遞進去的值。
實例方法 throwIfAborted
這個方法通過名稱大家也能猜出來是什么作用,那就是當調(diào)用 throwIfAborted 的時候,如果這個時候 signal 對象的狀態(tài)是終止的,那么就會拋出一個異常,異常的值就是對應(yīng) signal 的 reason 值??梢钥聪旅娴拇a例子:
const signal = AbortSignal.abort();
signal.throwIfAborted();
// try {
// signal.throwIfAborted();
// } catch (e) {
// console.log(e);
// }
運行后在控制臺的輸出如下:
image.png
可以看到直接拋出異常,這個時候我們可以通過 try ... catch ... 進行捕獲,然后再進行對應(yīng)的邏輯處理。這個方法也是很有幫助的,我們在后面會講到。當我們實現(xiàn)一個自定義的可以主動取消的 Promise 的時候這個方法就很有用。
事件監(jiān)聽 abort
對于 signal 對象來說,它還可以監(jiān)聽 abort 事件,然后我們就可以在 signal 被終止的時候做一些額外的操作。下面是事件監(jiān)聽的簡單例子:
const ac = new AbortController();
const { signal } = ac;
// 添加事件監(jiān)聽
signal.addEventListener('abort', function (e) {
console.log('signal is aborted');
console.warn(e);
});
setTimeout(() => {
ac.abort();
}, 100);
運行后在控制臺的輸出如下:
image.png
可以看到在 signal 被終止的時候,我們之前添加的事件監(jiān)聽函數(shù)就開始運行了。其中 e 表示的是接收到的事件對象,然后這個事件對象上的 target 和 currentTarget 表示的就是對應(yīng)的 signal 對象。
實現(xiàn)一個可以主動取消的 Promise
當我們對 AbortController 以及 AbortSignal 比較熟悉的時候,我們就可以很方便的構(gòu)造出我們自定義的可以取消的 Promise 了。下面就是一個比較簡單的版本,大家可以看一下:
/**
* 自定義的可以主動取消的 Promise
*/
function myCoolPromise ({ signal }) {
return new Promise((resolve, reject) => {
// 如果剛開始 signal 存在并且是終止的狀態(tài)可以直接拋出異常
signal?.throwIfAborted();
// 異步的操作,這里使用 setTimeout 模擬
setTimeout(() => {
Math.random() > 0.5 ? resolve('ok') : reject(new Error('not good'));
}, 1000);
// 添加 abort 事件監(jiān)聽,一旦 signal 狀態(tài)改變就將 Promise 的狀態(tài)改變?yōu)?rejected
signal?.addEventListener('abort', () => reject(signal?.reason));
});
}
/**
* 使用自定義可取消的 Promise
*/
const ac = new AbortController();
const { signal } = ac;
myCoolPromise({ signal }).then((res) => console.log(res), err => console.warn(err));
setTimeout(() => {
ac.abort();
}, 100); // 可以更改時間看不同的結(jié)果
這次的代碼稍微多了一點,不過相信大家還是很容易就知道上面的代碼要表示的是什么意思。
首先我們自定義了 myCoolPromise 這個函數(shù),然后函數(shù)接收一個非必傳的 signal 對象;然后立即返回一個新構(gòu)建的 Promise,這個 Promise 的內(nèi)部我們添加了一些額外的處理。首先我們判斷了 signal 是否存在,如果存在就調(diào)用它的 throwIfAborted 方法。因為有可能這個時候 signal 的狀態(tài)已經(jīng)是終止的狀態(tài)了,需要立即將 Promise 的狀態(tài)變更為 rejected 狀態(tài)。
如果此時 signal 的狀態(tài)還沒有改變,那么我們可以給這個 signal 添加一個事件監(jiān)聽,一旦 signal 的狀態(tài)改變,我們就需要立即去改變 Promise 的狀態(tài)。
當我們下面的 setTimeout 的時間設(shè)置為100毫秒的時候,上面的 Promise 總是拒絕的狀態(tài),所以會看到控制臺的打印結(jié)果如下:
image.png
如果我們把這個時間修改為2000毫秒的話,那么控制臺輸出的結(jié)果可能是 ok 也可能是一個 not good 的異常捕獲。
有同學看到這里可能會說,好像不需要 signal 也可以實現(xiàn)主動取消的 Promise,我可以使用一個普通的 EventTarget 結(jié)合 CustomEvent 也可以實現(xiàn)類似的效果。當然我們也可以這樣做,但是一般情況下我們的異步操作是包含網(wǎng)絡(luò)請求的,如果網(wǎng)絡(luò)請求使用的是 fetch 方法的話,那么就必須使用 AbortSignal 類型的實例 signal 進行信號的傳遞;因為 fetch 方法內(nèi)部會根據(jù) signal 的狀態(tài)來判斷到底需不需要終止正在進行的請求。
AbortSignal 的相關(guān)屬性和方法:
image.png
開發(fā)中其他場景的使用舉例
取消事件監(jiān)聽的一種便捷方法
一般情況下,如果我們對文檔中的某個 DOM 元素添加了事件監(jiān)聽,那么當這個元素被銷毀或者移除的時候,也需要相應(yīng)的把對應(yīng)的事件監(jiān)聽函數(shù)移除掉,不然很容易出現(xiàn)內(nèi)存泄漏的問題。所以一般情況下我們會按照下面的方式添加并且移除相關(guān)的事件監(jiān)聽函數(shù)。
const evtBtn = document.querySelector('.event');
const cancelBtn = document.querySelector('.cancel');
const evtHandler = (e) => {
console.log(e);
};
evtBtn.addEventListener('click', evtHandler);
// 點擊 cancelBtn 移除 evtBtn 按鈕的 click 事件監(jiān)聽
cancelBtn.addEventListener('click', function () {
evtBtn.removeEventListener('click', evtHandler);
});這種方式是最通用的方式,但是這種方式需要我們保留對應(yīng)事件監(jiān)聽函數(shù)的引用,比如上面的 evtHandler。一旦我們丟失了這個引用,那么后面就沒有辦法取消這個事件監(jiān)聽了。
另外,有些應(yīng)用場景需要你給某個元素添加很多事件處理函數(shù),取消的時候就需要一個一個去取消,很不方便。這個時候我們的 AbortSignal 就可以派上用場了,我們可以使用 AbortSignal 來同時取消很多事件的事件監(jiān)聽函數(shù)。就像我們同時取消很多個 fetch 請求一樣。代碼如下:
// ... HTML 部分參考上面的內(nèi)容
const evtBtn = document.querySelector('.event');
const cancelBtn = document.querySelector('.cancel');
const evtHandler = (e) => console.log(e);
const mdHandler = (e) => console.log(e);
const muHandler = (e) => console.log(e);
const ac = new AbortController();
const { signal } = ac;
evtBtn.addEventListener('click', evtHandler, { signal });
evtBtn.addEventListener('mousedown', mdHandler, { signal });
evtBtn.addEventListener('mouseup', muHandler, { signal });
// 點擊 cancelBtn 移除 evtBtn 按鈕的 click 事件監(jiān)聽
cancelBtn.addEventListener('click', function () {
ac.abort();
});
這樣的處理方式是不是就很方便,也非常的清楚明了。
addEventListener(type, listener, options);
addEventListener 的第三個參數(shù)可以是一個 options 對象,這個對象可以讓我們傳遞一個 signal 對象用來作為事件取消的信號對象。就像上面我們使用 signal 對象來取消 fetch 請求那樣。
image.png
從上面的兼容性來說,這個屬性的兼容性還是可以的;目前只有 Opera Android 和 Node.js 暫時還不支持,如果想要使用這個新的屬性,需要針對這兩個平臺和運行環(huán)境做一下兼容處理就好了。
一種值得借鑒的處理復(fù)雜業(yè)務(wù)邏輯的方法
我們有時開發(fā)中會遇到一些比較復(fù)雜的處理操作,比如你要先通過好幾個接口獲取數(shù)據(jù),然后組裝數(shù)據(jù);然后再把這些數(shù)據(jù)異步地繪制渲染到頁面上。如果用戶主動取消了這個操作或者因為超時了,我們要主動取消這些操作。對于這種場景,使用 AbortController 配合 AbortSignal 也有不錯的效果,下面舉一個簡單的例子:
// 多個串行或者并行的網(wǎng)絡(luò)請求
const requestUserData = (signal) => {
// TODO
};
// 異步的繪制渲染操作 里面包含了 Promise 的處理
const drawAndRenderImg = (signal) => {
// TODO
};
// 獲取服務(wù)端數(shù)據(jù)并且進行數(shù)據(jù)的繪制和渲染
function fetchServerDataAndDrawImg ({ signal }) {
signal?.throwIfAborted();
// 多個網(wǎng)絡(luò)請求
requestUserData(signal);
// 組裝數(shù)據(jù),開始繪制和渲染
drawAndRenderImg(signal);
// ... 一些其他的操作
}
const ac = new AbortController();
const { signal } = ac;
try {
fetchServerDataAndDrawImg({ signal });
} catch (e) {
console.warn(e);
}
// 用戶主動取消或者超時取消
setTimeout(() => {
ac.abort();
}, 2000);
上面是一個簡化的例子,用來表示這種復(fù)雜的操作;我們可以看到,如果用戶主動取消或者因為超時取消操作;我們上面的代碼邏輯可以很方便的處理這種情況。也不會因為少處理了一些操作而導致可能發(fā)生的內(nèi)存泄漏。
一旦我們想重新開始這個操作,我們只需要再次調(diào)用 fetchServerDataAndDrawImg 并且傳遞一個新的 signal 對象就可以了。這樣處理后,重新開始和取消的邏輯就非常清楚了。如果大家在自己的項目中有類似的這種操作,不妨可以試試這種處理方法。
在 Node.js 中的使用
我們不僅可以在瀏覽器環(huán)境中使用 AbortController 和 AbortSignal,還可以在 Node.js 環(huán)境中使用這兩個功能。對于 Node.js 中的 fs.readFile,fs.writeFile,http.request,https.request 和 timers 以及新版本支持的 Fetch API 都可以使用 signal 來進行操作的取消。下面我們來舉一個簡單的例子,關(guān)于讀取文件的操作:
const fs = require('fs');
const ac = new AbortController();
const { signal } = ac;
fs.readFile('data.json', { signal, encoding: 'utf8' }, (err, data) => {
if (err) {
console.error(err);
return;
}
console.log(data);
});
ac.abort();運行代碼可以看到終端的輸出如下:
image.png
經(jīng)常使用 Node.js 進行業(yè)務(wù)開發(fā)的同學可以嘗試使用這個新的特性,應(yīng)該對開發(fā)會很有幫助的。
反饋和建議
這篇文章到這里就算結(jié)束啦,不知道有多少同學堅持讀完了這篇文章;希望讀完的同學都能夠掌握好這篇文章中講解的知識。如果這篇文章幫到了你,或者打開了你的新世界;歡迎點贊轉(zhuǎn)發(fā)。
當前題目:如何優(yōu)雅地中斷Promise?
網(wǎng)站鏈接:http://m.fisionsoft.com.cn/article/dpcjeei.html


咨詢
建站咨詢
