react 于19年2月份在16.8版本中新增了hook这一特性,已通过去了半年多了,社区的各类文章解析页汗牛充栋,本文将结合本身的项目实践,对react hooks
作一个全面的讲解,俗话说没吃过猪肉,还没见过猪跑吗?确实,可能大部分同窗对hooks特性都有了本身的了解,可是在实际项目中使用又是另外一回事了,实践出真知,这篇文章是本身对react hooks
的理解,也是在上一个项目中使用react hooks
的总结
看着猪跑一千次,不如本身吃一次猪肉。javascript
hook
是 React 16.8
的新增特性。它可让你在不编写 class
的状况下使用 state
以及其余的 React
特性。function component
有内部状态state
的函数function
。解决上一个问题咱们一般经过 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
javascript
函数,相比于ClassComponent
,具备潜在的更好的性能。debug
。FC
有更多的优点,可是他没有生命周期,也没有本身的内部状态,咱们须要复杂的状态管理机制的时候,不得不转向ClassComponent
。 FC现有的这些问题,咱们能轻松结合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
方法更新state
。redux
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
的值,能够看到它的取值分为两种,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来举例说明。先说一下,其实官方的hook已经不少很全了,状态咱们能够useState
,复杂多状态咱们能够用useReducer
,共享和传递状态可使用useContext
,引用组件、引用状态能够useRef
,组件render完成以后的操做经过useEffect
完成...还有其余几个hook,那么咱们为何还须要自定义hooks呢?
在项目过程当中有这样一个业务场景,许多个地方(几十到几百不等)须要监听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一个函数的函数做为参数
这样一个场景:咱们须要一个全局的消息提示,已经写好了一个全局组件,并经过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)。我从这里吸取了一些经验。
class
写法舒服,不过对几个基础hook
,特别是useState
,useEffect
的掌握十分重要,结合setTimeout,setInterval每每会有意料以外的惊喜,网上文章也不少。本项目还没写完,目前看来,选择React hook是对的,过程当中也学习了很多知识。趁年轻,折腾吧!