一、什麼是hooks?
react 於19年2月份在16.8版本中新增了hook這一特性,已經過去了半年多了,社群的各種文章解析頁汗牛充棟,本文將結合自己的專案實踐,對react hooks
做一個全面的講解,俗話說沒吃過豬肉,還沒見過豬跑嗎?確實,可能大部分同學對hooks特性都有了自己的瞭解,但是在實際專案中使用又是另一回事了,實踐出真知,這篇文章是自己對react hooks
的理解,也是在上一個專案中使用react hooks
的總結
看著豬跑一千次,不如自己吃一次豬肉。
- 官方解釋:
hook
是React 16.8
的新增特性。它可以讓你在不編寫class
的情況下使用state
以及其他的React
特性。 - 個人理解:讓傳統的函式元件
function component
有內部狀態state
的函式function
。
二、為什麼需要hooks?
- 在以往的react開發流程中,我們的自定義元件通常需要定義幾個生命週期函式,在不同的生命週期處理各自的業務邏輯,有可能他們是重複的。
-
解決上一個問題我們通常通過
mixins
(不推薦) 或者HOC
實現,在hooks出現之前,的確是非常好的解決途徑,但是它不夠好,為什麼這麼說呢?來看一下我們的一個具有中英文切換,主題切換同時connect
一些redux
狀態倉庫裡面的資料的全域性元件alert
:export default translate('[index,tips]')(withStyles(styles, { withTheme: true })(connect(mapStateToProps,mapDispatchToProps)(Alert)));
其實如果我們還可以將 `withTheme`也提取成一個高階函式,那麼我們的元件就將由現在的3層變成4層,實際使用的時候可能還有別的屬性通過別的高階函式新增,巢狀層級就會更深。給人明顯的感覺就是不夠直觀。
-
this指向問題,react繫結this有幾種方式?哪一種方式效能相對來說好一些?
如果你答不上來,可以戳一下下面兩個連結。
-
hook 只能在FunctionComponent內部使用,而相比
ClassComponent
,傳統的FunctionComponent(FC)
具有更多的優勢,具體體現在:- FC 容易測試,相同的輸入總是有相同的輸出,
- FC 其實就是普通的
javascript
函式,相比於ClassComponent
,具有潛在的更好的效能。 - FC 沒有生命週期函式,更容易
debug
。 - FC 具有更好的可重用性。
- FC 可以減少程式碼耦合。
- September 10th, 2018 Comments React Functional or Class Components: Everything you need to know。
- 45% Faster React Functional Components, Now。
-
FC
有更多的優勢,但是他沒有生命週期,也沒有自己的內部狀態,我們需要複雜的狀態管理機制的時候,不得不轉向ClassComponent
。 FC現有的這些問題,我們能輕鬆結合hook
解決。
三、useState 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並渲染到頁面上:ClassComponent
實現: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
實現: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
。public handleClick = () => { const { count } = this.state; this.setState({ count: count + 1 }); }
-
-
深入原始碼分析hooks,這裡我們以剛使用過的hook
useState
為例,看看他是怎麼管理我們的FC state的。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就不展開了,感興趣的同志可以去看看原始碼,歡迎交流探討。
四、自定義hooks
阿西吧,東拉西扯的到了這塊最有趣的地方。這塊以專案中實際用到的幾個hook來舉例說明。先說一下,其實官方的hook已經很多很全了,狀態我們可以useState
,複雜多狀態我們可以用useReducer
,共享和傳遞狀態可以使用useContext
,引用元件、引用狀態可以useRef
,元件render完成之後的操作通過useEffect
完成...還有其他幾個hook,那麼我們為什麼還需要自定義hooks呢?
- 其實,自定義hook也是基於官方的hook進行組合,邏輯複用,業務程式碼解耦抽象後進一步提煉出來的具備一定功能的函式。它應當具有一定條件下的的通用性,可移植性。
- 目前的hook可能並不十分契合我們的需求,我們需要進行二次加工,成為我們的業務hook, 官方推薦自定義hook命名以use開頭。
useWindowLoad
-
在專案過程中有這樣一個業務場景,許多個地方(幾十到幾百不等)需要監聽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一個函式的函式作為引數
useMessage
這樣一個場景:我們需要一個全域性的訊息提示,已經寫好了一個全域性元件,並通過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 message = {
success,
info,
warn,
error
};
return message;
}
我們內部使用useDispatch拿到dispatch,封裝了四個不同功能的函式,直接對外提供封裝好的物件,就實現使用上了類似antd message元件的功能,哪裡需要哪裡useMessage就可以開心的玩耍了。
- 專案中還有其他的自定義hook,但是思路很上面兩個一致,提取共性,消除副作用。 這裡給大家推薦一個自定義的hook的一個[站點](https://usehooks.com)。我從這裡吸收了一些經驗。
五、總結
- 文章寫得雜亂,各位多多包含,有不對的地方歡迎指正。限於篇幅太長,其他hook就不一一細說了,有興趣,有問題的同學歡迎交流探討。
- 距離hook提出大半年了,很多第三方庫也逐漸支援hook寫法,現在使用起來遇到坑的機會不多了。總體寫起來比
class
寫法舒服,不過對幾個基礎hook
,特別是useState
,useEffect
的掌握十分重要,結合setTimeout,setInterval往往會有意料之外的驚喜,網上文章也很多。本專案還沒寫完,目前看來,選擇React hook是對的,過程中也學習了不少知識。趁年輕,折騰吧!