<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>

          【實戰(zhàn)總結篇】寫React Hooks前必讀

          共 6765字,需瀏覽 14分鐘

           ·

          2021-01-28 22:32

          最近團隊內有同學,由于寫react hooks引發(fā)了一些bug,甚至有1例是線上問題。團隊內也因此發(fā)起了一些爭執(zhí),到底要不要寫hooks?到底要不要加lint?到底要不要加autofix?爭論下來結論如下:

          1. 寫還是要寫的;
          2. 寫hooks前一定要先學習hooks;
          3. 團隊再出一篇必讀文檔,必須要求每位同學,先讀再寫。

          因此便有了此文。

          本文主要講兩大點:

          1. 寫hooks前的硬性要求;
          2. 寫hooks常見的幾個注意點。

          硬性要求

          1. 必須完整閱讀一次React Hooks官方文檔

          英文文檔:https://reactjs.org/docs/hooks-intro.html
          中文文檔:https://zh-hans.reactjs.org/docs/hooks-intro.html
          其中重點必看hooks: useState、useReducer、useEffect、useCallback、useMemo

          另外推薦閱讀:

          1. Dan的《useEffect完全指南》:

          https://link.zhihu.com/?target=https%3A//overreacted.io/zh-hans/a-complete-guide-to-useeffect/

          1. 衍良同學的《React Hooks完全上手指南》

          https://zhuanlan.zhihu.com/p/92211533

          2. 工程必須引入lint插件,并開啟相應規(guī)則

          lint插件:https://www.npmjs.com/package/eslint-plugin-react-hooks
          必開規(guī)則:

          {
          ??"plugins":?["react-hooks"],
          ??"rules":?{
          ????"react-hooks/rules-of-hooks":?"error",
          ????"react-hooks/exhaustive-deps":?"warn"
          ??}
          }

          其中,?react-hooks/exhaustive-deps?至少warn,也可以是error。建議全新的工程直接配"error",歷史工程配"warn"。

          切記,本條是硬性條件。

          如果你的工程,當前沒開啟hooks lint rule,請不要編寫任何hooks代碼。如果你CR代碼時,發(fā)現對方前端工程,沒有開啟相應規(guī)則,并且提交了hooks代碼,請不要合并。該要求適應于任何一個React前端工程。

          這兩條規(guī)則會避免我們踩坑。雖然對于hooks新手,這個過程可能會比較“痛苦”。不過,如果你覺得這兩個規(guī)則對你編寫代碼造成了困擾,那說明你還未完全掌握hooks。

          如果對于某些場景,確實不需要「exhaustive-deps」,可在代碼處加:
          // eslint-disable-next-line react-hooks/exhaustive-deps

          切記只能禁本處代碼,不能偷懶把整個文件都禁了。

          3. 如若有發(fā)現hooks相關lint導致的warning,不要全局autofix

          除了hooks外,正常的lint基本不會改變代碼邏輯,只是調整編寫規(guī)范。但是hooks的lint規(guī)則不同,exhaustive-deps?的變化會導致代碼邏輯發(fā)生變化,這極容易引發(fā)線上問題,所以對于hooks的waning,請不要做全局autofix操作。除非保證每處邏輯都做到了充分回歸。

          另外公司內部有個小姐姐補充道:eslint-plugin-react-hooks 從2.4.0版本開始,已經取消了?exhaustive-deps?的autofix。所以,請盡量升級工程的lint插件至最新版,減少出錯風險

          然后建議開啟vscode的「autofix on save」。未來無論是什么問題,能把error與warning 盡量遏制在最開始的開發(fā)階段,保證自測跟測試時就是符合規(guī)則的代碼。

          常見注意點

          依賴問題

          依賴與閉包問題是一定要開啟exhaustive-deps?的核心原因。最常見的錯誤即:mount時綁定事件,后續(xù)狀態(tài)更新出錯。

          錯誤代碼示例:(此處用addEventListener做onclick綁定,只是為了方便說明情況)

          function?ErrorDemo()?{
          ??const?[count,?setCount]?=?useState(0);
          ??const?dom?=?useRef(null);
          ??useEffect(()?=>?{
          ????dom.current.addEventListener('click',?()?=>?setCount(count?+?1));
          ??},?[]);
          ??return?<div?ref={dom}>{count}div>;
          }

          這段代碼的初始想法是:每當用戶點擊dom,count就加1。理想中的效果是一直點,一直加。但實際效果是 {count} 到「1」以后就加不上了。

          我們來梳理一下,?useEffect(fn, [])?代表只會在mount時觸發(fā)。也即是首次render時,fn執(zhí)行一次,綁定了點擊事件,點擊觸發(fā)?setCount(count + 1)?。乍一想,count還是那個count,肯定會一直加上去呀,當然現實在啪啪打臉。

          狀態(tài)變更 觸發(fā) 頁面渲染的本質是什么?本質就是?ui = fn(props, state, context)?。props、內部狀態(tài)、上下文的變更,都會導致渲染函數(此處就是ErrorDemo)的重新執(zhí)行,然后返回新的view。

          那現在問題來了,?ErrorDemo?這個函數執(zhí)行了多次,第一次函數內部的?count?跟后面幾次的?count?會有關系嗎?這么一想,感覺又應該沒有關系了。那為什么 第二次又知道 count 是1,而不是0了呢?第一次的setCount?跟后面的是同一個函數嗎?這背后涉及到hooks的一些底層原理,也關系到了為什么hooks的聲明需要聲明在函數頂部,不允許在條件語句中聲明。在這里就不多講了。

          結論是:每次?count?都是重新聲明的變量,指向一個全新的數據;每次的setCount?雖然是重新聲明的,但指向的是同一個引用。

          回到正題,我們知道了每次render,內部的count其實都是全新的一個變量。那我們綁定的點擊事件方法,也即:setCount(count + 1)?,這里的count,其實指的一直是首次render時的那個count,所以一直是0 ,因此 setCount,一直是設置count為1。

          那這個問題怎么解?

          首先,應該遵守前面的硬性要求,必須要加lint規(guī)則,并開啟autofix on save。然后就會發(fā)現,其實這個?effect?是依賴?count?的。autofix 會幫你自動補上依賴,代碼變成這樣:

          useEffect(()?=>?{
          ??dom.current.addEventListener('click',?()?=>?setCount(count?+?1));
          },?[count]);

          那這樣肯定就不對了,相當于每次count變化,都會重新綁定一次事件。所以對于事件的綁定,或者類似的場景,有幾種思路,我按我的常規(guī)處理優(yōu)先級排列:

          思路1:消除依賴
          在這個場景里,很簡單,我們主要利用?setCount?的另一個用法 functional updates。這樣寫就好了:
          () => setCount(prevCount => ++prevCount)?,不用關心什么新的舊的、什么閉包,省心省事。

          思路2:重新綁定事件
          那如果我們這個事件就是要消費這個count怎么辦?比如這樣:

          dom.current.addEventListener('click',?()?=>?{
          ??console.log(count);
          ??setCount(prevCount?=>?++prevCount);
          });

          我們不必執(zhí)著于一定只在mount時執(zhí)行一次。也可以每次重新render前移除事件,render后綁定事件即可。這里利用useEffect的特性,具體可以自己看文檔:

          useEffect(()?=>?{
          ??const?$dom?=?dom.current;
          ??const?event?=?()?=>?{
          ????console.log(count);
          ????setCount(prev?=>?++prev);
          ??};
          ??$dom.addEventListener('click',?event);
          ??return?()?=>?$dom.removeEventListener('click',?event);
          },?[count]);

          思路3:如果嫌這樣開銷大,或者編寫麻煩,也可以用?useRef
          其實用?useRef?也挺麻煩的,我個人不太喜歡這樣操作,但也能解決問題,代碼如下:

          const?[count,?setCount]?=?useState(0);
          const?countRef?=?useRef(count);
          useEffect(()?=>?{
          ??dom.current.addEventListener('click',?()?=>?{
          ????console.log(countRef.current);
          ????setCount(prevCount?=>?{
          ??????const?newCount?=?++prevCount;
          ??????countRef.current?=?newCount;
          ??????return?newCount;
          ????});
          ??});
          },?[]);

          useCallback與useMemo

          這兩個api,其實概念上還是很好理解的,一個是「緩存函數」, 一個是緩存「函數的返回值」。但我們經常會懶得用,甚至有的時候會用錯。

          從上面依賴問題我們其實可以知道,hooks對「有沒有變化」這個點其實很敏感。如果一個effect內部使用了某數據或者方法。若我們依賴項不加上它,那很容易由于閉包問題,導致數據或方法,都不是我們理想中的那個它。如果我們加上它,很可能又會由于他們的變動,導致effect瘋狂的執(zhí)行。真實開發(fā)的話,大家應該會經常遇到這種問題。

          所以,在此建議:

          1. 在組件內部,那些會成為其他useEffect依賴項的方法,建議用?useCallback?包裹,或者直接編寫在引用它的useEffect中。
          2. 己所不欲勿施于人,如果你的function會作為props傳遞給子組件,請一定要使用?useCallback?包裹,對于子組件來說,如果每次render都會導致你傳遞的函數發(fā)生變化,可能會對它造成非常大的困擾。同時也不利于react做渲染優(yōu)化。

          不過還有一種場景,大家很容易忽視,而且還很容易將useCallback與useMemo混淆,典型場景就是:節(jié)流防抖。

          舉個例子:

          function?BadDemo()?{
          ??const?[count,?setCount]?=?useState(1);
          ??const?handleClick?=?debounce(()?=>?{
          ????setCount(c?=>?++c);
          ??},?1000);
          ??return?<div?onClick={handleClick}>{count}div>;
          }

          我們希望防止用戶連續(xù)點擊觸發(fā)多次變更,加了防抖,停止點擊1秒后才觸發(fā)?count + 1?,這個組件在理想邏輯下是OK的。但現實是骨感的,我們的頁面組件非常多,這個?BadDemo?可能由于父級什么操作就重新render了。現在假使我們頁面每500毫秒會重新render一次,那么就是這樣:

          function?BadDemo()?{
          ??const?[count,?setCount]?=?useState(1);
          ??const?[,?setRerender]?=?useState(false);
          ??const?handleClick?=?debounce(()?=>?{
          ????setCount(c?=>?++c);
          ??},?1000);
          ??useEffect(()?=>?{
          ????//?每500ms,組件重新render
          ????window.setInterval(()?=>?{
          ??????setRerender(r?=>?!r);
          ????},?500);
          ??},?[]);
          ??return?<div?onClick={handleClick}>{count}div>;
          }

          每次render導致handleClick其實是不同的函數,那么這個防抖自然而然就失效了。這樣的情況對于一些防重點要求特別高的場景,是有著較大的線上風險的。

          那怎么辦呢?自然是想加上?useCallback?:

          const?handleClick?=?useCallback(debounce(()?=>?{
          ??setCount(c?=>?++c);
          },?1000),?[]);

          現在我們發(fā)現效果滿足我們期望了,但這背后還藏著一個驚天大坑。
          假如說,這個防抖的函數有一些依賴呢?比如?setCount(c => ++c);?變成了?setCount(count + 1)?。那這個函數就依賴了?count?。代碼就變成了這樣:

          const?handleClick?=?useCallback(
          ??debounce(()?=>?{
          ????setCount(count?+?1);
          ??},?1000),
          ??[]
          );

          大家會發(fā)現,你的lint規(guī)則,竟然不會要求你把 count 作為依賴項,填充到deps數組中去。這進而導致了最初的那個問題,只有第一次點擊會count++。這是為什么呢?

          因為傳入useCallback的是一段執(zhí)行語句,而不是一個函數聲明。只是說它執(zhí)行以后返回的新函數,我們將其作為了?useCallback?函數的入參,而這個新函數具體是個啥,其實lint規(guī)則也不知道。

          更合理的姿勢應該是使用?useMemo?:

          const?handleClick?=?useMemo(
          ??()?=>?debounce(()?=>?{
          ????setCount(count?+?1);
          ??},?1000),
          ??[count]
          );

          這樣保證每當?count?發(fā)生變化時,會返回一個新的加了防抖功能的新函數。

          總而言之,對于使用高階函數的場景,建議一律使用 useMemo

          有些網友提供了寶貴的反饋,我繼續(xù)補充:剛使用useMemo,依舊存在一些問題。

          問題1useMemo「將來」并不「穩(wěn)定」

          react的官方文檔中提到:

          你可以把?useMemo?作為性能優(yōu)化的手段,但不要把它當成語義上的保證。?將來,React 可能會選擇“遺忘”以前的一些 memoized 值,并在下次渲染時重新計算它們,比如為離屏組件釋放內存。先編寫在沒有?useMemo?的情況下也可以執(zhí)行的代碼 —— 之后再在你的代碼中添加?useMemo,以達到優(yōu)化性能的目的。

          也就是說,在將來的某種特殊情況下,這個防抖函數依舊會失效。當然,這種情況是發(fā)生在「將來」,且相對比較極端,出現概率較小,即使出現,也不會“短時間內連續(xù)”出現。所以對于不是 「前端防不住抖就要完蛋」的場景,風險相對較小。

          問題2useMemo并不能一勞永逸解決所有高階函數場景

          在示例的場景中,防抖的邏輯是:「連續(xù)點擊后1秒,真正執(zhí)行邏輯,在這過程中的重復點擊失效」。而如果業(yè)務邏輯改成了「點擊后立即發(fā)生狀態(tài)變更,再之后的1秒內重復點擊無效」,那么我們的代碼可能就變成了。

          const?handleClick?=?useMemo(?()?=>?throttle(()?=>?{?setCount(count?+?1);?},?1000),?[count]?);

          然后發(fā)現又失效了。原因是點擊以后,count立即發(fā)生了變化,然后handleClick又重復生成了新函數,這個節(jié)流就失效了。

          所以這種場景,思路又變回了前面提到的,「消除依賴」 或 「使用ref」。當然啦,也可以選擇自己手動實現一個?debounce?或?throttle。我建議可以直接使用社區(qū)的庫,比如react-use,或者參考他們的實現自己寫兩個實現。

          其他的注意點,后面想到了再持續(xù)補充,或者歡迎回復~

          來源:相學長?

          https://zhuanlan.zhihu.com/p/113216415


          • 回復資料包領取我整理的進階資料包
          • 回復加群,加入前端進階群
          • console.log("點贊===再看===快樂")
          • Bug離我更遠了,快樂離我更近了

          瀏覽 75
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <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>
                  99精品一区二区三区 | 嫩逼网 | 操逼的网站 | 在线超碰99 | 日本无码人妻在线 |