看完這篇,你也能把 React Hooks 玩出花

本文首發於政採雲前端團隊博客:看完這篇,你也能把 React Hooks 玩出花前端

https://www.zoo.team/article/react-hooks




本文中出現的部分名稱映射:
函數式組件  => Function Component
類組件 => Class Component
工具函數 => Util Function
鉤子 => React Hook
初始值 => initialValue

先講概念

React v16.7.0-alpha 中第一次引入了 Hooks 的概念,在 v16.8.0 版本被正式發佈。React Hooks 在 React 中只是對 React Hook 的概念性的描述,在開發中咱們用到的實際功能都應該叫作 React hook
React Hook 是一種特殊的函數,其本質能夠是函數式組件(返回 Dom 或 Dom 及 State ),也能夠只是一個工具函數(傳入配置項返回封裝後的數據處理邏輯)。

再總結

React Hooks 的出現使函數式組件變得面目一新,其帶來的最大的變化在於給予了函數式組件相似於類組件 生命週期的概念,擴大了函數式組件的應用範圍。
目前函數式組件基本用於純展現組件,一旦函數式組件耦合有業務邏輯,就須要經過 Props 的傳遞,經過子組件觸發父組件方法的方式來實現業務邏輯的傳遞,Hooks 的出現使得函數組件也有了本身的狀態與業務邏輯,簡單邏輯在本身內部處理便可,再也不須要經過 Props 的傳遞,使簡單邏輯組件抽離更加方便,也使使用者無需關心組件內部的邏輯,只關心 Hooks 組件返回的結果便可。
在我看來,Hooks 組件的目標並非取代類組件,而是增長函數式組件的使用率,明確通用工具函數與業務工具函數的邊界, 鼓勵開發者將業務通用的邏輯封裝成 React Hooks 而不是工具函數
之因此把總結放在前面,是想讓你們在看後面的內容時有一個總體的概念去引導你們去思考 React Hooks 具體給函數式組件帶來了什麼變化。

Hooks 初識

官方提供的鉤子

目前官方提供的鉤子共分爲兩種,分爲基本鉤子以及拓展鉤子
基本鉤子共有:
useState 、useEffect 、 useContext
額外的鉤子有:

useRef 、useCallback 、useMemo 、react

useReducer 、useLayoutEffect 、web

useImperativeHandle 、useDebugValuejson

不一樣鉤子用法

