在React中,随着时间变化的数据被称为状态(state)。你可以向任何组件中添加状态,并按需进行更新。
响应事件
事件处理函数的使用
-
你需要先定义一个函数,然后 将其作为 prop 传入 合适的 JSX 标签。
-
按照惯例,通常将事件处理程序命名为
handle,后接事件名。你会经常看到onClick={handleClick},onMouseEnter={handleMouseEnter}等。 -
你可以作为props传递事件处理函数
-
事件处理函数 props 应该以
on开头,后跟一个大写字母。例如onClick
export default function Button() {
function handleClick() {
alert('你点击了我!');
}
return (
<button onClick={handleClick}>
点我
</button>
);
}
------------------------------------------------
<button onClick={() => {
alert('你点击了我!');
}}>
事件传播
事件处理函数还将捕获任何来自子组件的事件。通常,我们会说事件会沿着树向上“冒泡”或“传播”:它从事件发生的地方开始,然后沿着树向上传播。
export default function Toolbar() {
return (
<div className="Toolbar" onClick={() => {
alert('你点击了 toolbar !');
}}>
<button onClick={() => alert('正在播放!')}>
播放电影
</button>
<button onClick={() => alert('正在上传!')}>
上传图片
</button>
</div>
);
}
如果你点击任一按钮,它自身的 onClick 将首先执行,然后父级 <div> 的 onClick 会接着执行。因此会出现两条消息。如果你点击 toolbar 本身,将只有父级 <div> 的 onClick 会执行。
阻止事件传播
事件处理函数接收一个 事件对象 作为唯一的参数。按照惯例,它通常被称为 e ,代表 “event”(事件)。你可以使用此对象来读取有关事件的信息。
这个事件对象还允许你阻止传播。如果你想阻止一个事件到达父组件,你需要像下面 Button 组件那样调用 e.stopPropagation()
function Button({ onClick, children }) {
return (
<button onClick={e => {
e.stopPropagation();
onClick();
}}>
{children}
</button>
);
}
export default function Toolbar() {
return (
<div className="Toolbar" onClick={() => {
alert('你点击了 toolbar !');
}}>
<Button onClick={() => alert('正在播放!')}>
播放电影
</Button>
<Button onClick={() => alert('正在上传!')}>
上传图片
</Button>
</div>
);
}
当你点击按钮时:
-
React 调用了传递给
<button>的onClick处理函数。 -
定义在
Button中的处理函数执行了如下操作:- 调用
e.stopPropagation(),阻止事件进一步冒泡。 - 调用
onClick函数,它是从Toolbar组件传递过来的 prop。
- 调用
-
在
Toolbar组件中定义的函数,显示按钮对应的 alert。 -
由于传播被阻止,父级
<div>的onClick处理函数不会执行。
由于调用了 e.stopPropagation(),点击按钮现在将只显示一个 alert(来自 <button>),而并非两个(分别来自 <button> 和父级 toolbar <div>)。点击按钮与点击周围的 toolbar 不同,因此阻止传播对这个 UI 是有意义的。
阻止默认行为
某些浏览器事件具有与事件相关联的默认行为。例如,点击 <form> 表单内部的按钮会触发表单提交事件,默认情况下将重新加载整个页面。
export default function Signup() {
return (
<form onSubmit={() => alert('提交表单!')}>
<input />
<button>发送</button>
</form>
);
}
你可以调用事件对象中的 e.preventDefault() 来阻止重新加载整个页面的情况发生
export default function Signup() {
return (
<form onSubmit={e => {
e.preventDefault();
alert('提交表单!');
}}>
<input />
<button>发送</button>
</form>
);
}
不要混淆 e.stopPropagation() 和 e.preventDefault()。它们都很有用,但二者并不相关:
e.stopPropagation()阻止触发绑定在外层标签上的事件处理函数。e.preventDefault()阻止少数事件的默认浏览器行为。
事件处理函数和副作用
事件处理函数是执行副作用的最佳位置。
-
访问组件状态和属性:事件处理函数可以直接访问组件的状态(state)和属性(props),并根据需要进行操作。这使得事件处理函数能够响应用户的操作,并更新组件的状态或触发其他逻辑。
-
生命周期钩子函数:事件处理函数可以在组件的生命周期钩子函数中被调用,这使得我们可以更好地控制副作用的发生时机。例如,在
componentDidMount钩子函数中添加事件监听器,可以确保在组件挂载到 DOM 后才开始监听事件。
React-State
state的由来及其基本使用
组件通常需要根据交互更改屏幕上显示的内容。输入表单应该更新输入字段,单击轮播图上的“下一个”应该更改显示的图片,单击“购买”应该将商品放入购物车。组件需要“记住”某些东西:当前输入值、当前图片、购物车。在 React 中,这种组件特有的记忆被称为 state。
当你调用 useState 时,你是在告诉 React 你想让这个组件记住一些东西,写在useState 的唯一参数是 state 变量的初始值
useState Hook 提供了这两个功能:
- State 变量 用于保存渲染间的数据。
- State setter 函数 更新变量并触发 React 再次渲染组件。
这里的 [ 和 ] 语法称为数组解构,它允许你从数组中读取值。 useState 返回的数组总是正好有两项。
import { useState } from 'react';
const [index, setIndex] = useState(0);
Hook
在 React 中,useState 以及任何其他以“use”开头的函数都被称为 Hook。
Hook 是特殊的函数,只在 React 渲染时有效(我们将在下一节详细介绍)。它们能让你 “hook” 到不同的 React 特性中去。
State 是隔离且私有的
如果你渲染同一个组件两次,每个副本都会有完全隔离的 state!改变其中一个不会影响另一个。
渲染和提交
组件显示到屏幕之前,其必须被 React 渲染。
步骤一:触发渲染
有两种原因会导致组件的渲染:
- 组件的 初次渲染。
- 组件(或者其祖先之一)的 状态发生了改变。
初次渲染
当应用启动时,会触发初次渲染。框架和沙箱有时会隐藏这部分代码,但它是通过调用目标 DOM 节点的 createRoot,然后用你的组件调用 render 函数完成的
import Image from './Image.js';
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'))
root.render(<Image />);
// 试着注释掉 `root.render()`,然后您将会看到组件消失。
状态更新时重新渲染
一旦组件被初次渲染,您就可以通过使用 set 函数 更新其状态来触发之后的渲染。更新组件的状态会自动将一次渲染送入队列。(您可以想象这种情况成餐厅客人在第一次下单之后又点了茶、点心和各种东西,具体取决于他们的胃口。)
步骤二:React渲染你的组件
在您触发渲染后,React 会调用您的组件来确定要在屏幕上显示的内容。 “渲染过程” 就是React在调用你的组件。“正在渲染” 就意味着 React 正在调用你的组件——一个函数。
- 在进行初次渲染时, React 会调用根组件。
- 对于后续的渲染, React 会调用内部状态更新触发了渲染的函数组件,即存在state的组件。
这是一个递归的过程: 如果更新后的组件会返回某个另外的组件,那么 React 接下来就会渲染 那个 组件,而如果那个组件又返回了某个组件,那么 React 接下来就会渲染 那个 组件,以此类推。这个过程会持续下去,直到没有更多的嵌套组件并且 React 确切知道哪些东西应该显示到屏幕上为止。
export default function Gallery() {
return (
<section>
<h1>鼓舞人心的雕塑</h1>
<Image />
<Image />
<Image />
</section>
);
}
function Image() {
return (
<img
src="https://i.imgur.com/ZF6s192.jpg"
alt="'Floralis Genérica' by Eduardo Catalano: a gigantic metallic flower sculpture with reflective petals"
/>
);
}
- 在初次渲染中, React 将会为
<section>、<h1>和三个<img>标签 创建 DOM 节点。 - 在一次重渲染过程中, React 将计算它们的哪些属性(如果有的话)自上次渲染以来已更改。在下一步(提交阶段)之前,它不会对这些信息执行任何操作。
步骤 3: React 把更改提交到 DOM 上
渲染(调用)您的组件之后,React 将会修改 DOM。
- 对于初次渲染, React 会使用
appendChild()DOM API 将其创建的所有 DOM 节点放在屏幕上。 - 对于重新渲染,React 仅在渲染之间存在差异时才会更改 DOM 节点。使得 DOM 与最新的渲染输出相互匹配。
步骤4:浏览器绘制
在渲染完成并且 React 更新 DOM 之后,浏览器就会重新绘制屏幕。
state渲染快照
以下是一个state渲染的一个例子,点击一次该组件的按钮,number只会递增一次而不是+3。
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
}}>+3</button>
</>
)
}
连续调用了三次 setNumber(number + 1),由于这三次调用是在同一次渲染周期内执行的,它们都是基于同一个初始值 number=0 来计算新的状态值,并将这些更新操作加入更新队列。然后,React 在组件渲染完成后才会去批量更新状态,将所有更新操作应用到最新的状态值上。
所以,当你连续点击按钮时,setNumber(number + 1) 在同一次渲染周期内执行了三次,但它们都是基于初始值 number=0 计算得到的,因此最终的结果是 number=1,而不是期望的 number=3。如果你想实现每次点击按钮都加3的效果,可以使用函数式更新来解决:
<button onClick={() => {
setNumber(prevNumber => prevNumber + 1);
setNumber(prevNumber => prevNumber + 1);
setNumber(prevNumber => prevNumber + 1);
}}>+3</button>
通过函数式更新,每个 setNumber 都会接收到前一个状态值作为参数,从而保证每次更新都是基于前一个状态值进行计算,最终实现每次点击按钮都加3的效果。
state
state渲染快照
一个 state 变量的值永远不会在一次渲染的内部发生变化, 即使其事件处理函数的代码是异步的。在 那次渲染的 onClick 内部,number 的值即使在调用 setNumber(number + 5) 之后也还是 0。它的值在 React 通过调用你的组件“获取 UI 的快照”时就被“固定”了。
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 5);
alert(number);
}}>+5</button>
</>
)
}
把一系列 state 更新加入队列
设置组件 state 会把一次重新渲染加入队列。但有时你可能会希望在下次渲染加入队列之前对 state 的值执行多次操作。为此,了解 React 如何批量更新 state 会很有帮助。
每一次渲染的 state 值都是固定的,因此无论你调用多少次 setNumber(1),在第一次渲染的事件处理函数内部的 number 值总是 0
React 会等到事件处理函数中的 所有 代码都运行完毕再处理你的 state 更新。 这就是为什么重新渲染只会发生在所有这些 setNumber() 调用 之后 的原因。
这让你可以更新多个 state 变量——甚至来自多个组件的 state 变量——而不会触发太多的重新渲染。但这也意味着只有在你的事件处理函数及其中任何代码执行完成 之后,UI 才会更新。
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
}}>+3</button>
</>
)
}
更新state中的对象
在 React 中,你需要将 state 视为不可变的!你应该把所有存放在 state 中的 JavaScript 对象都视为只读的。不应该直接修改存放在 React state 中的对象。相反,当你想要更新一个对象时,你需要创建一个新的对象(或者将其拷贝一份),然后将 state 更新为此对象。
注意:在事件处理函数中需要调用setThing才能触发React对组件的重新渲染,若使用obj.key = value直接修改存放在useState()对象,由于没有调用setThing()所以虽然改变你了state但是没有触发渲染。
创建一个新的对象
onPointerMove={e => {
position.x = e.clientX;
position.y = e.clientY;
}}
// 为了真正地 触发一次重新渲染,你需要创建一个新对象并把它传递给 state 的设置函数
onPointerMove={e => {
setPosition({
x: e.clientX,
y: e.clientY
});
}}
用展开语法复制原对象
你可以使用 ... 对象展开 语法,这样你就不需要单独复制每个属性。
setPerson({
...person, // 复制上一个 person 中的所有字段
firstName: e.target.value // 但是覆盖 firstName 字段
});
更新一个嵌套对象
const [person, setPerson] = useState({
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
});
setPerson({
...person, // 复制其它字段的数据
artwork: { // 替换 artwork 字段
...person.artwork, // 复制之前 person.artwork 中的数据
city: 'New Delhi' // 但是将 city 的值替换为 New Delhi!
}
});
使用 Immer 编写简洁的更新逻辑
如果你的 state 有多层的嵌套,你或许应该考虑 将其扁平化。但是,如果你不想改变 state 的数据结构,你可能更喜欢用一种更便捷的方式来实现嵌套展开的效果。Immer 是一个非常流行的库,它可以让你使用简便但可以直接修改的语法编写代码,并会帮你处理好复制的过程。
updatePerson(draft => {
draft.artwork.city = 'Lagos';
});
更新state中的数组
同对象一样,当你想要更新存储于 state 中的数组时,你需要创建一个新的数组(或者创建一份已有数组的拷贝值),并使用新数组设置 state。
向数组中添加元素
你应该创建一个 新 数组,其包含了原始数组的所有元素 以及 一个在末尾的新元素。这可以通过很多种方法实现,最简单的一种就是使用 ... 数组展开 语法:
// 将元素添加在数组末尾
setArtists( // 替换 state
[ // 是通过传入一个新数组实现的
...artists, // 新数组包含原数组的所有元素
{ id: nextId++, name: name } // 并在末尾添加了一个新的元素
]
);
// 数组展开运算符还允许你把新添加的元素放在原始的 `...artists` 之前:
// 将元素添加在数组开头
setArtists([
{ id: nextId++, name: name },
...artists // 将原数组中的元素放在末尾
]);
从数组中删除元素
从数组中删除一个元素最简单的方法就是将它过滤出去。换句话说,你需要生成一个不包含该元素的新数组。这可以通过 filter 方法实现,例如:
import { useState } from 'react';
let initialArtists = [
{ id: 0, name: 'Marta Colvin Andrade' },
{ id: 1, name: 'Lamidi Olonade Fakeye'},
{ id: 2, name: 'Louise Nevelson'},
];
export default function List() {
const [artists, setArtists] = useState(
initialArtists
);
return (
<>
<h1>振奋人心的雕塑家们:</h1>
<ul>
{artists.map(artist => (
<li key={artist.id}>
{artist.name}{' '}
<button onClick={() => {
setArtists(
artists.filter(a =>
a.id !== artist.id
)
);
}}>
删除
</button>
</li>
))}
</ul>
</>
);
}
转换数组
如果你想改变数组中的某些或全部元素,你可以用 map() 创建一个新数组。你传入 map 的函数决定了要根据每个元素的值或索引(或二者都要)对元素做何处理。
import { useState } from 'react';
let initialShapes = [
{ id: 0, type: 'circle', x: 50, y: 100 },
{ id: 1, type: 'square', x: 150, y: 100 },
{ id: 2, type: 'circle', x: 250, y: 100 },
];
export default function ShapeEditor() {
const [shapes, setShapes] = useState(
initialShapes
);
function handleClick() {
const nextShapes = shapes.map(shape => {
if (shape.type === 'square') {
// 不作改变
return shape;
} else {
// 返回一个新的圆形,位置在下方 50px 处
return {
...shape,
y: shape.y + 50,
};
}
});
// 使用新的数组进行重渲染
setShapes(nextShapes);
}
return (
<>
<button onClick={handleClick}>
所有圆形向下移动!
</button>
{shapes.map(shape => (
<div
key={shape.id}
style={{
background: 'purple',
position: 'absolute',
left: shape.x,
top: shape.y,
borderRadius:
shape.type === 'circle'
? '50%' : '',
width: 20,
height: 20,
}} />
))}
</>
);
}
替换数组中的元素
要替换一个元素,请使用 map 创建一个新数组。在你的 map 回调里,第二个参数是元素的索引。使用索引来判断最终是返回原始的元素(即回调的第一个参数)还是替换成其他值
import { useState } from 'react';
let initialCounters = [
0, 0, 0
];
export default function CounterList() {
const [counters, setCounters] = useState(
initialCounters
);
function handleIncrementClick(index) {
const nextCounters = counters.map((c, i) => {
if (i === index) {
// 递增被点击的计数器数值
return c + 1;
} else {
// 其余部分不发生变化
return c;
}
});
setCounters(nextCounters);
}
return (
<ul>
{counters.map((counter, i) => (
<li key={i}>
{counter}
<button onClick={() => {
handleIncrementClick(i);
}}>+1</button>
</li>
))}
</ul>
);
}
向数组中插入元素
有时,你也许想向数组特定位置插入一个元素,这个位置既不在数组开头,也不在末尾。为此,你可以将数组展开运算符 ... 和 slice() 方法一起使用。slice() 方法让你从数组中切出“一片”。为了将元素插入数组,你需要先展开原数组在插入点之前的切片,然后插入新元素,最后展开原数组中剩下的部分。
下面的例子中,插入按钮总是会将元素插入到数组中索引为 1 的位置。
import { useState } from 'react';
let nextId = 3;
const initialArtists = [
{ id: 0, name: 'Marta Colvin Andrade' },
{ id: 1, name: 'Lamidi Olonade Fakeye'},
{ id: 2, name: 'Louise Nevelson'},
];
export default function List() {
const [name, setName] = useState('');
const [artists, setArtists] = useState(
initialArtists
);
function handleClick() {
const insertAt = 1; // 可能是任何索引
const nextArtists = [
// 插入点之前的元素:
...artists.slice(0, insertAt),
// 新的元素:
{ id: nextId++, name: name },
// 插入点之后的元素:
...artists.slice(insertAt)
];
setArtists(nextArtists);
setName('');
}
return (
<>
<h1>振奋人心的雕塑家们:</h1>
<input
value={name}
onChange={e => setName(e.target.value)}
/>
<button onClick={handleClick}>
插入
</button>
<ul>
{artists.map(artist => (
<li key={artist.id}>{artist.name}</li>
))}
</ul>
</>
);
}
其他改变数组的情况
是你仅仅依靠展开运算符和 map() 或者 filter() 等不会直接修改原值的方法所无法做到的。例如,你可能想翻转数组,或是对数组排序。而 JavaScript 中的 reverse() 和 sort() 方法会改变原数组,所以你无法直接使用它们。
然而,你可以先拷贝这个数组,再改变这个拷贝后的值。
import { useState } from 'react';
let nextId = 3;
const initialList = [
{ id: 0, title: 'Big Bellies' },
{ id: 1, title: 'Lunar Landscape' },
{ id: 2, title: 'Terracotta Army' },
];
export default function List() {
const [list, setList] = useState(initialList);
function handleClick() {
const nextList = [...list];
nextList.reverse();
setList(nextList);
}
return (
<>
<button onClick={handleClick}>
翻转
</button>
<ul>
{list.map(artwork => (
<li key={artwork.id}>{artwork.title}</li>
))}
</ul>
</>
);
}
然而,即使你拷贝了数组,你还是不能直接修改其内部的元素。这是因为数组的拷贝是浅拷贝——新的数组中依然保留了与原始数组相同的元素。因此,如果你修改了拷贝数组内部的某个对象,其实你正在直接修改当前的 state。举个例子,像下面的代码就会带来问题。
const nextList = [...list];
nextList[0].seen = true; // 问题:直接修改了 list[0] 的值
setList(nextList);
然 nextList 和 list 是两个不同的数组,nextList[0] 和 list[0] 却指向了同一个对象。因此,通过改变 nextList[0].seen,list[0].seen 的值也被改变了。看以下解决方案
更新数组内部的对象
对象并不是 真的 位于数组“内部”。可能他们在代码中看起来像是在数组“内部”,但其实数组中的每个对象都是这个数组“指向”的一个存储于其它位置的值。
setMyList(myList.map(artwork => {
if (artwork.id === artworkId) {
// 创建包含变更的*新*对象
return { ...artwork, seen: nextSeen };
} else {
// 没有变更
return artwork;
}
}));
此处的 ... 是一个对象展开语法,被用来创建一个对象的拷贝.