react 於19年2月份在16.8版本中新增了hook這一特性,已通過去了半年多了,社區的各類文章解析頁汗牛充棟,本文將結合本身的項目實踐,對react hooks
作一個全面的講解,俗話說沒吃過豬肉,還沒見過豬跑嗎?確實,可能大部分同窗對hooks特性都有了本身的瞭解,可是在實際項目中使用又是另外一回事了,實踐出真知,這篇文章是本身對react hooks
的理解,也是在上一個項目中使用react hooks
的總結
看着豬跑一千次,不如本身吃一次豬肉。javascript
hook
是 React 16.8
的新增特性。它可讓你在不編寫 class
的狀況下使用 state
以及其餘的 React
特性。function component
有內部狀態state
的函數function
。解決上一個問題咱們一般經過 mixins
(不推薦) 或者 HOC
實現,在hooks出現以前,的確是很是好的解決途徑,可是它不夠好,爲何這麼說呢?來看一下咱們的一個具備中英文切換,主題切換同時connect
一些redux
狀態倉庫裏面的數據的全局組件alert
:html
export default translate('[index,tips]')(withStyles(styles, { withTheme: true })(connect(mapStateToProps,mapDispatchToProps)(Alert)));
其實若是咱們還能夠將 `withTheme`也提取成一個高階函數,那麼咱們的組件就將由如今的3層變成4層,實際使用的時候可能還有別的屬性經過別的高階函數添加,嵌套層級就會更深。給人明顯的感受就是不夠直觀。
this指向問題,react綁定this有幾種方式?哪種方式性能相對來講好一些?java
若是你答不上來,能夠戳一下下面兩個連接。
hook 只能在FunctionComponent內部使用,而相比ClassComponent
,傳統的FunctionComponent(FC)
具備更多的優點,具體體如今:react
javascript
函數,相比於ClassComponent
,具備潛在的更好的性能。debug
。FC
有更多的優點,可是他沒有生命週期,也沒有本身的內部狀態,咱們須要複雜的狀態管理機制的時候,不得不轉向ClassComponent
。 FC現有的這些問題,咱們能輕鬆結合hook
解決。React目前官方支持的hook有三個基礎Hook:useState
,useEffect
,useContext
,
和幾個額外的 Hook:useReducer
,useCallback
,useMemo
,useRef
,useImperativeHandle
,useLayoutEffect
,useDebugValue
,
他們的做用各不相同,可是能夠這麼總結一下:讓Function Component
有狀態(state),流氓不可怕,就怕流氓有文化。當咱們給比較有優點的FC 插上state
的翅膀以後,他就要起飛了。原來ClassComponent
能幹的事情他也能幹起來了,加上前文分析的優點,還乾的更加有聲有色。這裏咱們使用useState
作一個全面的解析,
首先咱們來看一下一個簡單的的計數器,點擊click 按鈕,state加1並渲染到頁面上:git
ClassComponent
實現:github
import React from 'react'; interface ITestState { count: number; } class Test extends React.Component<{}, ITestState> { constructor(props: {}) { super(props); this.state = { count: 0 }; } public handleClick = () => { const { count } = this.state; this.setState({ count: count + 1 }); } public render() { return ( <> <div>{this.state.count}</div> <button onClick={this.handleClick}>click</button> </> ); } } export default Test;
hooks
實現:typescript
import React, { useState } from 'react'; const Test: React.FunctionComponent<{}> = () => { const [count, setCount] = useState<number>(0); return ( <> <div>{count}</div> <button onClick={() => setCount(count + 1)}>click</button> </> ); }; export default Test;
對比兩種實現,直觀感覺是代碼變少了,沒錯,也不用關心this指向了,ClassComponent
裏面經過class fields
正確綁定回調函數的this
指向,使得咱們在handleClick函數中能正確的訪問this
,並調用this.setState
方法更新state
。redux
public handleClick = () => { const { count } = this.state; this.setState({ count: count + 1 }); }
深刻源碼分析hooks,這裏咱們以剛使用過的hook useState
爲例,看看他是怎麼管理咱們的FC state的。api
export function useState<S>(initialState: (() => S) | S) { const dispatcher = resolveDispatcher(); return dispatcher.useState(initialState); }
這個函數接收一個參數initialState: (() => S) | S
,初始state的函數或者咱們的state初始值。
而後調用dispatcher.useState(initialState);
,這裏咱們看一下dispatcher
是怎麼來的:數組
function resolveDispatcher() { const dispatcher = ReactCurrentDispatcher.current; ... return dispatcher; }
發現是經過ReactCurrentDispatcher.current
獲得,那ReactCurrentDispatcher
又是何方神聖呢?
咱們進一步看看它怎麼來的
import type {Dispatcher} from 'react-reconciler/src/ReactFiberHooks'; const ReactCurrentDispatcher = { current: (null: null | Dispatcher), }; export default ReactCurrentDispatcher;
根據type,咱們能夠判斷dispatcher的類型是react-reconciler/src/ReactFiberHooks
裏面定義的Dispatcher,能夠看到這個current屬性是個null。那它是何時被賦值的呢?
咱們來看看functionComponent的render過程renderWithHooks
,
export function renderWithHooks( current: Fiber | null, workInProgress: Fiber, Component: any, props: any, refOrContext: any, nextRenderExpirationTime: ExpirationTime, ): any{ .... if (__DEV__) { ReactCurrentDispatcher.current = HooksDispatcherOnMountInDEV; } else { ReactCurrentDispatcher.current = nextCurrentHook === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate; } }
這裏react源碼根據nextCurrentHook
作了一些判斷,我移除掉了,只關注ReactCurrentDispatcher.current
的值,能夠看到它的取值分爲兩種,HooksDispatcherOnMount
和 HooksDispatcherOnUpdate
分別對應mount/update
兩個組件狀態;這裏咱們先看HooksDispatcherOnMount
:
const HooksDispatcherOnMount: Dispatcher = { ... useState: mountState, ... };
這就是咱們尋尋覓覓的Dispatcher
的長相,最終咱們useState
在組件mount的時候執行的就是這個mountState
了,那咱們就火燒眉毛如飢似渴的來看看mountState
又作了什麼吧。
function mountState<S>( initialState: (() => S) | S, ): [S, Dispatch<BasicStateAction<S>>] { const hook = mountWorkInProgressHook(); if (typeof initialState === 'function') { initialState = initialState(); } hook.memoizedState = hook.baseState = initialState; const queue = (hook.queue = { last: null, dispatch: null, lastRenderedReducer: basicStateReducer, lastRenderedState: (initialState: any), }); const dispatch: Dispatch< BasicStateAction<S>, > = (queue.dispatch = (dispatchAction.bind( null, // Flow doesn't know this is non-null, but we do. ((currentlyRenderingFiber: any): Fiber), queue, ): any)); return [hook.memoizedState, dispatch]; }
進入這個函數首先執行的mountWorkInProgressHook()
獲取到當前的workInProgressHook
,看這個名字就知道他是和workInProgress
分不開了,這個workInProgress
表明了當前正在處理的fiber,fiber
是當前組件的須要完成或者已經完成的work的對象,也能夠理解爲咱們的這個正在執行mountState的組件的各類數據和狀態的集合。咱們來具體的看一下mountWorkInProgressHook的執行邏輯:
function mountWorkInProgressHook(): Hook { const hook: Hook = { memoizedState: null, baseState: null, queue: null, baseUpdate: null, next: null, }; if (workInProgressHook === null) { // This is the first hook in the list firstWorkInProgressHook = workInProgressHook = hook; } else { // Append to the end of the list workInProgressHook = workInProgressHook.next = hook; } return workInProgressHook; }
判斷當前fiber
的workInProgressHook
是否是null
,若是是,將全新的hook賦值給全局的workInProgressHook
和firstWorkInProgressHook
,不然,將初始值賦值給workInProgressHook
。至關於mountState
裏面的hook值就是
const hook: Hook = { memoizedState: null, baseState: null, queue: null, baseUpdate: null, next: null, };
實際上,workInProgressHook是這樣的一個鏈表結構,React裏面普遍使用了這樣的結構存儲反作用。
{ memoizedState: null, baseState: null, queue: null, baseUpdate: null, next: { ... next: { ... next: { next: {...}, ... }, }, } }
繼續往下看:
if (typeof initialState === 'function') { initialState = initialState(); } hook.memoizedState = hook.baseState = initialState;
useState接收的參數類型若是是函數,這裏就會執行傳進來的函數獲取initialState
,賦值給hook.memoizedState = hook.baseState
這兩個屬性,再往下,創建了當前hook
的更新隊列queue:<UpdateQueue>
,這個咱們後續再講,這裏暫時不用知道。繼續往下看,是咱們修改state的回調函數,一般是setState,經過改變dispatchAction
的this指向,將當前render的fiber和上面建立的queue做爲參數傳入,當咱們執行setState
的時候實際上調用的就是這裏的dispatchAction
,最後一行:return [hook.memoizedState, dispatch];
將state
和setState
以數組的形式返回,這也是咱們使用useState hook
的正確姿式。到這裏相信你們都很清楚了,useState
經過將咱們的初始state
暫存到workInProgressHook
的memoizedState
中,每次更新的時候經過dispatchAction
更新workInProgressHook
。
咱們回過頭來再看看剛纔沒深刻過的queue
,經過類型咱們能夠知道他是<UpdateQueue>
,具體看看<UpdateQueue>
的定義:
type UpdateQueue<S, A> = { last: Update<S, A> | null, dispatch: (A => mixed) | null, lastRenderedReducer: ((S, A) => S) | null, lastRenderedState: S | null, };
看到這個結構,熟悉react fiber
的同窗已經心中有數了,它的last屬性是一個鏈表,用來存儲當前hook的變化信息,可以經過next
迭代處理全部變動信息和狀態。這裏咱們就到此爲止,感興趣的同志能夠自行深刻琢磨,對於這個hook
,掌握到這裏已經夠了,不少文章說useState
和useReducer
的基友關係,從這裏咱們就看出來了,useState
最終使用的也是useReducer
一致的api,經過相似redux
的理念,經過dispatchAction
修改state
,有興趣的同志能夠看這裏useReducer
源碼;
阿西吧,東拉西扯的到了這塊最有趣的地方。這塊以項目中實際用到的幾個hook來舉例說明。先說一下,其實官方的hook已經不少很全了,狀態咱們能夠useState
,複雜多狀態咱們能夠用useReducer
,共享和傳遞狀態可使用useContext
,引用組件、引用狀態能夠useRef
,組件render完成以後的操做經過useEffect
完成...還有其餘幾個hook,那麼咱們爲何還須要自定義hooks呢?
在項目過程當中有這樣一個業務場景,許多個地方(幾十到幾百不等)須要監聽window.onload事件,等待onload後執行特定的業務邏輯,若是window已經load,須要返回當前的,同時但願拿到window loaded的狀態,處理後續的其餘邏輯,這裏咱們將業務邏輯用這個函數表示一下:
const executeOnload:()=>{alert('alert after loaded')}
傳統的實現思路:
{ if(window.loaded)executeOnload();return; const old = window.onload; window.onload = () => { window.loaded = true; executeOnload(); old && old(); }; }
在使用咱們的自定義hook useWindowLoad以後
const isWindowLoaded= useWindowLoad(executeOnload)
每一處須要監聽的地方都變得十分簡單有沒有,話很少說,直接上碼:
export default function useWindowLoad(func?: (params?: any) => any): boolean { useEffect(() => { let effect: (() => void) | null = null; const old = window.onload; window.onload = () => { effect = func && func(); old && old(); window.loaded = true; }; return () => { if (typeof effect === 'function') { effect(); } }; }); return window.loaded; })
最後,咱們返回load狀態。這裏咱們主要使用了useEffect這個hook,並在接受的參數的返回值中清除了對應的反作用。useEffect在每次組件render完成後執行,具體使用參考文檔。注意,反作用的清除很重要,由於咱們不能保證傳入的回調函數不會帶來反作用,因此使用時應該傳遞return一個函數的函數做爲參數
這樣一個場景:咱們須要一個全局的消息提示,已經寫好了一個全局組件,並經過redux管理狀態控制Message的顯示和隱藏,這實際上是一個很常見的功能,在使用hook以前,咱們的實現多是這樣的:
import React from 'react'; import { connect } from 'react-redux'; import { message } from './actions'; import Errors from './lib/errors'; interface IDemoProps { message(params: Message): void; } const mapStateToProps = (state: IStore) => ({}); const mapDispatchToProps = (dispatch: any) => ({ message: (params: Message) =>dispatch(message(params)) }); class Demo extends React.Component<IDemoProps, {}> { public handleClick() { this.props.message({ content: Errors.GLOBAL_NETWORK_ERROR.message, type: 'error', duration: 1600, show: true }); } public render() { return <button className='demo' onClick={this.handleClick}>click alert message</button>; } } export default connect(mapStateToProps, mapDispatchToProps)(Demo);
每次咱們要使用就得mapDispatchToProps,引入action,connect,...繁瑣至極,咱們也能夠用**高階組件**包裝一下,透傳一個message函數給須要的子組件,這裏咱們使用自定義hook來解決,先看看最終達到的效果:
import React from 'react'; import Errors from './lib/errors'; const Demo: React.FC<{}> = () => { const message = useMessage(); const handleClick = () => { message.info(content: Errors.GLOBAL_NETWORK_ERROR.message); }; return <button className='demo' onClick={handleClick}>alert message</button>; }; export default Demo;
簡單了許多,每次須要全局提示的地方,咱們只須要經過`const message = useMessage();` 而後再組件內部任何地方使用`message.info('content')`,`message.error('content')`,`message.success('content')`,`message.warn('content')`便可,不再關心action,redux connect等一系列操做。 咱們來看看這個邏輯如何實現的:
import { useDispatch } from 'react-redux'; import { message as alert } from '../actions/index'; /** * @param {type} * @return: */ export default function useMessage() { const dispatch = useDispatch(); const info = (content: Partial<Message>['content']) => dispatch(alert({ content, duration: 3000, show: true, type: 'info' })); const warn = (content: Partial<Message>['content']) => dispatch(alert({ content, duration: 3000, show: true, type: 'warn' })); const error = (content: Partial<Message>['content']) => dispatch(alert({ content, duration: 3000, show: true, type: 'error' })); const success = (content: Partial<Message>['content']) => dispatch(alert({ content, duration: 3000, show: true, type: 'success' })); const tmpMessage = { success, info, warn, error }; // 注意,不能直接返回tmpMessage,會致使有依賴message的effect重複執行(每次useMessage返回了不一樣的引用地址)。 const [message] = useState(tmpMessage); return message; }
咱們內部使用useDispatch拿到dispatch,封裝了四個不一樣功能的函數,直接對外提供封裝好的對象,就實現使用上了相似antd message組件的功能,哪裏須要哪裏useMessage就能夠開心的玩耍了。 - 項目中還有其餘的自定義hook,可是思路很上面兩個一致,提取共性,消除反作用。 這裏給你們推薦一個自定義的hook的一個[站點](https://usehooks.com)。我從這裏吸取了一些經驗。
class
寫法舒服,不過對幾個基礎hook
,特別是useState
,useEffect
的掌握十分重要,結合setTimeout,setInterval每每會有意料以外的驚喜,網上文章也不少。本項目還沒寫完,目前看來,選擇React hook是對的,過程當中也學習了很多知識。趁年輕,折騰吧!