日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

React競態條件Race?Condition實例詳解_React

作者:冴羽 ? 更新時間: 2022-12-08 編程語言

競態條件

Race Condition,中文譯為競態條件,旨在描述一個系統或者進程的輸出,依賴于不受控制事件的出現順序或者出現時機。

舉個簡單的例子:

if (x == 5) // The "Check"
{
   y = x * 2; // The "Act"
   // 如果其他的線程在 "if (x == 5)" and "y = x * 2" 執行之間更改了 x 的值
   // y 就可能不等于 10.
}

你可能想,JavaScript 是單線程,怎么可能出現這個問題?

React 與競態條件

確實如此,但前端有異步渲染,所以競態條件依然有可能出現,我們舉個 React 中常見的例子。

這是一個非常典型的數據獲取代碼:

class Article extends Component {
  state = {
    article: null
  };
  componentDidMount() {
    this.fetchData(this.props.id);
  }
  async fetchData(id) {
    const article = await API.fetchArticle(id);
    this.setState({ article });
  }
  // ...
}

看起來沒什么問題,但這段代碼還沒有實現數據更新,我們再改一下:

class Article extends Component {
  state = {
    article: null
  };
  componentDidMount() {
    this.fetchData(this.props.id);
  }
  componentDidUpdate(prevProps) {
    if (prevProps.id !== this.props.id) {
      this.fetchData(this.props.id);
    }
  }
  async fetchData(id) {
    const article = await API.fetchArticle(id);
    this.setState({ article });
  }
  // ...
}

當組件傳入新的 id 時,我們根據新的 id 請求數據,然后 setState 最新獲取的數據。

這時就可能出現競態條件,比如用戶選完立刻點擊下一頁,我們請求 id 為 1 的數據,緊接著請求 id 為 2 的數據,但因為網絡或者接口處理等原因,id為 2 的接口提前返回,便會先展示 id 為 2 的數據,再展示 id 為 1 的數據,這就導致了錯誤。

我們可以想想遇到這種問題的場景,比如類似于百度的搜索功能,切換 tab 等場景,雖然我們也可以使用諸如 debounce 的方式來緩解,但效果還是會差點,比如使用 debounce,用戶在輸入搜索詞的時候,展示內容會長期處于空白狀態,對于用戶體驗而言,我們可以做的更好。

那么我們該如何解決呢?一種是在切換的時候取消請求,還有一種是借助一個布爾值來判斷是否需要更新,比如這樣:

function Article({ id }) {
  const [article, setArticle] = useState(null);
  useEffect(() => {
    let didCancel = false;
    async function fetchData() {
      const article = await API.fetchArticle(id);
      // 如果 didCancel 為 true 說明用戶已經取消了
      if (!didCancel) {
        setArticle(article);
      }
    }
    fetchData();
    // 執行下一個 effect 之前會執行
    return () => {
      didCancel = true;
    };
  }, [id]);
  // ...
}

當然你也可以用 ahooks 中的 useRequest,它的內部有一個 ref 變量記錄最新的 promise,也可以解決 Race Condition 的問題:

function Article({ id }) {
  const { data, loading, error} = useRequest(() => fetchArticle(id), {
  	refreshDeps: [id]
  });
  // ...
}

效果演示

問題復現

為了方便大家自己測試這個問題,我們提供相對完整的代碼。以 《Avoiding Race Conditions when Fetching Data with React Hooks》中的例子為例,出現 Race Condition 問題的代碼如下:

const fakeFetch = person => {
  return new Promise(res => {
    setTimeout(() => res(`${person}'s data`), Math.random() * 5000);
  });
};
const App = () => {
  const [data, setData] = useState('');
  const [loading, setLoading] = useState(false);
  const [person, setPerson] = useState(null);
  useEffect(() => {
    setLoading(true);
    fakeFetch(person).then(data => {
      	setData(data);
      	setLoading(false);
    });
  }, [person]);
    const handleClick = (name) => () => {
    	setPerson(name)
    }
  return (
    <Fragment>
      <button onClick={handleClick('Nick')}>Nick's Profile</button>
      <button onClick={handleClick('Deb')}>Deb's Profile</button>
      <button onClick={handleClick('Joe')}>Joe's Profile</button>
      {person && (
        <Fragment>
          <h1>{person}</h1>
          <p>{loading ? 'Loading...' : data}</p>
        </Fragment>
      )}
    </Fragment>
  );
};

我們實現了一個 fakeFetch函數,用于模擬接口的返回,具體返回的時間為 Math.random() * 5000),用于模擬數據的隨機返回。

實現效果如下:

從效果圖中可以看到,我們按順序點擊了 NickDebJoe,理想情況下,結果應該顯示 Joe's Data,但最終顯示的數據為最后返回的 Nick's Data

布爾值解決

現在,我們嘗試用一個 canceled 布爾值解決:

const App = () => {
  const [data, setData] = useState('');
  const [loading, setLoading] = useState(false);
  const [person, setPerson] = useState(null);
  useEffect(() => {
    let canceled = false;
    setLoading(true);
    fakeFetch(person).then(data => {
      if (!canceled) {
        setData(data);
        setLoading(false);
      }
    });
    return () => (canceled = true);
  }, [person]);
  return (
    <Fragment>
      <button onClick={() => setPerson('Nick')}>Nick's Profile</button>
      <button onClick={() => setPerson('Deb')}>Deb's Profile</button>
      <button onClick={() => setPerson('Joe')}>Joe's Profile</button>
      {person && (
      <Fragment>
        <h1>{person}</h1>
        <p>{loading ? 'Loading...' : data}</p>
      </Fragment>
    )}
    </Fragment>
  );
};

實現效果如下:

即便接口沒有按照順序返回,依然不影響最終顯示的數據。

useRequest 解決

我們也可以借助 ahooks 的 useRequest 方法,修改后的代碼如下:

const App2 = () => {
  const [person, setPerson] = useState('Nick');
  const { data, loading} = useRequest(() => fakeFetch(person), {
    refreshDeps: [person],
  });
  const handleClick = (name) => () => {
    setPerson(name)
  }
  return (
    <Fragment>
      <button onClick={handleClick('Nick')}>Nick's Profile</button>
      <button onClick={handleClick('Deb')}>Deb's Profile</button>
      <button onClick={() => setPerson('Joe')}>Joe's Profile</button>
      {person && (
      <Fragment>
        <h1>{person}</h1>
        <p>{loading ? 'Loading...' : data}</p>
      </Fragment>
    )}
    </Fragment>
  );
};

代碼效果如上,就不重復錄制了。

考慮到部分同學可能會對 useRequest 的使用感到困惑,我們簡單介紹一下 useRequest的使用:

useRequest 的第一個參數是一個異步函數,在組件初次加載時,會自動觸發該函數執行。同時自動管理該異步函數的 loadingdataerror 等狀態。

useRequest 同樣提供了一個 options.refreshDeps 參數,當它的值變化后,會重新觸發請求。

const [userId, setUserId] = useState('1');
const { data, run } = useRequest(() => getUserSchool(userId), {
  refreshDeps: [userId],
});

上面的示例代碼,useRequest 會在初始化和 userId 變化時,觸發函數執行。與下面代碼實現功能完全一致:

const [userId, setUserId] = useState('1');
const { data, refresh } = useRequest(() => getUserSchool(userId));
useEffect(() => {
  refresh();
}, [userId]);

Suspense

這篇之所以講 Race Condition,主要還是為了引入講解 Suspense,借助 Suspense,我們同樣可以解決 Race Condition:

// 實現參考的 React 官方示例:https://codesandbox.io/s/infallible-feather-xjtbu
function wrapPromise(promise) {
  let status = "pending";
  let result;
  let suspender = promise.then(
    r => {
      status = "success";
      result = r;
    },
    e => {
      status = "error";
      result = e;
    }
  );
  return {
    read() {
      if (status === "pending") {
        throw suspender;
      } else if (status === "error") {
        throw result;
      } else if (status === "success") {
        return result;
      }
    }
  };
}
const fakeFetch = person => {
  return new Promise(res => {
    setTimeout(() => res(`${person}'s data`), Math.random() * 5000);
  });
};
function fetchData(userId) {
  return wrapPromise(fakeFetch(userId))
}
const initialResource = fetchData('Nick');
function User({ resource }) {
  const data = resource.read();
  return <p>{ data }</p>
}
const App = () => {
  const [person, setPerson] = useState('Nick');
  const [resource, setResource] = useState(initialResource);
  const handleClick = (name) => () => {
    setPerson(name)
    setResource(fetchData(name));
  }
  return (
    <Fragment>
      <button onClick={handleClick('Nick')}>Nick's Profile</button>
      <button onClick={handleClick('Deb')}>Deb's Profile</button>
	    <button onClick={handleClick('Joe')}>Joe's Profile</button>
      <Fragment>
        <h1>{person}</h1>
        <Suspense fallback={'loading'}>
          <User resource={resource} />
        </Suspense>
      </Fragment>
    </Fragment>
  );
};

原文鏈接:https://juejin.cn/post/7163202327594139679

欄目分類
最近更新