React笔记-Hooks篇
Hooks
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
Hook是什么?
Hook 是什么?
Hook 是一个特殊的函数,它可以让你“钩入” React 的特性。例如,useState
是允许你在 React 函数组件中添加 state 的 Hook。
什么时候我会用 Hook?
如果你在编写函数组件并意识到需要向其添加一些 state,以前的做法是必须将其它转化为 class。现在你可以在现有的函数组件中使用 Hook。
注意:
在组件中有些特殊的规则,规定什么地方能使用 Hook,什么地方不能使用。我们将在 Hook 规则中学习它们。
Hook规则
Hook 本质就是 JavaScript 函数
,但是在使用它时需要遵循两条规则。官方提供了一个 linter 插件来强制执行这些规则:
1.🍅只在最顶层使用 Hook
不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层以及任何 return 之前调用他们。
遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState
和 useEffect
调用之间保持 hook 状态的正确。(如果你对此感到好奇,在下面会有更深入的解释。)
2.🍅只在 React 函数中调用 Hook
不要在普通的 JavaScript 函数中调用 Hook。你可以:
- ✅ 在 React 的函数组件中调用 Hook
- ✅ 在自定义 Hook 中调用其他 Hook
遵循此规则,确保组件的状态逻辑在代码中清晰可见。
🍅🍅🍅说明
我们可以在单个组件中使用多个 State Hook 或 Effect Hook
function Form() { |
那么 React 怎么知道哪个 state 对应哪个 useState
?答案是 React 靠的是 Hook 调用的顺序
。因为我们的示例中,Hook 的调用顺序在每次渲染中都是相同的,所以它能够正常工作:
// ------------ |
只要 Hook 的调用顺序在多次渲染之间保持一致,React 就能正确地将内部 state 和对应的 Hook 进行关联
。但如果我们将一个 Hook (例如 persistForm
effect) 调用放到一个条件语句中会发生什么呢?
// 🔴 在条件语句中使用 Hook 违反第一条规则 |
在第一次渲染中 name !== ''
这个条件值为 true
,所以我们会执行这个 Hook。但是下一次渲染时我们可能清空了表单,表达式值变为 false
。此时的渲染会跳过该 Hook,Hook 的调用顺序发生了改变:
useState('Mary') // 1. 读取变量名为 name 的 state(参数被忽略) |
React 不知道第二个 useState
的 Hook 应该返回什么。React 会以为在该组件中第二个 Hook 的调用像上次的渲染一样,对应的是 persistForm
的 effect,但并非如此。从这里开始,后面的 Hook 调用都被提前执行,导致 bug 的产生。
这就是为什么 Hook 需要在我们组件的最顶层调用。如果我们想要有条件地执行一个 effect,可以将判断放到 Hook 的内部:
useEffect(function persistForm() { |
useState
在 class 中,我们通过在构造函数中设置 this.state
为 { count: 0 }
来初始化 count
state 为 0
:
class Example extends React.Component { |
在函数组件中,我们没有 this
,所以我们不能分配或读取 this.state
。我们直接在组件中调用 useState
Hook:
import React, { useState } from 'react'; |
调用 useState
方法的时候做了什么?
它定义一个 “state 变量”。我们的变量叫 count
, 但是我们可以叫他任何名字,比如 banana
。这是一种在函数调用时保存变量的方式 —— useState
是一种新方法,它与 class 里面的 this.state
提供的功能完全相同。一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留
。
useState
需要哪些参数?
useState()
方法里面唯一的参数就是初始 state
。不同于 class 的是,我们可以按照需要使用数字或字符串对其进行赋值,而不一定是对象。在示例中,只需使用数字来记录用户点击次数,所以我们传了 0
作为变量的初始 state。(如果我们想要在 state 中存储两个不同的变量,只需调用 useState()
两次即可。)
useState
方法的返回值是什么?
返回值为:当前 state 以及更新 state 的函数
。这就是我们写 const [count, setCount] = useState()
的原因。
注意:
你可能想知道:为什么叫
useState
而不叫createState
?“Create” 可能不是很准确,因为 state 只在组件首次渲染的时候被创建。在下一次重新渲染时,
useState
返回给我们当前的 state。否则它就不是 “state”了!这也是 Hook 的名字总是以use
开头的一个原因。
提示:方括号有什么用?
你可能注意到我们用方括号定义了一个 state 变量
const [count, setCount] = useState(0); |
等号左边名字并不是 React API 的部分,你可以自己取名字:
const [fruit, setFruit] = useState('banana'); |
这种 JavaScript 语法叫数组解构。它意味着我们同时创建了 fruit
和 setFruit
两个变量,fruit
的值为 useState
返回的第一个值,setFruit
是返回的第二个值。它等价于下面的代码:
var fruitStateVariable = useState('banana'); // 返回一个有两个元素的数组 |
当我们使用 useState
定义 state 变量时候,它返回一个有两个值的数组。第一个值是当前的 state,第二个值是更新 state 的函数。使用 [0]
和 [1]
来访问有点令人困惑,因为它们有特定的含义。这就是我们使用数组解构的原因。
注意
你可能会好奇 React 怎么知道
useState
对应的是哪个组件,因为我们并没有传递this
给 React。下面将解释这个问题。
🍅🍅🍅底层原理
React 是如何把对 Hook 的调用和组件联系起来的?
React 保持对当前渲染中的组件的追踪。多亏了 Hook 规范,我们得知 Hook 只会在 React 组件中被调用(或自定义 Hook —— 同样只会在 React 组件中被调用)
。
每个组件内部都有一个「记忆单元格」列表
。它们只不过是我们用来存储一些数据的 JavaScript 对象。当你用 useState()
调用一个 Hook 的时候,它会读取当前的单元格(或在首次渲染时将其初始化),然后把指针移动到下一个。这就是多个 useState()
调用会得到各自独立的本地 state 的原因。
注意:
与 class 组件中的
setState
方法不同,useState
不会自动合并更新对象。你可以用函数式的setState
结合展开运算符来达到合并更新对象的效果。
useReducer
是另一种可选方案,它更适合用于管理包含多个子值的 state 对象。
const [state, setState] = useState({}); |
useEffect
可以把 useEffect
Hook 看做 componentDidMount
,componentDidUpdate
和 componentWillUnmount
这三个函数的组合。
1.useEffect
做了什么?
通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。在这个 effect 中,我们设置了 document 的 title 属性,不过我们也可以执行数据获取或调用其他命令式的 API。
2.为什么在组件内部调用 useEffect
?
将 useEffect
放在组件内部让我们可以在 effect 中直接访问 count
state 变量(或其他 props)。我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中。Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的情况下,还引入特定的 React API。
3.useEffect
会在每次渲染后都执行吗?
是的,默认情况下,它在第一次渲染之后和每次更新之后都会执行。你可能会更容易接受 effect 发生在“渲染之后”这种概念,不用再去考虑“挂载”还是“更新”。React 保证了每次运行 effect 的同时,DOM 都已经更新完毕。
4.为什么要在 effect 中返回一个函数?
是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。它们都属于 effect 的一部分。
5.React 何时清除 effect?
React 会在组件卸载的时候执行清除操作。正如之前学到的,effect 在每次渲染的时候都会执行。这就是为什么 ==React 会在执行当前 effect 之前对上一个 effect 进行清除==。我们稍后将讨论为什么这将助于避免 bug以及如何在遇到性能问题时跳过此行为。
6.使用多个 Effect 实现关注点分离
使用 Hook 其中一个目的就是要解决 class 中生命周期函数经常包含不相关的逻辑,但又把相关逻辑分离到了几个不同方法中的问题。
使用多个 effect。这会将不相关逻辑分离到不同的 effect 中
Hook 允许我们按照代码的用途分离他们, 而不是像生命周期函数那样。React 将按照 effect 声明的顺序依次调用组件中的每一个 effect。
7.解释: 为什么每次更新的时候都要运行 Effect
如果你已经习惯了使用 class,那么你或许会疑惑为什么 effect 的清除阶段在每次重新渲染时都会执行,而不是只在卸载组件的时候执行一次。
看一个实际的例子,看看为什么这个设计可以帮助我们创建 bug 更少的组件。
componentDidMount() { |
但是当组件已经显示在屏幕上时,friend
prop 发生变化时会发生什么?
我们的组件==将继续展示原来的好友状态==。==这是一个 bug==。而且我们==还会因为取消订阅时使用错误的好友 ID 导致内存泄露或崩溃的问题==。
在 class 组件中,我们需要添加 componentDidUpdate
来解决这个问题:
componentDidMount() { |
==忘记正确地处理 componentDidUpdate
是 React 应用中常见的 bug 来源==。
现在看一下使用 Hook 的版本:
function FriendStatus(props) { |
它并不会受到此 bug 影响。(虽然我们没有对它做任何改动。)
并不需要特定的代码来处理更新逻辑,因为 useEffect
默认就会处理。它会在调用一个新的 effect 之前对前一个 effect 进行清理。为了说明这一点,下面按时间列出一个可能会产生的订阅和取消订阅操作调用序列:
// Mount with { friend: { id: 100 } } props |
8.★如果我的 effect 的依赖频繁变化,我该怎么办?
有时候,你的 effect 可能会使用一些频繁变化的值。你可能会忽略依赖列表中 state,但这通常会引起 Bug:
function Counter() { |
传入空的依赖数组 []
,意味着该 hook 只在组件挂载时运行一次,并非重新渲染时。但如此会有问题,在 setInterval
的回调中,count
的值不会发生变化。因为==当 effect 执行时,我们会创建一个闭包,并将 count
的值被保存在该闭包当中,且初值为 0
。每隔一秒,回调就会执行 setCount(0 + 1)
,因此,count
永远不会超过 1==。
指定 [count]
作为依赖列表就能修复这个 Bug,但会导致每次改变发生时定时器都被重置。事实上,每个 setInterval
在被清除前(类似于 setTimeout
)都会调用一次。但这并不是我们想要的。要解决这个问题,我们可以使用 setState
的函数式更新形式。它允许我们指定 state 该 如何 改变而不用引用 当前 state:
function Counter() { |
补充:函数式更新
如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给
setState
。该函数将接收先前的 state,并返回一个更新后的值。下面的计数器组件示例展示了setState
的两种用法:
function Counter({initialCount}) { |
Ref
1.何时使用 Refs
下面是几个适合使用 refs 的情况:
- 管理焦点,文本选择或媒体播放。
- 触发强制动画。
- 集成第三方 DOM 库。
避免使用 refs 来做任何可以通过声明式实现来完成的事情。
2.创建 Refs
Refs 是使用 React.createRef()
创建的,并通过 ref
属性附加到 React 元素。在构造组件时,通常将 Refs 分配给实例属性,以便可以在整个组件中引用它们。
class MyComponent extends React.Component { |
3.访问 Refs
当 ref 被传递给 render
中的元素时,对该节点的引用可以在 ref 的 current
属性中被访问。
const node = this.myRef.current; |
ref 的值根据节点的类型而有所不同:
- 当
ref
属性用于 HTML 元素时,构造函数中使用React.createRef()
创建的ref
接收底层 DOM 元素作为其current
属性。 - 当
ref
属性用于自定义 class 组件时,**ref
对象接收组件的挂载实例作为其current
属性**。 - 你不能在函数组件上使用
ref
属性,因为他们没有实例。
1.为 DOM 元素添加 ref
class CustomTextInput extends React.Component { |
React 会在组件挂载时给 current
属性传入 DOM 元素,并在组件卸载时传入 null
值。ref
会在 componentDidMount
或 componentDidUpdate
生命周期钩子触发前更新。
2.为 class 组件添加 Ref
如果我们想包装上面的 CustomTextInput
,来模拟它挂载之后立即被点击的操作,我们可以使用 ref 来获取这个自定义的 input 组件并手动调用它的 focusTextInput
方法:
class AutoFocusTextInput extends React.Component { |
注意,这仅在 CustomTextInput
声明为 class 时才有效。
3.Refs 与函数组件
默认情况下,你不能在函数组件上使用 ref
属性,因为它们没有实例:
function MyFunctionComponent() { |
如果要在函数组件中使用 ref
,你可以使用 forwardRef
(可与 useImperativeHandle
结合使用),或者可以将该组件转化为 class 组件。
使用useRef的hooks可以在函数组件中使用ref
function CustomTextInput(props) { |
⭕4.将 DOM Refs 暴露给父组件
在极少数情况下,你可能希望在父组件中引用子节点的 DOM 节点。通常不建议这样做,因为它会打破组件的封装,但它偶尔可用于触发焦点或测量子 DOM 节点的大小或位置。
虽然你可以向子组件添加 ref,但这不是一个理想的解决方案,因为你只能获取组件实例而不是 DOM 节点。并且,它还在函数组件上无效。
如果你使用 16.3 或更高版本的 React, 这种情况下我们推荐使用 ref 转发。Ref 转发使组件可以像暴露自己的 ref 一样暴露子组件的 ref。关于怎样对父组件暴露子组件的 DOM 节点,在 ref 转发文档中有一个详细的例子。
如果你使用 16.2 或更低版本的 React,或者你需要比 ref 转发更高的灵活性,你可以使用这个替代方案将 ref 作为特殊名字的 prop 直接传递。
This works both for classes and for functional components.
function CustomTextInput(props) { |
可能的话,我们不建议暴露 DOM 节点,但有时候它会成为救命稻草。注意这个方案需要你在子组件中增加一些代码。如果你对子组件的实现没有控制权的话,你剩下的选择是使用 findDOMNode()
,但在严格模式
下已被废弃且不推荐使用。
4.回调 Refs
React 也支持另一种设置 refs 的方式,称为“回调 refs”。它能助你更精细地控制何时 refs 被设置和解除。
不同于传递 createRef()
创建的 ref
属性,你会传递一个函数。这个函数中接受 React 组件实例或 HTML DOM 元素作为参数,以使它们能在其他地方被存储和访问。
使用 ref
回调函数,在实例的属性中存储对 DOM 节点的引用:
class CustomTextInput extends React.Component { |
React 将在组件挂载时,会调用 ref
回调函数并传入 DOM 元素,当卸载时调用它并传入 null
。在 componentDidMount
或 componentDidUpdate
触发前,React 会保证 refs 一定是最新的。
你可以在组件间传递回调形式的 refs,就像你可以传递通过 React.createRef()
创建的对象 refs 一样。
function CustomTextInput(props) { |
参考:
[react官方文档](React 官方中文文档 – 用于构建用户界面的 JavaScript 库 (docschina.org))