一、 Hook 简介

1.1 Hook历史

在React Hook呈现之前的版本中,组件次要分为两种:函数式组件和类组件。其中,函数式组件通常只思考负责UI的渲染,没有本身的状态也没有业务逻辑代码,是一个纯函数。而类组件则不同,类组件有本人的外部状态,界面的显示后果通常由props 和 state 决定,因而它也不再那么纯净。函数式组件,类组件有如下一些毛病:

  • 状态逻辑难以复用。在类组件中,为了重用某些状态逻辑,社区提出了render props 或者 hoc 等计划,然而这些计划对组件的侵入性太强,并且组件嵌套还容易造成嵌套天堂的问题。
  • 滥用组件状态。大多数开发者在编写组件时,不论这个组件有木有外部状态,会不会执行生命周期函数,都会将组件编写成类组件,这造成不必要的性能开销。
  • 额定的工作解决。应用类组件开发利用时,须要开发者额定去关注 this 、事件监听器的增加和移除等等问题。

在函数式组件大行其道的以后,类组件正在逐步被淘汰。不过,函数式组件也并非毫无毛病,在之前的写法中,想要治理函数式组件状态共享就是比拟麻烦的问题。例如,上面这个函数组件就是一个纯函数,它的输入只由参数props决定,不受其余任何因素影响。

function App(props) {  const {name, age } = props.info  return (      <div style={{ height: '100%' }}>        <h1>Hello,i am ({name}),and i am ({age}) old</h1>      </div>  )}

在下面的函数式组件中,一旦咱们须要给组件加状态,那就只能将组件重写为类组件,因为函数组件没有实例,没有生命周期。所以咱们说在Hook之前的函数组件和类组件最大的区别其实就是状态的有无。

1.2 Hook 概览

为了解决函数式组件状态的问题,React 在16.8版本新增了Hook个性,能够让开发者在不编写 类(class) 的状况下应用 state 以及其余的 React 个性。并且,如果你应用React Native进行挪动利用开发,那么React Native 从 0.59 版本开始反对 Hook。

并且,应用Hook后,咱们能够抽取状态逻辑,使组件变得可测试、可重用,而开发者能够在不扭转组件层次结构的状况下,去重用状态逻辑,更好的实现状态和逻辑拆散的目标。上面是应用State Hook的例子。

import React, { useState } from "react";const StateHook = () => {  const [count, setCount] = useState(0);  return (    <div>      <p>you clicked {count} times</p>      <button type="button" onClick={() => setCount(count + 1)}>        click me      </button>    </div>  );};

在下面的示例红,useState 就是一个 Hook ,即通过在函数组件里调用它来给组件增加一些外部 State,React 会在反复渲染时保留这个 State。useState 会返回一对值:以后状态和一个让你更新它的函数,你能够在事件处理函数中或其余一些中央调用这个函数。它相似 class 组件的 this.setState,然而它不会把新的 state 和旧的 state 进行合并。(咱们会在应用 State Hook 里展现一个比照 useState 和 this.state 的例子)。

二、Hook 基本概念

Hook为函数式组件提供了状态,它反对在函数组件中进行数据获取、订阅事件解绑事件等等,学习React Hook之前,咱们咱们先了解以下一些根底概念。

2.1 useState

useState让函数组件具备了状态的能力。例如,后面用到的计数器示例就用到了useState。

  function App () {    const [count, setCount ] = useState(0)    return (      <div>        点击次数: { count }         <button onClick={() => { setCount(count + 1)}}>点我</button>      </div>      )  }

能够发现,useState应用上非常简单,第一个值是咱们的 state, 第二个值是一个函数,用来批改该 state的值。useState反对指定 state 的默认值,比方 useState(0), useState({ a: 1 }),除此之外,useState还反对咱们传入一个通过逻辑计算出默认值,比方。

function App (props) {    const [ count, setCount ] = useState(() => {      return props.count || 0    })    return (      ...       )  }

2.2 useEffect

Effect Hook 能够让你处理函数组件中的副作用。在React中,数据获取、设置订阅、手动的更改 DOM都能够称为副作用,能够将副作用分为两种,一种是须要清理的,另外一种是不须要清理的。比方网络申请、DOM 更改、日志这些副作用都不要清理。而比方定时器,事件监听则是须要解决的,而useEffect让开发者能够解决这些副作用。

上面是应用useEffect更改document.title题目的示例,代码如下。

import React, { useState,useEffect } from "react";function App () {    const [ count, setCount ] = useState(0)    useEffect(() => {        document.title = count    })    return (        <div>            以后页面ID: { count }            <button onClick={() => { setCount(count + 1 )}}>点我</button>        </div>    )}export default App;

如果你相熟React 类组件的生命周期函数,那么咱们能够把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。

