Skip to content
已经学习

React 7/n - 交互

响应事件

https://zh-hans.react.dev/learn/responding-to-events

添加事件处理函数

如需添加一个事件处理函数,你需要先定义一个函数,然后 将其作为 prop 传入 合适的 JSX 标签。

事件处理函数有如下特点:

  • 通常在你的组件 内部 定义。
  • 名称以 handle 开头,后跟事件名称。

在事件处理函数中读取 props

由于事件处理函数声明于组件内部,因此它们可以直接访问组件的 props

function AlertButton({ message, children }) {
return (
<button onClick={() => alert(message)}>
{children}
</button>
);
}
export default function Toolbar() {
return (
<div>
<AlertButton message="正在播放!">
播放电影
</AlertButton>
<AlertButton message="正在上传!">
上传图片
</AlertButton>
</div>
);
}

将事件处理函数作为 props 传递

通常,我们会在父组件中定义子组件的事件处理函数。比如:置于不同位置的 Button 组件,可能最终执行的功能也不同

function Button({ onClick, children }) {
return (
<button onClick={onClick}>
{children}
</button>
);
}
function PlayButton({ movieName }) {
function handlePlayClick() {
alert(`正在播放 ${movieName}`);
}
return (
<Button onClick={handlePlayClick}>
播放 "{movieName}"
</Button>
);
}
function UploadButton() {
return (
<Button onClick={() => alert('正在上传!')}>
上传图片
</Button>
);
}
export default function Toolbar() {
return (
<div>
<PlayButton movieName="魔女宅急便" />
<UploadButton />
</div>
);
}

命名事件处理函数 prop

内置组件(<button> 和 <div>)仅支持 浏览器事件名称,例如 onClick。但是,当你构建自己的组件时,你可以按你个人喜好命名事件处理函数的 prop。这种处理非常优雅,因为是使程序非常可读

按照惯例,事件处理函数 props 应该以 on 开头,后跟一个大写字母。例如,Button 组件的 onClick prop 本来也可以被命名为 onSmash

function Button({ onSmash, children }) {
return (
<button onClick={onSmash}>
{children}
</button>
);
}
export default function App() {
return (
<div>
<Button onSmash={() => alert('正在播放!')}>
播放电影
</Button>
<Button onSmash={() => alert('正在上传!')}>
上传图片
</Button>
</div>
);
}
// 上述示例中,<button onClick={onSmash}> 代表浏览器内置的 <button>(小写)仍然需要使用 onClick prop,而自定义的Button 组件接收到的 prop 名称可由你决定!

事件传播

事件处理函数还将捕获任何来自子组件的事件。通常,我们会说事件会沿着树向上“冒泡”或“传播”:它从事件发生的地方开始,然后沿着树向上传播。

export default function Toolbar() {
return (
<div className="Toolbar" onClick={() => {
alert('你点击了 toolbar !');
}}>
<button onClick={() => alert('正在播放!')}>
播放电影
</button>
</div>
);
}
// “正在播放!”先出现,再出现 “你点击了 toolbar !”

阻止传播

https://zh-hans.react.dev/learn/responding-to-events#stopping-propagation

事件处理函数接收一个 事件对象 作为唯一的参数。按照惯例,它通常被称为 e ,代表 “event”(事件)。你可以使用此对象来读取有关事件的信息

这个事件对象还允许你阻止传播。如果你想阻止一个事件到达父组件,可以调用 e.stopPropagation():

捕获阶段事件 极少数情况下,你可能需要捕获子元素上的所有事件,即便它们阻止了传播。例如,你可能想对每次点击进行埋点记录,传播逻辑暂且不论。那么你可以通过在事件名称末尾添加 Capture 来实现这一点:

<div onClickCapture={() => { /* 这会首先执行 */ }}>
<button onClick={e => e.stopPropagation()} />
<button onClick={e => e.stopPropagation()} />
</div>

阻止默认行为

某些浏览器事件具有与事件相关联的默认行为。例如,点击 <form> 表单内部的按钮会触发表单提交事件,默认情况下将重新加载整个页面。可以调用事件对象中的 e.preventDefault() 来阻止这种情况发生

  • e.stopPropagation() 阻止触发绑定在外层标签上的事件处理函数。
  • e.preventDefault() 阻止少数事件的默认浏览器行为。

state: 组件的记忆

https://zh-hans.react.dev/learn/state-a-components-memory

要使用新数据更新组件,需要做两件事:

  • 保留 渲染之间的数据。
  • 触发 React 使用新数据渲染组件(重新渲染)。

useState Hook 提供了这两个功能:

  • State 变量 用于保存渲染间的数据。
  • State setter 函数 更新变量并触发 React 再次渲染组件。
const [index, setIndex] = useState(0);
// index 是一个 state 变量,setIndex 是对应的 setter 函数。(每次你的组件渲染时,useState 都会给你一个包含两个值的数组)。
// 这里的 [ 和 ] 语法称为数组解构,它允许你从数组中读取值。 useState 返回的数组总是正好有两项。

在 React 中,useState 以及任何其他以“use”开头的函数都被称为 Hook。 Hook 是特殊的函数,只在 React 渲染时有效。它们能让你 “hook” 到不同的 React 特性中去。

