你真的會用react hooks?看看eslint警告吧!(如何發請求、提升程式碼效能等問題)

Mr.聶發表於2021-12-30

前言

  看過幾個react hooks 的專案,控制檯上幾百條警告,大多是語法不規範,react hooks 使用有風險,也有專案直接沒開eslint。當然,這些專案肯定跑起來了,因為react自身或者其他的包,在編譯的時候彌補了一些缺陷,還有一些是不規範的警告,或者還沒執行到報錯的程式碼。

  在這,我想分享並解析一些react開發過程中,一些很常見的需求,以及正確的用法,至少也得做到控制檯沒有任何警告才行。當然,如果大家有更好的方式,也請留言。

  接下來我會把這些問題做個彙總,請看目錄。然後以我會以最常見的表格增刪改查介面舉例,配合程式碼做個講解。

  每個問題都是我初學react hooks的時候,一步步踩過的坑,錯誤案例肯定沒程式碼了,正確的自然有,原始碼在我的GitHub上   點這裡,看原始碼

   這篇部落格我會一直更新,想到啥寫啥,當做一個使用記錄本用。 

 

目錄

    1、useState如何合理的宣告變數

  2、元件載入後該如何傳送 http 請求(useEffect)

  3、如何獲取dom,並繫結監聽事件(useRef)

  4、useEffect中用到了navigate、dispatch 或者其他庫變數,導致多次執行怎麼處理

  5、useMemo 什麼時候用?怎麼用?

1、useState如何合理的宣告變數(這是個模糊的問題,沒有所謂的唯一答案,所以我想通過一個案例分析過程,幫助你找到最科學的答案)

  在class時代,一個元件的所有state都在一個物件中。然而函式元件允許你分離宣告,那現在我們是參考以前僅宣告一個大state變數?還是每個變數都單獨宣告呢?

  顯然,都不對。應該是一個折中的狀態。具體如何找這個邊界,我們先看下面的圖片,通過一個例子讓你理解。

     看圖,這是一個很簡單的場景,當點選查詢,請求資料,修改頁碼也要請求資料。以前我們通常會宣告2個屬性 search 代表查詢條件,pagination 代表頁碼,接下來按照這個思路試一下。

  a、第一次嘗試(失敗了)

  看程式碼,關於為什麼用 useEffect 監聽變數請求資料,我會在 目錄第二條http請求 有詳細說明,我們先看這個例子。

  首先,我們先看一下 useState 的用法。  const [變數,update更新函式] = useState(預設值);   這是一個標準的 useState 的用法。然後我們組織下程式碼,如下所示,執行一下。功能實現了,看起來沒問題。但是看一下控制檯的列印,點選搜尋後,useEffect執行了2次,發了兩次請求,這顯然不符合預期。原因是useState的第二個結果 update 函式是非同步執行的,看控制檯會發現,pagination變化了,search還沒變化。所以第一次請求只有頁碼,第二次請求才是正確的。顯然這樣寫有問題。以前class元件,我們會在setState的會調中直接請求資料。但是函式元件不行,第一,我並沒有發現update函式有回撥。第二,多個update,總不能先更新search,回撥裡面再更新pagination,回撥裡面再請求資料。

let [search, setSearch] = useState({}),
     [pagination, setPagination] = useState({current: 1, pageSize: 3});

// 搜尋
  const handleSearch = (d) => {
    setPagination({...pagination, current: 1});
    setSearch(d);
  };

