成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

如何解決前端常見的競態(tài)問題?

開發(fā) 前端
本文將深入研究 Promise 是如何導(dǎo)致競態(tài)條件的,以及防止競態(tài)條件發(fā)生的幾種方法!

大家好,我是 CUGGZ。

本文將深入研究 Promise 是如何導(dǎo)致競態(tài)條件的,以及防止競態(tài)條件發(fā)生的幾種方法!

1、Promise和競態(tài)條件

(1)Promise

我們知道,JavaScript 是單線程的,代碼會同步執(zhí)行,即按順序從上到下執(zhí)行。Promise 是可供我們異步執(zhí)行的方法之一。使用 Promise,可以觸發(fā)一個任務(wù)并立即進入下一步,而無需等待任務(wù)完成,該任務(wù)承諾它會在完成時通知我們。

最重要和最廣泛使用 Promise 的情況之一就是數(shù)據(jù)獲取。不管是 fetch 還是 axios,Promise 的行為都是一樣的。

從代碼的角度來看,就是這樣的:

console.log('first step');

fetch('/some-url') // 創(chuàng)建 Promise
.then(() { // 等待 Promise 完成
console.log('second step'); // 成功
}
)
.catch(() {
console.log('something bad happened'); // 發(fā)生錯誤
})

console.log('third step');

這里會創(chuàng)建 Promise fetch('/some-url'),并在 .then 中獲得結(jié)果時執(zhí)行某些操作,或者在 .catch 中處理錯誤。

圖片

(2)實際應(yīng)用

Promise 中最有趣的部分之一是它可能會導(dǎo)致競態(tài)條件。下面是一個非常簡單的應(yīng)用:

import "./styles.scss";
import { useState, useEffect } from "react";
type Issue = {
id: string;
title: string;
description: string;
author: string;
};
const url1 =
"https://run.mocky.io/v3/ebf1b8f3-0368-4e3b-a965-1c5fdcc5d490?mocky-delay=2000ms";
const url2 =
"https://run.mocky.io/v3/27398801-05e2-4a62-8719-2a2d40974e52?mocky-delay=2000ms";
const Page = ({ id }: { id: string }) => {
const [data, setData] = useState<Issue>({} as Issue);
const [loading, setLoading] = useState(false);
const url = id === "1" ? url1 : url2;
useEffect(() {
setLoading(true);
fetch(url)
.then((r) => r.json())
.then((r) => {
setData(r);
console.log(r);
setLoading(false);
});
}, [url]);
if (!data.id || loading) return <>loading issue {id}</>;

return (
<div>
<h1>My issue number {data.id}</h1>
<h2>{data.title}</h2>
<p>{data.description}</p>
</div>
);
};
const App = () {
const [page, setPage] = useState("1");

return (
<div className="App">
<div className="container">
<ul className="column">
<li>
<button onClick={() => setPage("1")}>Issue 1</button>
</li>
<li>
<button onClick={() => setPage("2")}>Issue 2</button>
</li>
</ul>

<Page id={page} />
</div>
</div>
);
};

export default App;

在線實例:https://codesandbox.io/s/app-with-race-condition-fzyrj5?from-embed。

頁面效果如下:

圖片可以看到,在左側(cè)有兩個選項卡,切換選項卡就會發(fā)送一個數(shù)據(jù)請求,請求的數(shù)據(jù)會在右側(cè)展示。當我們在選項卡之間進行快速切換時,內(nèi)容會發(fā)生閃爍,數(shù)據(jù)也是隨機出現(xiàn)。如下:

圖片

為什么會這樣呢?我們來看一下這個應(yīng)用是怎么實現(xiàn)的。這里有兩個組件,一個是根組件 APP,它會管理 active 的 page 狀態(tài),并渲染導(dǎo)航按鈕和實際的 Page 組件。

const App = () {
const [page, setPage] = useState("1");

return (
<>
<!-- 左側(cè)按鈕 -->
<button onClick={() => setPage("1")}>Issue 1</button>
<button onClick={() => setPage("2")}>Issue 2</button>

<!-- 實際內(nèi)容 -->
<Page id={page} />
</div>
);
};

