React hooks实战总结

开发 前端
可能大部分同学对hooks特性都有了自己的了解,但是在实际项目中使用又是另一回事了,实践出真知,这篇文章是自己对react hooks的理解,也是在上一个项目中使用react hooks的总结。

 [[274289]]

一、什么是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: 
  1. export default translate('[index,tips]')(withStyles(styles, { withTheme: true })(connect(mapStateToProps,mapDispatchToProps)(Alert)));    
  1. 其实如果我们还可以将 `withTheme`也提取成一个高阶函数,那么我们的组件就将由现在的3层变成4层,实际使用的时候可能还有别的属性通过别的高阶函数添加,嵌套层级就会更深。给人明显的感觉就是不够直观。 
  •  this指向问题,react绑定this有几种方式?哪一种方式性能相对来说好一些?   
  1. 如果你答不上来,可以戳一下下面两个链接。 
  •   React事件处理。
  •   React.js绑定this的5种方法。
  •     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实现: 

  1. import React from 'react';  
  2. interface ITestState {  
  3.     count: number;  
  4.  
  5. class Test extends React.Component<{}, ITestState> {  
  6.     constructor(props: {}) {  
  7.         super(props);  
  8.         this.state = {  
  9.             count: 0  
  10.         };  
  11.     } 
  12.      public handleClick = () => {  
  13.         const { count } = this.state;  
  14.         this.setState({ count: count + 1 });  
  15.     }  
  16.     public render() {  
  17.         return (  
  18.             <>  
  19.                 <div>{this.state.count}</div>  
  20.                 <button onClick={this.handleClick}>click</button>  
  21.             </>  
  22.         );  
  23.     }  
  24.  
  25. export default Test; 

hooks实现:   

  1. import React, { useState } from 'react';  
  2.    const Test: React.FunctionComponent<{}> = () => {  
  3.        const [count, setCount] = useState<number>(0);  
  4.        return (  
  5.            <>  
  6.                <div>{count}</div>  
  7.                <button onClick={() => setCount(count + 1)}>click</button>  
  8.            </>  
  9.        );  
  10.    };  
  11.    export default Test; 
  •  对比两种实现,直观感受是代码变少了,没错,也不用关心this指向了,ClassComponent里面通过class fields正确绑定回调函数的this指向,使得我们在handleClick函数中能正确的访问this,并调用this.setState方法更新state。     
  1. public handleClick = () => {  
  2.               const { count } = this.state;  
  3.               this.setState({ count: count + 1 });  
  4.           } 
  •  深入源码分析hooks,这里我们以刚使用过的hook useState为例,看看他是怎么管理我们的FC state的。   
  1. export function useState<S>(initialState: (() => S) | S) {  
  2.        const dispatcher = resolveDispatcher();  
  3.        return dispatcher.useState(initialState);  
  4.    } 

这个函数接收一个参数initialState: (() => S) | S,初始state的函数或者我们的state初始值。

然后调用 

  1. dispatcher.useState(initialState);,这里我们看一下dispatcher是怎么来的:  
  2.   function resolveDispatcher() {  
  3.       const dispatcher = ReactCurrentDispatcher.current; 
  4.       ...  
  5.       return dispatcher;  
  6.   } 

发现是通过ReactCurrentDispatcher.current得到,那ReactCurrentDispatcher又是何方神圣呢?

我们进一步看看它怎么来的   

  1. import type {Dispatcher} from 'react-reconciler/src/ReactFiberHooks';  
  2.    const ReactCurrentDispatcher = {  
  3.        current: (null: null | Dispatcher),  
  4.    };  
  5.    export default ReactCurrentDispatcher; 

根据type,我们可以判断dispatcher的类型是react-reconciler/src/ReactFiberHooks里面定义的Dispatcher,可以看到这个current属性是个null。那它是什么时候被赋值的呢?

我们来看看functionComponent的render过程renderWithHooks,   

  1. export function renderWithHooks(  
  2.         current: Fiber | null,  
  3.         workInProgress: Fiber,  
  4.         Component: any,  
  5.         props: any,  
  6.         refOrContext: any,  
  7.         nextRenderExpirationTime: ExpirationTime,  
  8.     ): any{  
  9.         ....  
  10.         if (__DEV__) {  
  11.             ReactCurrentDispatcher.current = HooksDispatcherOnMountInDEV 
  12.         } else {  
  13.             ReactCurrentDispatcher.current =  
  14.             nextCurrentHook === null  
  15.                 ? HooksDispatcherOnMount  
  16.                 : HooksDispatcherOnUpdate;  
  17.         }  
  18.     } 

这里react源码根据nextCurrentHook做了一些判断,我移除掉了,只关注ReactCurrentDispatcher.current的值,可以看到它的取值分为两种,HooksDispatcherOnMount和 HooksDispatcherOnUpdate分别对应mount/update两个组件状态;这里我们先看HooksDispatcherOnMount:   

  1. const HooksDispatcherOnMount: Dispatcher = {  
  2.    ...  
  3.    useState: mountState,  
  4.    ...  
  5.    }; 

这就是我们寻寻觅觅的Dispatcher的长相,最终我们useState在组件mount的时候执行的就是这个mountState了,那我们就迫不及待如饥似渴的来看看mountState又做了什么吧。 

  1. function mountState<S> 
  2.   initialState: (() => S) | S,  
  3.   ): [S, Dispatch<BasicStateAction<S>>] {  
  4.   const hook = mountWorkInProgressHook();  
  5.   if (typeof initialState === 'function') {  
  6.       initialStateinitialState = initialState();  
  7.   }  
  8.   hookhook.memoizedState = hook.baseState = initialState 
  9.   const queue = (hook.queue = {  
  10.       last: null,  
  11.       dispatch: null,  
  12.       lastRenderedReducer: basicStateReducer,  
  13.       lastRenderedState: (initialState: any),  
  14.   });  
  15.   const dispatch: Dispatch<  
  16.       BasicStateAction<S> 
  17.   > = (queue.dispatch = (dispatchAction.bind(  
  18.       null,  
  19.       // Flow doesn't know this is non-null, but we do.  
  20.       ((currentlyRenderingFiber: any): Fiber),  
  21.       queue,  
  22.   ): any));  
  23.   return [hook.memoizedState, dispatch];  
  24.   } 

进入这个函数首先执行的mountWorkInProgressHook()获取到当前的workInProgressHook,看这个名字就知道他是和workInProgress分不开了,这个workInProgress代表了当前正在处理的fiber,fiber是当前组件的需要完成或者已经完成的work的对象,也可以理解为我们的这个正在执行mountState的组件的各种数据和状态的集合。我们来具体的看一下mountWorkInProgressHook的执行逻辑: 

  1. function mountWorkInProgressHook(): Hook {  
  2.       const hook: Hook = {  
  3.           memoizedState: null,  
  4.           baseState: null,  
  5.           queue: null,  
  6.           baseUpdate: null,  
  7.           next: null,  
  8.       };  
  9.       if (workInProgressHook === null) {  
  10.       // This is the first hook in the list  
  11.           firstWorkInProgressHook = workInProgressHook = hook;  
  12.       } else {  
  13.       // Append to the end of the list  
  14.           workInProgressHookworkInProgressHook = workInProgressHook.next = hook 
  15.       }  
  16.       return workInProgressHook;  
  17.   } 

判断当前fiber的workInProgressHook是不是null,如果是,将全新的hook赋值给全局的workInProgressHook和firstWorkInProgressHook,否则,将初始值赋值给workInProgressHook。相当于mountState里面的hook值就是       

  1. const hook: Hook = {  
  2.            memoizedState: null,  
  3.            baseState: null,  
  4.            queue: null,  
  5.            baseUpdate: null,  
  6.            next: null,  
  7.        }; 

实际上,workInProgressHook是这样的一个链表结构,React里面广泛使用了这样的结构存储副作用。   

  1.  
  2.         memoizedState: null,  
  3.         baseState: null,  
  4.         queue: null,  
  5.         baseUpdate: null,  
  6.         next: {  
  7.             ...  
  8.             next: {  
  9.                 ...  
  10.                 next: {  
  11.                     next: {...},  
  12.                     ...  
  13.                 },  
  14.             },  
  15.         }  
  16.     } 

