React hooks實(shí)戰(zhàn)總結(jié)
一、什么是hooks?
react 于19年2月份在16.8版本中新增了hook這一特性,已經(jīng)過(guò)去了半年多了,社區(qū)的各種文章解析頁(yè)汗牛充棟,本文將結(jié)合自己的項(xiàng)目實(shí)踐,對(duì)react hooks做一個(gè)全面的講解,俗話說(shuō)沒(méi)吃過(guò)豬肉,還沒(méi)見(jiàn)過(guò)豬跑嗎?確實(shí),可能大部分同學(xué)對(duì)hooks特性都有了自己的了解,但是在實(shí)際項(xiàng)目中使用又是另一回事了,實(shí)踐出真知,這篇文章是自己對(duì)react hooks的理解,也是在上一個(gè)項(xiàng)目中使用react hooks的總結(jié)
看著豬跑一千次,不如自己吃一次豬肉。
- 官方解釋: hook 是 React 16.8 的新增特性。它可以讓你在不編寫(xiě) class 的情況下使用 state 以及其他的 React 特性。
- 個(gè)人理解:讓傳統(tǒng)的函數(shù)組件function component有內(nèi)部狀態(tài)state的函數(shù)function。
二、為什么需要hooks?
- 在以往的react開(kāi)發(fā)流程中,我們的自定義組件通常需要定義幾個(gè)生命周期函數(shù),在不同的生命周期處理各自的業(yè)務(wù)邏輯,有可能他們是重復(fù)的。
- 解決上一個(gè)問(wèn)題我們通常通過(guò) mixins(不推薦) 或者 HOC 實(shí)現(xiàn),在hooks出現(xiàn)之前,的確是非常好的解決途徑,但是它不夠好,為什么這么說(shuō)呢?來(lái)看一下我們的一個(gè)具有中英文切換,主題切換同時(shí)connect一些redux 狀態(tài)倉(cāng)庫(kù)里面的數(shù)據(jù)的全局組件alert:
- export default translate('[index,tips]')(withStyles(styles, { withTheme: true })(connect(mapStateToProps,mapDispatchToProps)(Alert)));
- 其實(shí)如果我們還可以將 `withTheme`也提取成一個(gè)高階函數(shù),那么我們的組件就將由現(xiàn)在的3層變成4層,實(shí)際使用的時(shí)候可能還有別的屬性通過(guò)別的高階函數(shù)添加,嵌套層級(jí)就會(huì)更深。給人明顯的感覺(jué)就是不夠直觀。
- this指向問(wèn)題,react綁定this有幾種方式?哪一種方式性能相對(duì)來(lái)說(shuō)好一些?
- 如果你答不上來(lái),可以戳一下下面兩個(gè)鏈接。
- React事件處理。
- React.js綁定this的5種方法。
- hook 只能在FunctionComponent內(nèi)部使用,而相比ClassComponent,傳統(tǒng)的FunctionComponent(FC)具有更多的優(yōu)勢(shì),具體體現(xiàn)在:
- FC 容易測(cè)試,相同的輸入總是有相同的輸出,
- FC 其實(shí)就是普通的javascript函數(shù),相比于ClassComponent,具有潛在的更好的性能。
- FC 沒(méi)有生命周期函數(shù),更容易debug。
- FC 具有更好的可重用性。
- FC 可以減少代碼耦合。
- September 10th, 2018 Comments React Functional or Class Components: Everything you need to know。
- 45% Faster React Functional Components, Now。
- FC有更多的優(yōu)勢(shì),但是他沒(méi)有生命周期,也沒(méi)有自己的內(nèi)部狀態(tài),我們需要復(fù)雜的狀態(tài)管理機(jī)制的時(shí)候,不得不轉(zhuǎn)向ClassComponent。 FC現(xiàn)有的這些問(wèn)題,我們能輕松結(jié)合hook解決。
三、useState hook 的執(zhí)行過(guò)程追蹤
- React目前官方支持的hook有三個(gè)基礎(chǔ)Hook:
useState,
useEffect,
useContext,
和幾個(gè)額外的 Hook:
useReducer,
useCallback,
useMemo,
useRef,
useImperativeHandle,
useLayoutEffect,
useDebugValue ,
他們的作用各不相同,但是可以這么總結(jié)一下:讓Function Component有狀態(tài)(state),流氓不可怕,就怕流氓有文化。當(dāng)我們給比較有優(yōu)勢(shì)的FC 插上state的翅膀之后,他就要起飛了。原來(lái)ClassComponent能干的事情他也能干起來(lái)了,加上前文分析的優(yōu)勢(shì),還干的更加有聲有色。這里我們使用useState做一個(gè)全面的解析,
首先我們來(lái)看一下一個(gè)簡(jiǎn)單的的計(jì)數(shù)器,點(diǎn)擊click 按鈕,state加1并渲染到頁(yè)面上:
ClassComponent實(shí)現(xiàn):
- 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實(shí)現(xiàn):
- 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;
- 對(duì)比兩種實(shí)現(xiàn),直觀感受是代碼變少了,沒(méi)錯(cuò),也不用關(guān)心this指向了,ClassComponent里面通過(guò)class fields正確綁定回調(diào)函數(shù)的this指向,使得我們?cè)趆andleClick函數(shù)中能正確的訪問(wèn)this,并調(diào)用this.setState方法更新state。
- public handleClick = () => {
- const { count } = this.state;
- this.setState({ count: count + 1 });
- }
- 深入源碼分析hooks,這里我們以剛使用過(guò)的hook useState為例,看看他是怎么管理我們的FC state的。
- export function useState<S>(initialState: (() => S) | S) {
- const dispatcher = resolveDispatcher();
- return dispatcher.useState(initialState);
- }
這個(gè)函數(shù)接收一個(gè)參數(shù)initialState: (() => S) | S,初始state的函數(shù)或者我們的state初始值。
然后調(diào)用
- dispatcher.useState(initialState);,這里我們看一下dispatcher是怎么來(lái)的:
- function resolveDispatcher() {
- const dispatcher = ReactCurrentDispatcher.current;
- ...
- return dispatcher;
- }
發(fā)現(xiàn)是通過(guò)ReactCurrentDispatcher.current得到,那ReactCurrentDispatcher又是何方神圣呢?
我們進(jìn)一步看看它怎么來(lái)的
- import type {Dispatcher} from 'react-reconciler/src/ReactFiberHooks';
- const ReactCurrentDispatcher = {
- current: (null: null | Dispatcher),
- };
- export default ReactCurrentDispatcher;
根據(jù)type,我們可以判斷dispatcher的類型是react-reconciler/src/ReactFiberHooks里面定義的Dispatcher,可以看到這個(gè)current屬性是個(gè)null。那它是什么時(shí)候被賦值的呢?
我們來(lái)看看functionComponent的render過(guò)程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源碼根據(jù)nextCurrentHook做了一些判斷,我移除掉了,只關(guān)注ReactCurrentDispatcher.current的值,可以看到它的取值分為兩種,HooksDispatcherOnMount和 HooksDispatcherOnUpdate分別對(duì)應(yīng)mount/update兩個(gè)組件狀態(tài);這里我們先看HooksDispatcherOnMount:
- const HooksDispatcherOnMount: Dispatcher = {
- ...
- useState: mountState,
- ...
- };
這就是我們尋尋覓覓的Dispatcher的長(zhǎng)相,最終我們useState在組件mount的時(shí)候執(zhí)行的就是這個(gè)mountState了,那我們就迫不及待如饑似渴的來(lái)看看mountState又做了什么吧。
- function mountState<S>(
- initialState: (() => S) | S,
- ): [S, Dispatch<BasicStateAction<S>>] {
- const hook = mountWorkInProgressHook();
- if (typeof initialState === 'function') {
- initialStateinitialState = initialState();
- }
- hookhook.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];
- }
進(jìn)入這個(gè)函數(shù)首先執(zhí)行的mountWorkInProgressHook()獲取到當(dāng)前的workInProgressHook,看這個(gè)名字就知道他是和workInProgress分不開(kāi)了,這個(gè)workInProgress代表了當(dāng)前正在處理的fiber,fiber是當(dāng)前組件的需要完成或者已經(jīng)完成的work的對(duì)象,也可以理解為我們的這個(gè)正在執(zhí)行mountState的組件的各種數(shù)據(jù)和狀態(tài)的集合。我們來(lái)具體的看一下mountWorkInProgressHook的執(zhí)行邏輯:
- 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
- workInProgressHookworkInProgressHook = workInProgressHook.next = hook;
- }
- return workInProgressHook;
- }
判斷當(dāng)前fiber的workInProgressHook是不是null,如果是,將全新的hook賦值給全局的workInProgressHook和firstWorkInProgressHook,否則,將初始值賦值給workInProgressHook。相當(dāng)于mountState里面的hook值就是
- const hook: Hook = {
- memoizedState: null,
- baseState: null,
- queue: null,
- baseUpdate: null,
- next: null,
- };
實(shí)際上,workInProgressHook是這樣的一個(gè)鏈表結(jié)構(gòu),React里面廣泛使用了這樣的結(jié)構(gòu)存儲(chǔ)副作用。
- {
- memoizedState: null,
- baseState: null,
- queue: null,
- baseUpdate: null,
- next: {
- ...
- next: {
- ...
- next: {
- next: {...},
- ...
- },
- },
- }
- }
繼續(xù)往下看:
- if (typeof initialState === 'function') {
- initialStateinitialState = initialState();
- }
- hookhook.memoizedState = hook.baseState = initialState;
useState接收的參數(shù)類型如果是函數(shù),這里就會(huì)執(zhí)行傳進(jìn)來(lái)的函數(shù)獲取initialState,賦值給hook.memoizedState = hook.baseState這兩個(gè)屬性,再往下,建立了當(dāng)前hook的更新隊(duì)列queue:<UpdateQueue>,這個(gè)我們后續(xù)再講,這里暫時(shí)不用知道。繼續(xù)往下看,是我們修改state的回調(diào)函數(shù),通常是setState,通過(guò)改變dispatchAction的this指向,將當(dāng)前render的fiber和上面創(chuàng)建的queue作為參數(shù)傳入,當(dāng)我們執(zhí)行setState的時(shí)候?qū)嶋H上調(diào)用的就是這里的dispatchAction,最后一行:
return [hook.memoizedState, dispatch];
將state和setState以數(shù)組的形式返回,這也是我們使用useState hook的正確姿勢(shì)。到這里相信大家都很清楚了,useState通過(guò)將我們的初始state暫存到workInProgressHook的memoizedState中,每次更新的時(shí)候通過(guò)dispatchAction更新workInProgressHook。
我們回過(guò)頭來(lái)再看看剛才沒(méi)深入過(guò)的queue,通過(guò)類型我們可以知道他是<UpdateQueue>,具體看看<UpdateQueue>的定義:
- type UpdateQueue<S, A> = {
- last: Update<S, A> | null,
- dispatch: (A => mixed) | null,
- lastRenderedReducer: ((S, A) => S) | null,
- lastRenderedState: S | null,
- };
看到這個(gè)結(jié)構(gòu),熟悉react fiber的同學(xué)已經(jīng)心中有數(shù)了,它的last屬性是一個(gè)鏈表,用來(lái)存儲(chǔ)當(dāng)前hook的變化信息,能夠通過(guò)next迭代處理所有變更信息和狀態(tài)。這里我們就到此為止,感興趣的同志可以自行深入琢磨,對(duì)于這個(gè)hook,掌握到這里已經(jīng)夠了,很多文章說(shuō)useState和useReducer的基友關(guān)系,從這里我們就看出來(lái)了,useState最終使用的也是useReducer一致的api,通過(guò)類似redux的理念,通過(guò)dispatchAction修改state,有興趣的同志可以看這里useReducer源碼;
- 其他的hook就不展開(kāi)了,感興趣的同志可以去看看源碼,歡迎交流探討。
四、自定義hooks
阿西吧,東拉西扯的到了這塊最有趣的地方。這塊以項(xiàng)目中實(shí)際用到的幾個(gè)hook來(lái)舉例說(shuō)明。先說(shuō)一下,其實(shí)官方的hook已經(jīng)很多很全了,狀態(tài)我們可以u(píng)seState,復(fù)雜多狀態(tài)我們可以用useReducer,共享和傳遞狀態(tài)可以使用useContext,引用組件、引用狀態(tài)可以u(píng)seRef,組件render完成之后的操作通過(guò)useEffect完成...還有其他幾個(gè)hook,那么我們?yōu)槭裁催€需要自定義hooks呢?
- 其實(shí),自定義hook也是基于官方的hook進(jìn)行組合,邏輯復(fù)用,業(yè)務(wù)代碼解耦抽象后進(jìn)一步提煉出來(lái)的具備一定功能的函數(shù)。它應(yīng)當(dāng)具有一定條件下的的通用性,可移植性。
- 目前的hook可能并不十分契合我們的需求,我們需要進(jìn)行二次加工,成為我們的業(yè)務(wù)hook, 官方推薦自定義hook命名以u(píng)se開(kāi)頭。
useWindowLoad
- 在項(xiàng)目過(guò)程中有這樣一個(gè)業(yè)務(wù)場(chǎng)景,許多個(gè)地方(幾十到幾百不等)需要監(jiān)聽(tīng)window.onload事件,等待onload后執(zhí)行特定的業(yè)務(wù)邏輯,如果window已經(jīng)load,需要返回當(dāng)前的,同時(shí)希望拿到window loaded的狀態(tài),處理后續(xù)的其他邏輯,這里我們將業(yè)務(wù)邏輯用這個(gè)函數(shù)表示一下:
- const executeOnload:()=>{alert('alert after loaded')}
傳統(tǒng)的實(shí)現(xiàn)思路:
- {
- if(window.loaded)executeOnload();return;
- const old = window.onload;
- window.onload = () => {
- window.loaded = true;
- executeOnload();
- old && old();
- };
- }
在使用我們的自定義hook useWindowLoad之后
- const isWindowLoaded= useWindowLoad(executeOnload)
每一處需要監(jiān)聽(tīng)的地方都變得十分簡(jiǎn)單有沒(méi)有,話不多說(shuō),直接上碼:
- export default function useWindowLoad(func?: (params?: any) => any): boolean {
- useEffect(() => {
- let effect: (() => void) | nullnull = 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狀態(tài)。這里我們主要使用了useEffect這個(gè)hook,并在接受的參數(shù)的返回值中清除了對(duì)應(yīng)的副作用。useEffect在每次組件render完成后執(zhí)行,具體使用參考文檔。注意,副作用的清除很重要,因?yàn)槲覀儾荒鼙WC傳入的回調(diào)函數(shù)不會(huì)帶來(lái)副作用,所以使用時(shí)應(yīng)該傳遞return一個(gè)函數(shù)的函數(shù)作為參數(shù)
useMessage
- 這樣一個(gè)場(chǎng)景:我們需要一個(gè)全局的消息提示,已經(jīng)寫(xiě)好了一個(gè)全局組件,并通過(guò)redux管理狀態(tài)控制Message的顯示和隱藏,這其實(shí)是一個(gè)很常見(jiàn)的功能,在使用hook之前,我們的實(shí)現(xiàn)可能是這樣的:
- 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,...繁瑣至極,我們也可以用**高階組件**包裝一下,透?jìng)饕粋€(gè)message函數(shù)給需要的子組件,這里我們使用自定義hook來(lái)解決,先看看最終達(dá)到的效果:
- 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;
- 簡(jiǎn)單了許多,每次需要全局提示的地方,我們只需要通過(guò)`const message = useMessage();`
- 然后再組件內(nèi)部任何地方使用`message.info('content')`,`message.error('content')`,`message.success('content')`,`message.warn('content')`即可,再也不關(guān)心action,redux connect等一系列操作。
- 我們來(lái)看看這個(gè)邏輯如何實(shí)現(xiàn)的:
- 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;
- }
我們內(nèi)部使用useDispatch拿到dispatch,封裝了四個(gè)不同功能的函數(shù),直接對(duì)外提供封裝好的對(duì)象,就實(shí)現(xiàn)使用上了類似antd message組件的功能,哪里需要哪里useMessage就可以開(kāi)心的玩耍了。
- 項(xiàng)目中還有其他的自定義hook,但是思路很上面兩個(gè)一致,提取共性,消除副作用。 這里給大家推薦一個(gè)自定義的hook的一個(gè)[站點(diǎn)](https://usehooks.com)。我從這里吸收了一些經(jīng)驗(yàn)。
五、總結(jié)
- 文章寫(xiě)得雜亂,各位多多包含,有不對(duì)的地方歡迎指正。限于篇幅太長(zhǎng),其他hook就不一一細(xì)說(shuō)了,有興趣,有問(wèn)題的同學(xué)歡迎交流探討。
- 距離hook提出大半年了,很多第三方庫(kù)也逐漸支持hook寫(xiě)法,現(xiàn)在使用起來(lái)遇到坑的機(jī)會(huì)不多了。總體寫(xiě)起來(lái)比class寫(xiě)法舒服,不過(guò)對(duì)幾個(gè)基礎(chǔ)hook,特別是useState,useEffect的掌握十分重要,結(jié)合setTimeout,setInterval往往會(huì)有意料之外的驚喜,網(wǎng)上文章也很多。本項(xiàng)目還沒(méi)寫(xiě)完,目前看來(lái),選擇React hook是對(duì)的,過(guò)程中也學(xué)習(xí)了不少知識(shí)。趁年輕,折騰吧!