State 是隔离且私有的

State 是屏幕上组件实例内部的状态。换句话说,如果你渲染同一个组件两次,每个副本都会有完全隔离的 state!改变其中一个不会影响另一个。

state的作用域“只限于”屏幕上的某块特定区域

渲染和提交

import Image from './Image.js';
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'))
root.render(<Image />);
//当应用启动时,会触发初次渲染。框架和沙箱有时会隐藏这部分代码,但它是通过调用 createRoot 方法并传入目标 DOM 节点,然后用你的组件调用 render 函数完成的

不要过早进行优化!

import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 5);
setTimeout(() => {
alert(number);
}, 3000);
}}>+5</button>
</>
)
}
// 用户与之交互时状态的*快照*进行调度的。alert的值为0

一个 state 变量的值永远不会在一次渲染的内部发生变化, 即使其事件处理函数的代码是异步的。在 那次渲染的 onClick 内部,number 的值即使在调用 setNumber(number + 5) 之后也还是 0。它的值在 React 通过调用你的组件“获取 UI 的快照”时就被“固定”了。

React 会对 state 更新进行批处理

setNumber(n => n + 1) 使用更新函数

总而言之,以下是你可以考虑传递给 setNumber state 设置函数的内容:

  • 一个更新函数(例如:n => n + 1)会被添加到队列中。
  • 任何其他的值(例如:数字 5)会导致“替换为 5”被添加到队列中,已经在队列中的内容会被忽略。

事件处理函数执行完成后,React 将触发重新渲染。在重新渲染期间,React 将处理队列。更新函数会在渲染期间执行,因此 更新函数必须是 纯函数 并且只 返回 结果。不要尝试从它们内部设置 state 或者执行其他副作用。在严格模式下,React 会执行每个更新函数两次(但是丢弃第二个结果)以便帮助你发现错误。

命名惯例: 通常可以通过相应 state 变量的第一个字母来命名更新函数的参数

setEnabled(e => !e);
setLastName(ln => ln.reverse());
setFriendCount(fc => fc * 2);

更新 state 中的对象

https://zh-hans.react.dev/learn/updating-objects-in-state

state 中可以保存任意类型的 JavaScript 值,包括对象。但是,你不应该直接修改存放在 React state 中的对象。相反,当你想要更新一个对象时,你需要创建一个新的对象(或者将其拷贝一份),然后将 state 更新为此对象

你应该像处理数字、布尔值、字符串一样将对象视为不可变的。因此你应该替换对象的值,而不是对对象进行修改。=> 将 state 视为只读的

把所有存放在 state 中的 JavaScript 对象都视为只读的

使用展开语法复制对象

重点: https://zh-hans.react.dev/learn/updating-objects-in-state#copying-objects-with-the-spread-syntax

使用展开语法,方便

setPerson({
...person, // 复制上一个 person 中的所有字段
firstName: e.target.value // 但是覆盖 firstName 字段
});

请注意 … 展开语法本质是是“浅拷贝”——它只会复制一层。这使得它的执行速度很快,但是也意味着当你想要更新一个嵌套属性时,你必须得多次使用展开语法。

setPerson({
...person, // 复制其它字段的数据
artwork: { // 替换 artwork 字段
...person.artwork, // 复制之前 person.artwork 中的数据
city: 'New Delhi' // 但是将 city 的值替换为 New Delhi!
}
});

如果想简洁可以可以使用https://github.com/immerjs/use-immer

Immer原理: 由 Immer 提供的 draft 是一种特殊类型的对象,被称为 Proxy,它会记录你用它所进行的操作。这就是你能够随心所欲地直接修改对象的原因所在!从原理上说,Immer 会弄清楚 draft 对象的哪些部分被改变了,并会依照你的修改创建出一个全新的对象。

可以看使用案例: https://zh-hans.react.dev/learn/updating-objects-in-state#write-concise-update-logic-with-immer

更新 state 中的数组

学习文档: https://zh-hans.react.dev/learn/updating-arrays-in-state

数组是另外一种可以存储在 state 中的 JavaScript 对象,它虽然是可变的,但是却应该被视为不可变。同对象一样,当你想要更新存储于 state 中的数组时,你需要创建一个新的数组(或者创建一份已有数组的拷贝值),并使用新数组设置 state。

在 JavaScript 中,数组只是另一种对象。同对象一样,你需要将 React state 中的数组视为只读的。这意味着你不应该使用类似于 arr[0] = ‘bird’ 这样的方式来重新分配数组中的元素,也不应该使用会直接修改原始数组的方法,例如 push() 和 pop()。

相反,每次要更新一个数组时,你需要把一个新的数组传入 state 的 setting 方法中。为此,你可以通过使用像 filter() 和 map() 这样不会直接修改原始值的方法,从原始数组生成一个新的数组。然后你就可以将 state 设置为这个新生成的数组。

使用 map 在没有 mutation 的前提下将一个旧的元素替换成更新的版本。

结论: 请使用 Immer 来保持代码简洁

TODO NEXT

网站当前构建日期: 2025.02.01