useEffect(() => {
    const getData = () => {// 這是請求資料的函式};
    console.log(search)
    console.log(pagination)
    getData();
  }, [search, pagination]);

  b、第二次嘗試(成功了,沒有任何問題)

  分開宣告失敗了,那我們先合併成一個變數,試一下有沒有問題。程式碼看下方。執行一下,發現沒問題,控制檯只列印一次,請求也只執行一次,非常nice。

 結論:

  現在我們捋一下這個問題,對於表格來說搜尋條件和分頁都是查詢條件,配合使用,而且還存在同時修改的情況。除此之外這兩個變數還分別需要給查詢條件元件和表格頁碼屬性使用。所以資料結構是不能改變的,但是職能對錶格來說是一樣的。所以這樣的變數就應該合到一起宣告。分開反而造成很多麻煩。至此,我們明白,至少他倆是需要合併在一起宣告的。

  那麼有人會問,表格查詢除了這兩個變數,還會有 loading載入狀態,表格資料,表頭、資料總條數等等資料,要不要合併。還是那句話,分析它的功能和使用場景。我們做個變數變化的簡單對比:

  請求資料之前:loading、查詢條件、頁碼  可能變化

  請求資料之後:loading、表格資料、資料總數total  可能變化

  很顯然,根據變化的時機就可以分為3種,如果強行把資料都湊在一起,一方面useEffect無法精細監聽,另一方便,修改一個資料,需要結構所有資料。最合適的方案就是分3次useState宣告。以小觀大,其他變數模組啊都是這樣去分析的。以前class元件無腦定義,現在需要仔細分析每個變數。

 

let [search, setSearch] = useState({data: {}, pagination: {current: 1, pageSize: 3}});

// 搜尋
  const handleSearch = (d) => {
    setSearch({
      data: {...d},
      pagination: {...search.pagination, current: 1}
    });
  };

useEffect(() => {
    const getData = () => {查詢資料};
    console.log('---------useEffect----2-----')
    console.log(search)
    getData();
  }, [search]);

 

 

2、元件載入後該如何傳送 http 請求

  相信很多人被react文件給坑過,看到文件 useEffect 的第二個引數傳空陣列,就只會觸發一次,可以在這裡傳送http請求獲取資料。哈哈哈!我一開始也被坑了,下邊的程式碼和圖片,就是錯誤的寫法和控制檯警告。但是,react原始碼其實是可以避免這種風險的,只是eslint不知道,這就導致寫法不好,但是功能正常。所以後來react FAQ特地解釋了一下,看下圖。

// 查詢表格資料
const getData = () => {};
useEffect(() => {
    getData();
  }, []);

 

 

 

 

 

 

   好吧,錯誤用法已經看過了,那究竟要怎麼寫才算正確的呢?現在我們已經可以確定,可以利用useEffect實現http請求,當然別的hooks也能實現。那麼在講解正確做法之前,得先看一段程式碼,你需要先理解 useEffect 到底是怎麼執行的,都在什麼時間執行。ps:接下來很關鍵,能讓你徹底理解useEffect,特別是我用紅色字型標記的3個地方。

  看程式碼,重新整理介面,控制檯的列印順序應該是 1 2 3。重新整理介面之後,函式元件執行,1列印了,然後執行return,渲染頁面,再然後依次執行useEffect,所以 2  3 依次列印。所以useEffect會在dom渲染之後執行,而且是初始化的時候就會執行一次,這個時機就是 componentDidMount。然後,如果現在修改一下變數 c 的值,再看控制檯,輸出為 1 2,而且1的地方列印c的值是最新的值,也就說useEffect只會在監聽的變數變化的時候,等dom渲染完了,再次觸發。每次變化都是這樣,這個時機就是  componentDidUpdate 。變數a 、b因為沒有變化,自然就不執行。最後,如果你有其他頁面,換到別的頁面,再看控制檯列印 2,只有2。如果你多放幾個console,你會發現useEffect沒有執行,函式元件也沒有執行,只有getData這個方法執行了。因為我寫了一個return getData; 什麼意思呢,useEffect裡面寫return,就會在元件解除安裝之前,執行你return的函式,常用於解除安裝一些監聽或者定時器等等。這個時機就是 componentWillUnmount

useEffect(() => {
   const getData = () => {
      console.log('----------2----------')
    };
    return getData;
  }, [c]);
useEffect(()
=> { console.log('--------3---------', a, b) }, [a, b]);
console.log(
'-----1------',c);

return (<span>111</span>)

  大家看文件都知道class元件那麼多生命週期,函式元件只有hooks,然後就不知道該怎麼組織程式碼邏輯了。又或者知道useEffect能實現上述3個生命週期的功能,卻不知道具體是怎麼實現的。現在大家應該都知道useEffect怎麼用了吧。接下來我們發http請求就簡單多了。

  看程式碼,因為useEffect內部是一個閉包,內部使用的變數都必須顯式的宣告依賴,要不然就會報警告,缺少xxx依賴。所以我們直接將請求表格資料的方法宣告在useEffect內部,然後呼叫,那他使用的所有的props或者state,都應該是他的依賴,需要寫到中括號裡面,否則都會報警告。首先程式碼這樣寫是沒問題的,也是官方推薦的寫法。這裡先把正統確立了,然後打假。

  如果大家百度一下,網上可能還會有2種其他做法,useCallback 和 /* eslint-disable */。我將官方的回答截圖放到下邊,大家可以好好看看。推薦的方法就不多說什麼了,useCallback 是在萬不得已,實在沒辦法的時候才會使用。而第二種就更搞笑了,直接遮蔽eslint檢測。當我搜到這種文章的時候,差點氣到岔氣,這是在掩耳盜鈴嗎。。。。(當然那種特殊需求的情況下確實需要遮蔽這個警告)至於官方說的另外2種方法,因為和請求無關,這裡我就不多說了,大家可以在函式元件外宣告一個變數,useEffect也是可以監聽的,可以玩一下。

  最後,言歸正傳,以前class元件的資料請求通常是靠回撥觸發,比如修改什麼變數直接請求資料。現在不行,比如下邊的寫法,你需要確定這個請求需要的變數,而且這些變數的變化都是需要觸發資料請求的。比如一個增刪改查的介面,表格資料獲取發生在初始化的時候、頁碼變化和點選查詢的時候。所以,我們需要確保點選查詢和修改頁碼一定改變變數,其他任何情況,不能修改變數,因為我們靠監聽變數觸發請求。

useEffect(() => {
    // 查詢表格資料
    const getData = () => {
      setLoading(true);
      const { data, pagination } = search;
      const params = {
        ...data,
        current: pagination.current,
        size: pagination.pageSize
      }
      getList(params).then(res => {
        if (res.code === 200) {
          let d = res.data;
          setDataSource(d.records);
          setTotal(d.total)
        }
        setLoading(false);
      })
      .catch(err => {
        setLoading(false);
      });
    };
    getData();
  }, [search]);

 

3、如何獲取dom,並繫結監聽事件(useRef)

  這也是我們常見的需求,獲取dom,然後動態 addEventListener 某些事件。實現這個功能我們使用useRef、useEffect兩個hooks。

  首先,你需要知道 useRef 的三個特點。 第一,他宣告的變數,將存活於元件的所有生命週期,注意是所有,元件登出,變數自動銷燬; 第二,他可以儲存任意型別變數,不僅僅是dom和普通物件; 第三,他宣告的變數,資料型別是一個物件,物件上有current屬性,賦值操作都在這個屬性上進行,而且useRef宣告的變數值變化了,不會引起函式元件的重新渲染,他只是一個儲存資料的倉庫,資料修改也是實時的。可以簡單理解為react開闢了一塊地址,專門用來儲存你宣告的變數,後續操作只是不斷往這個地址換資料而已,元件登出,地址釋放,都不需要我們額外操心。ps:我在想既然變數登出了,我真的還需要移除監聽嗎,這是我的疑問,不過我沒法去印證這個事情。。。

  上面是useRef的特點,然後我們看程式碼,節約篇幅,我刪掉了很多,原始碼在GitHub src\components\c-large-select  這個檔案。其實這個功能簡單,useRef宣告變數,然後繫結到div標籤的ref屬性上,這樣dom渲染之後 scrollEle.current 就可以拿到dom了,然後再useEffect中新增事件繫結,不懂的,看目錄第二條,我詳細介紹了useEffect的執行時機。這裡要注意一點,就是根據你的業務邏輯監聽依賴,不要頻繁的去做事件繫結。然後就是useEffect的return函式,執行刪除監聽的操作。

  useRef的用法很多,這裡僅介紹如何實現我們常用的獲取dom,繫結事件的功能。

const scrollEle = useRef();  // 滾動條dom物件

useEffect(() => {
    const handleMouse = (v) => {};
    const handleScroll = (e) => {};
    //  初始化事件
    const init = () => {
      if(list.length > rows) {
        scrollEle.current.scrollTop = 0
        scrollEle.current.addEventListener('scroll', handleScroll);
        scrollEle.current.addEventListener('mousedown', () => handleMouse(true));
        scrollEle.current.addEventListener('mouseup', () => handleMouse(false));
      }
    }
    // 解除安裝前,取消監聽
    const unInit = () => {
      if(list.length > rows && scrollEle.current) {
        scrollEle.current.removeEventListener('scroll', handleScroll, false);
        scrollEle.current.removeEventListener('mousedown', handleMouse, false);
        scrollEle.current.removeEventListener('mouseup', handleMouse, false);
      }
    };
    init();
    return unInit;
  }, [list, rows, rowHeight, listHeight]);
return (<div ref={scrollEle}></div>)

 

4、useEffect中用到了navigate、dispatch 或者其他庫變數,導致多次執行怎麼處理

  在目錄的第2條我們已經聊過useEffect執行機制,及發http請求的問題了,關於用法我不贅言。那麼有些情況下我們會在useEffect中用到navigate、dispatch 甚至是其他一些庫檔案,比如echarts等等,如果用到了,eslint提示我們需要注入依賴。這個時候又該怎麼辦。這裡有一個感覺不算特別合適辦法,用 useRef 處理。

  看下邊的刪減程式碼(原始碼檔案 src\layouts\BasicLayout.jsx),具體場景是我請求資料,校驗許可權,儲存redux,沒許可權的跳轉登陸。如果useEffect直接監聽navigate,那每次跳轉路由,navigate都會改變,導致useEffect執行,這不是我想要的。所以我麼你用useRef取一下navigate變數,useEffect中使用這個變數。因為useRef宣告的變數不會引起函式元件的變化,useEffect自然監聽不到。這樣可以變相的規避這個警告。包括其他場景也是,比如 a=b+c,但是你只想在b變化的時候重新計算,c只需要獲取他最新的值即可,那用useRef是最合適的方式。

const navigate = useNavigate();
const navigation = useRef(navigate);
useEffect(() => {
   dispatch(setUser(param))
   navigation.current.navigate('/login');
 }, [dispatch, navigation]);

 

5、useMemo 什麼時候用?怎麼用?

  useMemo他是一個輔助hook,官方建議大量使用,當然不是亂用,適可而止。既然是輔助鉤子,也就是說,不用,你的程式碼照常執行,只是可能比較費CPU,嚴重的系統就很卡頓。用了,會提升系統的執行速度和流暢度。

  那什麼時候用?第一,不用他,程式碼邏輯正常,加上他,邏輯也不會變,只是減少了渲染次數; 第二,你很確定某個地方會多執行多次,而且清楚哪些變數變化才應該重新渲染。做到這兩點就不會亂用useMemo了,官方也提示了,絕對不能利用useMemo的特性去實現自己的程式碼邏輯,有需要用其他鉤子。

  看程式碼(檔案路徑  src\layouts\BasicLayout.jsx),我們先了解一下 useMemo 的執行機制,看控制檯,輸出 1 2 3 ,思考一下,不難發現,useMemo 的執行時機很早,函式元件第一次執行的時候,他就執行了,而且執行在 return 的前面。然後,如果監聽的變數變化,他會再次執行,否則就不會在執行。

  再看程式碼,我有一個渲染路由的函式,他沒有使用任何變數,所以我認為他這輩子執行一次就夠了,不需要任何依賴再次觸發更新。所以我傳了空陣列。當然如果你用了別的依賴,放到陣列中監聽就好了。

  最後,react官方建議我們大量使用,但實際上我們不必較真,比如一個增刪改查介面,隨便一個變數變化都會重新執行函式元件,我們沒必要將所有元件都用useMemo都包裹一下。那樣程式碼可讀性也會降低很多。只是在那些元件巢狀層級比較深,比如3層、4層這種的,如果因為父元件執行會引起子元件不斷執行,就需要useMemo優化一下。

// 遞迴路由
const mapMenu = (l) => {};
const BasicLayout = () => {
  console.log('-----1------')
  // 渲染路由,減少頁面渲染次數
  const renderRoute = useMemo(() => {
    console.log('-----2---')
    mapMenu(MENU)
  }, []);
  console.log('-----3------')

return (<div >2121212</div>)
  }

 

相關文章