一、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文件,并添加如下代码。