欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

JS前端常見的競態(tài)問題解決方法詳解

 更新時間:2022年08月05日 09:37:44   作者:飛書技術(shù)  
這篇文章主要為大家介紹了JS前端常見的競態(tài)問題解決方法詳解,閱讀完本文,你將會知道:什么是競態(tài)問題;通常出現(xiàn)在哪些場景;解決競態(tài)問題有哪些方法,希望能夠有所幫助,祝大家多多進步,早日升職加薪的相關(guān)資料

什么是競態(tài)問題

競態(tài)問題,又叫競態(tài)條件(race condition),它旨在描述一個系統(tǒng)或者進程的輸出依賴于不受控制的事件出現(xiàn)順序或者出現(xiàn)時機。

此詞源自于兩個信號試著彼此競爭,來影響誰先輸出。

簡單來說,競態(tài)問題出現(xiàn)的原因是無法保證異步操作的完成會按照他們開始時同樣的順序。舉個??:

  • 有一個分頁列表,快速地切換第二頁,第三頁;
  • 先后請求 data2 與 data3,分頁器顯示當前在第三頁,并且進入 loading;
  • 但由于網(wǎng)絡(luò)的不確定性,先發(fā)出的請求不一定先響應,所以有可能 data3 比 data2 先返回;
  • 在 data2 最終返回后,分頁器指示當前在第三頁,但展示的是第二頁的數(shù)據(jù)。

這就是競態(tài)條件,在前端開發(fā)中,常見于搜索,分頁,選項卡等切換的場景。

那么如何解決競態(tài)問題呢?在以上這些場景中,我們很容易想到:

當發(fā)出新的請求時,取消掉上次請求即可。

取消過期請求

XMLHttpRequest 取消請求

XMLHttpRequest(XHR)是一個內(nèi)建的瀏覽器對象,它允許使用 JavaScript 發(fā)送 HTTP 請求。

如果請求已被發(fā)出,可以使用 abort() 方法立刻中止請求。

const xhr= new XMLHttpRequest();
xhr.open('GET', 'https://xxx');
xhr.send();
xhr.abort(); // 取消請求

fetch API 取消請求

fetch 號稱是 AJAX 的替代品,出現(xiàn)于 ES6,它也可以發(fā)出類似 XMLHttpRequest 的網(wǎng)絡(luò)請求。

主要的區(qū)別在于 fetch 使用了 promise,要中止 fetch 發(fā)出的請求,需要使用 AbortController。

const controller = new AbortController();
const signal = controller.signal;
fetch('/xxx', {
  signal,
}).then(function(response) {
  //...
});
controller.abort(); // 取消請求

相比原生 API,大多項目都會選擇 axios 進行請求。

axios 取消請求

axios 是一個 HTTP 請求庫,本質(zhì)是對原生 XMLHttpRequest 的封裝后基于 promise 的實現(xiàn)版本,因此 axios 請求也可以被取消。

可以利用 axios 的 CancelToken API 取消請求。

const source = axios.CancelToken.source();
axios.get('/xxx', {
  cancelToken: source.token
}).then(function (response) {
  // ...
});
source.cancel() // 取消請求

在 cancel 時,axios 會在內(nèi)部調(diào)用 promise.reject() 與 xhr.abort()。

所以我們在處理請求錯誤時,需要判斷 error 是否是 cancel 導致的,避免與常規(guī)錯誤一起處理。

axios.get('/xxx', {
  cancelToken: source.token
}).catch(function(err) { 
  if (axios.isCancel(err)) {
    console.log('Request canceled', err.message);
  } else {
    // 處理錯誤
  }
});

但 cancelToken 從 v0.22.0 開始已被 axios 棄用。原因是基于實現(xiàn)該 API 的提案 cancelable promises proposal 已被撤銷。

v0.22.0 開始,axios 支持以 fetch API 方式的 AbortController 取消請求

const controller = new AbortController();
axios.get('/xxx', {
  signal: controller.signal
}).then(function(response) {
   //...
});
controller.abort() // 取消請求

同樣,在處理請求錯誤時,也需要判斷 error 是否來自 cancel。

可取消的 promise

原生 promise 并不支持 cancel,但 cancel 對于異步操作來說又是個很常見的需求。所以社區(qū)很多倉庫都自己實現(xiàn)了 promise 的 cancel 能力。

我們以awesome-imperative-promise 為例,來看看 cancel 的實現(xiàn),它的 cancel 實現(xiàn)基于指令式 promise, 源碼一共只有 40 行。

什么是指令式 promise?

我們普遍使用的 promise,它的 resolve/reject 只能在 new Promise 內(nèi)部調(diào)用,而指令式 promise 支持在 promise 外部手動調(diào)用 resolve/reject 等指令。

通過它的用法能更好地理解何為指令式 promise:

import { createImperativePromise } from 'awesome-imperative-promise';
const { resolve, reject, cancel } = createImperativePromise(promise);
resolve("some value");
// or
reject(new Error());
// or
cancel();

內(nèi)部的 cancel 方法其實就是將 resolve,reject 設(shè)為 null,讓 promise 永遠不會 resolve/reject。

一直沒有 resolve 也沒有 reject 的 Promise 會造成內(nèi)存泄露嗎?

有興趣的同學可以了解下 http://www.dbjr.com.cn/article/258149.htm

我個人認為,如果沒有保留對 promise 的引用,就不會造成內(nèi)存泄露。

