Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
什么是Hook?
Hook 是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数。Hook 不能在 class 组件中使用 —— 这使得你不使用 class 也能使用 React。
React 内置了一些像 useState 这样的 Hook。你也可以创建你自己的 Hook 来复用不同组件之间的状态逻辑。
Hook使用规则
只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用(除自定义的Hook外)。
State Hook
通常,在class组件中,我们会使用this.state初始化state,例如:
class Example extends React.Component {
constructor(props) {
super(props);
// 将state初始化为一个对象: {count: 0}
this.state = {
count: 0
};
}
在Hook之前,function组件由于没有this,无法通过this.state的方式获得state。因此当我们发现当前的组件需要用到state时,我们只能将该组件改写为class组件。
Hook为我们提供了一个在function组件中使用state的途径。
import React, { useState } from 'react';
function Example() {
// 声明一个叫 “count” 的 state 变量,该变量的初始值为0
const [count, setCount] = useState(0);
useState方法的工作原理是:定义一个state变量,该变量在useState方法结束之后依然会被React保留。接下来会通过分析该方法的返回值和参数,详细解释该方法的使用方式。
参数:useState会接收一个参数,这个参数就是该方法创建的state变量的初始值。这个初始值可以是数字或字符串或者是对象。在示例中,这个初始值为0。
返回值:useState方法会提供两个返回值,第一个是创建的state变量的名称,另一个是更新state变量的函数。这个函数与this.setState类似。比如:
// class组件中,更改state的方法
onClick={() => this.setState({ count: this.state.count + 1 })}
// function组件中,用Hook更新state
onClick={() => setCount(count + 1)}
- 读取state:
// class:
You clicked {this.state.count} times
// hook:
You clicked {count} times
Example: 计时器
class
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
render() {
return (
You clicked {this.state.count} times
);
}
}
Hook
import React, { useState } from 'react';
function Example() {
// 声明一个叫 "count" 的 state 变量
const [count, setCount] = useState(0);
return (
You clicked {count} times
);
}
Effect Hook
React的class组件提供的生命周期方法能让我们在dom的不同生命周期,调用一些额外的代码。比如数据获取,设置订阅以及手动更改 React 组件中的DOM,这些额外的代码被称为副作用。
副作用分两种:需要清除的和不需要清除的。这两种副作用在使用hook的过程中,有一些区别。
无需清除的effect
常见的无需清除的effect有发送网络请求,手动变更 DOM,记录日志等。我们在执行完这些操作后,就可以忽略它们了。
class组件中,render函数是不应该有任何副作用的,因此我们会将副作用操作放到对应的生命周期里。比如,在上文的计数器示例中,我们想要为计数器增加一个小功能:将 document 的 title 设置为包含了点击次数的消息。
在class组件中的实现应该是:
// 在组建加载时
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
}
// 在组件更新时
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
而如果我们使用hook的实现应该是:
useEffect(() => {
document.title = `You clicked ${count} times`;
});
useEffect就是hook为我们提供的代替生命周期的方法,它的工作原理是,React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。
值得注意的是,与生命周期函数区分开挂载与更新不同,useEffect中的方法,会在dom的每一次变更之后执行,不管是第一次,还是第n次。
需要清除的effect
在开发中,为了防止敏感信息的泄露哦,有一些副作用是需要清除的,例如订阅外部数据源。
如果在计数器中,加入一个订阅好友在线状态的新功能。当我们
在class中,清除的操作可以如下实现:
// 在挂载时,调用 订阅好友的在线状态 方法
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
// 在即将销毁时,调用 取消订阅 方法
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
可以看到,不管是在挂载还是销毁时,调用的逻辑代码是重复的。
相应的,使用hook的实现方法如下:
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);
};
});
当DOM更新时,调用一个方法,这个方法中调用了handleStatusChange和订阅方法,返回一个cleanup方法。这个返回的cleanup方法会在每一次DOM变化后清除操作时被调用。
Example:显示好友状态的计数器
功能:
- 当点击“Click me”时,更新state,并且在页面展示和title中显示更新后的state;
- 从props中读取friend.id,然后在组件挂载后订阅好友的状态,并在卸载组件的时候取消订阅。
class
class FriendStatusWithCounter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0, isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
handleStatusChange(status) {
this.setState({
isOnline: status.isOnline
});
}
render() {
return (
You clicked {this.state.count} times
);
}
}
hook
function FriendStatusWithCounter(props) {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
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);
};
});
return (
You clicked {count} times
);
}
通过跳过 Effect 进行性能优化
在上述示例中, state只有一个变量count,但是在现实中,state可能会有很多歌变量,而如果任何一个变量的变化都会重新调用useEffect的话,可能会导致一些性能问题。在这种时候,我们希望这个useEffect函数在且只在它依赖的变量发生变化时被调用。
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新
对于有清除操作的 effect 同样适用:
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
}, [props.friend.id]); // 仅在 props.friend.id 发生变化时,重新订阅
useEffect中,第二个参数决定了是否再次执行该effect。
- 当第二个参数是空数组【】时,effect仅会在挂载和销毁时执行;
- 当第二个参数为空时,任何state变量的变化都会导致effect的再次执行;
- 当第二个参数为不为空的数组时,effect会在数组内的任意变量发生变化时执行。
我终于学完了~~~~~
References:
React官方文档:
- Hook概览
- 使用State Hook
- 使用Effect Hook