1. 前言
React Hooks 是 React 16.8 引入的新特性,容許咱們在不使用 Class 的前提下使用 state 和其餘特性。React Hooks 要解決的問題是狀態共享,是繼 render-props 和 higher-order components 以後的第三種狀態邏輯複用方案,不會產生 JSX 嵌套地獄問題。vue
2. 狀態邏輯複用
通常來講,組件是 UI 和邏輯,可是邏輯這一層面卻很難複用。對用戶而言,組件就像一個黑盒,咱們應該拿來即用。但當組件的樣式或者結構不知足需求的時候,咱們只能去從新實現這個組件。node
在咱們開發 React 應用的時候,常常會遇到相似下面這種場景,你可能會有兩個疑問:react
-
Loading 是否能夠複用? -
Loading 該怎麼複用?
這幾個例子都指向了同一個問題,那就是如何實現組件的邏輯複用?web
2.1 render props
將函數做爲 props 傳給父組件,父組件中的狀態共享,經過參數傳給函數,實現渲染,這就是 render props
。使用 render prop
的庫有 React Router、Downshift 以及 Formik。如下面這個 Toggle 組件爲例子,咱們通常能夠這樣用:api
能夠看到,控制 Modal 組件是否展現的狀態被提取到了 Toggle 組件中,這個 Toggle 組件還能夠拿來屢次複用到其餘組件裏面。那麼這個 Toggle 是怎麼實現的呢?看到實現後你就會理解 render props
的原理
數組
關於 render props
的更多內容能夠參考 React 中文網的相關章節:Render Props
瀏覽器
2.2 higher-order components
higher-order components 通常簡稱 hoc,中文翻譯爲高階組件。從名字上就能夠看出來,高階組件確定和高階函數有什麼千絲萬縷的關係。高階組件的本質是一個高階函數,它接收一個組件,返回一個新的組件。在這個新的組件中的狀態共享,經過 props 傳給原來的組件。以剛剛那個 Toggle 組件爲例子,高階組件一樣能夠被屢次複用,經常能夠配合裝飾器一塊兒使用。緩存
高階組件的實現和 render props
也不太同樣,主要是一個高階函數。性能優化
2.3 render props 和高階組件的弊端
無論是 render props 仍是高階組件,他們要作的都是實現狀態邏輯的複用,可這倆是完美的解決方案嗎?考慮一下,若是咱們依賴了多個須要複用的狀態邏輯的時候,該怎麼寫呢?以 render props 爲例:微信
看看這個代碼,你有沒有一種似曾相識的感受?這一天,咱們終於想起被「回調地獄」支配的恐懼。不得再也不次祭出這張圖了。
一樣地,高階組件也會有這個問題,但因爲裝飾器的簡潔性,沒有 render props 看起來那麼可怕。除此以外,他們倆還有另外一個問題,那就是組件嵌套過深以後,會給調試帶來很大的麻煩。這個是 render props 中組件嵌套在 React 開發者工具中的表現。
對於高階組件來講,若是你沒有對組件手動設置 name/displayName
,就會遇到更嚴重的問題,那就是一個個匿名組件嵌套。畢竟上面 render props 的嵌套至少能知道組件名。
社區裏面也已經有不少解決 render props 嵌套的方案,其中 Epitath 提供了一種以 generator 的方法來解決嵌套問題,利用 generator 實現了僞同步代碼。
更多細節能夠參考黃子毅的這篇文章:精讀《Epitath 源碼 - renderProps 新用法》
2.4 React Hooks
React Hooks 則能夠完美解決上面的嵌套問題,它擁有下面這幾個特性。
多個狀態不會產生嵌套,寫法仍是平鋪的
容許函數組件使用 state 和部分生命週期
更容易將組件的 UI 與狀態分離
上面是一個結合了 useState 和 useEffect 兩個 hook 方法的例子,主要是在 resize 事件觸發時獲取到當前的 window.innerWidth
。這個 useWindowWidth 方法能夠拿來在多個地方使用。經常使用的 Hook 方法以下:
3. useState & useRef
useState 是 React Hooks 中很基本的一個 API,它的用法主要有這幾種:
-
useState 接收一個初始值,返回一個數組,數組裏面分別是當前值和修改這個值的方法(相似 state 和 setState)。 -
useState 接收一個函數,返回一個數組。 -
setCount 能夠接收新值,也能夠接收一個返回新值的函數。
const [ count1, setCount1 ] = useState(0);const [ count2, setCount2 ] = useState(() => 0);setCount1(1); // 修改 state
3.1 和 class state 的區別
雖然函數組件也有了 state,可是 function state 和 class state 仍是有一些差別:
-
function state 的粒度更細,class state 過於無腦。 -
function state 保存的是快照,class state 保存的是最新值。 -
引用類型的狀況下,class state 不須要傳入新的引用,而 function state 必須保證是個新的引用。
3.2 快照(閉包) vs 最新值(引用)
在開始前,先拋出這麼一個問題。在 1s 內頻繁點擊10次按鈕,下面代碼的執行表現是什麼?
若是是這段代碼呢?它又會是什麼表現?
若是你能成功答對,那麼恭喜你,你已經掌握了 useState 的用法。在第一個例子中,連續點擊十次,頁面上的數字會從0增加到10。而第二個例子中,連續點擊十次,頁面上的數字只會從0增加到1。
這個是爲何呢?其實這主要是引用和閉包的區別。
class 組件裏面能夠經過 this.state 引用到 count,因此每次 setTimeout 的時候都能經過引用拿到上一次的最新 count,因此點擊多少次最後就加了多少。
在 function component 裏面每次更新都是從新執行當前函數,也就是說 setTimeout 裏面讀取到的 count 是經過閉包獲取的,而這個 count 實際上只是初始值,並非上次執行完成後的最新值,因此最後只加了1次。
3.3 快照和引用的轉換
若是我想讓函數組件也是從0加到10,那麼該怎麼來解決呢?聰明的你必定會想到,若是模仿類組件裏面的 this.state
,咱們用一個引用來保存 count 不就行了嗎?沒錯,這樣是能夠解決,只是這個引用該怎麼寫呢?我在 state 裏面設置一個對象好很差?就像下面這樣:
const [state, setState] = useState({ count: 0 })
答案是不行,由於即便 state 是個對象,但每次更新的時候,要傳一個新的引用進去,這樣的引用依然是沒有意義。
setState({ count: state.count + 1})
3.3 useRef
想要解決這個問題,那就涉及到另外一個新的 Hook 方法 —— useRef。useRef 是一個對象,它擁有一個 current 屬性,而且無論函數組件執行多少次,而 useRef 返回的對象永遠都是原來那一個。
useRef 有下面這幾個特色:
-
useRef
是一個只能用於函數組件的方法。 -
useRef
是除字符串ref
、函數ref
、createRef
以外的第四種獲取ref
的方法。 -
useRef
在渲染週期內永遠不會變,所以能夠用來引用某些數據。 -
修改 ref.current
不會引起組件從新渲染。
useRef vs createRef:
-
二者都是獲取 ref 的方式,都有一個 current 屬性。 -
useRef 只能用於函數組件,createRef 能夠用在類組件中。 -
useRef 在每次從新渲染後都保持不變,而 createRef 每次都會發生變化。
3.4 寫需求遇到的坑
以前在寫需求的時候遇到過這樣的一個坑。bankId
和 ref
都是從接口獲取到的,這裏很天然就想到在 useCallback
裏面指定依賴。
可是呢,這個 handlerReappear
方法須要在第一次進入頁面的時候,向 JS Bridge 註冊的事件,這就致使了一個問題,無論後來 handlerReappear
如何變化,registerHandler
裏面依賴的 callback
都是第一次的,這也是閉包致使的問題。固然,你可能會說,我在 useEffect
裏面也指定了依賴很差嗎?但要注意這是個註冊事件,意味着每次我都要清除上一次的事件,須要調用到 JS Bridge,在性能上確定不是個好辦法。
最終,我選擇使用 useRef
來保存 bankId
和 ref
,這樣就能夠經過引用來獲取到最新的值。
3.5 Vue3 Composition API
在 vue3 裏面提供了新的 Composition API,以前知乎有個問題是 React Hooks 是否能夠改成用相似 Vue 3 Composition API 的方式實現?
而後我寫了一篇文章,利用 Object.defineProperty
簡單實現了 Composition API,能夠參考:用 React Hooks 簡單實現 Vue3 Composition API
固然這個實現還有不少問題,也比較簡單,能夠參考工業聚寫的完整實現:react-use-setup
4. useEffect
useEffect
是一個 Effect Hook
,經常使用於一些反作用的操做,在必定程度上能夠充當 componentDidMount
、componentDidUpdate
、componentWillUnmount
這三個生命週期。useEffect
是很是重要的一個方法,能夠說是 React Hooks 的靈魂,它用法主要有這麼幾種:
-
useEffect
接收兩個參數,分別是要執行的回調函數、依賴數組。 -
若是依賴數組爲空數組,那麼回調函數會在第一次渲染結束後( componentDidMount
)執行,返回的函數會在組件卸載時(componentWillUnmount
)執行。 -
若是不傳依賴數組,那麼回調函數會在每一次渲染結束後( componentDidMount
和componentDidUpdate
)執行。 -
若是依賴數組不爲空數組,那麼回調函數會在依賴值每次更新渲染結束後(componentDidUpdate)執行,這個依賴值通常是 state 或者 props。
useEffect 比較重要,它主要有這幾個做用:
-
代替部分生命週期,如 componentDidMount、componentDidUpdate、componentWillUnmount。 -
更加 reactive,相似 mobx 的 reaction 和 vue 的 watch。 -
從命令式變成聲明式,不須要再關注應該在哪一步作某些操做,只須要關注依賴數據。 -
經過 useEffect 和 useState 能夠編寫一系列自定義的 Hook。
4.1 useEffect vs useLayoutEffect
useLayoutEffect 也是一個 Hook 方法,從名字上看和 useEffect 差很少,他倆用法也比較像。在90%的場景下咱們都會用 useEffect,然而在某些場景下卻不得不用 useLayoutEffect。useEffect 和 useLayoutEffect 的區別是:
-
useEffect 不會 block 瀏覽器渲染,而 useLayoutEffect 會。 -
useEffect 會在瀏覽器渲染結束後執行,useLayoutEffect 則是在 DOM 更新完成後,瀏覽器繪製以前執行。
這兩句話該怎麼來理解呢?咱們以一個移動的方塊爲例子:
在 useEffect 裏面會讓這個方塊日後移動 600px 距離,能夠看到這個方塊在移動過程當中會閃一下。但若是換成了 useLayoutEffect 呢?會發現方塊不會再閃動,而是直接出如今了 600px 的位置。
緣由是 useEffect 是在瀏覽器繪製以後執行的,因此方塊一開始就在最左邊,因而咱們看到了方塊移動的動畫。然而 useLayoutEffect 是在繪製以前執行的,會阻塞頁面的繪製,因此頁面會在 useLayoutEffect 裏面的代碼執行結束後纔去繼續繪製,因而方塊就直接出如今了右邊。那麼這裏的代碼是怎麼實現的呢?以 preact 爲例,useEffect 在 options.commit
階段執行,而 useLayoutEffect 在 options.diffed
階段執行。然而在實現 useEffect 的時候使用了 requestAnimationFrame
,requestAnimationFrame
能夠控制 useEffect 裏面的函數在瀏覽器重繪結束,下次繪製以前執行。
5. useMemo
useMemo 的用法相似 useEffect,經常用於緩存一些複雜計算的結果。useMemo 接收一個函數和依賴數組,當數組中依賴項變化的時候,這個函數就會執行,返回新的值。
const sum = useMemo(() => { // 一系列計算}, [count])
舉個例子會更加清楚 useMemo 的使用場景,咱們就如下面這個 DatePicker 組件的計算爲例:
DatePicker 組件每次打開或者切換月份的時候,都須要大量的計算來算出當前須要展現哪些日期。而後再將計算後的結果渲染到單元格里面,這裏可使用 useMemo 來緩存,只有當傳入的日期變化時纔去計算。
6. useCallback
和 useMemo 相似,只不過 useCallback 是用來緩存函數。
6.1 匿名函數致使沒必要要的渲染
在咱們編寫 React 組件的時候,常常會用到事件處理函數,不少人都會簡單粗暴的傳一個箭頭函數。
class App extends Component { render() { return <h1 onClick={() => {}}></h1> }}
這種箭頭函數有個問題,那就是在每一次組件從新渲染的時候都會生成一個重複的匿名箭頭函數,致使傳給組件的參數發生了變化,對性能形成必定的損耗。
在函數組件裏面,一樣會有這個傳遞新的匿名函數的問題。從下面這個例子來看,每次點擊 div,就會引發 Counter 組件從新渲染。此次更新明顯和 Input 組件無關,但每次從新渲染以後,都會建立新的 onChange 方法。這樣至關於傳給 Input 的 onChange 參數變化,即便 Input 內部作過 shadowEqual 也沒有意義了,都會跟着從新渲染。本來只想更新 count 值的,可 Input 組件 卻作了沒必要要的渲染。
這就是體現 useCallback 價值的地方了,咱們能夠用 useCallback 指定依賴項。在無關更新以後,經過 useCallback 取的仍是上一次緩存起來的函數。所以,useCallback 經常配合 React.memo
來一塊兒使用,用於進行性能優化。
7. useReducer && useContext
7.1 useReducer
useReducer 和 useState 的用法很類似,甚至在 preact 中,二者實現都是同樣的。useReducer 接收一個 reducer 函數和初始 state,返回了 state 和 dispatch 函數,經常用於管理一些複雜的狀態,適合 action 比較多的場景。
7.2 useContext
在上一節講解 React16 新特性的時候,咱們講過新版 Context API 的用法。
新版 Context 經常有一個提供數據的生產者(Provider),和一個消費數據的消費者(Consumer),咱們須要經過 Consumer 來以 render props
的形式獲取到數據。若是從祖先組件傳來了多個 Provider,那最終就又陷入了 render props
嵌套地獄。
useContext 容許咱們以扁平化的形式獲取到 Context 數據。即便有多個祖先組件使用多個 Context.Provider 傳值,咱們也能夠扁平化獲取到每個 Context 數據。
7.3 實現一個簡單的 Redux
經過 useReducer 和 useContext,咱們徹底能夠實現一個小型的 Redux。
reducer.js
Context.js
export const Context = createContext(null);
App.js
8. Custom Hooks
對於 react 來講,在函數組件中使用 state 當然有一些價值,但最有價值的仍是能夠編寫通用 custom hooks 的能力。想像一下,一個單純不依賴 UI 的業務邏輯 hook,咱們開箱即用。不只能夠在不一樣的項目中複用,甚至還能夠跨平臺使用,react、react native、react vr 等等。編寫自定義 hook 也須要以 use 開頭,這樣保證能夠配合 eslint 插件使用。在 custom hooks 中也能夠調用其餘 hook,當前的 hook 也能夠被其餘 hook 或者組件調用。以官網上這個獲取好友狀態的自定義 Hook 爲例:
這個自定義 Hook 裏面對好友的狀態進行了監聽,每次狀態更新的時候都會去更新 isOnline,當組件卸載的時候會清除掉這個監聽。這就是 React Hooks 最有用的地方,它容許咱們編寫自定義 Hook,而後這個自定義 Hook 能夠複用給多個組件,而且不會和 UI 耦合到一塊兒。
9. React Hooks 原理
因爲 preact hooks 的代碼和原有的邏輯耦合度很小,這裏爲了更加淺顯易懂,我選用了 preact hooks 的源碼來解讀。
9.1 Hooks 執行流程
在 React 中,組件返回的 JSX 元素也會被轉換爲虛擬 DOM,就是下方的 vnode,每一個 vnode 上面掛載了一個 _component 屬性,這個屬性指向了組件實例。而在組件實例上面又掛載了一個 _hooks 屬性,這個 _hooks 屬性裏面保存了咱們執行一個組件的時候,裏面全部 Hook 方法相關的信息。
首先,咱們有一個全局的 currentIndex 變量,當組件第一次渲染或者更新的時候,它會在每次進入一個函數組件的時候都重置爲0,每次遇到一個 Hook 方法就會增長1,同時將這個 Hook 方法的信息放到 _list 裏面。
當咱們下次進來或者進入下一個組件的時候, currentIndex 又會被置爲0。
★組件渲染 => currentIndex 重置 0 => 遇到 Hooks 方法,放進 _list => currentIndex++ => 渲染結束
」
★組件更新 => currentIndex 重置 0 => 遇到 Hooks 方法,獲取 _list[currentIndex]=> currentIndex++ => 重複上面步驟 => 更新結束
」
這個時候就會從剛纔的 _list 裏面根據 currentIndex 來取出對應項,因此咱們每次進來執行 useState,它依然能拿到上一次更新後的值,由於這裏是緩存了起來。
經過上面的分析,你就不難發現,爲何 hooks 方法不能放在條件語句裏面了。由於每次進入這個函數的時候,都是要和 currentIndex 一一匹配的,若是更新先後少了一個 Hook 方法,那麼就徹底對不上了,致使出現大問題。
9.2 useState 和 useReducer
這樣你再來看下面 useState 和 useReducer 的源碼就會更容易理解一些。
很明顯,getHookState 是根據 currentIndex 來從 _list 裏面取和當前 Hook 相關的一些信息。若是是初始化狀態(即沒有 hookState._component
)這個屬性的時候,就會去初始化 useState 的兩個返回值,不然就會直接返回上一次緩存的結果。
9.3 useEffect
useEffect 和 useState 差很少,區別就在 useEffect 接收的函數會放到一個 _pendingEffects 裏面,而非 _list 裏面。
在 diff 結束以後會從 _pendingEffects 裏面取出來函數一個個執行。afterPaint 裏面使用了 requestAnimateFrame 這個方法,因此傳給 useEffect 裏面的方法是在瀏覽器繪製結束以後纔會執行的。
9.4 總結
最後,這裏對 React Hooks 的整個運行流程來進行一下總結和梳理。
-
每一個組件實例上掛載一個 _hooks 屬性,保證了組件之間不會影響。 -
每當遇到一個 hooks 方法,就將其 push 到 currentComponent._hooks._list
中,且 currentIndex 加一。 -
每次渲染進入一個組件的時候,都會從將 currentIndex 重置爲 0 。遇到 hooks 方法時,currentIndex 重複第二步。這樣能夠把 currentIndex 和 currentComponent._hooks._list
中的對應項匹配起來,直接取上次緩存的值。 -
函數組件每次從新執行後,useState 中還能保持上一次的值,就是來自於步驟3中的緩存。 -
因爲依賴了 currentComponent 實例,因此 hooks 不能用於普通函數中。
10. React Hooks 實踐
得益於 react hooks 將業務邏輯從 ui 中抽離出來,目前社區裏面關於 react hooks 的實踐,大都是從功能點出發。
從最簡單的 api 封裝,例如 useDebounce、useThrottle、useImmerState 等等,再到業務層面功能封裝,比較出名的庫有 react-use、umijs/hooks 等等。
舉個栗子:umijs/hooks 的表格:
在後臺管理系統開發中,表格是很是常見的場景,將分頁、查詢、loading、排序等等功能打包封裝成通用 Hook,就能發揮很大的潛力。
11. 推薦閱讀
-
Umi Hooks - 助力擁抱 React Hooks -
爲何 React 如今要推行函數式組件,用 class 很差嗎? -
useRequest- 螞蟻中臺標準請求 Hooks
本文分享自微信公衆號 - 牧碼的星星(gh_0d71d9e8b1c3)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。