在类组件中,咱们绑定事件、解绑事件、设定定时器、查找 Dom都须要通过 componentDidMount、componentDidUpdate、componentWillUnmount 生命周期来实现,而 useEffect的作用就相当于这三个生命周期函数,只不过须要通过传参来决定是否调用它。useEffect 会返回一个回调函数,作用于革除上一次副作用遗留下来的状态,如果该 useEffect 只调用一次,该回调函数相当于 componentWillUnmount 生命周期。

例如有上面一个useEffect综合的例子,代码如下。

import React, { useState,useEffect } from "react";function App () {    const [ count, setCount ] = useState(0)    const [ width, setWidth ] = useState(document.body.clientWidth)    const onChange = () => {        setWidth(document.body.clientWidth)    }    useEffect(() => {        //相当于 componentDidMount        window.addEventListener('resize', onChange, false)        return () => {            //相当于componentWillUnmount            window.removeEventListener('resize', onChange, false)        }    }, [])    useEffect(() => {        //相当于componentDidUpdate        document.title = count;    })    useEffect(() => {        console.log(`count change: count is ${count}`)    }, [ count ])    return (        <div>            页面名称: { count }            页面宽度: { width }            <button onClick={() => { setCount(count + 1)}}>点我</button>        </div>    )}export default App;

在下面例子中,咱们须要解决两种副作用,即既要解决title,还要监听屏幕宽度的扭转,依照 类组件的写法咱们须要在生命周期中解决这些逻辑,不过在Hooks中,咱们只须要应用 useEffect 就能解决这些问题。

后面说过,useEffect就是用来解决副作用的,而革除上一次留下的状态就是它的作用之一。因为useEffect是每次render之后就会被调用,此时title的扭转就相当于 componentDidUpdate,但咱们不心愿事件监听每次 render 之后进行一次绑定和解绑,此时就用到了useEffect 函数的第二个参数。

那什么时候会用到useEffect 的第二个参数呢?次要有以下场景:

  • 组件每次执行render之后 useEffect 都会调用,此时相当于执行类组件的componentDidMount 和 componentDidUpdate生命周期。
  • 传入一个空数组[], 此时useEffect只会调用一次,相当于执行类组件的componentDidMount 和 componentWillUnmount生命周期。
  • 传入一个数组,其中包含变量,只有这些变量变动时,useEffect 才会执行。

2.3 useMemo

在传统的函数组件中,当在一个父组件中调用一个子组件的时候,因为父组件的state产生扭转会导致父组件更新,而子组件尽管没有产生扭转然而也会进行更新,而useMemo就是函数组件为了避免这种不必要的更新而采取的伎俩,其作用相似于类组件的 PureComponent。

那useMemo 是如何应用的呢,看上面的一个例子。

function App () {  const [ count, setCount ] = useState(0)  const add = useMemo(() => {    return count + 1  }, [count])  return (    <div>      点击次数: { count }      <br/>      次数加一: { add }      <button onClick={() => { setCount(count + 1)}}>点我</button>    </div>    )}

须要留神的是,useMemo 会在渲染的时候执行,而不是渲染之后执行,这一点和 useEffect 有区别,所以 useMemo不倡议办法中有副作用相干的逻辑。

2.4 useCallback

useCallback是useMemo 的语法糖,基本上能用useCallback实现的都能够应用useMemo,不过useCallback也有本人的应用场景。比方,在React 中咱们常常会面临子组件渲染优化的问题,尤其在向子组件传递函数props时,每次的渲染 都会创立新函数,导致子组件不必要的渲染。而useCallback应用的是缓存的函数,这样把这个缓存函数作为props传递给子组件时就起到了缩小不必要渲染的作用。

import React, { useState, useCallback, useEffect } from 'react';function Parent() {    const [count, setCount] = useState(1);    const [val, setVal] = useState('');    const callback = useCallback(() => {        return count;    }, [count]);    return <div>        <h4>父组件:{count}</h4>        <Child callback={callback}/>        <button onClick={() => setCount(count + 1)}>点我+1</button>    </div>;}function Child({ callback }) {    const [count, setCount] = useState(() => callback());    useEffect(() => {        setCount(callback());    }, [callback]);    return <div>        子组件:{count}    </div>}export default Parent;

须要阐明的是,React.memo和 React.useCallback肯定记得配对应用,缺了一个都可能导致性能不升反“降”,毕竟无意义的浅比拟也会耗费一些性能。

2.5 useRef

在React中,咱们应用Ref来获取组件的实例或者DOM元素,咱们能够应用两种形式来创立 Ref:createRef和useRef,如下所示。

import React, { useState, useRef } from 'react'function App(){    const [count, setCount] = useState(0)    const counterEl = useRef(null)    const increment = () => {        setCount(count + 1)        console.log(counterEl)    }    return (        <>            Count: <span ref={counterEl}>{count}</span>            <button onClick={increment}>点我+</button>        </>    )}

2.6 useReducer

useReducer的作用相似redux中的性能,相较于useState,useReducer适宜一些逻辑较简单且蕴含多个子值的状况。reducer承受两个参数,第一个参数是一个reducer,第二个参数是初始 state,返回值为最新的state和dispatch函数。

依照官网的说法,useReducer适宜用于简单的state操作逻辑,嵌套的state的对象的场景。上面是官网给出的示例。

import React, { useReducer } from 'react';function Reducers () {    const initialState={count:0}    const [count,dispatch] = useReducer((state,avtion) => {        switch(avtion.type) {            case 'add':                return state+1;            case 'minus':                return state-1            default:                return state        }    },0)    return (        <div>            <div>{count}</div>            <button onClick={() => {dispatch({type: 'add'})}}>加</button>            <button onClick={() => {dispatch({type: 'minus'})}}>减</button>        </div>    )}export default Reducers

2.7 useImperativeHandle

useImperativeHandle 能够让开发者在应用 ref 时自定义裸露给父组件的实例值。其意思就是,子组件能够选择性的裸露一些办法给父组件,而后暗藏一些公有办法和属性,官网倡议,useImperativeHandle最好与 forwardRef 一起应用。

import React, { useRef, forwardRef, useImperativeHandle } from 'react'const App = forwardRef((props,ref) => {    const inputRef = useRef()    useImperativeHandle(ref,()=>({        focus : () =>{            inputRef.current.focus()        }    }),[inputRef])    return <input type="text" ref={inputRef}/>})export default function Father() {    const inputRef = useRef()       return (        <div>            <App ref={inputRef}/>            <button onClick={e=>inputRef.current.focus()}>获取焦点</button>        </div>    )}

在示例中,咱们通过 useImperativeHandle 将子组件的实例属性输入到父组件,而子组件外部通过 ref 更改 current 对象后组件不会从新渲染,须要扭转 useState 设置的状态能力更改。

除了下面介绍的几种Hook API之外,React Hook常见的API还包含useLayoutEffect、useDebugValue。

自定义 Hook

应用Hook技术,React函数组件的this指向、生命周期逻辑冗余的问题都已失去解决,不过React开发中另一个比拟常见的问题,逻辑代码复用依然没有失去解决。如果要解决这个问题,须要通过自定义Hook。

所谓的自定义Hook,其实就是指函数名以use结尾并调用其余Hook的函数,自定义Hook的每个状态都是齐全独立的。例如,上面是应用自定义Hook封装axios实现网络申请的示例,代码如下。

import axios from 'axios'import { useEffect, useState} from 'react';const useAxios = (url, dependencies) => {    const [isLoading, setIsLoading] = useState(false);    const [response, setResponse] = useState(null);    const [error, setError] = useState(null);    useEffect(() => {        setIsLoading(true);        axios.get(url).then((res) => {            setIsLoading(false);            setResponse(res);        }).catch((err) => {            setIsLoading(false);            setError(err);        });    }, dependencies);    return [isLoading, response, error];}export default useAxios;

在下面的代码中,咱们应用React已有的API实现自定义Hook的性能。而具体应用时,自定义Hook的应用办法和React官网提供的Hook API应用上相似,如下所示。

function App() {    let url = 'http://api.douban.com/v2/movie/in_theaters';    const [isLoading, response, error] = useAxios(url, []);    return (        <div>            {isLoading ? <div>loading...</div> :                (error ? <div> There is an error happened </div> : <div> Success, {response} </div>)}        </div>    )}export default App;

能够发现,相比于函数属性和高阶组件等形式,自定义Hook则更加的简洁易读,不仅于此,自定义Hook也不会引起之组件嵌套天堂问题。

尽管React的Hooks有着诸多的劣势。不过,在应用Hooks的过程中,须要留神以下两点:

  • 不要在循环、条件或嵌套函数中应用Hook,并且只能在React函数的顶层应用Hook。之所以要这么做,是因为React须要利用调用程序来正确更新相应的状态,以及调用相应的生命周期函数函数。一旦在循环或条件分支语句中调用Hook,就容易导致调用程序的不一致性,从而产生难以预料到的结果。
  • 只能在React函数式组件或自定义Hook中应用Hook。

同时,为了防止在开发中造成一些低级的谬误,能够装置一个eslint插件,命令如下。

yarn add eslint-plugin-react-hooks --dev

而后,在eslint的配置文件中增加如下一些配置。

{  "plugins": [    // ...    "react-hooks"  ],  "rules": {    // ...    "react-hooks/rules-of-hooks": "error",    "react-hooks/exhaustive-deps": "warn"  }}