继续往下看:   

  1. if (typeof initialState === 'function') {  
  2.         initialStateinitialState = initialState();  
  3.     }  
  4.     hookhook.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>的定义:   

  1. type UpdateQueue<S, A> = {  
  2.        last: Update<S, A> | null,  
  3.        dispatch: (A => mixed) | null,  
  4.        lastRenderedReducer: ((S, A) => S) | null,  
  5.        lastRenderedState: S | null,  
  6.    }; 

看到这个结构,熟悉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的状态,处理后续的其他逻辑,这里我们将业务逻辑用这个函数表示一下: 
  1. const executeOnload:()=>{alert('alert after loaded')} 

传统的实现思路:   

  1.  
  2.        if(window.loaded)executeOnload();return;  
  3.        const old = window.onload;  
  4.            window.onload = () => {  
  5.                window.loaded = true 
  6.                executeOnload();  
  7.                old && old();  
  8.        };  
  9.    } 

在使用我们的自定义hook useWindowLoad之后 

  1. const isWindowLoadeduseWindowLoad(executeOnload) 

每一处需要监听的地方都变得十分简单有没有,话不多说,直接上码:   

  1. export default function useWindowLoad(func?: (params?: any) => any): boolean {  
  2.    useEffect(() => {  
  3.        let effect: (() => void) | nullnull = null;  
  4.        const old = window.onload;  
  5.        window.onload = () => {  
  6.            effect = func && func();  
  7.            old && old();  
  8.            window.loaded = true 
  9.        };  
  10.        return () => {  
  11.            if (typeof effect === 'function') {  
  12.                effect();  
  13.            }  
  14.        };  
  15.    });  
  16.    return window.loaded;  
  17.    }) 

