<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          React Hooks 使用誤區(qū),駁官方文檔

          共 9526字,需瀏覽 20分鐘

           ·

          2022-02-22 22:00

          作為 React Hooks 庫 ahooks[1] ?的作者,我應(yīng)該算一個非常非常資深的 React Hooks 用戶。在兩年多的 React Hooks 使用過程中,我越來越發(fā)現(xiàn)大家(包括我自己)對 React Hooks 的使用姿勢存在很大誤區(qū),歸根到底是官方文檔的教程很不嚴(yán)謹(jǐn),存在錯誤的指引。

          1. 不是所有的依賴都必須放到依賴數(shù)組中

          對于所有的 React Hooks 用戶,都有一個共識:“useEffect 中使用到外部變量,都應(yīng)該放到第二個數(shù)組參數(shù)中”,同時我們會安裝 eslint-plugin-react-hooks[2] 插件,來提醒自己是不是忘了某些變量。

          以上共識來自官方文檔:

          我愿稱該條規(guī)則為萬惡之源,這條規(guī)則以高亮展示,所有的新人都很重視,包括我自己。然而在實際的開發(fā)中,發(fā)現(xiàn)事情并不是這樣的。

          下面舉一個比較簡單的例子,要求如下:

          1. 有兩個字段 User 和 Email,都是可以隨時變化的
          2. 只有當(dāng) User 變化時,打印 User 和 Email 的值

          這個例子比較簡單,先貼下源碼:

          function?App()?{
          ??const?[email,?setEmail]?=?useState('');
          ??const?[user,?setUser]?=?useState('Tom');

          ??useEffect(()?=>?{
          ????console.log(user,?email);
          ??},?[user]);

          ??return?(
          ????<div?style={{?padding:?64?}}>
          ??????<label?style={{?display:?'block'?}}>
          ????????User:
          ????????<select?value={user}?onChange={(e)?=>?setUser(e.target.value)}>
          ??????????<option?value="Tom">Tomoption>

          ??????????<option?value="Jack">Jackoption>
          ????????select>
          ??????label>
          ??????<label?style={{?display:?'block',?marginTop:?16?}}>
          ????????Email:
          ????????<input?value={email}?onChange={e?=>?setEmail(e.target.value)}?/>
          ??????label>
          ????div>
          ??);
          }

          我們能看到示例代碼中,useEffect 是不符合 React 官方建議的,email 變量沒有放到依賴數(shù)組中,ESLint 警告如下:


          那如果按照規(guī)范,我們把依賴項都放到第二個數(shù)組參數(shù)中,會怎樣呢?

          useEffect(()?=>?{
          ??console.log(user,?email);
          },?[user,?email]);

          如上的代碼雖然符合了 React 官方的規(guī)范,但不滿足我們的業(yè)務(wù)需求了,當(dāng) email 變化時,也觸發(fā)了函數(shù)執(zhí)行。

          此時陷入了困境,當(dāng)滿足 useEffect 使用規(guī)范時,業(yè)務(wù)需求就不能滿足了。當(dāng)滿足業(yè)務(wù)需求時,useEffect 就不規(guī)范了。

          我的建議為:

          1. 不要使用 eslint-plugin-react-hooks 插件,或者可以選擇性忽略該插件的警告。
          2. 只有一種情況,需要把變量放到 deps 數(shù)組中,那就是當(dāng)該變量變化時,需要觸發(fā) useEffect 函數(shù)執(zhí)行。而不是因為 useEffect 中用到了這個變量!

          2. deps 參數(shù)不能緩解閉包問題

          假如完全按第二個建議來寫代碼,很多人又擔(dān)心,會不會造成一些不必要的閉包問題?我的結(jié)論是:閉包問題和 useEffect 的 deps 參數(shù)沒有太大關(guān)系。

          比如我有一個這樣的需求:當(dāng)進(jìn)入頁面 3s 后,輸出當(dāng)前最新的 count。代碼如下:

          function?Demo()?{
          ??const?[count,?setCount]?=?useState(0);

          ??useEffect(()?=>?{
          ????const?timer?=?setTimeout(()?=>?{
          ??????console.log(count)
          ????},?3000);
          ????return?()?=>?{
          ??????clearTimeout(timer);
          ????}
          ??},?[])

          ??return?(
          ????<button
          ??????onClick={()?=>
          ?setCount(c?=>?c?+?1)}
          ????>
          ??????click
          ????button>

          ??)
          }

          以上代碼,實現(xiàn)了初始化 3s 后,輸出 count。但很遺憾,這里肯定會出閉包問題,哪怕進(jìn)來之后我們多次點擊了 button,輸出的 count 仍然為 0。

          那假如我們把 count 放到 deps 中,是不是就好了?

          ??useEffect(()?=>?{
          ????const?timer?=?setTimeout(()?=>?{
          ??????console.log(count)
          ????},?3000);
          ????return?()?=>?{
          ??????clearTimeout(timer);
          ????}
          ??},?[count])

          如上代碼,此時確實沒有閉包問題了,但在每次 count 變化時,定時器卸載并重新開始計時了,不滿足我們的最初需求了。

          要解決的唯一辦法為:

          const?[count,?setCount]?=?useState(0);

          //?通過?ref?來記憶最新的?count
          const?countRef?=?useRef(count);
          countRef.current?=?count;

          useEffect(()?=>?{
          ??const?timer?=?setTimeout(()?=>?{
          ????console.log(countRef.current)
          ??},?3000);
          ??return?()?=>?{
          ????clearTimeout(timer);
          ??}
          },?[])

          雖然上面的代碼,很繞,但確實,只有這個解決方案。請記住這段代碼,功能真的很強大。

          const?countRef?=?useRef(count);
          countRef.current?=?count;

          上面的例子,可以發(fā)現(xiàn),閉包問題是不能僅僅通過遵守 React 規(guī)則來避免的。我們必須清晰的知道,在什么場景下會出現(xiàn)閉包問題。

          2.1 正常情況下是不會有閉包問題的

          const?[a,?setA]?=?useState(0);
          const?[b,?setB]?=?useState(0);

          const?c?=?a?+?b;

          useEffect(()=>{
          ?console.log(a,?b,?c)
          },?[a]);

          useEffect(()=>{
          ?console.log(a,?b,?c)
          },?[b]);

          useEffect(()=>{
          ?console.log(a,?b,?c)
          },?[c]);

          在一般的使用過程中,是不會有閉包問題的,如上代碼中,完全不會有閉包問題,和 deps 怎么寫沒有任何關(guān)系。

          2.2 延遲調(diào)用會存在閉包問題

          在延遲調(diào)用的場景下,一定會存在閉包問題。 什么是延遲調(diào)用?

          1. 使用 setTimeout、setInterval、Promise.then 等
          2. useEffect 的卸載函數(shù)
          const?getUsername?=?()?=>?{
          ??return?new?Promise((resolve,?reject)?=>?{
          ????setTimeout(()?=>?{
          ??????resolve('John');
          ????},?3000);
          ??})
          }

          function?Demo()?{
          ??const?[count,?setCount]?=?useState(0);

          ??//?setTimeout?會造成閉包問題
          ??useEffect(()?=>?{
          ????const?timer?=?setTimeout(()?=>?{
          ??????console.log(count);
          ????},?3000);
          ????return?()?=>?{
          ??????clearTimeout(timer);
          ????}
          ??},?[])

          ??//?setInterval?會造成閉包問題
          ??useEffect(()?=>?{
          ????const?timer?=?setInterval(()?=>?{
          ??????console.log(count);
          ????},?3000);
          ????return?()?=>?{
          ??????clearInterval(timer);
          ????}
          ??},?[])

          ??//?Promise.then?會造成閉包問題
          ??useEffect(()?=>?{
          ????getUsername().then(()?=>?{
          ??????console.log(count);
          ????});
          ??},?[])

          ??//?useEffect?卸載函數(shù)會造成閉包問題
          ??useEffect(()?=>?{
          ????return?()?=>?{
          ??????console.log(count);
          ????}
          ??},?[]);

          ??return?(
          ????<button
          ??????onClick={()?=>
          ?setCount(c?=>?c?+?1)}
          ????>
          ??????click
          ????button>

          ??)
          }

          在以上示例代碼中,四種情況均會出現(xiàn)閉包問題,永遠(yuǎn)輸出 0。這四種情況的根因都是一樣的,我們看一下代碼的執(zhí)行順序:

          1. 組件初始化,此時 count = 0
          2. 執(zhí)行 useEffect,此時 useEffect 的函數(shù)執(zhí)行,JS 引用鏈記錄了對 count=0 的引用關(guān)系
          3. 點擊 button,count 變化,但對之前的引用已經(jīng)無能為力了

          可以看到,閉包問題均是出現(xiàn)在延遲調(diào)用的場景下。解決辦法如下:

          const?[count,?setCount]?=?useState(0);

          //?通過?ref?來記憶最新的?count
          const?countRef?=?useRef(count);
          countRef.current?=?count;

          useEffect(()?=>?{
          ??const?timer?=?setTimeout(()?=>?{
          ????console.log(countRef.current)
          ??},?3000);
          ??return?()?=>?{
          ????clearTimeout(timer);
          ??}
          },?[])

          ......

          通過 useRef 來保證任何時候訪問的 countRef.current 都是最新的,以解決閉包問題。

          到這里,我重申下我對 useEffect 的建議:

          1. 只有變化時,需要重新執(zhí)行 useEffect 的變量,才要放到 deps 中。而不是 useEffect 用到的變量都放到 deps 中。
          2. 在有延遲調(diào)用場景時,可以通過 ref 來解決閉包問題。

          3. 盡量不要用 useCallback

          我建議在項目中盡量不要用 useCallback,大部分場景下,不僅沒有提升性能,反而讓代碼可讀性變的很差。

          3.1 useCallback 大部分場景沒有提升性能

          useCallback 可以記住函數(shù),避免函數(shù)重復(fù)生成,這樣函數(shù)在傳遞給子組件時,可以避免子組件重復(fù)渲染,提高性能。

          const?someFunc?=?useCallback(()=>?{
          ???doSomething();
          },?[]);

          return?<ExpensiveComponent?func={someFunc}?/>

          基于以上認(rèn)知,很多同學(xué)(包括我自己)在寫代碼時,只要是個函數(shù),都加個 useCallback,是你么?反正我以前是。

          但我們要注意,提高性能還必須有另外一個條件,子組件必須使用了 shouldComponentUpdate 或者 React.memo 來忽略同樣的參數(shù)重復(fù)渲染。

          假如 ExpensiveComponent 組件只是一個普通組件,是沒有任何用的。比如下面這樣:

          const?ExpensiveComponent?=?({?func?})?=>?{
          ??return?(
          ????<div?onClick={func}>
          ?????hello
          ????div>

          ??)
          }

          必須通過 React.memo 包裹 ExpensiveComponent ,才會避免參數(shù)不變的情況下的重復(fù)渲染,提高性能。

          const?ExpensiveComponent?=?React.memo(({?func?})?=>?{
          ??return?(
          ????<div?onClick={func}>
          ?????hello
          ????div>

          ??)
          })

          所以,useCallback 是要和 shouldComponentUpdate/React.memo 配套使用的,你用對了嗎?當(dāng)然,我建議一般項目中不用考慮性能優(yōu)化的問題,也就是不要使用 useCallback 了,除非有個別非常復(fù)雜的組件,單獨使用即可。

          3.2 useCallback 讓代碼可讀性變差

          我看到過一些代碼,使用 useCallback 后,大概長這樣:

          const?someFuncA?=?useCallback((d,?g,?x,?y)=>?{
          ???doSomething(a,?b,?c,?d,?g,?x,?y);
          },?[a,?b,?c]);

          const?someFuncB?=?useCallback(()=>?{
          ???someFuncA(d,?g,?x,?y);
          },?[someFuncA,?d,?g,?x,?y]);

          useEffect(()=>{
          ??someFuncB();
          },?[someFuncB]);

          在上面的代碼中,變量依賴一層一層傳遞,最終要判斷具體哪些變量變化會觸發(fā) useEffect 執(zhí)行,是一件很頭疼的事情。

          我期望不要用 useCallback,直接裸寫函數(shù)就好:

          const?someFuncA?=?(d,?g,?x,?y)=>?{
          ???doSomething(a,?b,?c,?d,?g,?x,?y);
          };

          const?someFuncB?=?()=>?{
          ???someFuncA(d,?g,?x,?y);
          };

          useEffect(()=>{
          ??someFuncB();
          },?[...]);

          在 useEffect 存在延遲調(diào)用的場景下,可能造成閉包問題,那通過咱們?nèi)f能的方法就能解決:

          const?someFuncA?=?(d,?g,?x,?y)=>?{
          ???doSomething(a,?b,?c,?d,?g,?x,?y);
          };

          const?someFuncB?=?()=>?{
          ???someFuncA(d,?g,?x,?y);
          };

          +?const?someFuncBRef?=?useRef(someFuncB);
          +?someFuncBRef.current?=?someFuncB;

          useEffect(()=>{
          +??setTimeout(()=>{
          +????someFuncBRef.current();
          +??},?1000)
          },?[...]);

          對 useCallback 的建議就一句話:沒事別用 useCallback。

          4. useMemo 建議大量使用

          相較于 useCallback 而言,useMemo 的收益是顯而易見的。

          //?沒有使用?useMemo
          const?memoizedValue?=?computeExpensiveValue(a,?b);

          //?使用?useMemo
          const?memoizedValue?=?useMemo(()?=>?computeExpensiveValue(a,?b),?[a,?b]);

          如果沒有使用 useMemo,computeExpensiveValue 會在每一次渲染的時候執(zhí)行。如果使用了 useMemo,只有在 ab 變化時,才會執(zhí)行一次 computeExpensiveValue

          這筆賬大家應(yīng)該都會算,所以我建議 useMemo 可以大量使用。

          當(dāng)然也不是無節(jié)制的使用,在很簡單的基礎(chǔ)類型計算時,可能 useMemo 并不劃算。

          const?a?=?1;
          const?b?=?2;

          const?c?=?useMemo(()=>?a?+?b,?[a,?b]);

          比如上面的例子,請問計算 a+b 的消耗大?還是記錄 a/b ,并比較a/b 是否變化的消耗大?

          明顯 a+b 消耗更小。

          const?a?=?1;
          const?b?=?2;

          const?c?=?a?+?b;

          這筆賬大家可以自己算,我建議簡單的基礎(chǔ)類型計算,就不要用 useMemo 了~

          5. useState 的正確使用姿勢

          useState 應(yīng)該算最簡單的一個 Hooks,但在使用中,也有很多技巧可循,如果嚴(yán)格按照以下幾點,代碼可維護(hù)性直接翻倍。

          5.1 能用其他狀態(tài)計算出來就不用單獨聲明狀態(tài)

          一個 state 必須不能通過其它 state/props 直接計算出來,否則就不用定義 state。

          const?SomeComponent?=?(props)?=>?{
          ??const?[a,?setA]?=?useState(1);
          ??const?[b,?setB]?=?useState(2);
          ??
          ??const?onClick?=?()?=>?{
          ????const?current?=?a?+?1;
          ??
          ????setA(current);
          ????setB(current*2)
          ??}
          ??
          ??return?(
          ????<div?onClick={onClick}>
          ???????increment?
          ????div>

          ??)
          }

          上面的示例中,變量 b 可以通過變量 a 計算出來,那就不要定義 b 了!

          const?SomeComponent?=?(props)?=>?{
          ??const?[a,?setA]?=?useState(1);
          ??
          ??const?b?=?a*2;
          ??
          ??const?onClick?=?()?=>?{
          ????const?current?=?a?+?1;
          ??
          ????setA(current);
          ??}
          ??
          ??return?(
          ????<div?onClick={onClick}>
          ???????increment?
          ????div>

          ??)
          }

          一般在項目中此類問題都比較隱晦,層層傳遞,在 Code Review 中很難一眼看出。如果能把變量定義清楚,那事情就成功了一半。

          5.2 保證數(shù)據(jù)源唯一

          在項目中同一個數(shù)據(jù),保證只存儲在一個地方。

          不要既存在 redux 中,又在組件中定義了一個 state 存儲。

          不要既存在父級組件中,又在當(dāng)前組件中定義了一個 state 存儲。

          不要既存在 url query 中,又在組件中定義了一個 state 存儲。

          function?SearchBox({?data?})?{
          ??const?[searchKey,?setSearchKey]?=?useState(getQuery('key'));
          ??
          ??const?handleSearchChange?=?e?=>?{
          ????const?key?=?e.target.value;
          ????setSearchKey(key);
          ????history.push(`/movie-list?key=${key}`);
          ??}
          ??
          ??return?(
          ??????<input
          ????????value={searchKey}
          ????????placeholder="Search..."
          ????????onChange={handleSearchChange}
          ??????/>

          ??);
          }

          在上面的示例中,searchKey 存儲在兩個地方,既在 url query 上,又定義了一個 state。完全可以優(yōu)化成下面這樣:

          function?SearchBox({?data?})?{
          ??const?searchKey?=?parse(localtion.search)?.key;
          ??
          ??const?handleSearchChange?=?e?=>?{
          ????const?key?=?e.target.value;
          ????history.push(`/movie-list?key=${key}`);
          ??}
          ??
          ??return?(
          ??????<input
          ????????value={searchKey}
          ????????placeholder="Search..."
          ????????onChange={handleSearchChange}
          ??????/>

          ??);
          }

          在實際項目開發(fā)中,此類問題也是比較隱晦,編碼時應(yīng)注意。

          5.3 useState 適當(dāng)合并

          項目中有木有寫過這樣的代碼:

          const?[firstName,?setFirstName]?=?useState();
          const?[lastName,?setLastName]?=?useState();
          const?[school,?setSchool]?=?useState();
          const?[age,?setAge]?=?useState();
          const?[address,?setAddress]?=?useState();

          const?[weather,?setWeather]?=?useState();
          const?[room,?setRoom]?=?useState();

          反正我最開始是寫過,useState 拆分過細(xì),導(dǎo)致代碼中一大片 useState。

          我建議,同樣含義的變量可以合并成一個 state,代碼可讀性會提升很多:

          const?[userInfo,?setUserInfo]?=?useState({
          ??firstName,
          ??lastName,
          ??school,
          ??age,
          ??address
          });

          const?[weather,?setWeather]?=?useState();
          const?[room,?setRoom]?=?useState();

          當(dāng)然這種方式我們在變更變量時,一定不要忘記帶上老的字段,比如我們只想修改 firstName

          setUserInfo(s=>?({
          ??...s,
          ??fristName,
          }))

          其實如果是 React Class 組件,state 是會自動合并的:

          this.setState({
          ??firstName
          })

          在 Hooks 中,可以有這種用法嗎?其實是可以的,我們自己封裝一個 Hooks 就可以,比如 ahooks 的 useSetState[3],就封裝了類似的邏輯:

          const?[userInfo,?setUserInfo]?=?useSetState({
          ??firstName,
          ??lastName,
          ??school,
          ??age,
          ??address
          });

          //?自動合并
          setUserInfo({
          ??firstName
          })

          我自己在項目中大量使用了 useSetState 來代替 useState,來管理復(fù)雜類型的 state,媽媽更愛我了。

          六、總結(jié)

          作為資深的 React Hooks 用戶,我很認(rèn)可 React Hooks 帶來的提效,這也是我這幾年完全擁抱 Hooks 的原因。同時我也越來越覺得 React Hooks 難駕馭,尤其隨著 React 18 的 concurrent mode 的到來,不知道會帶來什么坑。

          最后再給大家三個建議:

          1. 可以多使用別人封裝好的高級 Hooks 來提效,比如 ahooks[4] 庫(哈哈哈
          2. 可以多看看別人封裝好的 Hooks 源碼,加深對 React Hooks 理解,比如 ahooks[5] 庫(哈哈哈
          3. 可以關(guān)注下我的公眾號,我會經(jīng)常發(fā)布一些我自己寫的技術(shù)文章,以及轉(zhuǎn)發(fā)一些我認(rèn)為比較好的文章,愛你喲(づ ̄3 ̄)づ╭?~

          參考資料

          [1]

          ahooks: https://github.com/alibaba/hooks

          [2]

          eslint-plugin-react-hooks: https://www.npmjs.com/package/eslint-plugin-react-hooks#installation

          [3]

          useSetState: https://ahooks.js.org/zh-CN/hooks/use-set-state

          [4]

          ahooks: https://github.com/alibaba/hooks

          [5]

          ahooks: https://github.com/alibaba/hooks

          瀏覽 78
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美成人性爱可下载视频 | 黄在线免费观看视频 | 影音先锋亚洲天堂 | 一级黄色录像片 | 亚洲色图图片区 |