另一個就是 Page 組件,它接受活動頁面 的 id 作為 props,發(fā)送一個 fetch 請求來獲取數(shù)據(jù),然后渲染它。簡化的實現(xiàn)(沒有加載狀態(tài))如下所示:

const Page = ({ id }: { id: string }) => {
const [data, setData] = useState({});

// 通過 id 獲取相關(guān)數(shù)據(jù)
const url = `/some-url/${id}`;

useEffect(() {
fetch(url)
.then((r) => r.json())
.then((r) => {
setData(r);
});
}, [url]);

return (
<>
<h2>{data.title}</h2>
<p>{data.description}</p>
</>
);
};

這里通過 id 來確定獲取數(shù)據(jù)的 url。然在 useEffect 中發(fā)送 fetch 請求,并將獲取到的數(shù)據(jù)存儲在 state 中。那么競態(tài)條件和奇怪的行為是從哪里來的呢?

(3)競態(tài)條件

這可以歸結(jié)于兩個方面:Promises 的本質(zhì)和 React 生命周期。

從生命周期的角度來看,執(zhí)行如下:

  1. App 組件掛載。
  2. Page 組件使用默認的 prop 值 1 掛載。
  3. Page 組件中的 useEffect 首次執(zhí)行。

那么 Promises 的本質(zhì)就生效了:useEffect 中的 fetch 是一個 Promise,它是異步操作。它發(fā)送實際的請求,然后 React 繼續(xù)它的生命周期而不會等待結(jié)果。大約 2 秒后,請求完成,.then 開始執(zhí)行,在其中我們調(diào)用 setData 來將獲取到的數(shù)據(jù)保存狀態(tài)中,Page 組件使用新數(shù)據(jù)更新,我們在屏幕上看到它。

如果在所有內(nèi)容渲染完成后再點擊導(dǎo)航按鈕,事件流如下:

  1. App 組件將其狀態(tài)更改為另一個頁面;
  2. 狀態(tài)改變觸發(fā)App 組件的重新渲染;
  3. Page 組件也會重新渲染;
  4. Page 組件中的 useEffect 依賴于 id,id變了就會再次觸發(fā) useEffect;
  5. useEffect 中的 fetch 將使用新 id 觸發(fā),大約 2 秒后 setData 將再次調(diào)用,Page 組件更新,我們將在屏幕上看到新數(shù)據(jù)。

圖片

但是,如果在第一次 fetch 正在進行但尚未完成時單擊導(dǎo)航按鈕,這時 id 發(fā)生了變化,會發(fā)生什么呢?

  1. App 組件將再次觸發(fā) Page 的重新渲染。
  2. useEffect 將再次被觸發(fā)(因為依賴的 id 更改)。
  3. fetch 將再次被觸發(fā)。
  4. 第一次 fetch 完成,setData 被觸發(fā),Page 組件使用第一次 fecth 的數(shù)據(jù)進行更新。
  5. 第二次 fetch 完成,setData 被觸發(fā),Page 組件使用第二次 fetch 的數(shù)據(jù)進行更新。

這樣,競態(tài)條件就產(chǎn)生了。在導(dǎo)航到新頁面后,我們會看到內(nèi)容的閃爍:第一次 fetch 的內(nèi)容先被渲染,然后被第二次 fetch 的內(nèi)容替換。

圖片

如果第二次 fetch 在第一次 fetch 之前完成,這種效果會更加有趣。我們會先看到下一頁的正確內(nèi)容,然后將其替換為上一頁的錯誤內(nèi)容。

圖片

來看下面的例子,等到第一次加載完所有內(nèi)容,然后導(dǎo)航到第二頁,然后快速導(dǎo)航回第一頁。頁面效果如下:

圖片

在線實例:https://codesandbox.io/s/app-without-race-condition-reversed-yuoqkh?from-embed。

可以看到,我們先點擊 Issues 2,再點擊的 Issue 1。而最終先顯示了 Issue 1 的結(jié)果,后顯示了 Issue 2 的結(jié)果。那該如何解決這個問題呢?

2、修復(fù)競態(tài)條件

(1)強制重新掛載

其實這一個并不是解決方案,它更多地解釋了為什么這些競態(tài)條件實際上并不會經(jīng)常發(fā)生,以及為什么我們通常在常規(guī)頁面導(dǎo)航期間看不到它們。

