函数的高级特性
经过四节课的洗礼,你应该已经对Ract的用法有了一个比较好的理解,这节课我们来全面的聊一聊函数组件的高级特性 Hooks
, Hook是函数组件所特有的功能。 请在src目录下创建目录:Test05
Hooks的引用
React
中所有的Hooks都是以use开头的一种函数,但是它是符合 React
的设计规则,能实现特定的功能的。下面详细介绍Hooks
的用法及如何自定义Hooks
.
useState
这个 Hooks 是用来在函数组件中实现状态的管理。比如,下面的示例表示通过 useState 来管理和记录单击的次数。
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>
);
}
解构出来的变量有两个: count
和 setCount
, useState
根据指定的值初始化状态 count
, 更改count
只能通过setCount
来更改,我们给button
的onClick
事件传递了一个匿名的箭头函数,在这个函数里调用了 setCount(count + 1)
来改变状太变量 count
, 从而实现了组件UI
的更新。只要组件检测到任何的状态的变化,就会立即更新相应的UI
。 状态也可以同时声名多个,如下所示:
function ExampleWithManyStates() {
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
}
数组的解构语法允许我们通过调用useState来给我们声明的状态变量提供不同的名称。
useEffect
useEffect官方说法是为函数组件提供执行副作用的功能。 这句话很抽象。相当于把类组件中的三个生命周期(componentDidMount
、componentDidUpdate
、componentWillUnmount
)合为一处的作法。比如:
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
// 其它的更新操作
document.title = `你单击了 ${count} 次`;
});
return (
<div>
<p>你单击了 {count} 次</p>
<button onClick={() => setCount(count + 1)}>
单击这里
</button>
</div>
);
}
上面的useEffect 会在组件 componentDidMount
和 componentDidUpdate
时执行。当你调用 useEffect
时,相当于告诉 React
在刷新对 DOM 的更改后运行你的 “effect” 函数。 useEffect是在组件内部声明的,因此它们可以访问组件的 props
和状态。默认情况下,React
在每次渲染后(包括第一次渲染)都会运行useEffect。那么如何让它执行清理
工作呢, 就像我们在类组件中的生命周期 componentWillUnmount
中写的代码一样。 看下面的示例:
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
上面是一个显示好友的在线状态的一个组件。 假定, 有一个对象 ChatAPI, 它提供一个订阅好友在线状态的函数功能 subscribeToFriendStatus
, 同时它也提供了一个去除订阅的一个功能函数:unsubscribeFromFriendStatus
, 我们希望我们组件加载后就开始订阅好友的状态,当组件卸载时就要清除订阅功能。useEffect 是一个高阶函数, 它的参数是要传入一个函数, 这个函数要么返回 null, 要么返回一个函数。 在上面的示例中,我们传入了一个箭头函数,这个箭头函数返回了另一个函数。 说到这儿或放你已经猜了它的精妙之所在了,对,我们传入的这个函数的函数体内的代码会在组件的 加载
和 更新
这两个生命周期内执行。而这个 返回的函数(比如上面的 retun () => { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }
)就是在组件卸载时执行的过程。 所以,上面的示例实现的功能是 在 FriendStatus 组件被加载后 执行 ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange)
, 当这个组件 卸载后执行 ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange)
去除订阅功能。 这样你能理解了吧。你在useEffect参数中返回了一函数,那么在组件卸载时执行它。就这么简单。 和useState一样,你也可以在组件中使用多个useEffect, 这样你可以更好的分割功能块,让代码更清晰明了。如下所示。
function FriendStatusWithCounter(props) {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
// ...
使用Hooks规则
Hooks
是JavaScript
函数,但它们施加了两个额外的规则:
- 仅在顶层调用
Hooks
。不要在循环、条件或嵌套函数中调用 Hook。 - 仅从 React 函数组件调用 Hooks。不要从常规 JavaScript 函数调用 Hooks。
- 还有一个有效的地方可以调用 Hooks —— 我们自己的自定义 Hooks。
自定义 Hooks
有时,我们希望在组件之间重用一些有状态逻辑。以上面的示例为例,假设我们还想在另一个组件中重用订阅逻辑,我们也许会将这个逻辑提到到一个名为:useFriendStatus。
import React, { 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;
}
它根据参数friendID
,返回我们的好友是否在线的一个功能。现在我们可以从两个组件中使用它:
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>
);
}
每个组件的状态是完全独立的。Hooks是一种重用状态逻辑的方法,而不是状态本身。事实上,对Hook
的每次调用都有一个完全隔离的状态,因此我们甚至可以在一个组件多次使用相同的自定义Hook
。
自定义Hook
与其说是功能,不如说是一种约定。如果一个函数的名称以 “use” 开头,并且它调用了其他Hook
,我们说它是一个自定义的Hook
。这种强制命名约定是我们的React
内部插件能够使用Hooks
在代码中查找错误的方式。
useState的相关补充
看下面这个示例
import React, { useState } from 'react';
function Example() {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
我们将通过将此代码与等效类示例进行比较来开始学习
等效的类示例组件
我们前面的章节已经介绍过类组件的创建方法了。所以下面的示例你应该能理解
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 和 函数组件
再次强调,Hooks在类组件中不起作用。在函数的组件中使用 Hooks 合法位置:
const Example = (props) => {
// 我们可认在这儿使用 Hooks!
return <div />;
}
function Example(props) {
// 我们可认在这儿使用 Hooks!
return <div />;
}
什么是Hooks
我们的新示例首先从 React 导入 Hook:useState
import React, { useState } from 'react';
function Example() {
// ...
}
Hook 是一个特殊的函数,可以让你“挂钩”到 React 功能。例如,useState是一个 Hook,可让您将 React 状态添加到函数组件。
我什么时候会使用 Hook?如果你编写了一个函数组件,并意识到你需要向它添加一些状态,那么以前你必须把它转换为一个类。现在,您可以在现有函数组件中使用 Hook。
state的声明
在类组件中,我们必须在构造函数中声明
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
...
}
在函数组件中,我们没有this
,因此我们无法赋值或读取this.state
。取而代之的是useState
,我们直接在组件中调用 Hook:
import React, { useState } from 'react';
function Example() {
// 定义一个新的状态变量
const [count, setCount] = useState(0);
...
}
1. 调用 useState
有什么作用?
它声明了一个“状态变量count”。我们的变量是被调用的,但我们可以称它为其他任何变量,比如 banana
。这是一种在函数调用之间“保留”某些值的方法,useState
是一种使用与类中提供的this.state
完全相同的功能的新方法。通常,当函数退出时,变量会“消失”,但 React 会保留状态变量。
2. 我们传递给 useState 作为参数的是什么?
Hook 的唯一参数是初始状态。与类不同,状态不必是对象。
3. useState 返回什么?
它返回一对值:当前状态
和更新它的函数
。这就是我们写const [count, setCount] = useState()
, 这与类中this.state.类似,只是你把它们count
、 this.setState
成对地得到。
import React, { useState } from 'react';
function Example() {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0);
...
}
我们声明一个名为 count
的状态变量,并将其设置为 0 。React 将在重新渲染之间记住它的当前值,并为我们的函数提供最新的值。如果我们想更新当前的count
,我们可以调用 setCount
读取状态
当我们想显示一个类中的当前计数时,我们读到this.state.count
:
<p>You clicked {this.state.count} times</p>
在一个函数中,我们可以直接使用count
:
<p>You clicked {count} times</p>
更新状态
在一个类中,我们需要调用this.setState()
来更新状态count
:
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
在函数中,我们已经有 setCount
和 count
作为变量,所以我们不需要this
:
<button onClick={() => setCount(count + 1)}>
Click me
</button>
回顾
现在让我们逐行回顾一下我们学到的东西,并检查我们的理解。
1: import React, { useState } from 'react';
2:
3: function Example() {
4: const [count, setCount] = useState(0);
5:
6: return (
7: <div>
8: <p>You clicked {count} times</p>
9: <button onClick={() => setCount(count + 1)}>
10: Click me
11: </button>
12: </div>
13: );
14: }
- 第 1 行:我们从 React 导入 Hook。
useState
它允许我们在函数组件中保持本地状态。 - 第 4 行:在组件
Example
内部,我们通过调用useState
声明一个新的状态变量。它返回一对值,我们为其命名为count
和setCount
。我们之所以调用变量,是因为它包含按钮点击次数。我们通过作为唯一参数将其初始化为 0 。第二个返回项本身就是一个函数。它让我们更新count
,所以我们将其命名为setCount
- 第 9 行:当用户单击时,我们使用一个新值进行调用
setCount
。然后,React 将重新渲染组件Example
,并将新值count
传递给它。
乍一看,这似乎有很多东西要接受。不要着急!如果您迷失在解释中,请再次查看上面的代码并尝试从上到下阅读它。我们保证,一旦你试图“忘记”状态在类中是如何工作的时候,并用新的眼光看待这段代码时,它就会有很有意义。
提示:方括号是什么意思?
当我们声明状态变量时,您可能已经注意到了方括号:
const [count, setCount] = useState(0);
左边的名字不是 React API 的一部分。您可以命名自己的状态变量:
const [fruit, setFruit] = useState('banana');
这种JavaScript
语法称为“数组解构”。这意味着我们正在创建两个新变量,其中设置为fruit
返回的第一个值,并且setFruit
是第二个值。它等效于以下代码
var fruitStateVariable = useState('banana'); // 返回一对值
var fruit = fruitStateVariable[0]; // 第一个值
var setFruit = fruitStateVariable[1]; // 第二个值
useEffect的相关补充
Effect Hook
允许您在函数组件中执行副作用:
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
此代码段基于上一页中的计数器示例,但我们添加了一项新功能:我们将文档标题设置为包含单击次数的自定义消息。
在 React 组件中获取数据、设置订阅和手动更改 DOM 都是副作用的例子。无论你是否习惯于将这些操作称为“副作用” 。
React 组件中有两种常见的副作用:不需要清理的副作用和需要清理的副作用。让我们更详细地看一下这种区别
有时,我们想在 React 更新 DOM 后运行一些额外的代码。网络请求、手动 DOM 突变和日志记录是不需要清理的常见效果示例。我们之所以这样说,是因为我们可以运行它们并立即忘记它们。让我们比较一下类和钩子如何让我们表达这种副作用。
没有清理的效果类示例
在 React 类组件中,render
方法本身不应该引起副作用。现在还为时过早——我们通常希望在 React 更新 DOM 之后执行我们的效果, 这就是为什么在React类中,我们将副作用放入componentDidmount
和componentDidupdate
中。回到我们的示例,这是一个React Counter
类组件,在React
后立即更新文档标题对DOM进行更改:
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>
);
}
}
请注意,我们必须在类组件中的这两种生命周期方法之间复制代码。
这是因为在许多情况下,我们都希望执行相同的副作用,而不管组件是否已经加载,还是已更新
现在,让我们看看如何使用useEffect
做同样的事情。
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有什么作用?
通过使用此Hook,您可以告诉React,渲染后您的组件需要做点什么。React会记住您传递的功能(我们将其称为“effect”),并在执行DOM更新后回调。在此effect中,我们设置了文档标题.
为什么在组件中调用useEffect?
将useEffect放在组件中,使我们可以从中访问count
状态变量。我们不需要特殊的API来阅读它 - 它已经在功能范围中了。
每次渲染后useEffect会运行吗?
是的!默认情况下,它在第一个渲染和每个更新之后都运行。
详细说明
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
}
我们声明了状态变量count
,然后告诉 React
我们需要useEffect
。我们将一个函数传递给 Hook。我们传递的这个函数就是我们的效果
。在我们的效果
中,我们使用浏览器 API 设置文档标题。我们可以读取最新的效果
,因为它在我们的功能范围内。当 React
渲染我们的组件时,它会记住我们使用的效果
,然后在更新 DOM 后运行我们的效果
。每次渲染都会发生这种情况,包括第一次渲染。
有经验的 JavaScript 开发人员可能会注意到,传递给的函数在每次渲染时都会有所不同。这是有意为之的。事实上,这就是让我们从useEffect
内部读取值而不必担心它变得陈旧的原因。每次重新渲染时,我们都会安排不同的效果
,替换前一个效果
。在某种程度上,这使得效果
的行为更像是渲染结果的一部分 —— 每个效果
都“属于”一个特定的渲染。
清理副作用类示例
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';
}
}
使用useEffect示例
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);
// Specify how to clean up after this effect:
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
为什么我们从效应中返回一个函数?
这是useEffect的可选清理机制。每个effect都可能返回一个函数,该函数在它之后进行清理。这样,我们就可以使添加和删除订阅的逻辑彼此接近。它们是相同effect的一部分!
React 究竟什么时候清理效果?
React 在组件卸载时执行清理。但是,正如我们之前所了解的,效果针对每个渲染运行,而不仅仅是一次。这就是为什么 React 在下次运行效果之前也会清理上一次渲染中的效果。