原創不易,但願能關注下咱們,再順手點個贊~~ |
本文首發於政採雲前端團隊博客: 看完這篇,你也能把 React Hooks 玩出花javascript
本文中出現的部分名稱映射:前端
函數式組件 => Function Componentjava
類組件 => Class Componentreact
工具函數 => Util Function數組
鉤子 => React Hook緩存
初始值 => initialValue性能優化
React v16.7.0-alpha 中第一次引入了 Hooks 的概念,在 v16.8.0 版本被正式發佈。React Hooks 在 React 中只是對 React Hook 的概念性的描述,在開發中咱們用到的實際功能都應該叫作 React hook。markdown
React Hook 是一種特殊的函數,其本質能夠是函數式組件(返回 Dom 或 Dom 及 State ),也能夠只是一個工具函數(傳入配置項返回封裝後的數據處理邏輯)。antd
React Hooks 的出現使函數式組件變得面目一新,其帶來的最大的變化在於給予了函數式組件相似於類組件生命週期的概念,擴大了函數式組件的應用範圍。閉包
目前函數式組件基本用於純展現組件,一旦函數式組件耦合有業務邏輯,就須要經過 Props 的傳遞,經過子組件觸發父組件方法的方式來實現業務邏輯的傳遞,Hooks 的出現使得函數組件也有了本身的狀態與業務邏輯,簡單邏輯在本身內部處理便可,再也不須要經過 Props 的傳遞,使簡單邏輯組件抽離更加方便,也使使用者無需關心組件內部的邏輯,只關心 Hooks 組件返回的結果便可。
在我看來,Hooks 組件的目標並非取代類組件,而是增長函數式組件的使用率,明確通用工具函數與業務工具函數的邊界,鼓勵開發者將業務通用的邏輯封裝成 React Hooks 而不是工具函數。
之因此把總結放在前面,是想讓你們在看後面的內容時有一個總體的概念去引導你們去思考 React Hooks 具體給函數式組件帶來了什麼變化。
目前官方提供的鉤子共分爲兩種,分爲基本鉤子以及拓展鉤子
基本鉤子共有:useState
、useEffect
、 useContext
額外的鉤子有:useCallback
、 useReducer
、 useMemo
、 useRef
、 useLayoutEffect
、 useImperativeHandle
、 useDebugValue
該鉤子用於建立一個新的狀態,參數爲一個固定的值或者一個有返回值的方法。鉤子執行後的結果爲一個數組,分別爲生成的狀態以及改變該狀態的方法,經過解構賦值的方法拿到對應的值與方法。
使用方法以下:
export default function HookDemo() { const [count, changeCount] = useState(0); return ( <div> {count} <button onClick={() => { changeCount(Math.ceil(Math.random() * 1000)); }}> 改變count </button> </div> ); } 複製代碼
顧名思義,執行反作用鉤子。主要用於如下兩種狀況:
useEffect
的一些特性去實現。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。
生成 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
接受一個參數,爲 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({ count: 1 }); const countRef = useRef(count); return ( <div> {count.count} <button onClick={() => { countRef.current.count = 10; }}> 改變ref </button> </div> ); } 複製代碼
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 其實和最基本的 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,其實能夠封裝的內容還有不少不少,能夠經過配置項的設置實現更豐富的封裝。
演示效果如圖:
鉤子 | 用法 | 做用 |
---|---|---|
useState | const [state, changeState] = useState(initialValue) |
用於生成狀態以及改變狀態的方法 |
useEffect | useEffect(fn, [...relativeState]) |
用於生成與狀態綁定的反作用 |
useCallback | useCallback(fn, [...relativeState]) |
用於生成與狀態綁定的回調函數 |
useMemo | useMemo(fn, [...relativeState]) |
用於生成與狀態綁定的組件/計算結果 |
useRef | const newRef = useRef(initialValue) |
用於 獲取節點實例 / 數據保存 |
從上面的表格中咱們能夠看出,在官方提供的 Hook 中,除了基本的 useState
與 useRef
外,其餘鉤子都存在第二個參數,第一個方法的執行與第二個參數相互關聯。因而咱們能夠得出一個結論,在使用了 Hook 的函數式組件中,咱們在使用反作用/引用子組件時都須要時刻注意對代碼進行性能上的優化。
我在前面的總結裏是這麼評價 React Hooks 的:
Hooks 組件的目標並非取代 class component 組件,而是增長函數式組件的使用率,明確通用工具函數與業務工具函數的邊界,鼓勵開發者將業務通用的邏輯封裝成 React Hooks 而不是工具函數。
但願看完這篇文章的你也有本身的一些見解,歡迎拍磚討論。
招人,前端,隸屬政採雲前端大團隊(ZooTeam),50 餘個小夥伴正等你加入一塊兒浪。若是你想改變一直被事折騰,但願開始能折騰事;若是你想改變一直被告誡須要多些想法,卻無從破局;若是你想改變你有能力去作成那個結果,卻不須要你;若是你想改變你想作成的事須要一個團隊去支撐,但沒你帶人的位置;若是你想改變既定的節奏,將會是「5年工做時間3年工做經驗」;若是你想改變原本悟性不錯,但老是有那一層窗戶紙的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但願參與到隨着業務騰飛的過程,親手參與一個有着深刻的業務理解、完善的技術體系、技術創造價值、影響力外溢的前端團隊的成長曆程,我以爲咱們該聊聊。任什麼時候間,等着你寫點什麼,發給 ZooTeam@cai-inc.com