最后,我们返回load状态。这里我们主要使用了useEffect这个hook,并在接受的参数的返回值中清除了对应的副作用。useEffect在每次组件render完成后执行,具体使用参考文档。注意,副作用的清除很重要,因为我们不能保证传入的回调函数不会带来副作用,所以使用时应该传递return一个函数的函数作为参数

useMessage

 

  1. 这样一个场景:我们需要一个全局的消息提示,已经写好了一个全局组件,并通过redux管理状态控制Message的显示和隐藏,这其实是一个很常见的功能,在使用hook之前,我们的实现可能是这样的:  
  1. import React from 'react';  
  2.  import { connect } from 'react-redux';  
  3.  import { message } from './actions';  
  4.  import Errors from './lib/errors';  
  5.  interface IDemoProps {  
  6.      message(params: Message): void;  
  7.  }  
  8.  const mapStateToProps = (state: IStore) => ({});  
  9.  const mapDispatchToProps = (dispatch: any) => ({  
  10.       message: (params: Message) =>dispatch(message(params))   
  11.  });  
  12.  class Demo extends React.Component<IDemoProps, {}> {  
  13.      public handleClick() {  
  14.          this.props.message({ content: Errors.GLOBAL_NETWORK_ERROR.message, type: 'error', duration: 1600, show: true });  
  15.      }  
  16.      public render() {  
  17.          return <button className='demo' onClick={this.handleClick}>click alert message</button> 
  18.      }  
  19.  }  
  20.  export default connect(mapStateToProps, mapDispatchToProps)(Demo); 

