一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第18天,点击查看活动详情
react有两种组件——类式和函数式,在以前,只有类式组件才能拥有状态和生命周期函数,不过,这种情况在2019年2月发布的react16.8版本中引入hooks之后得到了改变。
现在函数式组件也可以拥有状态和生命周期函数,对于简单的状态,可以使用useState hook,对于复杂的状态,可以使用 useReducer hook,对于生命周期函数,我们可以使用useEffect hook。
本文将对useState hook做一个深入的介绍。
1.hooks是什么?
hooks是常规的js函数,hooks帮助我们勾住函数式组件的state和生命周期函数。hooks使得我们可以在函数式组件中使用状态和生命周期函数,hooks使用了react中的fiber架构。
2.useState hook
useState 钩子允许我们在函数式组件中拥有一个简单的状态。 它在react中被命名导出了,这意味着我们可以使用以下方式使用。
import React, { useState } from "react"
或者也可以用以下方式使用
import React from "react"
React.useState() //也能运行
useState hook接受一个参数,并返回一个数组,该数组包含两个元素,即状态变量和setter函数。因此,如果我们来看看useState钩子的基本结构,它将是这样的:
export function useState(initialState) {
// logic
return [state, setState]
}
在初始渲染时,state将与initialState相同。如果没有初始状态,则状态变量将在第一次渲染时返回undefined。setter函数(setState)用于更改状态的值。
3.基本用法
首先我们从react中导入useState
import React, { useState } from "react"
然后我们使用数组解构来访问state变量和setter函数,同时将初始状态作为参数传递给useState hook。
// ..
function Counter() {
const [count, setCount] = useState(0)
return <p>{count}</p>
}
// renders 0 in the UI
也可以使用数组索引。
//..
const count = useState(0)[0]
const setCount = useState(0)[1]
//..
虽然这样也能工作,但显然不方便。 或许,你可能在想为什么useState不返回一个对象来代替数组。那是因为我们必须要通过额外的代码重命名变量名。
// 如果 useState 返回对象
const { state: count, setState: setCount } = useState(0)
但是,如果返回值较大,则返回对象是有意义的。这是因为我们可以有选择地解构对象属性。有些第三方库具有利用此功能的hook。但是react的默认hook只返回几个值,这使得它变得不必要。
4.更新State
为了更新状态,我们使用useState钩子返回的setter函数。在上面的例子中,它是setCount函数。
setCount(1)
执行此函数时,我们将状态变量的值更新为1。从技术上讲,将创建一个值为1的新状态变量。稍后我们将对此进行更多讨论。
通常,我们通常在useEffect hook中或者直接在onClick属性中使用setter函数。
//..
return (
<>
<p>{count}<p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</>
)
现在,当我们点击按钮时,计数变量增加1。然而,需要注意的是,setter函数实际上是异步的。因此,当我们单击按钮时,我们只是将更新动作添加到队列中。它不会立即执行。这与react的fiber架构有关。 现在,我们已经介绍了useState钩子的最基本用法。
5.高级useState用法
5.1.延迟初始化
之前提到过,我们可以延迟初始化一个状态。延迟初始化的意思是,我们将只在需要时分配状态值。这意味着在react首次渲染时,状态值不会传递给状态变量。
你可能会想为什么要这样做?如果状态值是某种复杂计算的结果,例如,调用API的解析到的JSON。在这种情况下,在计算完成并初始化状态之前,我们不希望UI冻结。这将是一种很差的用户体验。 实际上,你要做的就是延迟初始化状态。这很简单,只需传递返回状态的函数。
//..
const [count, setCount] = useState(() => 0)
//..
在上述情况下,函数作为初始状态传递给useState钩子。useState hook的工作方式是,如果传递了一个函数,那么它将执行并将返回值赋给state变量。在计算完成前它不会让useState保持活动状态,直到。因此,无论状态是否已初始化,它都确保渲染UI。 您也可以在外部定义一个函数,然后像这样将其传递给useState。
function computation() {
// 复杂计算
}
//..
const [state, setState] = useState(() => computation())
//..
这里的computation()函数根据需要运行,不会阻止UI渲染。
5.2.使用上一次的状态更新当前状态
让我们重温一下上面的例子。
const React, {useState} from 'react';
function Counter() {
const [count, setCount] = useState(0)
}
return (
<>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</>
)
下面我们直接传递state变量,并在setCount()中添加1。这种方法的问题是,我们不能用前一次更新后的值进行第二次更新。例如
function Counter() {
const [count, setCount] = useState(0)
}
function handleClick() {
setCount(count + 1) // returns 0 + 1 = 1
setCount(count + 2) // returns 0 + 2 = 2
}
return (
<>
<p>{count}</p>
<button onClick={() => handleClick()}>Increment by 3</button>
</>
)
// 最终的state 是2,而不是3
在上面的例子中,你会认为第一次计数是1,然后加2变成3。那你就想错了。
React仅在UI更新后才真正更新状态变量。组件更新时,React有两个阶段:
渲染阶段
提交阶段
在提交阶段,ui界面是更新的。但是在渲染阶段,组件必须处于相同的状态。也就是说,如果调用多个setter函数,所有setter函数都将以相同的状态初始化。
在上述情况下,两个setcount都用0初始化并添加到队列中。因为JavaScript将最后一个值作为最终值,所以状态值为2。
现在,react团队显然知道这将是一个问题。因为我们肯定希望能够在同一渲染阶段的某个时刻使用前一次更新后的值。
这里的解决方案非常类似于延迟初始化。我们只是传递箭头函数。箭头函数接受1个参数,该参数将是前一个更新后的状态。
//..
function handleClick() {
setCount(prevCount => prevCount + 1) // returns 0 + 1 = 1
setCount(prevCount => prevCount + 2) // returns 1 + 2 = 3
}
//..
这是因为我们传递了一个函数作为参数,而setter函数的工作方式是,如果它遇到一个函数作为参数,那么它将自动传递前一个更新后的状态作为该函数的第一个参数。 因此,useState hook和setter函数都有一个特定的检查,以查看传递的值是否是函数。
6.将useState与基本数据类型一起使用
前面已经将useState与number类型一起使用。但实际上,我们也可以使用其他基本数据类型,如布尔或字符串。
//..
const [isLoggedIn, setIsLoggedIn] = useState(false) // 使用 布尔值
const [name, setName] = useState("Elon Musk") // 使用 字符串
//..
当我们更改isLoggedIn或name的值时,我们实际上是在用该值创建一个新变量。这就是JavaScript的运行方式。 更新状态变量时确保使用setIsLoggedIn或setName函数也很重要。否则,react将不知道已经更新了状态,从而导致ui不会进行渲染。
isLoggedIn = true
console.log(isLoggedIn) // 打印出 "true" console
// 但是, ui不会渲染
// 因为react不知道state变量更改了
// 正确的方式
setIsLoggedIn(true)
setIsLoggedIn函数将修改状态变量为true,但这一次它也将重新渲染组件。因此,整个UI逻辑将使用更新的状态后的值再次渲染。
7.useState 和数组
我们也可以使用数组作为我们的状态变量
//..
const [list, setList] = useState(["Apple", "Orange", "Grape"])
//..
重要的是我们要记住,react将在每次状态更新期间创建一个新数组。因此,当我们更新时,请确保首先克隆原始数组,然后更新新值。
//..
setList([...list, "Banana"])
//..
这里展开符将克隆现有数组的内容,最后将Banana添加到末尾。或者我们也可以通过改变位置在顶部添加Banana。
//..
setList(["Banana", ...list])
//..
如果你想删除一个元素,那么你必须使用常规的JavaScript逻辑来创建一个新的数组,然后更新状态如下。
//..
const newList = list.filter(item => item !== "Grape") // 或其他逻辑
setList([...newList])
// list 变为 ["Apple", "Orange", "Banana"]
// (Grape 被删除)
你也可以使用pop()、splice()、shift()和其他方法从数组中删除元素。
8.useState和对象
我们也可以传递对象做为userState hook的参数
//..
const [user, setUser] = useState({
firstName: "Elon",
lastName: "Musk",
})
//..
和前面一样,重要的是要记住,react将在每次状态更新期间创建一个新对象。所以我们必须先克隆原始对象,然后再进行更新。
//..
setUser({ ...user, age: "50" })
//..
请注意语法上的差异。对于数组,我们使用[]括号,对于对象,我们使用{}。 我们还可以像这样更新现有属性:
//..
setUser({ ...user, lastName: "Dusk" })
// user becomes {
// firstName: "Elon",
// lastName: "Dusk",
//}
9.使用props作为初始状态
我们可以将props作为初始状态传递,但不要期望props发生变化时状态会发生变化。例如,如果我们写这样的代码:
//..
function User(props) {
const [name, setName] = useState(props.name)
}
这里我们传递一个prop作为初始状态。如果该prop在第一次渲染后更改,并不会以任何方式影响useState。useState钩子在第一次渲染期间获取初始值,并且只在调用setter函数时更新。所以prop改变不会改变状态。 在这种情况下,我们可以将useEffect钩子与该特定prop一起用作依赖项,并调用setter函数。
//..
function User(props) {
const [name, setName] = useState(props.name)
}
useEffect(() => {
setName(props.name)
}, [props.name])
如果你不知道useEffect钩子,它会被用来模仿生命周期方法。需要两个参数,
函数
依赖项数组
函数体包含我们需要执行的内容,依赖的数组决定什么样的更改触发useEffect。
10.fiber架构和更新流程
正如我已经提到的,react使用了一种叫做fiber架构的东西。详细讨论fiber体系结构超出了本文的范围。但我们仍然需要了解fiber体系结构是如何与useState hook 一起使用的。
所以fiber架构有两个阶段,
渲染阶段
提交阶段
我们之前已经简要讨论过这一点。渲染阶段是完全异步的。这意味着我们可以在渲染阶段内启动和停止进程。然而,提交阶段是同步的,不能被干扰。
fiber体系结构的工作方式是将某些流程置于其他流程之上。在react中,我们可以称之为work。 fiber只是一个work的单位。所以fiber可以是状态更新,可以是任何一种逻辑来完成某件事。 你需要记住的是react fiber有一个优先级列表,最重要的是UI更新。这就是为什么提交阶段实际上是同步的。这意味着它永远不能也不应该被打断。 然而,在渲染阶段,一切都是异步工作的。因此,显然有很多基于优先级的中断。
因此,当我们使用setter函数更新状态时,真正发生的是我们将更新过程添加到队列中。 所有状态更新都是按顺序调用的,这就是为什么必须按正确的顺序声明它们。因为先声明的优先顺序高于后声明的优先顺序。 每个状态更新都以指向下一个状态更新结束。因此,这实际上形成了一棵树,这棵树从上到下执行,直到不再指向任何东西为止。 所有这些复杂的逻辑都是抽象出来的,但它所做的事情是非常美妙的。fiber体系结构不仅使react运行很快,而且使其智能化。
现在来看看实际的更新过程。
正如上面提到的,react fiber有两个阶段——渲染阶段和提交阶段。当useState钩子被执行时,整个过程都会经历这两个阶段。 渲染和提交阶段的工作原理如下:
渲染阶段
组件的JSX被转换为react元素。这是使用React.createElement()方法完成的。 这又会创建一个组件树。这只是实际组件的对象表示。我们称之为虚拟DOM。
提交阶段
在提交阶段,使用虚拟DOM的值更新真实DOM。
如果我们的组件只渲染一次,这一切都很好。
但是当我们的组件中有一个useState或useReducer时,有趣的事情就会发生。这是因为拥有状态意味着组件可以标记自身以进行重新渲染。
在这种情况下,当我们运行setter函数时,组件会被标记。
PS:如果prop发生变化或由于父级重新渲染,也可能发生重新渲染。我们只讨论由于useState中的setter函数而发生的重新渲染。
在重新渲染期间,渲染阶段和提交阶段的行为略有不同。
在渲染阶段,将检查状态变量,以查看值是否有变化。
如果有的话,再次调用React.createElement() ,并将JSX转换为组件树。这一次,将新创建的树与当前树进行比较,以查看是否有任何更改。这些更改将应用于虚拟DOM,并将更改传递到提交阶段。
如果状态变量没有更改,则react将退出渲染进程。
在提交阶段,真实的DOM会正常更新。请注意,只有更改会被更新,因为只有更改才会通过react传递到提交阶段。
11.hook的规则
React钩子有一些必须遵守的规则。规则很简单,
钩子只能在组件的顶层调用。
钩子只能在函数式组件上调用。
第二条规则是显而易见的。类组件中不需要hook。
然而,对于第一个:
这意味着我们不能在条件语句内或其他钩子如useEffect等内调用钩子。
function App() {
const [state, setState] = useState(0)
}
12.useReducer钩子和复杂状态
最后再介绍一下useReducer钩子, useReducer钩子也能帮助我们管理react中的状态。事实上,useState是useReducer钩子的抽象。在幕后,我们仍在使用useReducer。 useReducer钩子接受初始状态和一个reducer函数,并返回一个数组。数组具有状态变量和dispatch方法。
import React, { useReducer } from "react"
function App() {
const [state, dispatch] = useReducer(reducer, initialState)
}
reducer函数包含一组条件,并根据该条件返回一些内容。实现它的最佳方法是使用switch语句。 useReducer的一个简单实现,带有计数器示例:
const initialState = { count: 0 }
function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 }
case "decrement":
return { count: state.count - 1 }
default:
throw new Error()
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
</>
)
}
每当调用dispatch函数时,它都会将其参数分派给reducer函数。reducer函数将其与预定义的条件匹配,并返回一个新状态。这样,就可以管理更复杂的状态。