想象一下如下組件:

const App = () {
const [page, setPage] = useState('issue');
return (
<>
{page === 'issue' && <Issue />}
{page === 'about' && <About />}
</>
)
}

這里我們并沒有傳遞 props,Issue 和 About 組件都有各自的 url,它們可以從中獲取數(shù)據(jù)。并且數(shù)據(jù)獲取發(fā)生在 useEffect Hook 中:

const About = () {
const [about, setAbout] = useState();
useEffect(() {
fetch("/some-url-for-about-page")
.then((r) => r.json())
.then((r) => setAbout(r));
}, []);
...
}

這次導(dǎo)航時沒有發(fā)生競態(tài)條件。盡可能多地和盡可能快地進行導(dǎo)航:應(yīng)用運行正常。

圖片

在線實例:https://codesandbox.io/s/issue-and-about-no-bug-5udo04?from-embed。

這是為什么呢?答案就在這里:{page === ‘issue’ && <Issue />}。當 page 值發(fā)生更改時,Issue 和 About 頁面都不會重新渲染,而是會重新掛載。當值從 issue 更改為 about 時,Issue 組件會自行卸載,而 About 組件會進行掛載。

從 fetch 的角度來看:

  1. App 組件首先渲染,掛載 Issue 組件,并獲取相關(guān)數(shù)據(jù)。
  2. 當 fetch 仍在進行時導(dǎo)航到下一頁時,App 組件會卸載 Issue 頁面并掛載 About 組件,它會執(zhí)行自己的數(shù)據(jù)獲取。

當 React 卸載一個組件時,就意味著它已經(jīng)完全消失了,從屏幕上消失,其中發(fā)生的一切,包括它的狀態(tài)都丟失了。將其與前面的代碼進行比較,我們在其中編寫了 <Page id={page} />,這個 Page 組件從未被卸載,我們只是在導(dǎo)航時重新使用它和它的狀態(tài)。

回到卸載的情況,當我們跳轉(zhuǎn)到在 About 頁面時,Issue 的 fetch 請求完成時,Issue 組件的 .then 回調(diào)將嘗試調(diào)用 setIssue,但是組件已經(jīng)消失了,從 React 的角度來看,它已經(jīng)不存在了。所以 Promise 會消失,它獲取的數(shù)據(jù)也會消失。

圖片

順便說一句,React 中經(jīng)常會提示:Can't perform a React state update on an unmounted component,當組件已經(jīng)消失后完成數(shù)據(jù)獲取等異步操作時就會出現(xiàn)這個警告。

理論上,這種行為可以用來解決應(yīng)用中的競態(tài)條件:只需要強制頁面組件重新掛載。可以使用 key 屬性:

<Page id={page} key={page} />

在線實例:https://codesandbox.io/s/app-without-race-condition-twv1sm?file=/src/App.tsx。

?? 這并不是推薦使用的競態(tài)條件問題的解決方案,其影響較大:性能可能會受到影響,狀態(tài)的意外錯誤,渲染樹下的 useEffect 意外觸發(fā)。有更好的方法來處理競爭條件(見下文)。

(2)丟棄錯誤的結(jié)果

解決競爭條件的另外一種方法就是確保傳入 .then 回調(diào)的結(jié)果與當前“active”的 id 匹配。

如果結(jié)果可以返回用于生成 url 的id,就可以比較它們,如果不匹配就忽略它。這里的技巧就是在函數(shù)中避免 React 生命周期和本地數(shù)據(jù),并在 useEffect 中訪問最新的 id。React ref 就非常適合:

const Page = ({ id }) => {
// 創(chuàng)建 ref
const ref = useRef(id);
useEffect(() {
// 用最新的 id 更新 ref 值
ref.current = id;

fetch(`/some-data-url/${id}`)
.then((r) => r.json())
.then((r) => {
// 將最新的 id 與結(jié)果進行比較,只有兩個 id 相等時才更新狀態(tài)
if (ref.current === r.id) {
setData(r);
}
});
}, [id]);
}

在線示例:https://codesandbox.io/s/app-with-race-condition-fixed-with-id-and-ref-jug1jk?file=/src/App.tsx。

