【JS】一文看懂React Hooks

一文看懂React Hooks

xiangzhihong发布于 今天 10:13

一、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规范,那么系统就会给出相应的警告,并提示开发者进行对应的修改。

javascript前端vue.js

阅读 54发布于 今天 10:13

本作品系原创,采用《署名-非商业性使用-禁止演绎 4.0 国际》许可协议

avatar

xiangzhihong

著有《React Native移动开发实战》1,2、《Kotlin入门与实战》《Weex跨平台开发实战》、《Flutter跨平台开发与实战》和《Android应用开发实战》

3.5k 声望

7.1k 粉丝

0 条评论

得票时间

avatar

xiangzhihong

著有《React Native移动开发实战》1,2、《Kotlin入门与实战》《Weex跨平台开发实战》、《Flutter跨平台开发与实战》和《Android应用开发实战》

3.5k 声望

7.1k 粉丝

宣传栏

一、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规范,那么系统就会给出相应的警告,并提示开发者进行对应的修改。

以上是 【JS】一文看懂React Hooks 的全部内容, 来源链接: www.h5w3.com/114453.html

回到顶部