什么是Hook?
Hook是React 16.8的新增特性。 它可以让你在不编写class的情况下使用state以及其他的React特性。 是一些可以让你在函数组件里“勾入” React state及生命周期等特性的函数。 Hook不能在class组件中使用,这使你不使用class也能使用React。
如何使用?
一个最简单的Hooks
首先我们来看一下,一个简单的有状态组件
代码语言:javascript复制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>
);
}
}
再来看一下使用hooks的版本
代码语言:javascript复制import { 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>
);
}
我们两者对比会很清楚的发现,使用Hooks与不使用Hooks的区别,后者比前者是更简单的。可以看到,Example是一个函数,但这个函数却拥有着自己的状态(count),同时它还可以更新自己的状态(setCount)。这个函数能这样写,是因为它使用了Hooks的useState这个Hook,这个Hook让函数变成了一个有状态的函数。 除了useState这个Hook外,还有很多别的Hook,比如useEffect提供了类似于componentDidMount等生命周期钩子的功能,useContext提供了上下文(context)的功能等等。 Hooks本质上就是一类特殊的函数,它们可以为你的函数型组件(function component)注入一些特殊的功能。
什么是useState?
useState是react自带的一个Hook函数,它的作用就是用来声明状态变量。useState这个函数接收的参数是我们的状态初始值(initial state),它返回了一个数组,这个数组的第[0]项是当前当前的状态值,第[1]项是可以改变状态值的方法函数。 所以就上方的例子我们做的事情其实就是,声明了一个状态变量count,把它的初始值设为0,同时提供了一个可以更改count的函数setCount。 下面来分解理解一下上方例子:
代码语言:javascript复制import React, { useState } from 'react';
function Example() {
// 声明一个叫 “count” 的 state 变量
const [count, setCount] = useState(0);
调用useState方法的时候做了什么?
它定义一个 “state 变量”。我们的变量叫count,但是我们可以叫它任何名字,比如banana。这是一种在函数调用时保存变量的方式,useState是一种新方法,它与class里面的this.state提供的功能完全相同。一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被React保留。
useState需要哪些参数?
useState() 方法里面唯一的参数就是初始state。不同于class的是,我们可以按照需要使用数字或字符串对其进行赋值,而不一定是对象。在示例中,只需使用数字来记录用户点击次数,所以我们传了 0 作为变量的初始 state。
useState方法的返回值是什么?
返回值为当前state以及更新state的函数。所以这就是我们写下方这段代码的原因。
代码语言:javascript复制const [count, setCount] = useState()
这与class里面this.state.count和this.setState类似,唯一区别就是你需要成对的获取它们。
读取State
当在class中显示当前的count,是通过this.state.count:
代码语言:javascript复制<p>You clicked {this.state.count} times</p>
在函数中,可以直接用count:
代码语言:javascript复制<p>You clicked {count} times</p>
更新State
在class中,需要调用this.setState()来更新count值:
代码语言:javascript复制<button onClick={() => this.setState({ count: this.state.count 1 })}>
Click me
</button>
在函数中,已经有了setCount和count变量,所以不需要this:
代码语言:javascript复制<button onClick={() => setCount(count 1)}>
Click me
</button>
什么是Effect Hooks?
在上一节的例子中增加一个新功能:
代码语言:javascript复制import { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// 类似于componentDidMount 和 componentDidUpdate:
useEffect(() => {
// 更新文档的标题
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count 1)}>
Click me
</button>
</div>
);
}
对比着看一下,如果没有hooks,应该怎么写?
代码语言:javascript复制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>
);
}
}
写的有状态组件,通常会产生很多的副作用(side effect),比如发起ajax请求获取数据,添加一些监听的绑定和取消绑定,手动修改dom等等。我们之前都把这些副作用的函数写在生命周期函数钩子里,比如componentDidMount,componentDidUpdate和componentWillUnmount。而现在的useEffect就相当与这些声明周期函数钩子的集合体。 同时,由于前文所说hooks可以反复多次使用,相互独立。所以我们合理的做法是,给每一个副作用一个单独的useEffect钩子。这样一来,这些副作用不再一股脑堆在生命周期钩子里,代码变得更加清晰。
useEffect做了什么?
通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。在这个 effect 中,我们设置了 document 的 title 属性,不过我们也可以执行数据获取或调用其他命令式的 API。
为什么在组件内部调用useEffect?
将useEffect放在组件内部让我们可以在effect中直接访问count state变量(或其他props)。我们不需要特殊的 API 来读取它,它已经保存在函数作用域中。Hook使用了JavaScript的闭包机制,而不用在JavaScript已经提供了解决方案的情况下,还引入特定的React API。
useEffect如何取消绑定一些副作用?
这种场景很常见,当我们在componentDidMount里添加了一个绑定,我们得马上在componentWillUnmount中,也就是组件被注销之前清除掉我们添加的绑定,否则内存泄漏的问题就出现了。
当不使用Hook useEffect的示例
代码语言:javascript复制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之间相互对应。使用生命周期函数迫使我们拆分这些逻辑代码,即使这两部分代码都作用于相同的副作用。
使用Hook useEffect的示例
代码语言:javascript复制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);
// 一定注意下这个顺序:告诉react在下次重新渲染组件之后,同时是下次调用ChatAPI.subscribeToFriendStatus之前执行cleanup
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
这里有一个点需要重视!这种解绑的模式跟componentWillUnmount不一样。componentWillUnmount只会在组件被销毁前执行一次而已,而useEffect里的函数,每次组件渲染后都会执行一遍,包括副作用函数返回的这个清理函数也会重新执行一遍。
为什么要在effect中返回一个函数?
这是effect可选的清除机制。每个effect都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。
React何时清除effect?
React会在组件卸载的时候执行清除操作。正如之前学到的,effect在每次渲染的时候都会执行。这就是为什么React会在执行当前effect之前对上一个effect进行清除。
为什么要让副作用函数每次组件更新都执行一遍?
先看以前的模式:
代码语言:javascript复制componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
很清楚,在componentDidMount绑定,再在componentWillUnmount取消绑定。但假如这时候props.friend.id变了怎么办?我们不得不再添加一个componentDidUpdate来处理这种情况:
代码语言:javascript复制componentDidUpdate(prevProps) {
// 先把上一个friend.id解绑
ChatAPI.unsubscribeFromFriendStatus(
prevProps.friend.id,
this.handleStatusChange
);
// 再重新注册新但friend.id
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
看,很繁琐对吧。所以useEffect没这个问题,因为它在每次组件更新后都会重新执行一遍。
那如何跳过一些不必要的副作用函数呢?
按照上一节的思路,每次重新渲染都要执行一遍这些副作用函数,显然是不经济的。怎么跳过一些不必要的计算呢?我们只需要给useEffect传第二个参数即可。用第二个参数来告诉react只有当这个参数的值发生改变时,才执行我们传的副作用函数(第一个参数)。
代码语言:javascript复制useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 只有当count的值发生变化时,才会重新执行`document.title`这一句
当第二个参数传一个空数组[]时,其实就相当于只在首次渲染的时候执行。也就是componentDidMount加componentWillUnmount的模式。不过这种用法可能会带来bug,建议少用。
如何自定义的Effect Hooks?
为什么要自己去写一个Effect Hooks? 因为这样我们才能把可以复用的逻辑抽离出来,变成一个个可以随意调用的代码块,哪个组件要用,就可以调用在哪个组件里! 比如我们可以把上面写的FriendStatus组件中判断朋友是否在线的功能抽出来,新建一个useFriendStatus的Hook专门用来判断某个id是否在线。
代码语言:javascript复制import { useState, useEffect } from 'react';
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
useEffect(() => {
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
这时候FriendStatus组件就可以简写为:
代码语言:javascript复制function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
这样抽离出来公共代码,就能够更加满足复用性。 假如这个时候我们又有一个朋友列表也需要显示是否在线的信息:
代码语言:javascript复制function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
}
还有哪些自带的Effect Hooks?
除了上文重点介绍的useState和useEffect,react还给我们提供了很多有用的Hooks:
- useContext
- useReducer
- useCallback
- useMemo
- useRef
- useImperativeMethods
- useMutationEffect
- useLayoutEffect
这里,我就不再一一介绍了,大家可以自行查阅官方文档。