一、Hook简介
React Hooks是从React 16.8版本推出的新个性,目标是解决React的状态共享以及组件生命周期管理混乱的问题。React Hooks的呈现标记着,React不会再存在无状态组件的状况,React将只有类组件和函数组件的概念。
家喻户晓,React利用开发中,组件的状态共享是一件很麻烦的事件,而React Hook只共享数据处理逻辑,并不会共享数据自身,因而也就不须要关怀数据与生命周期绑定的问题。如下所示,是应用类组件实现计数器的示例。
class Example extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; } render() { return ( <div> <p>You clicked {this.state.count} times</p> <button onClick={() => this.setState({ count: this.state.count + 1 })}> Click me </button> </div> ); }}
能够发现,类组件须要本人申明状态,并编写操作状态的办法,并且还须要保护状态的生命周期,显得特地麻烦。如果应用React Hook提供的State Hook来解决状态,那么代码将会简洁许多,重构后的代码如下所示。
import React, { useState } from 'react';function Example() { const [count, setCount] = useState(0); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> );}
能够看到,Example从一个类组件变成了一个函数组件,此函数组件领有本人的状态,并且不须要调用setState()办法也可更新本人的状态。之所以能够如此操作,是因为类组件应用了useState函数。
二、根底Hook
2.1 useState
useState函数是React自带的一个Hook函数,而Hook函数领有React状态和生命周期治理的能力。
能够看到,useState函数的入参只有一个,就是state的初始值,这个初始值能够是数字、字符串、对象,甚至是一个函数,如下所示。
function Example (props) { const [ count, setCount ] = useState(() => { return props.count || 0 }) return ( <div> You clicked : { count } <button onClick={() => { setCount(count + 1)}}> Click me </button> </div> ) }
并且,当入参是一个函数时,此函数只会在类组件初始渲染的时候才会被执行一次。
如果须要同时对一个state对象进行操作,那么能够间接应用函数进行操作,该函数会接管state对象的值,而后执行更新操作,如下所示。
function Example() { const [count, setCount] = useState(0); function handleClick() { setCount(count + 1) } function handleClickFn() { setCount((prevCount) => { return prevCount -1 }) } return ( <> You clicked: {count} <button onClick={handleClick}>+</button> <button onClick={handleClickFn}>-</button> </> );}
在下面的代码中,handleClick和handleClickFn都是更新的最新的状态值。并且操作同一个状态对象值的时候,为了节约性能,React会把屡次状态更新进行合并,并一次性的更新状态对象的值。
在React利用开发中,当某个组件的状态发生变化时,它会以该组件为根,从新渲染整个组件树,如下所示。
function Child({ onButtonClick, data }) { return ( <button onClick={onButtonClick}>{data.number}</button> )}function Example () { const [number, setNumber] = useState(0) const [name, setName] = useState('hello') const addClick = () => setNumber(number + 1) const data = { number } return ( <div> <input type="text" value={name} onChange={e => setName(e.target.value)} /> <Child onButtonClick={addClick} data={data} /> </div> )}
在下面的代码中,子组件援用number对象的数据,当父组件的name对象的数据发生变化时,尽管子组件没有产生任何变动,它也会执行重绘操作。在我的项目开发中,为了防止这种不必要的子组件反复渲染,须要应用useMemo和useCallback进行包裹,如下所示。
import {memo, useCallback, useMemo, useState} from "react";function Child({ onButtonClick, data }) { return ( <button onClick={onButtonClick}>{data.number}</button> )}Child = memo(Child)function Example () { const [number, setNumber] = useState(0) const [name, setName] = useState('hello') const addClick = useCallback(() => setNumber(number + 1), [number]) const data = useMemo(() => ({ number }), [number]) return ( <div> <input type="text" value={name} onChange={e => setName(e.target.value)} /> <Child onButtonClick={addClick} data={data} /> </div> )}
其中,useMemo和useCallback是React Hook提供的两个API,次要用于缓存数据、优化晋升利用性能。它俩的共同点是,只有当依赖的数据发生变化时,才会调用回调函数去从新计算结果,不同点如下。
- useMemo:缓存的后果是回调函数返回的值。
- useCallback:缓存的是函数。因为函数式组件每当state发生变化,就会触发整个组件更新,当应用useCallback之后,一些没有必要更新的函数组件就会缓存起来。
在下面的示例中,咱们把函数对象和依赖项数组作为参数传入useMemo,因为应用了useMemo,所以只有当某个依赖项发生变化时才会从新计算缓存的值。通过useMemo和useCallback的优化解决后,能够无效防止每次渲染带来的性能开销。
2.2 useEffect
失常状况下,在React的函数组件的函数体中,网络申请、模块订阅以及DOM操作都属于副作用的领域,官网不倡议开发者在函数体中写这些副作用代码的,而Effect Hook就是专门用来解决这些副作用的。上面是应用类组件实现计数器的例子,副作用代码都写在componentDidMount和componentDidUpdate生命周期函数中,如下所示。
class Example extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; } componentDidMount() { document.title = `You clicked ${this.state.count} times`; } componentDidUpdate() { document.title = `You clicked ${this.state.count} times`; } render() { return ( <div> <p>You clicked {this.state.count} times</p> <button onClick={() => this.setState({ count: this.state.count + 1 })}> Click me </button> </div> ); }}
能够看到,componentDidMount和componentDidUpdate两个生命周期函数中的代码是一样的。之所以呈现同样的代码,是因为很多状况下,咱们心愿在组件加载和更新时执行同样的操作。从概念上说,咱们心愿能够对它进行合并解决,遗憾的是类组件病没有提供这样的办法。不过,当初应用Effect Hook就能够防止这种问题,如下所示。
import React, { useState, useEffect } from 'react';function Example() { const [count, setCount] = useState(0); useEffect(() => { document.title = `You clicked ${count} times`; }); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> );}
事实上,useEffect只会在每次DOM渲染后执行,因而不会阻塞页面的渲染。并且,useEffect同时具备componentDidMount、componentDidUpdate和componentWillUnmount等生命周期函数的执行机会。同时,咱们还能够应用useEffect在组件外部间接拜访state变量或是props,因而能够在useEffect执行函数值的更新操作。
在类组件中,通常会在componentDidMount生命周期中设置订阅音讯,并在componentWillUnmount生命周期中革除。例如,有一个ChatAPI模块,用来订阅好友的在线状态,如下所示。
class FriendStatus extends React.Component { constructor(props) { super(props); this.state = { isOnline: null }; this.handleStatusChange = this.handleStatusChange.bind(this); } componentDidMount() { ChatAPI.subscribeToFriendStatus( this.props.friend.id, this.handleStatusChange ); } componentWillUnmount() { ChatAPI.unsubscribeFromFriendStatus( this.props.friend.id, this.handleStatusChange ); } handleStatusChange(status) { this.setState({ isOnline: status.isOnline }); } render() { if (this.state.isOnline === null) { return 'Loading...'; } return this.state.isOnline ? 'Online' : 'Offline'; }}
能够发现,componentDidMount和componentWillUnmount是绝对应的,即在componentDidMount生命周期中的设置须要在componentWillUnmount生命周期中进行解除。不过,手动解决模块订阅是相当麻烦的,如果应用Effect Hook进行解决就会简略许多,如下所示。
import React, { useState, useEffect } from 'react';function FriendStatus(props) { const [isOnline, setIsOnline] = useState(null); useEffect(() => { function handleStatusChange(status) { setIsOnline(status.isOnline); } ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); return function cleanup() { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; }); if (isOnline === null) { return 'Loading...'; } return isOnline ? 'Online' : 'Offline';}
事实上,每个Effect都会返回一个革除函数,当useEffect的返回值是一个函数的时候,React会在组件卸载的时候执行一遍革除操作。useEffect会在每次渲染后执行,但有时候咱们心愿只有在state或props扭转的状况下才执行渲染,上面是类组件的写法。
if (prevState.count !== this.state.count) { document.title = `You clicked ${this.state.count} times`; }}
如果应用React Hook,只须要传入第二个参数即可,如下所示。
useEffect(() => { document.title = `You clicked ${count} times`;}, [count]);
能够发现,第二个参数是一个数组,能够将Effect用到的所有props和state都传进去。如果只须要在组件挂载和卸载时才执行,那么第二个参数能够传一个空数组。
除了useEffect外,useLayoutEffect也能够执行副作用和清理操作。不同之处在于,useEffect会在浏览器渲染实现后执行,而useLayoutEffect是在浏览器渲染前执行。
2.3 useContext
在类组件中,组件之间的数据共享是通过属性props来实现的。在函数组件中,因为没有构造函数constructor和属性props的概念,组件之间传递数据只能通过useContext来实现。
useContext是React Hook提供的跨级组件数据传递的一种形式,能够很不便的去订阅上下文的扭转,并在适合的时候从新渲染组件。useContext的应用形式如下。
const value = useContext(MyContext);
能够看到,useContext接管一个上下文对象context,并返回该上下文对象的以后值。以后上下文对象的值由下层组件中距离以后组件最近的 数据提供者决定。
useContext的次要作用就是实现组件之间的数据传递。首先,新建一个命名Example.js文件,并增加如下代码。
import { useState,createContext} from "react";const CountContext = createContext()function Example(){ const [ count , setCount ] = useState(0); return ( <div> <p>You clicked {count} times</p> <button onClick={()=>{setCount(count+1)}}>click me</button> <CountContext.Provider value={count}> </CountContext.Provider> </div> )}
在下面的代码中,咱们把count变量应用Provider包裹起来,即容许它实现跨层级组件值的传递,当父组件的count变量发生变化时子组件也会发生变化。
有了上下文变量之后,接下来就能够应用useContext接管上下文变量的值了。在Example.js文件中新建一个Counter组件,用来显示上下文对象count变量的值,代码如下。
function Counter(){ const count = useContext(CountContext) return (<h2>{count}</h2>)}
而后,咱们还须要在<CountContext.Provider>标签中引入Counter组件,如下所示。
<CountContext.Provider value={count}> <Counter/></CountContext.Provider>
能够发现,应用useContext形式在组件之间传递值时,须要应用Provider包裹须要传递的变量。
三 其余Hook
3.1 useReducer
家喻户晓,JavaScript的Redux状态治理框架由Action、Reducer和Store三个对象形成,而Reducer是惟一能够更新组件中State的路径。Reducer实质上是一个函数,它接管两个参数,状态和管制业务逻辑的判断参数,如下所示。
function countReducer(state, action) { switch(action.type) { case 'add': return state + 1; case 'sub': return state - 1; default: return state; }}
useReducer是React Hooks提供的一个函数,次要用来在某些简单的场景中替换useState。例如,蕴含简单逻辑的state且蕴含多个子值,或者前面的state依赖于后面的state等。useReducer的语法格局如下。
const [state, dispatch] = useReducer(reducer, initialArg, init);
能够发现,useReducer的应用形式与Redux状态框架时十分类似的,它接管一个形如(state, action) => newState的Reducer,并返回以后state以及dispatch办法。例如,上面是应用useReducer实现计数器的代码。
const initialState = {count: 0};function reducer(state, action) { switch (action.type) { case 'increment': return {count: state.count + 1}; case 'decrement': return {count: state.count - 1}; default: throw new Error(); }}function Counter() { const [state, dispatch] = useReducer(reducer, initialState); return ( <> Count: {state.count} <button onClick={() => dispatch({type: 'decrement'})}>-</button> <button onClick={() => dispatch({type: 'increment'})}>+</button> </> );}
有时候,须要惰性地创立初始state,那么只须要将初始化函数作为useReducer的第三个参数传入即可,如下所示。
function init(initialCount) { return {count: initialCount};}function reducer(state, action) { …. //省略其余代码}function Example({initialCount}) { const [state, dispatch] = useReducer(reducer, initialCount, init); return ( … //省略其余代码 );}
有时候,Reducer Hook的返回值与以后state的值是雷同的,此时须要跳过子组件的渲染及副作用的执行。
须要留神的是,因为React不会对组件树的深层节点进行不必要的渲染,所以不用放心跳过渲染后再次渲染该组件。如果为了防止在渲染期间执行高开销的计算,能够应用useMemo进行优化。
3.2 useMemo
在类组件中,每一次状态的更新都会触发组件树的从新绘制,而从新绘制必然会带来不必要的性能开销。同样,在函数组件中,为了防止useState每次渲染时带来的高开销计算,React Hook提供了useMemo函数。
useMemo之所以可能带来性能上的晋升,是因为在依赖不变的状况下,useMemo会返回雷同的援用,防止子组件进行无意义的反复渲染。例如,上面是一个一般的useState的应用示例。
function Example() { const [count, setCount] = useState(1); const [val, setValue] = useState(''); function expensive() { let sum = 0; for (let i = 0; i < count * 100; i++) { sum += i; } return sum; } return ( <> {count}:{expensive()} <button onClick={() => setCount(count + 1)}>+</button> <input value={val} onChange={event => setValue(event.target.value)}/> </> );}
在下面的示例中,无论是批改count还是val的值,都会触发expensive办法的执行。不过,因为expensive办法的执行只依赖于count的值,而在批改val值的时候是没有必要再次计算的。所以,为了防止这种不必要的计算,能够应用useMemo优化下面的代码,如下所示。
function Example() { const [count, setCount] = useState(1); const [val, setValue] = useState(''); const expensive = useMemo(() => { let sum = 0; for (let i = 0; i < count * 100; i++) { sum += i; } return sum; }, [count]); return ( <> {count}:{expensive()} <button onClick={() => setCount(count + 1)}>+</button> <input value={val} onChange={event => setValue(event.target.value)}/> </> );}
在下面的代码中,咱们应用useMemo来解决耗时计算,而后将计算结果传递给count并触发状态刷新。通过useMemo解决后,count只会在扭转的时候才会触发耗时计算执行状态刷新,而批改val则不会触发刷新。
3.3 useCallback
和useMemo一样,useCallback也是用来做性能优化的,即只有当依赖的数据发生变化时,才会调用回调函数从新计算结果。不同之处在于,useMemo次要用于缓存计算结果的值,而useCallback缓存的是函数。useCallback的语法格局如下。
const fnA = useCallback(fnB, [a])
在下面的语句中,useCallback会将传递给它的函数fnB返回,并且会将函数fnB的运行后果进行缓存。并且,当依赖a变更时还会返回新的函数。因为返回的是函数,无奈判断返回的函数是否产生变更,所以须要借助ES6新增的数据类型Set来辅助判断,如下所示。
function Example() { const [count, setCount] = useState(1); const [val, setVal] = useState(''); const callback = useCallback(() => { }, [count]); set.add(callback); return ( <div> {count}:{set.size} <div> <button onClick={() => setCount(count + 1)}>+</button> <input value={val} onChange={e => setVal(e.target.value)}/> </div> </div> );}
能够看到,每次批改count时set.size就会加1,而useCallback依赖变量count,所以每次count产生变更时就会返回一个新的函数。而val产生变更时set.size则不会发生变化,阐明它返回的是缓存的旧函数。
再看另外一个场景:有一个蕴含子组件的父组件,子组件会接管一个函数作为props。通常来说,如果父组件产生更新,那么子组件也会执行更新,但在大多数场景下,子组件的更新是没有必要的。此时咱们能够应用useCallback返回缓存的函数,并把这个缓存的函数作为props传递给子组件,如下所示。
function Parent() { const [count, setCount] = useState(1); const [val, setVal] = useState(''); const callback = useCallback(() => { return count;}, [count]); return ( <div> {count} <Child callback={callback}/> <div> <button onClick={() => setCount(count + 1)}>+</button> <input value={val} onChange={e => setVal(e.target.value)}/> </div> </div> );}function Child({callback}) { const [count, setCount] = useState(() => callback()); useEffect(() => { setCount(callback()); }, [callback]); return <div> {count} </div>}
事实上,useEffect、useMemo、useCallback都是自带闭包的。即每次组件渲染时,它们都会捕捉组件函数上下文中的状态信息,所以应用这三种Hook时,它们反映的都是以后的状态。如果要获取组件上一次的状态,那么能够应用ref来进行获取。
3.4 useRef
在React开发中,Ref次要作用是获取组件实例或者DOM元素,创立Ref次要有两种形式,即createRef和useRef。其中,应用createRef形式创立的Ref每次渲染都会返回一个新的援用,而useRef每次渲染都会返回雷同的援用。
应用createRef形式创立Ref次要是类组件。例如,上面是应用createRef()办法来创立Ref的例子,如下所示。
class Example extends React.Component { constructor(props) { super(props); this.myRef = React.createRef(); } componentDidMount() { this.myRef.current.focus(); } render() { return <input ref={this.myRef} type="text" />; }}
如果应用React Hooks的useRef()办法创立Ref,代码如下。
function Example() { const myRef = useRef(null); useEffect(() => { myRef.current.focus(); }, []) return <input ref={myRef} type="text" />;}
应用useRef形式创立的Ref返回一个援用的DOM对象,返回的对象将在组件的整个生存期内继续存在。
四、自定义Hook
通过自定义Hook,咱们能够将组件逻辑提取到可重用的函数中。在后面的聊天程序中,应用React Hook显示好友在线状态的代码如下所示。
import React, { useState, useEffect } from 'react';function FriendStatus(props) { const [isOnline, setIsOnline] = useState(null); useEffect(() => { function handleStatusChange(status) { setIsOnline(status.isOnline); } ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; }); if (isOnline === null) { return 'Loading...'; } return isOnline ? 'Online' : 'Offline';}
当初,假如聊天利用中有一个联系人列表,当用户在线时须要把名字设置为绿色。要实现这个需要,须要新建一个FriendListItem组件,并增加如下代码。
function FriendListItem(props) { …. //省略其余雷同的代码 return ( <li style={{ color: isOnline ? 'green' : 'black' }}> {props.friend.name} </li> );}
能够发现,FriendStatus和FriendListItem之间的状态逻辑根本是一样的。在类组件中,共享组件之间的状态逻辑有props和高阶组件两种形式。而在React Hook中,如果要共享两个函数之间的逻辑,能够自定义一个Hook来封装订阅的逻辑,如下所示。
import React, { useState, useEffect } from 'react';function useFriendStatus(friendID) { const [isOnline, setIsOnline] = useState(null); useEffect(() => { function handleStatusChange(status) { setIsOnline(status.isOnline); } ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange); }; }); return isOnline;}
在React中,自定义的Hook是一个以use结尾的函数,函数外部能够调用其余的Hook,并且自定义Hook时,入参和返回值都能够依据须要自定义,没有非凡的约定。
当初,须要共享的状态逻辑曾经被提取到useFriendStatus的自定义Hook中。而后,咱们就能够像应用一般函数一样调用自定义的Hook函数,如下所示。
function FriendStatus(props) { const isOnline = useFriendStatus(props.friend.id); if (isOnline === null) { return 'Loading...'; } return isOnline ? 'Online' : 'Offline';}function FriendListItem(props) { const isOnline = useFriendStatus(props.friend.id); return ( <li style={{ color: isOnline ? 'green' : 'black' }}> {props.friend.name} </li> );}
自定义Hook是一种重用状态逻辑的机制,所以每次应用自定义Hook时,所有state和副作用都是齐全隔离的。须要再次强调的是,自定义Hook时须要以use结尾来命名,这也是为了动态代码检测工具的检测。
五、Hook规定
Hook实质上来说就是一个JavaScript函数,然而在应用它时须要遵循两条规定。
- 只在最顶层应用Hook,不能在循环、条件或嵌套函数中调用Hook。
- 只能在React函数中调用Hook,不能在一般JavaScript函数中调用Hook。
Hooks的设计极度依赖事件定义的程序,如果在后序的渲染环节中Hooks的调用程序发生变化,就可能会呈现不可预知的问题。在React利用开发过程中,为了保障Hooks调用程序的稳定性,官网开发了一个名叫eslint-plugin-react-hooks的ESLint 插件来进行动态代码检测。应用前,须要先将此插件增加到React我的项目中,如下所示。
npm install eslint-plugin-react-hooks --save-dev
装置实现后,会在package.json配置文件中看到如下配置脚本。
{ "plugins": [ ... //省略其余插件包 "react-hooks" ], "rules": { ... //省略其余规定 "react-hooks/rules-of-hooks": "error", //查看Hook的规定 "react-hooks/exhaustive-deps": "warn" //查看effect的依赖 }}
通过下面的配置后,如果代码 不合乎Hook标准,那么零碎就会给出相应的正告,并提醒开发者进行对应的批改。