每次我们要使用就得mapDispatchToProps,引入action,connect,...繁琐至极,我们也可以用**高阶组件**包装一下,透传一个message函数给需要的子组件,这里我们使用自定义hook来解决,先看看最终达到的效果:

 

  1. import React from 'react';  
  2. import Errors from './lib/errors';  
  3. const Demo: React.FC<{}> = () => {  
  4.     const message = useMessage();  
  5.     const handleClick = () => {  
  6.         message.info(content: Errors.GLOBAL_NETWORK_ERROR.message);  
  7.     };  
  8.     return <button className='demo' onClick={handleClick}>alert message</button> 
  9. };  
  10. export default Demo; 

 

  1. 简单了许多,每次需要全局提示的地方,我们只需要通过`const message = useMessage();`  
  2. 然后再组件内部任何地方使用`message.info('content')`,`message.error('content')`,`message.success('content')`,`message.warn('content')`即可,再也不关心action,redux connect等一系列操作。 
  3. 我们来看看这个逻辑如何实现的: 

 

  1. import { useDispatch } from 'react-redux';  
  2. import { message as alert } from '../actions/index';  
  3. /**  
  4. * @param {type}  
  5. * @return:  
  6. */  
  7. export default function useMessage() {  
  8.     const dispatch = useDispatch();  
  9.     const info = (content: Partial<Message>['content']) => dispatch(alert({ content, duration: 3000, show: true, type: 'info' }));  
  10.     const warn = (content: Partial<Message>['content']) => dispatch(alert({ content, duration: 3000, show: true, type: 'warn' }));  
  11.     const error = (content: Partial<Message>['content']) => dispatch(alert({ content, duration: 3000, show: true, type: 'error' }));  
  12.     const success = (content: Partial<Message>['content']) => dispatch(alert({ content, duration: 3000, show: true, type: 'success' }));  
  13.     const message = {  
  14.         success,  
  15.         info,  
  16.         warn,  
  17.         error  
  18.     };  
  19.     return message;  

我们内部使用useDispatch拿到dispatch,封装了四个不同功能的函数,直接对外提供封装好的对象,就实现使用上了类似antd message组件的功能,哪里需要哪里useMessage就可以开心的玩耍了。

- 项目中还有其他的自定义hook,但是思路很上面两个一致,提取共性,消除副作用。 这里给大家推荐一个自定义的hook的一个[站点](https://usehooks.com)。我从这里吸收了一些经验。

五、总结

  • 文章写得杂乱,各位多多包含,有不对的地方欢迎指正。限于篇幅太长,其他hook就不一一细说了,有兴趣,有问题的同学欢迎交流探讨。
  • 距离hook提出大半年了,很多第三方库也逐渐支持hook写法,现在使用起来遇到坑的机会不多了。总体写起来比class写法舒服,不过对几个基础hook,特别是useState,useEffect的掌握十分重要,结合setTimeout,setInterval往往会有意料之外的惊喜,网上文章也很多。本项目还没写完,目前看来,选择React hook是对的,过程中也学习了不少知识。趁年轻,折腾吧!

 

 

责任编辑:庞桂玉 来源: segmentfault
相关推荐

2021-03-18 08:00:55

组件Hooks React

2023-11-06 08:00:00

ReactJavaScript开发

2019-03-13 10:10:26

React组件前端

2022-03-31 17:54:29

ReactHooks前端

2022-08-21 09:41:42

ReactVue3前端

2022-07-18 09:01:58

React函数组件Hooks

2020-09-19 17:46:20

React Hooks开发函数

2020-10-28 09:12:48

React架构Hooks

2020-03-16 10:25:49

前端React Hooks响应式布局

2022-04-16 20:10:00

React Hookfiber框架

2023-05-11 08:57:49

ReactHooks

2022-03-22 09:09:17

HookReact前端

2021-05-11 08:48:23

React Hooks前端

2024-02-05 21:48:25

VueReactHooks

2023-05-08 07:52:29

JSXReactHooks

2022-02-10 19:15:18

React监听系统模式

2021-05-21 06:13:35

React Hooks react-refrReact

2022-03-16 22:24:50

ReactstateHooks

2021-11-05 10:36:19

性能优化实践

2020-08-10 06:31:01

React Hooks前端开发
点赞
收藏

51CTO技术栈公众号