我們也可以直接比較 url:

const Page = ({ id }) => {
// 創(chuàng)建 ref
const ref = useRef(id);

useEffect(() {
// 用最新的 url 更新 ref 值
ref.current = url;

fetch(`/some-data-url/${id}`)
.then((result) => {
// 將最新的 url 與結(jié)果進行比較,僅當結(jié)果實際上屬于該 url 時才更新狀態(tài)
if (result.url === ref.current) {
result.json().then((r) => {
setData(r);
});
}
});
}, [url]);
}

在線示例:https://codesandbox.io/s/app-with-race-condition-fixed-with-url-and-ref-whczob?file=/src/App.tsx。

(3)丟棄以前的結(jié)果

useEffect 有一個清理函數(shù),可以在其中清理訂閱等內(nèi)容。它的語法如下所示:

useEffect(() {
return () {
// 清理的內(nèi)容
}
}, [url]);

清理函數(shù)會在組件卸載后執(zhí)行,或者在每次更改依賴項導(dǎo)致的重新渲染之前執(zhí)行。因此重新渲染期間的操作順序?qū)⑷缦滤荆?/p>

  • url 更改。
  • 清理函數(shù)被觸發(fā)。
  • useEffect 的實際內(nèi)容被觸發(fā)。

JavaScript 中函數(shù)和閉包的性質(zhì)允許我們這樣做:

useEffect(() {
// useEffect中的局部變量
let isActive = true;

// 執(zhí)行 fetch 請求

return () {
// 上面的局部變量
isActive = false;
}
}, [url]);

我們引入了一個局部布爾變量 isActive,并在 useEffect 運行時將其設(shè)置為 true,在清理時將其設(shè)置為 false。每次重新渲染時都會重新創(chuàng)建 useEffect 中的變量,因此最新的 useEffect 會將 isActive 始終重置為 true。但是,在它之前運行的清理函數(shù)仍然可以訪問前一個變量的作用域,并將其重置為 false。這就是 JavaScript 閉包的工作方式。

雖然 fetch 是異步的,但仍然只存在于該閉包中,并且只能訪問啟動它的 useEffect 中的局部變量。因此,當檢查 .then 回調(diào)中的 isActive 時,只有最近的運行(即尚未清理的運行)才會將變量設(shè)置為 true。所以,現(xiàn)在只需要檢查是否處于活動閉包中,如果是,則將獲取的數(shù)據(jù)設(shè)置狀態(tài)。如果不是,什么都不做,數(shù)據(jù)將再次消失。

useEffect(() {
// 將 isActive 設(shè)置為 true
let isActive = true;
fetch(`/some-data-url/${id}`)
.then((r) => r.json())
.then((r) => {
// 如果閉包處于活動狀態(tài),更新狀態(tài)
if (isActive) {
setData(r);
}
});

return () {
// 在下一次重新渲染之前將 isActive 設(shè)置為 false
isActive = false;
}
}, [id]);

在線示例:https://codesandbox.io/s/app-with-race-condition-fixed-with-cleanup-4du0wf?file=/src/App.tsx。

(4)取消之前的請求

對于競態(tài)條件問題,我們可以取消之前的請求,而不是清理或比較結(jié)果。如果之前的請求不能完成(取消),那么使用過時數(shù)據(jù)的狀態(tài)更新將永遠不會發(fā)生,問題也就不會存在。可以為此使用 AbortController 來取消請求。

我們可以在 useEffect 中創(chuàng)建 AbortController 并在清理函數(shù)中調(diào)用 .abort():

useEffect(() {
// 創(chuàng)建 controller
const controller = new AbortController();

// 將 controller 作為signal傳遞給 fetch
fetch(url, { signal: controller.signal })
.then((r) => r.json())
.then((r) => {
setData(r);
});

return () {
// 中止請求
controller.abort();
};
}, [url]);

這樣,在每次重新渲染時,正在進行的請求將被取消,新的請求將是唯一允許解析和設(shè)置狀態(tài)的請求。

中止一個正在進行的請求會導(dǎo)致 Promise 被拒絕,所以需要在 Promise 中捕捉錯誤。因為 AbortController 而拒絕會給出特定類型的錯誤:

