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是对的,过程当中也学习了很多知识。趁年轻,折腾吧!
相关文章
相关标签/搜索