React hooks實戰總結

1、什麼是hooks?

react 於19年2月份在16.8版本中新增了hook這一特性,已通過去了半年多了,社區的各類文章解析頁汗牛充棟,本文將結合本身的項目實踐,對react hooks作一個全面的講解,俗話說沒吃過豬肉,還沒見過豬跑嗎?確實,可能大部分同窗對hooks特性都有了本身的瞭解,可是在實際項目中使用又是另外一回事了,實踐出真知,這篇文章是本身對react hooks的理解,也是在上一個項目中使用react hooks的總結
看着豬跑一千次,不如本身吃一次豬肉。javascript

  • 官方解釋: hookReact 16.8 的新增特性。它可讓你在不編寫 class 的狀況下使用 state 以及其餘的 React 特性。
  • 我的理解:讓傳統的函數組件function component有內部狀態state的函數function

2、爲何須要hooks?

  • 在以往的react開發流程中,咱們的自定義組件一般須要定義幾個生命週期函數,在不一樣的生命週期處理各自的業務邏輯,有可能他們是重複的。
  • 解決上一個問題咱們一般經過 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

3、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並渲染到頁面上: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方法更新stateredux

      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的值,能夠看到它的取值分爲兩種,HooksDispatcherOnMountHooksDispatcherOnUpdate分別對應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;
    }

    判斷當前fiberworkInProgressHook是否是null,若是是,將全新的hook賦值給全局的workInProgressHookfirstWorkInProgressHook,不然,將初始值賦值給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];
    statesetState以數組的形式返回,這也是咱們使用useState hook的正確姿式。到這裏相信你們都很清楚了,useState經過將咱們的初始state暫存到workInProgressHookmemoizedState中,每次更新的時候經過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,掌握到這裏已經夠了,不少文章說useStateuseReducer的基友關係,從這裏咱們就看出來了,useState最終使用的也是useReducer一致的api,經過相似redux的理念,經過dispatchAction修改state,有興趣的同志能夠看這裏useReducer源碼;

    • 其餘的hook就不展開了,感興趣的同志能夠去看看源碼,歡迎交流探討。

4、自定義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 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)。我從這裏吸取了一些經驗。

5、總結

  • 文章寫得雜亂,各位多多包含,有不對的地方歡迎指正。限於篇幅太長,其餘hook就不一一細說了,有興趣,有問題的同窗歡迎交流探討。
  • 距離hook提出大半年了,不少第三方庫也逐漸支持hook寫法,如今使用起來遇到坑的機會很少了。整體寫起來比class寫法舒服,不過對幾個基礎hook,特別是useStateuseEffect的掌握十分重要,結合setTimeout,setInterval每每會有意料以外的驚喜,網上文章也不少。本項目還沒寫完,目前看來,選擇React hook是對的,過程當中也學習了很多知識。趁年輕,折騰吧!
相關文章
相關標籤/搜索