fetch(url, { signal: controller.signal })
.then((r) => r.json())
.then((r) => {
setData(r);
})
.catch((error) => {
// 由于 AbortController 導(dǎo)致的錯誤
if (error.name === 'AbortError') {
// ...
} else {
// ...
}
});

在線示例:https://codesandbox.io/s/app-with-race-condition-fixed-with-abort-controller-6u0ckk?file=/src/App.tsx。

3、Async/await

上面我們說了 Promise 的競態(tài)條件的解決方案,那 Async/await 會有所不同嗎?其實,Async/await 只是編寫 Promise 的一種更好的方式。它只是將 Promise 變成“同步”函數(shù),但不會改變它們的異步的性質(zhì)。

對于 Promise:

fetch('/some-url')
.then(r r.json())
.then(r setData(r));

使用 Async/await 這樣寫:

const response = await fetch('/some-url');
const result = await response.json();
setData(result);

使用 async/await 而不是“傳統(tǒng)”promise 實現(xiàn)的完全相同的應(yīng)用,將具有完全相同的競態(tài)條件。以上所有解決方案和原因都適用,只是語法會略有不同。可以在在線示例中查看:https://codesandbox.io/s/app-with-race-condition-async-away-q39lgi?file=/src/App.tsx。

責(zé)任編輯:姜華 來源: 前端充電寶
相關(guān)推薦

2022-11-11 15:49:09

前端JavaScript開發(fā)

2023-06-27 13:46:00

前端競態(tài)promise

2023-07-18 16:05:00

IP地址

2021-06-06 13:05:15

前端跨域CORS

2022-03-11 10:01:47

開發(fā)跨域技術(shù)

2009-07-06 18:53:52

ESXESX主機VMware

2009-01-09 23:01:24

2020-02-17 13:05:37

物聯(lián)網(wǎng)IOT物聯(lián)網(wǎng)應(yīng)用

2012-09-05 11:09:15

SELinux操作系統(tǒng)

2020-05-31 18:55:47

遠程桌面連接網(wǎng)絡(luò)故障虛擬桌面

2018-04-25 07:34:59

物聯(lián)網(wǎng)卡網(wǎng)絡(luò)運營商

2024-06-07 07:56:35

2017-10-17 09:21:06

2019-12-17 08:54:39

物聯(lián)網(wǎng)IoT物聯(lián)網(wǎng)項目

2010-04-29 17:46:31

Oracle死鎖

2019-11-26 14:30:20

Spring循環(huán)依賴Java

2024-12-05 09:06:58

2009-09-21 17:10:14

struts Hibe

2021-10-20 20:27:55

MySQL死鎖并發(fā)

2023-03-14 08:01:53

Go開發(fā)原子操作
點贊
收藏

51CTO技術(shù)棧公眾號

主站蜘蛛池模板: 国产一区免费 | 毛片免费视频 | 国产成人精品一区二区三区 | 99爱在线免费观看 | 国产高清视频在线 | 午夜影院网站 | 91视频国产区 | 欧美亚洲国产一区二区三区 | 国产精品久久久久无码av | 亚洲二区在线 | 天天综合网7799精品 | 人人人人人爽 | 成人国内精品久久久久一区 | 久久久www成人免费无遮挡大片 | 亚洲国产黄色av | 老司机狠狠爱 | 欧美在线 | 1级毛片| 国产精品久久久一区二区三区 | 国产成人av在线播放 | 国产精品xxxx | 欧美视频在线看 | 天天操,夜夜爽 | 欧美成人a∨高清免费观看 色999日韩 | 91福利网址 | 一区二区小视频 | 粉嫩粉嫩芽的虎白女18在线视频 | 在线视频一区二区三区 | 免费成人高清在线视频 | 日韩免费视频 | 国产在线观看网站 | 亚洲美女av网站 | 少妇精品久久久久久久久久 | 99这里只有精品视频 | 中文字幕蜜臀av | 国产成人精品免费视频大全最热 | 精品麻豆剧传媒av国产九九九 | 91精品久久久久久久久久小网站 | 综合久久久久 | 欧美一区二区大片 | 国产99视频精品免费播放照片 |