亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

JS前端常見(jiàn)的競(jìng)態(tài)問(wèn)題解決方法詳解

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

什么是競(jìng)態(tài)問(wèn)題

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

此詞源自于兩個(gè)信號(hào)試著彼此競(jìng)爭(zhēng),來(lái)影響誰(shuí)先輸出。

簡(jiǎn)單來(lái)說(shuō),競(jìng)態(tài)問(wèn)題出現(xiàn)的原因是無(wú)法保證異步操作的完成會(huì)按照他們開(kāi)始時(shí)同樣的順序。舉個(gè)??:

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

這就是競(jìng)態(tài)條件,在前端開(kāi)發(fā)中,常見(jiàn)于搜索,分頁(yè),選項(xiàng)卡等切換的場(chǎng)景。

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

當(dāng)發(fā)出新的請(qǐng)求時(shí),取消掉上次請(qǐng)求即可。

取消過(guò)期請(qǐng)求

XMLHttpRequest 取消請(qǐng)求

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

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

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

fetch API 取消請(qǐng)求

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

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

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

相比原生 API,大多項(xiàng)目都會(huì)選擇 axios 進(jìn)行請(qǐng)求。

axios 取消請(qǐng)求

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

可以利用 axios 的 CancelToken API 取消請(qǐng)求。

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

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

所以我們?cè)谔幚碚?qǐng)求錯(cuò)誤時(shí),需要判斷 error 是否是 cancel 導(dǎo)致的,避免與常規(guī)錯(cuò)誤一起處理。

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

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

v0.22.0 開(kāi)始,axios 支持以 fetch API 方式的 AbortController 取消請(qǐng)求

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

同樣,在處理請(qǐng)求錯(cuò)誤時(shí),也需要判斷 error 是否來(lái)自 cancel。

可取消的 promise

原生 promise 并不支持 cancel,但 cancel 對(duì)于異步操作來(lái)說(shuō)又是個(gè)很常見(jiàn)的需求。所以社區(qū)很多倉(cāng)庫(kù)都自己實(shí)現(xiàn)了 promise 的 cancel 能力。

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

什么是指令式 promise?

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

通過(guò)它的用法能更好地理解何為指令式 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 方法其實(shí)就是將 resolve,reject 設(shè)為 null,讓 promise 永遠(yuǎn)不會(huì) resolve/reject。

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

有興趣的同學(xué)可以了解下 http://chabaoo.cn/article/258149.htm

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

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

因此解決競(jìng)態(tài)問(wèn)題的方法,除了「取消請(qǐng)求」,還可以「忽略請(qǐng)求」。

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

忽略過(guò)期請(qǐng)求

我們又有哪些方式來(lái)忽略過(guò)期的請(qǐng)求呢?

封裝指令式 promise

利用指令式 promise,我們可以手動(dòng)調(diào)用 cancel API 來(lái)忽略上次請(qǐng)求。

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

我們可以基于指令式 promise 封裝一個(gè)自動(dòng)忽略過(guò)期請(qǐng)求的高階函數(shù) onlyResolvesLast。

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

function onlyResolvesLast(fn) {
  // 保存上一個(gè)請(qǐng)求的 cancel 方法
  let cancelPrevious = null; 
  const wrappedFn = (...args) => {
    // 當(dāng)前請(qǐng)求執(zhí)行前,先 cancel 上一個(gè)請(qǐng)求
    cancelPrevious && cancelPrevious();
    // 執(zhí)行當(dāng)前請(qǐng)求
    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… 的實(shí)現(xiàn)。

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

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 標(biāo)識(shí)每次請(qǐng)求

除了指令式 promise,我們還可以給「請(qǐng)求標(biāo)記 id」的方式來(lái)忽略上次請(qǐng)求。

具體思路是:

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

偽代碼如下:

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

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

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

用法也一樣,使用 onlyResolvesLast 包裝一下請(qǐng)求方法,實(shí)現(xiàn)過(guò)期請(qǐng)求自動(dòng)忽略。

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

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

「取消」更實(shí)際

如果請(qǐng)求被「取消」了沒(méi)有到達(dá)服務(wù)端,那么可以一定程度減輕服務(wù)的壓力。

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

「忽略」更通用

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

一個(gè)更實(shí)際,一個(gè)更通用,兩者的使用需要根據(jù)具體場(chǎng)景來(lái)權(quán)衡。

總結(jié)

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

解決競(jìng)態(tài)問(wèn)題,我們可以選擇「取消」或「忽略」過(guò)期請(qǐng)求。

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

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

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

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

相關(guān)文章

最新評(píng)論