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以后运行一些额外的操做。如:html
这些操做不须要清理,也就是说能够运行它们并当即忘记它们。下面咱们分别看看class和Hook是如何处理的react
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的时候执行了一样的代码。下面看看Hooks怎么处理的git
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作了什么?github
为何要在组件内部调用useEffect?npm
useEffect每次render后都执行吗?数组
如今咱们对effect有了必定的了解,下面的代码应该很容易懂了浏览器
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
复制代码
也许你会发现每次render传给useEffect的函数都不一样,这是有意为之的。实际上,这就是为何咱们即便在useEffect内部读取state也不用担忧state过时。每次re-render,咱们都会安排一个不一样的effect去取代以前的那个effect。在某种程度上,这使得effect更像是render的结果的一部分——每一个effect“属于”特定的render。网络
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的代码需“相互镜像”。生命周期方法迫使咱们拆分相互关联的逻辑函数
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';
}
复制代码
Note性能
反作用函数不必定要返回具名函数。
以前在使用Hooks的动机那一章就有提到,使用Hook的缘由之一是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
});
}
// ...
复制代码
使用Hooks如何解决这个问题呢?
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);
};
});
// ...
}
复制代码
若是你习惯使用class组件,你可能会很疑惑为何不是组件卸载的时候执行清理反作用的工做,而是在每次re-render的时候都要执行。下面咱们就看看为何
前面咱们介绍了一个示例FriendStatus组件,该组件显示朋友是否在线。咱们的类从this.props读取friend.id,在组件挂载以后订阅朋友状态,并在卸载前取消订阅。
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
复制代码
**可是若是在该组件还在显示的状态下,friend属性改变了怎么办?**组件显示的将是原来那个friend的在线状态。这是一个bug。而后后面取消订阅调用又会使用错误的friend ID,还会在卸载时致使内存泄漏或崩溃。
在class组件中,咱们须要添加componentDidUpdate来处理这种状况
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentDidUpdate(prevProps) {
// Unsubscribe from the previous friend.id
ChatAPI.unsubscribeFromFriendStatus(
prevProps.friend.id,
this.handleStatusChange
);
// Subscribe to the next friend.id
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
复制代码
忘记正确处理componentDidUpdate经常致使bug。
如今考虑使用Hooks实现这个组件
function FriendStatus(props) {
// ...
useEffect(() => {
// ...
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
复制代码
这下没bug了,即便咱们什么也没改
默认状况下useEffect会在应用下一个effect以前清除以前的effect。为了解释清楚,请看下面这个订阅和取消订阅的调用序列
// Mount with { friend: { id: 100 } } props
// Run first effect
ChatAPI.subscribeToFriendStatus(100, handleStatusChange);
// Update with { friend: { id: 200 } } props
// Clean up previous effect
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange);
// Run next effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange);
// Update with { friend: { id: 300 } } props
// Clean up previous effect
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange);
// Run next effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange);
// Unmount
// Clean up last effect
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange);
复制代码
默认状况下这种作法确保了逻辑连贯性,而且防止了经常在class组件里出现的由于忘写update逻辑而致使的bug
某些状况下,在每次渲染后清理或执行effect可能会产生性能问题。在class组件中,咱们能够经过在componentDidUpdate中编写与prevProps或prevState的比较来解决这个问题
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `You clicked ${this.state.count} times`;
}
}
复制代码
此要求很常见,它已内置到useEffect Hook API中。
若是从新渲染之间某些值没有改变,你能够告诉React跳过执行effect。只须要将一个数组做为可选的第二个参数传递给useEffect
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes
复制代码
在上面的例子中,咱们将[count]做为第二个参数传递。
这是什么意思?
若是数组中有多个项,即便其中一个项不一样,React也会从新执行这个effect。
对具备清理工做的effect一样适用
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
}, [props.friend.id]); // Only re-subscribe if props.friend.id changes
复制代码
未来,第二个参数可能会被构建时转换自动添加。
Note
- 若是你适用了这个优化,请确保数组包含了组件做用域内(例如props和state)effect用到的、随时间变化的全部值。不然代码可能引用了上一次render的那个过时的变量。Learn more about how to deal with functions and what to do when the array changes too often
- 若是仅执行effect并清理一次(在mount和unmount上),能够传递一个空数组([])做为第二个参数。
- 不要忘记React延迟执行行useEffect直到浏览器绘制完成,因此作额外的工做并非什么问题。
- 咱们推荐使用
exhaustive-deps
规则(这是eslint-plugin-react-hooks
的一部分)。它会在错误地指定依赖项时发出警告并建议修复。
下面咱们将了解钩子规则 - 它们对于使钩子工做相当重要。