一、 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" }}