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

解決前端常見問(wèn)題:競(jìng)態(tài)條件

開發(fā) 前端
使用 async/await 而不是“傳統(tǒng)”promise 實(shí)現(xiàn)的完全相同的應(yīng)用,將具有完全相同的競(jìng)態(tài)條件。以上所有解決方案和原因都適用,只是語(yǔ)法會(huì)略有不同。

大家好,我是 CUGGZ。

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

1. Promise和競(jìng)態(tài)條件

(1)Promise

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

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

從代碼的角度來(lái)看,就是這樣的:

console.log('first step');

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

console.log('third step');

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

圖片

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

Promise 中最有趣的部分之一是它可能會(huì)導(dǎo)致競(jìng)態(tài)條件。下面是一個(gè)非常簡(jiǎn)單的應(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;

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

頁(yè)面效果如下:

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

圖片

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

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

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

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

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

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

  // 通過(guò) 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>
    </>
  );
};

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

(3)競(jìng)態(tài)條件

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

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

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

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

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

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

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

  1. App 組件將再次觸發(fā) Page 的重新渲染;
  2. useEffect 將再次被觸發(fā)(因?yàn)橐蕾嚨?nbsp;id 更改);
  3. fetch 將再次被觸發(fā);
  4. 第一次 fetch 完成,setData 被觸發(fā),Page 組件使用第一次 fecth 的數(shù)據(jù)進(jìn)行更新;
  5. 第二次 fetch 完成,setData 被觸發(fā),Page 組件使用第二次 fetch 的數(shù)據(jù)進(jìn)行更新。

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

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

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

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

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

2. 修復(fù)競(jìng)態(tài)條件

(1)強(qiáng)制重新掛載

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

想象一下如下組件:

const App = () => {
  const [page, setPage] = useState('issue');

  return (
    <>
      {page === 'issue' && <Issue />}
      {page === 'about' && <About />}
    </>
  )
}

這里我們并沒(méi)有傳遞 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)航時(shí)沒(méi)有發(fā)生競(jìng)態(tài)條件。盡可能多地和盡可能快地進(jìn)行導(dǎo)航:應(yīng)用運(yùn)行正常。

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

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

從 fetch 的角度來(lái)看:

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

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

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

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

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

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

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

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

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

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

如果結(jié)果可以返回用于生成 url 的id,就可以比較它們,如果不匹配就忽略它。這里的技巧就是在函數(shù)中避免 React 生命周期和本地?cái)?shù)據(jù),并在 useEffect 中訪問(wèn)最新的 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é)果進(jìn)行比較,只有兩個(gè) id 相等時(shí)才更新狀態(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é)果進(jìn)行比較,僅當(dāng)結(jié)果實(shí)際上屬于該 url 時(shí)才更新狀態(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 有一個(gè)清理函數(shù),可以在其中清理訂閱等內(nèi)容。它的語(yǔ)法如下所示:

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

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

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

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

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

  // 執(zhí)行 fetch 請(qǐng)求

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

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

雖然 fetch 是異步的,但仍然只存在于該閉包中,并且只能訪問(wèn)啟動(dòng)它的 useEffect 中的局部變量。因此,當(dāng)檢查 .then 回調(diào)中的 isActive 時(shí),只有最近的運(yùn)行(即尚未清理的運(yùn)行)才會(huì)將變量設(shè)置為 true。所以,現(xiàn)在只需要檢查是否處于活動(dòng)閉包中,如果是,則將獲取的數(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) => {
      // 如果閉包處于活動(dòng)狀態(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)取消之前的請(qǐng)求

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

我們可以在 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 () => {
    // 中止請(qǐng)求
    controller.abort();
  };
}, [url]);

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

中止一個(gè)正在進(jìn)行的請(qǐng)求會(huì)導(dǎo)致 Promise 被拒絕,所以需要在 Promise 中捕捉錯(cuò)誤。因?yàn)?nbsp;AbortController 而拒絕會(huì)給出特定類型的錯(cuò)誤:

fetch(url, { signal: controller.signal })
  .then((r) => r.json())
  .then((r) => {
    setData(r);
  })
  .catch((error) => {
    // 由于 AbortController 導(dǎo)致的錯(cuò)誤
    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

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

對(duì)于 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 實(shí)現(xiàn)的完全相同的應(yīng)用,將具有完全相同的競(jìng)態(tài)條件。以上所有解決方案和原因都適用,只是語(yǔ)法會(huì)略有不同。可以在在線示例中查看:https://codesandbox.io/s/app-with-race-condition-async-away-q39lgi?file=/src/App.tsx

參考:https://www.developerway.com/posts/fetching-in-react-lost-promises

責(zé)任編輯:武曉燕 來(lái)源: 前端充電寶
相關(guān)推薦

2022-11-11 15:49:09

前端JavaScript開發(fā)

2022-11-11 10:22:54

前端Promise

2011-02-22 14:00:16

vsftpd

2020-10-18 12:00:27

前端開發(fā)架構(gòu)

2009-07-17 10:01:14

Swing和AWT

2019-10-08 16:05:19

Redis數(shù)據(jù)庫(kù)系統(tǒng)

2011-01-21 14:13:10

2023-06-27 07:21:51

前端開發(fā)坑點(diǎn)

2023-09-28 08:51:58

Java數(shù)據(jù)

2014-01-07 13:54:02

HadoopYARN

2010-05-17 10:01:09

MySql字符集

2011-05-06 17:25:58

硒鼓

2013-11-14 15:47:29

SDN問(wèn)題答疑

2011-04-01 13:55:24

Java

2011-05-06 15:39:55

硒鼓

2010-07-21 09:10:02

Perl常見問(wèn)題

2024-02-05 13:37:16

Go語(yǔ)言方法

2023-12-29 09:28:25

Java編程

2010-08-31 16:09:04

DIV+CSS

2011-10-28 10:56:24

jQTouchjQueryiPhone
點(diǎn)贊
收藏

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

主站蜘蛛池模板: 日韩一二区 | 久久国产精品视频观看 | 国产高清精品一区二区三区 | 一级片av | 亚洲精品乱码久久久久久按摩观 | 黄色小视频大全 | 午夜影院操 | 中文字字幕在线中文乱码范文 | 免费h在线 | 免费一区二区在线观看 | 国产一区二区三区欧美 | 狠狠操av| ririsao久久精品一区 | 丁香六月激情 | 欧美高清性xxxxhdvideosex | 婷婷在线网站 | 18成人在线观看 | 久久久99国产精品免费 | 国产精品一级在线观看 | 视频三区| 日韩影院在线观看 | 欧美性tv | av中文字幕在线 | 成人在线免费观看 | 在线观看国产 | 日本欧美在线 | 亚洲综合二区 | 精品啪啪 | 亚洲欧美一区二区在线观看 | 人成在线视频 | 精品国产免费人成在线观看 | 久久一区二区三区电影 | 国产精品国产精品国产专区不片 | 黄色毛片一级 | 国产91久久久久蜜臀青青天草二 | 精品综合视频 | 久久精品91久久久久久再现 | 久久中文字幕av | 天天澡天天狠天天天做 | 亚洲成人黄色 | 色爽女 |