系列文章目录
一、 React快速入门
二、React描述IU
三、React添加交互
文章目录
- 系列文章目录
- 前言
- 三、添加交互
-
- 1.响应事件
-
- 1.1添加事件处理函数
- 1.2事件处理函数读取props
- 1.2将事件处理函数作为props传递
- 1.3命名事件处理函数
- [1.4 事件传播(冒泡)](#1.4 事件传播(冒泡))
- [1.5 阻止事件传播(冒泡) `e.stopPropagation()`](#1.5 阻止事件传播(冒泡)
e.stopPropagation()
) - [1.6 传递处理函数作为事件传播的替代方案](#1.6 传递处理函数作为事件传播的替代方案)
- 1.7阻止默认行为`e.preventDefault()`
- [1.8 作业](#1.8 作业)
- [1.9 小结](#1.9 小结)
- [2. State](#2. State)
-
- [2.1 添加state变量](#2.1 添加state变量)
- [2.2 Hook](#2.2 Hook)
- [2.3 useState详解](#2.3 useState详解)
- [2.4 使用多个state变量](#2.4 使用多个state变量)
- [2.5 小结](#2.5 小结)
- [3. 渲染和提交](#3. 渲染和提交)
-
- [3.1 步骤一:触发一次渲染](#3.1 步骤一:触发一次渲染)
- [3.2 步骤二:React渲染组件](#3.2 步骤二:React渲染组件)
- [3.3 步骤三:React把更改提交到DOM](#3.3 步骤三:React把更改提交到DOM)
- 3.4小结
- [4. state如同一张快照](#4. state如同一张快照)
-
- 4.1设置state会触发渲染
- [4.2 渲染会及时生成一张快照](#4.2 渲染会及时生成一张快照)
- [4.3 随时间变化的state](#4.3 随时间变化的state)
- [4.4 小结](#4.4 小结)
- [4.5 作业](#4.5 作业)
- [5. 把一系列的state更新加入队列](#5. 把一系列的state更新加入队列)
-
- [5.1 React对state更新进行批处理](#5.1 React对state更新进行批处理)
- [5.2 在下次渲染前多次更新同一个 state](#5.2 在下次渲染前多次更新同一个 state)
- [5.3 替换 state 后更新 state](#5.3 替换 state 后更新 state)
- [5.4 关于更新函数](#5.4 关于更新函数)
- 5.5小结
- 6.更新state中的对象
-
- [6.1 mutation](#6.1 mutation)
- [6.2 将state视为只读](#6.2 将state视为只读)
- [6.3 展开语法复制对象](#6.3 展开语法复制对象)
- 6.4更新一个嵌套对象
- [6.5 Immer简洁逻辑](#6.5 Immer简洁逻辑)
- [7. 更新state中的数组](#7. 更新state中的数组)
-
- [7.1 使用数组方法更新数组](#7.1 使用数组方法更新数组)
-
- [7.1.1 像数组中添加元素concat/[...arr]](#7.1.1 像数组中添加元素concat/[...arr])
- 7.1.2从数组中删除元素filter/slice
- [7.1.3 替换(修改)数组中的元素](#7.1.3 替换(修改)数组中的元素)
- [7.1.4 数组中插入元素](#7.1.4 数组中插入元素)
- [7.1.5 其他改变数组的情况](#7.1.5 其他改变数组的情况)
- [7.2 更新数组内部的对象](#7.2 更新数组内部的对象)
- [7.3 Immer简洁逻辑](#7.3 Immer简洁逻辑)
- [7.4 小结](#7.4 小结)
- 总结
前言
界面上的一些元素(比如文本框、按钮、下拉菜单等)会根据用户在这些元素上输入或者选择的内容而发生变化,例如,点击按钮切换轮播图的展示。
在 React
中,随时间变化的数据被称为状态(state)
在本章节中,你将学习如何编写处理交互 的组件,更新它们的状态,并根据时间变化显示不同的效果。
继续往下看吧 ^皿^
三、添加交互
1.响应事件
React
可以在JSX
添加事件处理函数
事件处理函数:自定义函数,它将在响应交互(如点击、悬停、表单输入框获得焦点等)时触发
1.1添加事件处理函数
步骤一:定义一个Button组件,内部声明一个handleClick
函数
javascript
function Button() {
return (
<button>
点击弹出提示
</button>
)
}
步骤二:实现内部逻辑(栗子中的逻辑是alert
显示消息)
javascript
function handleClick() {
alert('点我了!')
}
步骤三:添加onClick={handleClick}
到button
标签上
<button onClick={handleClick}>
全部代码:
javascript
export default function Button() {
function handleClick() {
alert('点我了!');
}
return (
<button onClick={handleClick}>
点击弹出提示
</button>
);
}
这样我们就完成了
a.事件处理函数的特点:通常在组件内部 定义; 名称以
handle
开头,后跟事件的名称b.事件处理函数简短的话我们也可以使用内联 的方式:
<button onClick={function handleClick() { alert('你点击了我!'); }}>
or<button onClick={() => { alert('你点击了我!'); }}>
c.>
1.2事件处理函数读取props
事件处理函数可以直接访问组件的props
javascript
function AButton({ message, children }) {
return (
<button onClick={() => alert(message)}>
{children}
</button>
)
}
export default function ToolBar() {
return (
<>
<AButton message="我被点击了!">点击我</AButton>
<AButton message="你被点击了!">点击你</AButton>
</>
)
}
这段代码描述的是:
定义一个Button
组件,接收message
和children
并处理。导出组件ToolBar
,这个组件里面使用了Button
组件,Button
组件传入了message
的值。
1.2将事件处理函数作为props传递
在父组件中定义子组件的事件处理函数
将组件从父组件接收的 prop
作为事件处理函数传递
javascript
//父组件
function AButton({ message, children }) {
function handleClick() {
//使用的是模板字符串``,可以在字符串中使用${}插入变量或者表达式
alert(`目前弹出的消息是${message}`)
}
return (
<button onClick={handleClick}>
{children}
</button>
)
}
//子组件
export default function ToolBar() {
return (
<>
<AButton message="我被点击了!">点击我</AButton>
<AButton message="你被点击了!">点击你</AButton>
</>
)
}
1.3命名事件处理函数
内置组件比如button、div
这种只支持浏览器事件名称如onClick
。
当构建自己的组件时可以自命名事件处理函数的prop
,惯例 :事件处理函数的名称需要:on
开头后跟一个大写字母。
可能有人疑问这里这个prop是啥,诺,就是这个:
Button
是自定义组件,自命名栗子的例如这个:
1.4 事件传播(冒泡)
事件处理函数还将捕获任何来自子组件的事件。
cpp
function AButton({ message, children }) {
function handleClick() {
// 模板字符串
alert(`目前弹出的消息是${message}`)
}
return (
<div onClick={() => alert('我是button的父元素')}>
<button onClick={handleClick}>
{children}
</button>
</div>
)
}
export default function ToolBar() {
return (
<>
<AButton message="我被点击了!">点击我</AButton>
<br />
<AButton message="你被点击了!">点击你</AButton>
</>
)
}
上述代码是一个事件冒泡的示例,当我点击两个按钮中的任何一个,他会弹出两个提示框,首先是'xx被点击了',一个是'我是button的父元素'
在 React 中所有事件都会传播,除了 onScroll,它仅适用于你附加到的 JSX 标签。
1.5 阻止事件传播(冒泡) e.stopPropagation()
事件处理函数接收一个事件对象作为唯一的参数,它通常被称为 e
,代表 "event"
(事件)。你可以使用此对象来读取有关事件的信息。
如果你想阻止一个事件到达父组件,调用 e.stopPropagation()
javascript
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>
</div>
);
}
当你点击按钮时:
a.
React
调用了传递给<button>
的 onClick 处理函数。b.调用
e.stopPropagation()
,阻止事件进一步冒泡。调用onClick
函数c.在 Toolbar 组件中定义的函数,显示
alert
。d.由于传播被阻止,父级
<div>
的 onClick 处理函数不会执行
1.6 传递处理函数作为事件传播的替代方案
如果你依赖于事件传播,而且很难追踪哪些处理程序在执行,及其执行的原因
javascript
<button onClick={e => {
e.stopPropagation();
onClick();
}}>
可以考虑像上面代码一样,添加一些代码让组件做出额外的行为,这样有利于追踪某个事件触发时的一整个过程(链)。
1.7阻止默认行为e.preventDefault()
比如form
表单内部点击按钮会默认重新加载页面
javascript
export default function Signup() {
return (
<form onSubmit={() => alert('提交表单')}>
<input type="text" />
<button>提交</button>
</form>
)
}
我们可以使用事件对象中的 e.preventDefault()
来阻止这种情况发生:
javascript
export default function Signup() {
return (
<form onSubmit={(e) => {
alert('提交表单')
e.preventDefault();
}}>
<input type="text" />
<button>提交</button>
</form>
)
}
1.8 作业
a. 修复事件处理函数
点击此按钮理论上应该在黑白主题之间切换页面背景
javascript
export default function LightSwitch() {
function handleClick() {
let bodyStyle = document.body.style;
if (bodyStyle.backgroundColor === 'black') {
bodyStyle.backgroundColor = 'white';
} else {
bodyStyle.backgroundColor = 'black';
}
}
return (
<button onClick={handleClick()}>
切换背景
</button>
);
}
b. 修改问题使得点击按钮 只 改变颜色,并且 不 增加计数器。
javascript
export default function ColorSwitch({
onChangeColor
}) {
return (
<button>
改变颜色
</button>
);
}
答案:a.去掉handleClick的小括号 b.阻止冒泡
1.9 小结
- 渲染函数须是纯函数,而事件处理函数不需要。
- 可以将函数作为
prop
传递给元素如<button>
来处理事件。 - 必须传递事件处理函数,而非函数调用!
onClick={handleClick} ,不是 onClick={handleClick()}
。 - 事件处理函数在组件内部定义,所以它们可以访问 props。
- 可以在父组件中定义一个事件处理函数,并将其作为 prop 传递给子组件。
- 事件会向上传播。通过事件的第一个参数调用 e.stopPropagation() 来防止这种情况。
- 事件可能具有不需要的浏览器默认行为。调用 e.preventDefault() 来阻止这种情况。
- 从子组件显式调用事件处理函数 prop 是事件传播的另一种优秀替代方案。
2. State
组件通常需要根据交互更改屏幕上显示的内容。表单的输入,插入的图片等,组件需要记录某些数据:当前输入值,当前图片等,我们需要State
来记忆这些数据。
要使用新数据更新组件,需要满足:
a.保留 渲染之前的数据
b.触发 React使用新数据渲染组件(即重新渲染)
useState Hook
提供了这两个功能:State变量
和 State setter函数
2.1 添加state变量
步骤1:导入useState
import { useState } from 'react';
步骤2:声明变量,数组解构
const [index, setIndex] = useState(0);
步骤3:书写逻辑
setIndex(index + 1);
一个简单小栗子全部代码,帮助你更好的理解。
javascript
import React from 'react';
import { useState } from 'react';
export default function Sum() {
const [index, setIndex] = useState(0);
function handleAdd() {
setIndex(index + 1);
}
return (
<div>
<button onClick={handleAdd}>
点击+1
</button>
<span>{index}</span>
</div>
)
}
2.2 Hook
题外话:读起这个名词就让我想到绿巨人浩克
在React
中,useState
以及任何其他以 "use"开头 的函数都被称为 Hook
。
特殊的函数,只在 React 渲染时有效。
注意:不能在条件语句、循环语句或其他嵌套函数内调用 Hook。只能在组件或自定义 Hook 的最顶层调用。
2.3 useState详解
当你调用useState
时,就告诉了React你想让组件'记住'某些数据
const [index, setIndex] = useState(0)
是让React
记住index
。
a. 惯例命名为
const [thing, setThing]
0是变量index
的初始值 即useState的唯一参数是state变量的初始值
b. 每次你的组件渲染时,
useState
都会给你一个包含两个值的数组:
state 变量 (index)
会保存上次渲染的值。
state setter 函数 (setIndex)
可以更新 state 变量并触发 React 重新渲染组件。
2.4 使用多个state变量
很简单,看代码就懂了
javascript
import React from 'react';
import { useState } from 'react';
export default function Sum() {
const [index, setIndex] = useState(0);
const [num, setNum] = useState(2);
function handleAdd() {
setIndex(index + 2);
}
function handleMul() {
setNum(num * 2)
}
return (
<>
<div>
<button onClick={handleAdd}>
点击+2
</button>
<span>{index}</span>
</div>
<div>
<button onClick={handleMul}>
点击*2
</button>
<span>{num}</span>
</div>
</>
)
}
如果你发现经常同时更改两个 state
变量,那么最好将它们合并为一个。
后续会学习到如何优化。
State 是组件实例内部的状态。渲染同一个组件两次,两个state不会相互影响,相互独立。
2.5 小结
- 当一个组件需要在多次渲染间"记住"某些信息时使用
state
变量。 State
变量是通过调用useState Hook
来声明的。Hook
是以use
开头的特殊函数,需要在非条件语句中调用。- 调用
Hook
时,包括useState
,仅在组件或另一个Hook
的顶层被调用才有效。 useState Hook
返回:当前state
和更新它的函数。- 你可以拥有多个
state
变量,按顺序匹配它们。 State
是组件私有的。如果你在两个地方渲染它,则每个副本都有独属于自己的state
。
3. 渲染和提交
理解这些处理步骤有利于引发对代码的思考和解释代码的逻辑
分为三步:
步骤一:触发渲染(菜单分配给厨房)
步骤二:渲染组件(做菜)
步骤三:提交到DOM(上菜)
3.1 步骤一:触发一次渲染
导致组件渲染的原因:
- a.组件初次渲染
通过调用DOM节点的creatRoot
,然后组件调用render
函数完成。 - b.组件状态发生改变。
初次渲染后,可调用set函数
更新状态,触发渲染。
3.2 步骤二:React渲染组件
"渲染中" 即 React
在调用你的组件
- 初次渲染,调用根组件
- 后续渲染,调用内部状态更新触发了渲染的函数组件
注意:渲染必须始终是一次 纯计算!给定的输入结果返回始终唯一,不更改任何存在于渲染之前的对象或变量。
3.3 步骤三:React把更改提交到DOM
在渲染(调用)你的组件之后,React
将会修改 DOM
- 初次渲染,使用
appendChild() DOM API
将其创建的所有DOM
节点放在屏幕上。 - 重渲染,React 将应用最少的必要操作(在渲染时计算),以使得
DOM
与最新的渲染输出相互匹配。
React
仅在渲染之间存在差异时才会更改 DOM 节点。
3.4小结
React
屏幕更新会发生三个步骤:触发、渲染、提交- 可以使用严格模式去寻找组件中的错误
- 如果渲染结果与上次一样,那么
React
将不会修改DOM
- 在渲染完成并且
React
更新DOM
之后,浏览器就会重新绘制屏幕
4. state如同一张快照
4.1设置state会触发渲染
当你设置组件的状态时,React
检测到状态变化,调用 render
方法来重新渲染组件,从而确保UI能够反映最新的状态。
当你按下 "send"
时,setIsSent(true)
会通知 React
重新渲染 UI
javascript
import { useState } from 'react';
export default function Form() {
const [isSent, setIsSent] = useState(false);
const [message, setMessage] = useState('Hi!');
if (isSent) {
return <h1>Your message is on its way!</h1>
}
return (
<form onSubmit={(e) => {
e.preventDefault();
setIsSent(true);
sendMessage(message);
}}>
<textarea
placeholder="Message"
value={message}
onChange={e => setMessage(e.target.value)}
/>
<button type="submit">Send</button>
</form>
);
}
function sendMessage(message) {
// ...
}
4.2 渲染会及时生成一张快照
当 React
重新渲染一个组件时:
React
会再次调用你的函数- 函数会返回新的
JSX
快照 React
会更新界面以匹配返回的快照
javascript
<button onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
}}>+3</button>
这段代码,点击+3按钮数字会变成1,2,3这样递增
尽管你调用了三次 setNumber(number + 1)
,但在 这次渲染的 事件处理函数中 number
会一直是 0,所以你会三次将state
设置成 1。这就是为什么在你的事件处理函数执行完以后,React
重新渲染的组件中的 number
等于 1 而不是 3 。
4.3 随时间变化的state
- 这段代码弹出的数字是 0
javascript
<button onClick={() => {
setNumber(number + 5);
alert(number);
}}>+5</button>
- 这段代码也弹出的是 0
javascript
<button onClick={() => {
setNumber(number + 5);
setTimeout(() => {
alert(number);
}, 3000);
}}>+5</button>
他们俩渲染的页面不同。
一个 state
变量的值永远不会在一次渲染的内部发生变化, 即使其事件处理函数的代码是异步的。
React
会使state
的值始终"固定"在一次渲染的各个事件处理函数内部
试一试:如下代码,点击send
,快速切换Alice
为Bob
,你猜会给谁说hello
?
javascript
import { useState } from 'react';
export default function Form() {
const [to, setTo] = useState('Alice');
const [message, setMessage] = useState('Hello');
function handleSubmit(e) {
e.preventDefault();
setTimeout(() => {
alert(`You said ${message} to ${to}`);
}, 5000);
}
return (
<form onSubmit={handleSubmit}>
<label>
To:{' '}
<select
value={to}
onChange={e => setTo(e.target.value)}>
<option value="Alice">Alice</option>
<option value="Bob">Bob</option>
</select>
</label>
<textarea
placeholder="Message"
value={message}
onChange={e => setMessage(e.target.value)}
/>
<button type="submit">Send</button>
</form>
);
}
还是Alice
因为:React 会使state 的值始终"固定"在一次渲染的各个事件处理函数内部
想要读取最新的state可以使用状态更新函数,之后会学习到。
4.4 小结
React
将state
存储在组件之外,就像在架子上一样。- 当你调用
useState
时,React 会为你提供该次渲染 的一张state
快照。 - 每个渲染都有自己的事件处理函数。
- 每个渲染(以及其中的函数)始终"看到"的是
React
提供给这个 渲染的state
快照。 - 过去创建的事件处理函数拥有的是创建它们的那次渲染中的
state
值。
4.5 作业
向 click 事件处理函数添加一个 alert 。当灯为绿色且显示"Walk"时,单击按钮应显示"Stop is next"。当灯为红色并显示"Stop"时,单击按钮应显示"Walk is next"。
javascript
import { useState } from 'react';
export default function TrafficLight() {
const [walk, setWalk] = useState(true);
function handleClick({children}) {
setWalk(!walk);
if(walk){
alert('Stop is next')
}else{
alert('walk is next')
}
}
return (
<>
<button onClick={handleClick}>
Change to {walk ? 'Stop' : 'Walk'}
</button>
<h1 style={{
color: walk ? 'darkgreen' : 'darkred'
}}>
{walk ? 'Walk' : 'Stop'}
</h1>
</>
);
}
答案:略
5. 把一系列的state更新加入队列
下次渲染之前对state
进行操作,如何批量更新state
5.1 React对state更新进行批处理
点击按钮1次,值会变成1而不是3
每一次渲染的 state
值都是固定的,因此无论你调用多少次 setNumber(1)
,在第一次渲染的事件处理函数内部的 number
值总是 0
BUT React
会等到事件处理函数中的所有代码都运行完毕,再处理你的 state
更新。
这让你可以更新多个 state
变量。这也意味着只有在你的事件处理函数及其中任何代码执行完成 之后,UI
才会更新。这种特性也就是 批处理
5.2 在下次渲染前多次更新同一个 state
这是一个不常见的用例。
可以像setNumber(n=> n+ 1)
这样传入依赖前一个 state
来计算下一个 state
的函数,而不是像 setNumber(number + 1)
这样传入 下一个 state
值。这样就告诉 React
用 state
值做某事"而不是单纯替换。
javascript
<button onClick={() => {
setNumber(number => n + 1);
setNumber(number => n + 1);
setNumber(number => n + 1);
}}>+3</button>
在这里,n => n + 1 被称为 更新函数
当你将它传递给一个 state 设置函数时:
React 会将此函数加入队列,以便在事件处理函数中的所有其他代码运行后进行处理。
在下一次渲染期间,React 会遍历队列并给你更新之后的最终 state。
5.3 替换 state 后更新 state
javascript
<button onClick={() => {
setNumber(number + 5);
setNumber(n => n + 1);
}}>增加数字</button>
得到的是 6
实现步骤 :a. setNumber(number + 5)
:number
为 0
,所以 setNumber(0 + 5)
。React 将 "替换为 5
" 添加到其队列中。
b. setNumber(n => n + 1)
:n => n + 1
是一个更新函数。React 将该函数添加到其队列中。
c.下一次渲染期间,react遍历state队列:
react
保存 6 为最终结果并从useState
中返回。
再看一个例子:
javascript
<button onClick={() => {
setNumber(number + 5);
setNumber(n => n + 1);
setNumber(42);
}}>增加数字</button>
得到的是42
实现步骤 :
a. setNumber(number + 5)
:number
为 0
,所以 setNumber(0 + 5)
。React 将 "替换为 5" 添加到其队列中。
b. setNumber(n => n + 1)
:n => n + 1
是更新函数。React
将该函数添加到其队列。
c. setNumber(42)
:React
将 "替换为 42" 添加到其队列中。
在下一次渲染期间,React 会遍历 state 队列:
5.4 关于更新函数
更新函数必须是纯函数,而且只返回结果。
命名通常可以通过相应state
变量的第一个字母 来命名更新函数的参数:比如state
变量是number
那么更新函数参数命名为n
5.5小结
- 设置
state
不改变现有渲染中变量,但会请求一次新渲染。 React
会在事件处理函数执行完成之后处理state
更新。这被称为批处理。- 要在一个事件中多次更新某些
state
,你可以使用setNumber(n => n + 1) 更新函数
。
6.更新state中的对象
state中可以保存任意类型的 JS 值,包括对象。
不要直接修改state中的对象,需要新创建一个(可copy一个),将state更新为此对象。
6.1 mutation
目前再state存放过数字、字符串、布尔类型的值,这些再js中是不可变的,可以通过替换它们得值来触发新的渲染。
在 JS 中,无法对内置的原始值,如数字、字符串和布尔值,进行任何更改。
现在我们来存放一个对象:
const [position, setPosition] = useState({ x: 0, y: 0 });
修改一下position中x的值:position.x = 5
。改变对象自身的内容,你这样做时相当于制造了一个mutation
虽然严格来说 React state 中存放的对象是可变的,但你应该像处理数字、布尔值、字符串一样将它们视为不可变的。因此你应该替换它们的值,而不是对它们进行修改。
6.2 将state视为只读
把所有存放在state
中的JS
对象都视为只读的。
栗子:
我们用一个存放在 state
中的对象来表示指针当前的位置。当你在预览区触摸或移动光标时,红色的点本应移动。但是实际上红点仍停留在原处
javascript
import { useState } from 'react';
export default function MovingDot() {
const [position, setPosition] = useState({
x: 0,
y: 0
});
return (
<div
onPointerMove={e => {
position.x = e.clientX;
position.y = e.clientY;
}}
style={{
position: 'relative',
width: '100vw',
height: '100vh',
}}>
<div style={{
position: 'absolute',
backgroundColor: 'red',
borderRadius: '50%',
transform: `translate(${position.x}px, ${position.y}px)`,
left: -10,
top: -10,
width: 20,
height: 20,
}} />
</div>
);
}
问题出在这⬇:这段代码直接修改了 上一次渲染中 分配给 position
的对象。但是因为并没有使用 state
的设置函数,React
并不知道对象已更改。所以React
没有做出任何响应。这就像在吃完饭之后才尝试去改变要点的菜一样。虽然在一些情况下,直接修改 state
可能是有效的,但我们并不推荐这么做。你应该把在渲染过程中可以访问到的 state
视为只读的。
javascript
//直接修改上一次渲染的对象
onPointerMove={e => {
position.x = e.clientX;
position.y = e.clientY;
}}
正确做法是:创建一个新对象,传递给state
,触发渲染。
通过setPosition
你告诉react
:使用这个新对象替换position
的值,再次渲染。
javascript
onPointerMove={e => {
setPosition({
x: e.clientX,
y: e.clientY
});
}}
全部代码:此时就可以实现要求的效果了
javascript
import { useState } from 'react';
export default function MovingDot() {
const [position, setPosition] = useState({
x: 0,
y: 0
});
return (
<div
onPointerMove={e => {
setPosition({
x: e.clientX,
y: e.clientY
});
}}
style={{
position: 'relative',
width: '100vw',
height: '100vh',
}}>
<div style={{
position: 'absolute',
backgroundColor: 'red',
borderRadius: '50%',
transform: `translate(${position.x}px, ${position.y}px)`,
left: -10,
top: -10,
width: 20,
height: 20,
}} />
</div>
);
}
6.3 展开语法复制对象
如下是一个表单,修改输入框数据不能成功,因为是直接修改了state
javascript
import { useState } from 'react'
export default function FullName() {
const [person, setPerson] = useState({
firstname: 'W',
lastname: 'yr',
email: '123@qq.com'
})
function handleFirstNameChange(e) {
person.firstname = e.target.value //直接修改的state
}
function handleLastNameChange(e) {
person.lastname = e.target.value
}
function handleEmailChange(e) {
person.email = e.target.value
}
return (
<form>
First name <input type="text" value={person.firstname} placeholder='请输入姓' onChange={handleFirstNameChange} /><br />
Last name <input type="text" value={person.lastname} placeholder='请输入名' onChange={handleLastNameChange} /><br />
Email <input type="text" value={person.email} placeholder='请输入电子邮件' onChange={handleEmailChange} />
<p>{person.firstname}{''}{person.lastname}{'的电子邮件是:'}{person.email}</p>
</form>
)
}
想实现我们的需求,可以创建一个新的对象并传递给setPerson
,这个栗子中我们还需要把其它的属性复制到新对象中,因为我们每次只改变一个字段,比如输入姓的时候其他两个字段是不变的。
javascript
setPerson({
firstname: e.target.value,
lastname: person.lastname,
email: person.email
})
这样写要是属性忒多真的是很麻烦呢
我们可以使用==...对象展开==语法,这样不需要单独复制每个属性了。
注意:加,
号,注意书写代码的顺序
javascript
...person, //复制person的所有字段
firstName: e.target.value //firstName
代码修改后就可以正常运行了
扩展:上述代码是使用三个事件处理函数分别更新字段,也可以一个事件处理函数解决,看下面的代码:
javascript
import { useState } from 'react';
export default function Form() {
const [person, setPerson] = useState({
firstName: 'Barbara',
lastName: 'Hepworth',
email: 'bhepworth@sculpture.com'
});
function handleChange(e) {
setPerson({
...person,
[e.target.name]: e.target.value //一个事件处理函数就可以更新多个字段了
});
}
6.4更新一个嵌套对象
嵌套对象:对象套对象,比如⬇
javascript
const [person, setPerson] = useState({
name: 'ruru',
age: 18,
family: {
boy: 'fufu',
animal:'shushu'
}
})
比如你想更新一下family
的animal
的值
使用mutation
的方法来实现很容易理解BUT在react
将state视为不可变,所有下面代码方法不可取!!!:
person.family.animal = 'mimi'
想要修改animai
,创建一个新family
对象将family
数据复制进去,再创建一个新person
对象,将person
的数据复制进去。让family
中的属性指向新的family
对象。上代码:
javascript
const newFamily = { ...person.family, animal: 'mimi' } //创建新对象,赋值,改值
const newPerson = { ...person, family: newFamily } //创建新对象,复制,改family指向
setPerson(newPerson)
or
写成一个函数调用:
javascript
setPerson({
...person,
family: {
...person.family,
animal: 'mimi'
}
})
感觉这个方式我更易用
虽然繁杂,但是原理简单,可以有效解决问题。
思考一下,对象是嵌套的吗?当然代码看起来是嵌套的,其实对象是相互独立的,只是存在 指向 问题
6.5 Immer简洁逻辑
如果state有多层的嵌套,可以考虑使其扁平化,不改变state的数据解构,使用更加便捷的方式实现嵌套展开。
Immer
:库,可以让你使用简便但可以直接修改的语法编写代码,并会帮你处理好复制的过程。代码看起来就像"打破了规则"而直接修改了对象:
不同于一般的mutation
,它不会覆盖原来的state
javascript
updatePerson((draft) => {
draft.family.animal = 'mimi'
})
Immer运行的原理 :
由 Immer 提供的 draft 是一种特殊类型的对象,被称为 Proxy,它会记录你用它所进行的操作。这就是你能够随心所欲地直接修改对象的原因所在!从原理上说,Immer 会弄清楚 draft 对象的哪些部分被改变了,并会依照你的修改创建出一个全新的对象。
7. 更新state中的数组
可以存储在state
中的一种JS对象。
同对象一样,当你想要更新存储于 state
中的数组时,你需要创建一个新的数组(或者创建一份已有数组的拷贝值),并使用新数组设置 state
。
7.1 使用数组方法更新数组
将 React state
中的数组视为只读。这意味着有些数组方法可用,有些不可用
使用Immer
库的话,表格中的方法都可以使用了。
不能使用(会改变原始数组) | 推荐使用(返回一个新的数组) | |
---|---|---|
添加元素 | push,unshift |
concat,[...arr ] 展开语法 |
删除元素 | pop,shift,splice |
filter,slice |
替换元素 | splice ,arr[i] = ... 赋值 |
map |
排序 | reverse,sort |
先将数组复制一份 |
下面我们展开这些推荐使用的数组方法来更好的了解如何使用它们
7.1.1 像数组中添加元素concat/[...arr]
这个栗子用的是...arr
,使用map
遍历数组
如果你想改变数组中的某些或全部元素,你可以用
map()
创建一个新数组。你传入 map 的函数决定了要根据每个元素的值或索引(或二者都要)对元素做何处理。
javascript
import { useState } from 'react'
let nextId = 0;
export default function ToDoList() {
const [lists, setLists] = useState([]); //清单列表
const [todo, setTodo] = useState(''); //输入框输入
return (
<div>
<h1>ToDo清单</h1>
<input type='text' value={todo} onChange={(e) => {
setTodo(e.target.value)
}} />
<button onClick={() => {
setLists([
...lists,
{ id: nextId++, todo: todo }
]);
}}>添加</button>
<ul>
{lists.map(list => ( //遍历数据展示
<li key={list.id}> {list.todo} </li>
))}
</ul>
</div>
)
}
7.1.2从数组中删除元素filter/slice
这个栗子用的是filter
,基本用法,不多解释,多敲多练
cpp
import { useState } from 'react'
let todolists = [
{ id: 0, name: '吃饭' },
{ id: 1, name: '睡觉' },
{ id: 2, name: '打豆豆' }
];
export default function ToDoList() {
const [list, setList] = useState(todolists); //初始值就是数组
return (
<div>
<h1>TODO清单</h1>
<ul>
{list.map(l => (
<li key={l.id}> {l.name}{''}
<button onClick={() => {
setList(
list.filter(a => a.id !== l.id)
)
}}>
删除</button>
</li>
))}
</ul>
</div>
)
}
注意!我在书写代码的时候,将 list.filter(a => a.id !== l.id)
用{}括号包裹起来了,这是错误的!
因为我习惯性的像if
语句一样即使是只有一条语句也用{}
包起来。真是个坏习惯。包起来效果会有出入,大家可以试试。
7.1.3 替换(修改)数组中的元素
做法:map
创建一个数组,map
回调函数的第二个参数是元素的索引。
使用索引来判断是返回原来的元素还是替换成其他的值。
javascript
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>
);
}
7.1.4 数组中插入元素
任意位置插入元素,你可以将数组展开运算符...
和 slice()
方法一起使用
下面的例子中,插入按钮总是会将元素插入到数组中索引为 1 的位置。
通过切片,添加新元素,再切片创建一个新的数组
javascript
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>
</>
);
}
7.1.5 其他改变数组的情况
你可能想翻转数组,或是对数组排序。而 JS 中的 reverse() 和 sort()
方法会改变原数组,所以你无法直接使用 它们。
先复制原数组创建一个新数组,再对新数组使用reverse()
和sort()
方法,因为数组的拷贝是浅拷贝,这种方法也不可取。
可以用类似于 更新嵌套的JS
对象 的方式解决这个问题------拷贝想要修改的特定元素,而不是直接修改它。下面是具体的操作。
7.2 更新数组内部的对象
对象并不是 真的 位于数组"内部"。可能他们在代码中看起来像是在数组"内部"
javascript
import { useState } from 'react';
let nextId = 3;
const initialList = [
{ id: 0, title: 'Big Bellies', seen: false },
{ id: 1, title: 'Lunar Landscape', seen: false },
{ id: 2, title: 'Terracotta Army', seen: true },
];
export default function BucketList() {
const [myList, setMyList] = useState(initialList);
const [yourList, setYourList] = useState(
initialList
);
function handleToggleMyList(artworkId, nextSeen) {
setMyList(myList.map(artwork => {
if (artwork.id === artworkId) {
// 创建包含变更的*新*对象
return { ...artwork, seen: nextSeen };
} else {
// 没有变更
return artwork;
}
}));
}
function handleToggleYourList(artworkId, nextSeen) {
setYourList(yourList.map(artwork => {
if (artwork.id === artworkId) {
// 创建包含变更的*新*对象
return { ...artwork, seen: nextSeen };
} else {
// 没有变更
return artwork;
}
}));
}
return (
<>
<h1>艺术愿望清单</h1>
<h2>我想看的艺术清单:</h2>
<ItemList
artworks={myList}
onToggle={handleToggleMyList} />
<h2>你想看的艺术清单:</h2>
<ItemList
artworks={yourList}
onToggle={handleToggleYourList} />
</>
);
}
function ItemList({ artworks, onToggle }) {
return (
<ul>
{artworks.map(artwork => (
<li key={artwork.id}>
<label>
<input
type="checkbox"
checked={artwork.seen}
onChange={e => {
onToggle(
artwork.id,
e.target.checked
);
}}
/>
{artwork.title}
</label>
</li>
))}
</ul>
);
}
效果:
7.3 Immer简洁逻辑
如果state层级很深,可以调整一下数据结构,使数据变得扁平
如果你不想改变 state 的数据结构,可以使用 Immer ,它让你可以继续使用方便的,但会直接修改原值的语法,并负责为你生成拷贝值。
下面是我们用Immer
来重写的艺术愿望清单的例子:
JSON数据:
json
{
"dependencies": {
"immer": "1.7.3",
"react": "latest",
"react-dom": "latest",
"react-scripts": "latest",
"use-immer": "0.5.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
"devDependencies": {}
}
JS代码:
javascript
import { useState } from 'react';
import { useImmer } from 'use-immer';
let nextId = 3;
const initialList = [
{ id: 0, title: 'Big Bellies', seen: false },
{ id: 1, title: 'Lunar Landscape', seen: false },
{ id: 2, title: 'Terracotta Army', seen: true },
];
export default function BucketList() {
const [myList, updateMyList] = useImmer(
initialList
);
const [yourList, updateYourList] = useImmer(
initialList
);
function handleToggleMyList(id, nextSeen) {
updateMyList(draft => {
const artwork = draft.find(a =>
a.id === id
);
artwork.seen = nextSeen;
});
}
function handleToggleYourList(artworkId, nextSeen) {
updateYourList(draft => {
const artwork = draft.find(a =>
a.id === artworkId
);
artwork.seen = nextSeen;
});
}
return (
<>
<h1>艺术愿望清单</h1>
<h2>我想看的艺术清单:</h2>
<ItemList
artworks={myList}
onToggle={handleToggleMyList} />
<h2>你想看的艺术清单:</h2>
<ItemList
artworks={yourList}
onToggle={handleToggleYourList} />
</>
);
}
function ItemList({ artworks, onToggle }) {
return (
<ul>
{artworks.map(artwork => (
<li key={artwork.id}>
<label>
<input
type="checkbox"
checked={artwork.seen}
onChange={e => {
onToggle(
artwork.id,
e.target.checked
);
}}
/>
{artwork.title}
</label>
</li>
))}
</ul>
);
}
7.4 小结
- 以把数组放入
state
中不应该直接修改 - 不要直接修改数组,复制数据创建新数组,然后使用新的数组来更新它的状态
- 可以使用
[...arr, newItem]
这种数组展开语法,向数组中添加元素。 - 可以使用
filter()
和map()
来创建数组过滤或变换。 - 可以使用
Immer
来保持代码简洁。
总结
多敲多练就可以记住。
下一章:状态管理
码字好累啊,给个三连,然后一起加油吧 ^皿^