回到 promise cancel,可以看到,雖然 API 命名為 cancel,但實際上沒有任何 cancel 的動作,promise 的狀態(tài)還是會正常流轉(zhuǎn),只是回調(diào)不再執(zhí)行,被“忽略”了,所以看起來像被 cancel 了。

因此解決競態(tài)問題的方法,除了「取消請求」,還可以「忽略請求」。

當請求響應時,只要判斷返回的數(shù)據(jù)是否需要,如果不是則忽略即可。

忽略過期請求

我們又有哪些方式來忽略過期的請求呢?

封裝指令式 promise

利用指令式 promise,我們可以手動調(diào)用 cancel API 來忽略上次請求。

但是如果每次都需要手動調(diào)用,會導致項目中相同的模板代碼過多,偶爾也可能忘記 cancel。

我們可以基于指令式 promise 封裝一個自動忽略過期請求的高階函數(shù) onlyResolvesLast

在每次發(fā)送新請求前,cancel 掉上一次的請求,忽略它的回調(diào)。

function onlyResolvesLast(fn) {
  // 保存上一個請求的 cancel 方法
  let cancelPrevious = null; 
  const wrappedFn = (...args) => {
    // 當前請求執(zhí)行前,先 cancel 上一個請求
    cancelPrevious && cancelPrevious();
    // 執(zhí)行當前請求
    const result = fn.apply(this, args); 
    // 創(chuàng)建指令式的 promise,暴露 cancel 方法并保存
    const { promise, cancel } = createImperativePromise(result);
    cancelPrevious = cancel;
    return promise;
  };
  return wrappedFn;
}

以上就是 github.com/slorber/awe… 的實現(xiàn)。

只需要將 onlyResolvesLast 包裝一下請求方法,就能實現(xiàn)自動忽略,減少很多模板代碼。

const fn = (duration) => 
  new Promise(r => {    
    setTimeout(r, duration);  
  });
const wrappedFn = onlyResolvesLast(fn);
wrappedFn(500).then(() => console.log(1));
wrappedFn(1000).then(() => console.log(2));
wrappedFn(100).then(() => console.log(3));
// 輸出 3

使用唯一 id 標識每次請求

除了指令式 promise,我們還可以給「請求標記 id」的方式來忽略上次請求。

具體思路是:

  • 利用全局變量記錄最新一次的請求 id
  • 在發(fā)請求前,生成唯一 id 標識該次請求
  • 在請求回調(diào)中,判斷 id 是否是最新的 id,如果不是,則忽略該請求的回調(diào)

偽代碼如下:

let fetchId = 0; // 保存最新的請求 id
const getUsers = () => {
  // 發(fā)起請求前,生成新的 id 并保存
  const id = fetchId + 1;
  fetchId = id;
  await 請求
  // 判斷是最新的請求 id 再處理回調(diào)
  if (id === fetchId) {
    // 請求處理
  }
}

上面的使用方法也會在項目中產(chǎn)生很多模板代碼,稍做封裝后也能實現(xiàn)一套同樣用法的 onlyResolvesLast

function onlyResolvesLast(fn) {
  // 利用閉包保存最新的請求 id
  let id = 0;
  const wrappedFn = (...args) => {
    // 發(fā)起請求前,生成新的 id 并保存
    const fetchId = id + 1;
    id = fetchId;
    // 執(zhí)行請求
    const result = fn.apply(this, args);
    return new Promise((resolve, reject) => {
      // result 可能不是 promise,需要包裝成 promise
      Promise.resolve(result).then((value) => {
        // 只處理最新一次請求
        if (fetchId === id) { 
          resolve(value);
        }
      }, (error) => {
        // 只處理最新一次請求
        if (fetchId === id) {
          reject(error);
        }
      });
    })
  };
  return wrappedFn;
}

用法也一樣,使用 onlyResolvesLast 包裝一下請求方法,實現(xiàn)過期請求自動忽略。

而且,這樣的實現(xiàn)不依賴指令式 promise,也更輕量。

「取消」和「忽略」的比較

「取消」更實際

如果請求被「取消」了沒有到達服務端,那么可以一定程度減輕服務的壓力。

但是取消請求也依賴底層的請求 API,比如 XMLHttpRequest 需要用 abort,而 fetch API 和 axios 需要用 AbortController。

「忽略」更通用

而「忽略」的方式,不依賴請求的 API,更加通用,更容易抽象和封裝。本質(zhì)上所有的異步方法都可以使用 onlyResolvesLast 來忽略過期的調(diào)用。

一個更實際,一個更通用,兩者的使用需要根據(jù)具體場景來權(quán)衡。

總結(jié)

在前端常見的搜索,分頁,選項卡等切換的場景中。由于網(wǎng)絡(luò)的不確定性,先發(fā)出的請求不一定先響應,這會造成競態(tài)問題。

解決競態(tài)問題,我們可以選擇「取消」或「忽略」過期請求。

  • 「取消請求」,XMLHttpRequest 可以使用 abort 方法,fetch API 以及 axios 可以使用 AbortController
  • 「忽略請求」,可以基于指令式 promise 或請求 id 的方式封裝高階函數(shù)來減少模板代碼

兩種方式各有各的好,需要根據(jù)實際場景權(quán)衡利弊。

其實解決方式不止這些,像 React Query,GraphQL,rxjs 等都有競態(tài)處理,感興趣的同學可以再繼續(xù)深入了解。

以上就是JS前端常見的競態(tài)問題解決方法詳解的詳細內(nèi)容,更多關(guān)于JS前端競態(tài)的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評論