React 16.8.0 是第一个支持 Hook 的版本。多数情况下,不可能将组建拆分为更小的粒度,因为状态逻辑无处不在。这个也给测试带来了一定挑战,这也是很多人将React与状态管理库结合使用的原因之一。为了解决这个问题,Hook将组建中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照声明周期划分。为了解决这些问题,Hook 使你在非 class 的情况下可以使用更多的 React 特性。 从概念上讲,React 组件一直更像是函数。而 Hook 则拥抱了函数,同时也没有牺牲 React 的精神原则。Hook 提供了问题的解决方案,无需学习复杂的函数式或响应式编程技术。
关于计数器组件(通过点击按钮,增加点击次数),可以比较一下用class的方式和用state方式的区别。
下面代码是class代码的方式:按钮的点击事件里面只可以用箭头函数访问,如果用普通的function,会有问题。
import React, { useState } from ‘react‘;
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
render() {
return (
<div>
<p>You click {this.state.count} times.</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>click me </button>
{/* <button onClick={ function() { this.setState({ count: this.state.count + 1 }) }}>click me </button> */}
</div>
)
}
}
export default Example;
当每次按钮点击事件发生时,只有render函数会被再次渲染执行。
import React, { useState } from ‘react‘;
function Example() {
const [count, setCount] = useState(0);
return (
<div>
<p>You click {count} times</p>
<button onClick={() => setCount(count + 1)}>click me</button>
{/* <button onClick={function () { setCount(count + 1) }}>click me</button> */}
</div>
)
}
export default Example;
当每次按钮点击事件发生时,整个Example函数都要被重新执行,界面也会被重新渲染。
上面的代码是通过使用state hook的方式来实现这个功能。可以看出,代码变得简单了很多,在按钮的点击事件里面是可以使用普通的function函数和箭头函数来访问state hook里面的函数和变量的。
useState
需要哪些参数 **useState唯一的参数就是初始state,这里的state不一定要是一个对象。这个初始state参数只有在第一次渲染时会被用到。**那什么是hook呢?Hook是一些可以让你在函数组件里‘钓入’React state及声明周期等特性的函数。**Hook不能在class组件中使用。useState是react内置的hook,你也可以创建自己的hook来复用不同组件的逻辑。
什么时候我会用 Hook? 如果你在编写函数组件并意识到需要向其添加一些 state,以前的做法是必须将其它转化为 class。现在你可以在现有的函数组件中使用 Hook。
import React, { useState } from ‘react‘;
function Example() {
const [count, setCount] = useState(0);
const [count2, setCount2] = useState(20);
const [count3, setCount3] = useState(20);
return (
<div>
<p>You click {count} times</p>
<button onClick={() => setCount(count + 1)}>click me</button>
<p>You click {count2} times</p>
<button onClick={() => setCount2(count2 + 1)}>click me</button>
<p>You click {count3} times</p>
<button onClick={() => setCount3(count3 + 1)}>click me</button>
</div>
)
}
export default Example;
如上面的代码,声明了多个state变量,以及按钮,运行的界面如下,可以看出3个按钮的状态互不相关,是相互分离的,每次点击按钮,只能让各按钮对应的次数发生变化。setCount里面就直接要修改的变量的值。
You click 4 times
click me
You click 24 times
click me
You click 25 times
click me
下面的代码中使用了Effect Hook,增加了一个小功能,计数器组件中的按钮每点击一次,title会被设置为包含了点击次数的消息。
import React, { useState } from ‘react‘;
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You click {count} times</p>
<button onClick={() => setCount(count + 1)}>click me</button>
</div>
)
}
export default Example;
Effect Hook可以让你在函数组件中执行副作用操作。什么是副作用呢?数据获取,设置订阅以及更改React组件中的DOM都属于副作用。
如果你熟悉 React class 的生命周期函数,你可以把 useEffect
Hook 看做 componentDidMount
,componentDidUpdate
和 componentWillUnmount
这三个函数的组合。
在 React 组件中有两种常见副作用操作:需要清除的和不需要清除的。
有时候,我们只想在react更新DOM之后运行一些额外的代码。比如发送网络请求,手动变更DOM,记录日志等等,这些都是常见的无需清除的操作。因为执行完这些操作,就可以忽略他们了。对比一下class和hook都是怎么实现这些副作用的。
在React的组件中,render函数是不应该有任何副作用的。一般来说,在这里执行操作太早了,我们基本都希望在React更新DOM之后,才执行我们的操作。
这就是为什么在React Class中,我们把副作用操作放到componentDidMount
和 componentDidUpdate
函数中。看下面的代码,通过调试发现,它在react对DOM进行操作之后,立即更新了document的title属性。
mport React, { useState, useEffect } from ‘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 click {this.state.count} times.</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>click me </button>
{/* <button onClick={ function() { this.setState({ count: this.state.count + 1 }) }}>click me </button> */}
</div>
)
}
}
export default Example;
调试发现,页面按钮在第一次加载时,会调用到componentDidMount()函数,之后如果按钮被点击之后,render函数会被重新执行,componentDidUpdate函数也会被调用来更新title,componentDidMount就不会被再次调用了。
在class中,我们需要在2个声明周期函数中编写重复的代码。这是因为很多情况下,我们希望在组件加载和更新时执行同样的操作。从概念上,我们希望组件在每次渲染之后执行-但是React的class组件没有提供这样的方法
import React, { useState } from ‘react‘;
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You click {count} times</p>
<button onClick={() => setCount(count + 1)}>click me</button>
</div>
)
}
export default Example;
因为代码调试的发现,每次dom被刷新的时候,Example都会被再次执行,所以每次useEffect 函数也会被重新执行。
useEffect
做了什么? 通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。在这个 effect 中,我们设置了 document 的 title 属性,不过我们也可以执行数据获取或调用其他命令式的 API。
为什么在组件内部调用 useEffect
? 将 useEffect
放在组件内部让我们可以在 effect 中直接访问 count
state 变量(或其他 props)。我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中。Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的情况下,还引入特定的 React API。
useEffect
会在每次渲染后都执行吗? 是的,默认情况下,它在第一次渲染之后和每次更新之后都会执行。可能会更容易接受 effect 发生在“渲染之后”这种概念,不用再去考虑“挂载”还是“更新”。React 保证了每次运行 effect 的同时,DOM 都已经更新完毕。
上面的例子,我们研究了如何使用不需要清除的副作用,还有一些副作用是需要清除的,例如订阅外部数据源,这种情况下,清除工作比较重要,可以防止内存泄漏。比较一下class和hook如何实现。例如,假设我们有一个 ChatAPI
模块,它允许我们订阅好友的在线状态。以下是我们如何使用 class 订阅和显示该状态:
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 之间相互对应,使用声明周期函数迫使我们拆分这些逻辑代码,即使这2部分代码都作用于相同的副作用。
你可能认为需要单独的effect来执行清除操作,但是由于添加和删除订阅的代码的紧密性,所以useEffect的设计是在同一个地方执行。如果你的effect返回一个函数,React将在执行清除操作时调用她。
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‘;
}
为什么要在effect中返回一个函数??这是effect可选的清除机制,每个effet都可以返回一个清除函数,如此可以将添加和移除订阅的逻辑房子放在一起。它们都属于effect的一部分。
react何时清除effect? ??react会在组件卸载的时候执行清除操作。正如上面所说的,effect在每次渲染的时候都会执行。
并不是必须为effect中返回的函数命名。其实也可以返回一个箭头函数或者其他的名字。
使用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
});
}
// ...
看代码就会发现,代码显得很乱。
但是使用hook代码就会看起来简单很多,如下面的代码,就像可以使用多个state的hook一样,可以使用多个effect,这会将不相关逻辑分离到不同的effect中。
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);
};
});
// ...
}
Hook允许我们按照代码的用途分离他们,而不是像声明周期函数那样。React按照effect声明的顺序依次调用组件中每一个effect。
如果已经习惯了使用class,那你会疑惑为什么effct的清除阶段在每次重新渲染时都会执行,而不是只在卸载组件的时候执行依次。看一个实际例子,就会更加清楚一点。
在本章节开始时,我们介绍了一个用于显示好友是否在线的 FriendStatus
组件。从 class 中 props 读取 friend.id
,然后在组件挂载后订阅好友的状态,并在卸载组件的时候取消订阅:
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
但是当组件已经显示在屏幕上时,friend
prop 发生变化时会发生什么? 我们的组件将继续展示原来的好友状态。这是一个 bug。而且我们还会因为取消订阅时使用错误的好友 ID 导致内存泄露或崩溃的问题。
在 class 组件中,我们需要添加 componentDidUpdate
来解决这个问题:
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentDidUpdate(prevProps) {
// 取消订阅之前的 friend.id
ChatAPI.unsubscribeFromFriendStatus(
prevProps.friend.id,
this.handleStatusChange
);
// 订阅新的 friend.id
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
忘记正确地处理 componentDidUpdate
是 React 应用中常见的 bug 来源。
现在看一下使用 Hook 的版本:
function FriendStatus(props) {
// ...
useEffect(() => {
// ...
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
并不需要特定的代码来处理更新逻辑,因为 useEffect
默认就会处理。它会在调用一个新的 effect 之前对前一个 effect 进行清理。为了说明这一点,下面按时间列出一个可能会产生的订阅和取消订阅操作调用序列:
// Mount with { friend: { id: 100 } } props
ChatAPI.subscribeToFriendStatus(100, handleStatusChange); // 运行第一个 effect
// Update with { friend: { id: 200 } } props
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // 清除上一个 effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange); // 运行下一个 effect
// Update with { friend: { id: 300 } } props
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // 清除上一个 effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange); // 运行下一个 effect
// Unmount
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // 清除最后一个 effect
此默认行为保证了一致性,避免了在 class 组件中因为没有处理更新逻辑而导致常见的 bug。
某些情况下,每次渲染后都执行清理或者执行Effect可能会导致性能问题,在class组件中,我们可以通过在componentDidUpdate中添加对prevPrps或prevState的比较逻辑解决:
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `You clicked ${this.state.count} times`;
}
}
这是很常见的需求,所以它被内置到useEffect 的hook API中,如果某些特定值在2次重新渲染之间没有发生变化,它可以通知react跳过对effect的调用,只要传递数组作为useEffect的第2个可选参数即可:
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新
上面这个示例中,我们传入 [count] 作为第二个参数。这个参数是什么作用呢?如果 count 的值是 5,而且我们的组件重渲染的时候 count 还是等于 5,React 将对前一次渲染的 [5] 和后一次渲染的 [5] 进行比较。因为数组中的所有元素都是相等的(5 === 5),React 会跳过这个 effect,这就实现了性能的优化。 当渲染时,如果 count 的值更新成了 6,React 将会把前一次渲染时的数组 [5] 和这次渲染的数组 [6] 中的元素进行对比。这次因为 5 !== 6,React 就会再次调用 effect。如果数组中有多个元素,即使只有一个元素发生变化,React 也会执行 effect。
如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([]
)作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。这并不属于特殊情况 —— 它依然遵循依赖数组的工作方式。
通过计时器来理解Effect Hook,如下代码:
import React, { useState, useEffect } from ‘react‘
function EffectHook() {
console.log("EffectHook");
let [count, setCount] = useState(0);
useEffect(() => {
console.log("useEffect " + count);
setInterval(() => setCount(count + 2), 1000);
}, [count])
console.log("return");
return (
<div>
<p>点击了{count}次</p>
<button onClick={() => setCount(count + 1)}>点击事件</button>
</div>
)
}
export default EffectHook;
上面的代码,控制台会打印输出
EffectHook.jsx:4 EffectHook
EffectHook.jsx:12 return
EffectHook.jsx:8 useEffect 0
EffectHook.jsx:4 EffectHook
EffectHook.jsx:12 return
EffectHook.jsx:8 useEffect 2
EffectHook.jsx:4 EffectHook
EffectHook.jsx:12 return
EffectHook.jsx:8 useEffect 4....
可以看出,定时器通过setCount函数每次都会触发页面渲染,那么之后如果组件卸载的时候清除定时器呢
当[count]数组为[]时候,状态更新时不会调用useEffect函数,也就是说componetwillmount函数里面定义的,可以用 useEffect(() => { }, []) 的方式来实现。
还有一种情况是当组件卸载时,需要清除定时器,这个操作通过useEffect函数返回一个函数即可实现。
import React, { useState, useEffect } from ‘react‘
import ReactDOM from ‘react-dom‘
function EffectHook() {
console.log("EffectHook");
let [count, setCount] = useState(0);
useEffect(() => {
console.log("useEffect " + count);
const intervalId = setInterval(() => setCount(count + 2), 1000);
return () => {
//组件卸载时调用
clearInterval(intervalId);
}
})
console.log("return");
return (
<div>
<p>点击了{count}次</p>
<button onClick={() => ReactDOM.unmountComponentAtNode(document.getElementById("root"))
}>卸载</button>
</div>
)
}
export default EffectHook;
只在最顶层使用hook;
不要在循环,条件和嵌套函数中调用Hook。
只在React函数中调用Hook:不要在普通的javascript函数中调用Hook。
我们可以在单个组件中使用多个 State Hook 或 Effect Hook
function Form() {
// 1. Use the name state variable
const [name, setName] = useState(‘Mary‘);
// 2. Use an effect for persisting the form
useEffect(function persistForm() {
localStorage.setItem(‘formData‘, name);
});
// 3. Use the surname state variable
const [surname, setSurname] = useState(‘Poppins‘);
// 4. Use an effect for updating the title
useEffect(function updateTitle() {
document.title = name + ‘ ‘ + surname;
});
// ...
}
那么 React 怎么知道哪个 state 对应哪个 useState
?答案是 React 靠的是 Hook 调用的顺序。因为我们的示例中,Hook 的调用顺序在每次渲染中都是相同的,所以它能够正常工作:
// ------------
// 首次渲染
// ------------
useState(‘Mary‘) // 1. 使用 ‘Mary‘ 初始化变量名为 name 的 state
useEffect(persistForm) // 2. 添加 effect 以保存 form 操作
useState(‘Poppins‘) // 3. 使用 ‘Poppins‘ 初始化变量名为 surname 的 state
useEffect(updateTitle) // 4. 添加 effect 以更新标题
// -------------
// 二次渲染
// -------------
useState(‘Mary‘) // 1. 读取变量名为 name 的 state(参数被忽略)
useEffect(persistForm) // 2. 替换保存 form 的 effect
useState(‘Poppins‘) // 3. 读取变量名为 surname 的 state(参数被忽略)
useEffect(updateTitle) // 4. 替换更新标题的 effect
// ...
只要 Hook 的调用顺序在多次渲染之间保持一致,React 就能正确地将内部 state 和对应的 Hook 进行关联。但如果我们将一个 Hook (例如 persistForm
effect) 调用放到一个条件语句中会发生什么呢?
// ?? 在条件语句中使用 Hook 违反第一条规则
if (name !== ‘‘) {
useEffect(function persistForm() {
localStorage.setItem(‘formData‘, name);
});
}
React 不知道第二个 useState
的 Hook 应该返回什么。React 会以为在该组件中第二个 Hook 的调用像上次的渲染一样,对应得是 persistForm
的 effect,但并非如此。从这里开始,后面的 Hook 调用都被提前执行,导致 bug 的产生。
**这就是为什么 Hook 需要在我们组件的最顶层调用。**如果我们想要有条件地执行一个 effect,可以将判断放到 Hook 的内部:
useEffect(function persistForm() {
// ?? 将条件判断放置在 effect 中
if (name !== ‘‘) {
localStorage.setItem(‘formData‘, name);
}
});
通过自定义Hook,可以将组件逻辑提取到可重用的函数中。
如果我们想在2个函数之间共享逻辑时,我们会把它提取到第三个函数中。而组件和Hook都是函数,所以也同样适用这种方式。
自定义Hook是一个函数,其名称以“use”开头,函数内部可以调用其他的Hook。例如下面的useFriendStatus是我们自定义的第一个Hook:
import React, { useState, useEffect } from ‘react‘;
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
此组件并未包含任何新的内容---逻辑时从上面组件拷贝来的。与组件中一致,请确保只在自定义Hook的顶层无条件地调用其他Hook。
与react组件不同的是,自定义Hook不需要具有特殊的表示。我们可以自由地决定它的参数是什么,以及它应该返回什么(如果需要的话)。换句话说,它就像一个正常的函数。但是它的名字应该始终以use开头,这样可以一眼看出其符合Hook的规则。
自定义 Hook 是一种自然遵循 Hook 设计的约定,而并不是 React 的特性。
自定义 Hook 必须以 “use
” 开头吗?必须如此。这个约定非常重要。不遵循的话,由于无法判断某个函数是否包含对其内部 Hook 的调用,React 将无法自动检查你的 Hook 是否违反了 Hook 的规则。
在两个组件中使用相同的 Hook 会共享 state 吗?不会。自定义 Hook 是一种重用状态逻辑的机制(例如设置为订阅并存储当前值),所以每次使用自定义 Hook 时,其中的所有 state 和副作用都是完全隔离的。
自定义 Hook 如何获取独立的 state?每次调用 Hook,它都会获取独立的 state。由于我们直接调用了 useFriendStatus
,从 React 的角度来看,我们的组件只是调用了 useState
和 useEffect
。 正如我们在之前章节中了解到的一样,我们可以在一个组件中多次调用 useState
和 useEffect
,它们是完全独立的。
ref可以存储任意对象,通常用的时候是将原先this.的对象保存到href里面。
1.首先用ref保存this.ref里面的对象组件
import React, { useRef } from ‘react‘;
function RefHook() {
const h1Ref = useRef(null);
function testClick() {
console.log(h1Ref.current.innerText);
}
return (
<div>
<h1 ref={h1Ref}>href content</h1>
<button onClick={testClick}>test h1 content</button>
</div>
)
}
export default RefHook;
通过useRef存储ref对象,然后直接通过h1ref对象就是ref 对象对应的组件本身了。
2.将要保存的对象放到ref对象里面,如下代码:
import React, { useRef, useState } from ‘react‘;
function RefHook() {
console.log("RefHook");
const h1Ref = useRef(null);
const timeRef = useRef(Date.now());
const [count, setCount] = useState(0);
function testClick() {
console.log(h1Ref.current.innerText);
}
function testSave() {
timeRef.current = Date.now();
console.log("save now: " + timeRef.current);
setCount(count + 1);
}
function testSee() {
console.log(timeRef.current);
}
return (
<div>
<h1 ref={h1Ref}>href content</h1>
<button onClick={testClick}>test h1 content</button>
<button onClick={testSave}>保存当前的时间</button>
<button onClick={testSee}>查看保存的当前的时间</button>
</div>
)
}
export default RefHook;
RefHook
save now: 1584965957402
RefHook
1584965957402
上面代码中的timeRef存储当前的时间值,之后如果组件有刷新过,该timeRef中的属性值也不会随之消失。
timeRef.current来获取保存的对象。
原文:https://www.cnblogs.com/zdjBlog/p/12564352.html