React hooks 实战总结
一、什么是 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 有几种方式?哪一种方式性能相对来说好一些?
如果你答不上来,可以戳一下下面两个链接。- [React 事件处理](https://zh-hans.reactjs.org/docs/handling-events.html)。- [React.js 绑定 this 的 5 种方法](https://juejin.im/post/5b13c3a16fb9a01e75462a64)。
-
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](https://programmingwithmosh.com/react/react-functional-components/)。- [45% Faster React Functional Components, Now](https://medium.com/missive-app/45-faster-react-functional-components-now-3509a668e69f)。- `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';
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 赚到了, 过程中也学习了不少知识。