useState
該鉤子用於建立一個新的狀態,參數爲一個固定的值或者一個有返回值的方法。鉤子執行後的結果爲一個數組,分別爲生成的狀態以及改變該狀態的方法,經過解構賦值的方法拿到對應的值與方法。
使用方法以下:

    
export  default  function HookDemo({
   const [count, changeCount] = useState( 0);

   return (
     <div>
      {count}
      <button onClick={() => { changeCount(Math.ceil(Math.random() * 1000)); }}>
        改變count
      </button>
    </div>

  );
}

useEffect
顧名思義,執行反作用鉤子。主要用於如下兩種狀況:
  1. 函數式組件中不存在傳統類組件生命週期的概念,若是咱們須要在一些特定的生命週期或者值變化後作一些操做的話,必須藉助   useEffect  的一些特性去實現。
  2. useState  產生的 changeState 方法並無提供相似於   setState  的第二個參數同樣的功能,所以若是須要在 State 改變後執行一些方法,必須經過   useEffect  實現。
該鉤子接受兩個參數,第一個參數爲反作用須要執行的回調,生成的回調方法能夠返回一個函數( 將在組件卸載時運行);第二個爲該反作用監聽的狀態數組,當對應狀態發生變更時會執行反作用, 若是第二個參數爲空,那麼在每個 State 變化時都會執行該反作用。
使用方法以下:

    
const [count, changeCount] = useState( 0);

// 將在count變化時打印最新的count數據
useEffect( () => {
  message.info( `count發生變更,最新值爲${count}`);
}, [count])

在上面代碼中咱們實現了在   useEffect  這個鉤子適用狀況中的第二種狀況,那麼如何使用該鉤子才能實現相似於類組件中生命週期的功能呢?既然第一個參數是反作用執行的回調,那麼實現咱們所要功能的重點就應該在第二個參數上了。
componentDidMount  &&   componentWillUnmout:這兩個生命週期只在頁面掛載/卸載後執行一次。前面講過,全部的反作用在組件掛載完成後會執行一次 ,若是反作用存在返回函數,那麼返回的函數將在卸載時運行。藉助這樣的特性,咱們要作的就是讓目標反作用在初始化執行一次後不再會被調用,因而只要讓與該反作用相關聯的狀態爲空,無論其餘狀態如何變更,該反作用都不會再次執行,即實現了   componentDidMount  與   componentWillUnmout

    
import React, { useState, useEffect }  from  'react';
import { message }  from  'antd';

function Child({ visible }{
  useEffect( () => {
    message.info( '我只在頁面掛載時打印');
     return  () => {
      message.info( '我只在頁面卸載時打印');
    };
  }, []);

   return visible ?  'true' :  'false';
}

export  default  function HookDemo({
   const [visible, changeVisible] = useState( true);


   return (
     <div>
      {
        visible && <Child visible={visible} />
      }
      <button onClick={() => { changeVisible(!visible); }}>
        改變visible
      </button>
    </div>
  );
}

componentDidUpdate:該生命週期在每次頁面更新後都會被調用。那麼按照以前的邏輯,就應該把全部的狀態所有放在第二個狀態中,可是這樣的話新增/刪除一個狀態都須要改變第二參數。其實, 若是第二個參數爲空,那麼在每個 State 變化時都會執行該反作用,那麼若是要實現   componentDidUpdate  就很是簡單了。

    
useEffect( () => {
   // ...反作用邏輯
})  // 注意上面說的關聯狀態爲空不是說不傳遞第二個參數,而是第二個參數應該爲一個空數組


在類組件中,若是在   componentDidMount  中屢次調用   setState  設置一個值( 固然不推薦這樣作),並在成功的回調中打印該值,那麼最後的結果極可能會打印不少個相同的最後一次設置的值。是由於類的 setState  是一個類異步的結果,他們會將全部變更的內容進行收集而後在合適的時間去統一賦值。
而在   useEffect  中,全部的變量的值都會保留在該反作用執行的時刻,相似於 for 循環中的 let 或者 閉包,全部的變量都維持在反作用執行時的狀態,也有人稱這個爲 Capture Value。
useCallback
生成 Callback 的鉤子。用於對不一樣   useEffect  中存在的相同邏輯的封裝,減小代碼冗餘,配合   useEffect   使用。
該鉤子先看例子會比較好理解一下:

    
const [count1, changeCount1] = useState( 0);
const [count2, changeCount2] = useState( 10);

const calculateCount = useCallback( () => {
   if (count1 && count2) {
     return count1 * count2;
  }
   return count1 + count2;
}, [count1, count2])

useEffect( () => {
     const result = calculateCount(count, count2);
    message.info( `執行反作用,最新值爲${result}`);
}, [calculateCount])
在上面的例子中咱們經過   useCallback  的使用生成了一個回調, useCallback  的使用方法和   useEffect  一致,第一個參數爲生成的回調方法,第二個參數爲該方法關聯的狀態, 任一狀態發生變更都會從新生成新的回調
經過上面代碼的使用,咱們將 count1 / count2 的值與一個叫作 calculateCount 的方法關聯了起來,若是組件的反作用中用到計算 count1 和 count2 的值的地方,直接調用該方法便可。
其中和直接使用   useEffect  不一樣的地方在於使用   useCallback  生成計算的回調後,在使用該回調的反作用中, 第二個參數應該是生成的回調。其實這個問題是很好理解的,咱們使用   useCallback  生成了一個與 count1 / count2 相關聯的回調方法,那麼當關聯的狀態發生變化時會從新生成新的回調,反作用監聽到了回調的變化就會去從新執行反作用, 此時  useCallback  和 useEffect 是按順序執行的, 這樣就實現了反作用邏輯的抽離。
useRef
useRef  接受一個參數,爲 ref 的初始值。相似於類組件中的   createRef  方法 ,該鉤子會返回一個對象,對象中的 current 字段爲咱們 指向的實例 / 保存的變量,能夠實現得到目標節點實例或保存狀態的功能。
useRef 保存的變量不會隨着每次數據的變化從新生成,而是保持在咱們最後一次賦值時的狀態,依靠這種特性,再配合 useCabllback  和  useEffect 咱們能夠實現 preProps/preState 的功能。

    
const [count, changeCount] = useState( 0);
const [count1, changeCount1] = useState( 0);
// 建立初始值爲空對象的prestate
const preState = useRef({});
// 依賴preState進行判斷時能夠先判斷,最後保存最新的state數據
useEffect( () => {
   const { ... } = preState.current;
   if ( // 條件判斷) {
     // 邏輯
  }
   // 保存最新的state
  preState.current = {
    count,
    count1,
  }
});

另外,當咱們將使用   useState 建立的狀態賦值給   useRef  用做初始化時, 手動更改 Ref 的值並不會引發關聯狀態的變更。從該現象來看, useRef 彷佛只是在內存空間中開闢了一個堆空間將初始化的值存儲起來,該值與初始化的值存儲在不一樣的內存空間,修改 Ref 的值不會引發視圖的變化。

    
export  default  function HookDemo({
   const [count] = useState({  count1 });

   const countRef = useRef(count);

   return (
     <div>
      {count.count}
      <button onClick={() => { countRef.current.count = 10; }}>
        改變ref
      </button>
    </div>

  );
}
useRef正常
useMemo
Memo 爲 Memory 簡寫, useMemo 即便用記憶的內容。該鉤子主要用於作性能的優化。
前面咱們說過了當狀態發生變化時,沒有設置關聯狀態的   useEffect  會所有執行。一樣的, 經過計算出來的值或者引入的組件也會從新計算/掛載一遍,即便與其關聯的狀態沒有發生任何變化
在類組件中咱們有 
  shouldComponetUpdate  以及   React.memo  
幫助咱們去作性能優化,若是在函數組件中沒有相似的功能顯示是違背了官方的初衷的,因而就有了   useMemo  這個鉤子。
在業務中,咱們能夠用   useMemo  來處理計算結果的緩存或引入組件的防止重複掛載優化。其接受兩個參數,第一個參數爲一個 Getter 方法, 返回值爲要緩存的數據或組件,第二個參數爲該返回值相關聯的狀態,當其中任何一個狀態發生變化時就會從新調用 Getter 方法生成新的返回值。
具體代碼以下:

    
import React, { useState, useMemo }  from  'react';
import { message }  from  'antd';

export  default  function HookDemo({
   const [count1, changeCount1] = useState( 0);
   const [count2, changeCount2] = useState( 10);

   const calculateCount = useMemo( () => {
    message.info( '從新生成計算結果');
     return count1 *  10;
  }, [count1]);
   return (
     <div>
      {calculateCount}
      <button onClick={() => { changeCount1(count1 + 1); }}>改變count1</button>
      <button onClick={() => { changeCount2(count2 + 1); }}>改變count2</button>
    </div>

  );
}

初次接受   useMemo  時可能咱們會以爲該鉤子只是用來作計算結果的緩存,返回值只能是一個數字或字符串。其實   useMemo  並不關心咱們的返回值類型是什麼,它只是在關聯狀態發生變更時從新調用咱們傳遞的 Getter 方法 生成新的返回值,也就是說   useMemo  生成的是 Getter 方法與依賴數組的關聯關係。所以,若是咱們將函數的返回值替換爲一個組件,那麼就能夠 實現對組件掛載/從新掛載的性能優化
代碼以下:

    
import React, { useState, useMemo }  from  'react';
import { message }  from  'antd';

function Child({ count }{
   return  <p>當前傳遞的count爲:{count}</p>;
}

export  default  function HookDemo({
   const [count1, changeCount1] = useState( 0);
   const [count2, changeCount2] = useState( 10);

   const child = useMemo( () => {
    message.info( '從新生成Child組件');
     return  <Child count={count1} />;
  }, [count1]);
  return (
    <div>
      {child}
      <button onClick={() => { changeCount1(count1 + 1); }}>改變count1</button>
      <button onClick={() => { changeCount2(count2 + 1); }}>改變count2</button>
    </div>
  );
}

其餘鉤子
今天主要講了組件中經常使用的幾個鉤子,剩下的未講解的鉤子中,如
useLayoutEffect  useImperativeHandle  useDebugValue ,
其功能都比較簡單就不在此贅述。
還有一個比較重要的鉤子   useContext,是 createContext 功能在函數式組件中的實現。經過該功能能夠實現不少強大的功能,能夠是說官方的 Redux,不少人對此應該有很多的瞭解。該鉤子內容太多,後續單獨使用一個章節進行描述。

編寫本身的鉤子

其實從上面講解的內容來看,鉤子並非什麼高深莫測的東西,它只是對咱們經常使用邏輯的一些封裝,接下來就會經過具體的代碼來教你們寫一個本身的鉤子。

最基本的鉤子

最基本的鉤子也就是返回包含了更多邏輯的 State 以及改變 State 方法的鉤子。拿計數器來講,其最基本的就是返回當前的數字以及減小/增長/重置等功能,明確完功能後能夠開始動手作了。

    
import React, { useState }  from  'react';

// 編寫咱們本身的hook,名字以use開頭
function useCounter(initialValue{
   // 接受初始化的值生成state
   const [count, changeCount] = useState(initialValue);
   // 聲明減小的方法
   const decrease =  () => {
    changeCount(count -  1);
  }
   // 聲明增長的方法
   const increase =  () => {
    changeCount(count +  1);
  }
   // 聲明重置計數器方法
   const resetCounter =  () => {
    changeCount( 0);
  }
   // 將count數字與方法返回回去
   return [count, { decrease, increase, resetCounter }]
}

export  default  function myHooksView({
   // 在函數組件中使用咱們本身編寫的hook生成一個計數器,並拿到全部操做方法的對象
   const [count, controlCount] = useCounter( 10);
   return (
       <div>
        當前數量:{count}
            <button onClick={controlCount.decrease}>減小</button>
            <button onClick={controlCount.increase}>增長</button>
            <button onClick={controlCount.resetCounter}>重置</button>
    </div>

  )
}
在上面的例子中,咱們將在   useCounter  這個鉤子中建立了一個關聯了   initialValue  的狀態,並建立減小/增長/重置的方法,最終將其經過   return  返回出去。這樣在其餘組件須要用到該功能的地方,經過調用該方法拿到其返回值,便可實現對   useCounter  組件封裝邏輯的複用。
演示效果如圖:

返回 DOM 的鉤子

返回 DOM 其實和最基本的 Hook 邏輯是相同的,只是在返回的數據內容上有一些差別,具體仍是看代碼,以一個 Modal 框爲例。

    
import React, { useState }  from  'react';
import { Modal }  from  'antd';

function useModal({
   const [visible, changeVisible] = useState( false);

   const toggleModalVisible =  () => {
    changeVisible(!visible);
  };

   return [(
     <Modal
      visible={visible}
      onOk={toggleModalVisible}
      onCancel={toggleModalVisible}
    >

      彈窗內容
      </Modal>

  ), toggleModalVisible];
}

export  default  function HookDemo({
   const [modal, toggleModal] = useModal();
   return (
     <div>
      {modal}
      <button onClick={toggleModal}>打開彈窗</button>
    </div>

  );
}

這樣咱們就實現了一個返回了彈窗內容以及改變彈窗顯示狀態的 Hook,其實能夠封裝的內容還有不少不少,能夠經過配置項的設置實現更豐富的封裝。
演示效果如圖:

鉤子/最終總結

鉤子總結



從上面的表格中咱們能夠看出,在官方提供的 Hook 中,除了基本的  useState  與  useRef  外,其餘鉤子都存在第二個參數,第一個方法的執行與第二個參數相互關聯。因而咱們能夠得出一個結論,在使用了 Hook 的函數式組件中,咱們在使用 反作用/引用子組件時都須要時刻注意對代碼進行性能上的優化

最終總結

我在前面的總結裏是這麼評價 React Hooks 的:
Hooks 組件的目標並非取代 class component 組件,而是增長函數式組件的使用率,明確通用工具函數與業務工具函數的邊界, 鼓勵開發者將業務通用的邏輯封裝成 React Hooks 而不是工具函數
但願看完這篇文章的你也有本身的一些見解,歡迎拍磚討論。

招賢納士

招人,前端,隸屬政採雲前端大團隊(ZooTeam),50 餘個小夥伴正等你加入一塊兒浪~ 若是你想改變一直被事折騰,但願開始能折騰事;若是你想改變一直被告誡須要多些想法,卻無從破局;若是你想改變你有能力去作成那個結果,卻不須要你;若是你想改變你想作成的事須要一個團隊去支撐,但沒你帶人的位置;若是你想改變既定的節奏,將會是「5年工做時間3年工做經驗」;若是你想改變原本悟性不錯,但老是有那一層窗戶紙的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但願參與到隨着業務騰飛的過程,親手參與一個有着深刻的業務理解、完善的技術體系、技術創造價值、影響力外溢的前端團隊的成長曆程,我以爲咱們該聊聊。任什麼時候間,等着你寫點什麼,發給  ZooTeam@cai-inc.com


本文分享自微信公衆號 - 政採雲前端團隊(Zoo-Team)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。數組

相關文章
相關標籤/搜索