为什么多个 JSX 标签需要被一个父元素包裹?
JSX 虽然看起来很像 HTML,但在底层其实被转化为了 JavaScript 对象,你不能在一个函数中返回多个对象,除非用一个数组把他们包装起来。这就是为什么多个 JSX 标签必须要用一个父元素或者 Fragment 来包裹。
神奇的大括号
当你想把一个字符串属性传递给 JSX 时,把它放到单引号或双引号中:
1 2 3 4 5 6 7 8 9 |
export default function Avatar() { return ( <img className="avatar" src="https://i.imgur.com/7vQD0fPs.jpg" alt="Gregorio Y. Zara" /> ); } |
这里的”https://i.imgur.com/7vQD0fPs.jpg“和”Gregorio Y. Zara”就是被作为字符串传递的。
但是如果你想要动态地指定src或alt的值呢?你可以用{和}替代“和“以使用 JavaScript 变量:
1 2 3 4 5 6 7 8 9 10 11 |
export default function Avatar() { const avatar = 'https://i.imgur.com/7vQD0fPs.jpg'; const description = 'Gregorio Y. Zara'; return ( <img className="avatar" src={avatar} alt={description} /> ); } |
请注意 className=”avatar” 和 src={avatar} 之间的区别,className=”avatar” 指定了一个就叫 “avatar” 的使图片在样式上变圆的 CSS 类名,而 src={avatar} 这种写法会去读取 JavaScript 中 avatar 这个变量的值。这是因为大括号可以使你直接在标签中使用 JavaScript!
使用 props 互相通信
React 组件使用 props 来互相通信。每个父组件都可以提供 props 给它的子组件,从而将一些信息传递给它。Props 可能会让你想起 HTML 属性,但你可以通过它们传递任何 JavaScript 值,包括对象、数组和函数。
Props 是你传递给 JSX 标签的信息。例如,className、src、alt、width 和 height 便是一些可以传递给 <img> 的 props:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
function Avatar() { return ( <img className="avatar" src="https://i.imgur.com/1bX5QH6.jpg" alt="Lin Lanying" width={100} height={100} /> ); } export default function Profile() { return ( <Avatar /> ); } |
步骤 1: 将 props 传递给子组件
首先,将一些 props 传递给 Avatar。例如,让我们传递两个 props:person(一个对象)和 size(一个数字):
1 2 3 4 5 6 7 8 |
export default function Profile() { return ( <Avatar person={{ name: 'Lin Lanying', imageId: '1bX5QH6' }} size={100} /> ); } |
注意
如果 person=后面的双花括号让你感到困惑,请记住,在 JSX 花括号中,它们只是一个对象。
现在,你可以在Avatar组件中读取这些 props 了。
步骤 2: 在子组件中读取 props
你可以通过在function Avatar之后直接列出它们的名字person, size来读取这些 props。这些 props 在({和})之间,并由逗号分隔。这样,你可以在Avatar的代码中使用它们,就像使用变量一样。
1 2 3 |
function Avatar({ person, size }) { // 在这里 person 和 size 是可访问的 } |
向使用 person 和 size props 渲染的 Avatar 添加一些逻辑,你就完成了。
注意
在声明 props 时, 不要忘记 ( 和 ) 之间的一对花括号 { 和 } :
1 2 3 |
function Avatar({ person, size }) { // ... } |
state 如同一张快照
也许 state 变量看起来和一般的可读写的 JavaScript 变量类似。但 state 在其表现出的特性上更像是一张快照。设置它不会更改你已有的 state 变量,但会触发重新渲染。
设置 state 会触发渲染
你可能会认为你的用户界面会直接对点击之类的用户输入做出响应并发生变化。在 React 中,它的工作方式与这种思维模型略有不同。在上一页中,你看到了来自 React 的设置 state 请求重新渲染。这意味着要使界面对输入做出反应,你需要设置其 state。
在这个例子中,当你按下 “send” 时,setIsSent(true) 会通知 React 重新渲染 UI:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
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) { // ... } |
当你单击按钮时会发生以下情况:
- 执行 onSubmit 事件处理函数。
- setIsSent(true) 将 isSent 设置为 true 并排列一个新的渲染。
- React 根据新的 isSent 值重新渲染组件。
让我们仔细看看 state 和渲染之间的关系。
渲染会及时生成一张快照
“正在渲染”就意味着 React 正在调用你的组件——一个函数。你从该函数返回的 JSX 就像是 UI 的一张及时的快照。它的 props、事件处理函数和内部变量都是根据当前渲染时的 state被计算出来的。
与照片或电影画面不同,你返回的 UI “快照”是可交互的。它其中包括类似事件处理函数的逻辑,这些逻辑用于指定如何对输入作出响应。React 随后会更新屏幕来匹配这张快照,并绑定事件处理函数。因此,按下按钮就会触发你 JSX 中的点击事件处理函数。
当 React 重新渲染一个组件时:
- React 会再次调用你的函数
- 你的函数会返回新的 JSX 快照
- React 会更新界面来匹配你返回的快照
作为一个组件的记忆,state 不同于在你的函数返回之后就会消失的普通变量。state 实际上“活”在 React 本身中——就像被摆在一个架子上!——位于你的函数之外。当 React 调用你的组件时,它会为特定的那一次渲染提供一张 state 快照。你的组件会在其 JSX 中返回一张包含一整套新的 props 和事件处理函数的 UI 快照 ,其中所有的值都是 根据那一次渲染中 state 的值 被计算出来的!
React 受到 setUpdate 通知 —> React 更新 state 的值 —> React 向组件内传入一张 state 的快照
有趣的例子
这里有个向你展示其运行原理的小例子。在这个例子中,你可能会以为点击“+3”按钮会调用setNumber(number + 1)三次从而使计数器递增三次。
看看你点击“+3”按钮时会发生什么:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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> </> ) } |
请注意,每次点击只会让number递增一次!
设置 state 只会为下一次渲染变更 state 的值。在第一次渲染期间,number为0。这也就解释了为什么在那次渲染中的onClick处理函数中,即便在调用了setNumber(number + 1)之后,number的值也仍然是0:
1 2 3 4 5 6 |
<button onClick={() => { setNumber(number + 1); setNumber(number + 1); setNumber(number + 1); }}> +3 </button> |
以下是这个按钮的点击事件处理函数通知 React 要做的事情:
- setNumber(number + 1):number 是 0 所以 setNumber(0 + 1)。
- React 准备在下一次渲染时将 number 更改为 1。
- setNumber(number + 1):number 是0 所以 setNumber(0 + 1)。
- React 准备在下一次渲染时将 number 更改为 1。
- setNumber(number + 1):number 是0 所以 setNumber(0 + 1)。
- React 准备在下一次渲染时将 number 更改为 1。
尽管你调用了三次setNumber(number + 1),但在这次渲染的事件处理函数中number会一直是0,所以你会三次将 state 设置成1。这就是为什么在你的事件处理函数执行完以后,React 重新渲染的组件中的number等于1而不是3。
██React 会等到事件处理函数中的所有代码都运行完毕再处理你的 state 更新。这就是为什么重新渲染只会发生在所有这些setNumber()调用之后的原因。██
██如果上面的代码,你想调用三次setNumber方法后达到state中number的值变成3,则:如果你想在下次渲染之前多次更新同一个 state,你可以像setNumber(n => n + 1)这样传入一个根据队列中的前一个 state 计算下一个 state 的函数,而不是像setNumber(number + 1)这样传入下一个 state 值。██
随时间变化的 state
好的,刚才那些很有意思。试着猜猜点击这个按钮会发出什么警告:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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> </> ) } |
如果你使用之前替换的方法,你就能猜到这个提示框将会显示 “0”【确实是0,<button../>里面的setNumber(number+5)和alert(number)读取到的number都是0】:
1 2 |
setNumber(0 + 5); alert(0); |
但如果你在这个提示框上加上一个定时器, 使得它在组件重新渲染 之后 才触发,又会怎样呢?是会显示 “0” 还是 “5” ?猜一猜!【<h1../>标签里显示的是5,alert弹窗里面是0,因为在第N次渲染中alert读到的值是0那么即使延迟弹窗,那个值还是5】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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> </> ) } |
惊讶吗?你如果使用替代法,就能看到被传入提示框的 state “快照”。
1 2 3 4 |
setNumber(0 + 5); setTimeout(() => { alert(0); }, 3000); |
到提示框运行时,React 中存储的 state 可能已经发生了更改,但它是使用用户与之交互时状态的快照进行调度的!
一个 state 变量的值永远不会在一次渲染的内部发生变化,即使其事件处理函数的代码是异步的。在那次渲染的onClick内部,number的值即使在调用setNumber(number + 5)之后也还是0。它的值在 React 通过调用你的组件“获取 UI 的快照”时就被“固定”了。
这里有个示例能够说明上述特性会使你的事件处理函数更不容易出现计时错误。下面是一个会在五秒延迟之后发送一条消息的表单。想象以下场景:
- 你按下“发送”按钮,向 Alice 发送“你好”。
- 在五秒延迟结束之前,将“To”字段的值更改为“Bob”。
你觉得alert会显示什么?它是会显示“你向 Alice 说了你好“还是会显示“你向 Bob 说了你好”?根据你已经学到的知识猜一猜,然后动手试一试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
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> ); } |
React 会使 state 的值始终”固定“在一次渲染的各个事件处理函数内部。你无需担心代码运行时 state 是否发生了变化。
但是,万一你想在重新渲染之前读取最新的 state 怎么办?你应该使用状态更新函数,下一页将会介绍!
摘要
- 设置 state 请求一次新的渲染。
- React 将 state 存储在组件之外,就像在架子上一样。
- 当你调用 useState 时,React 会为你提供该次渲染 的一张 state 快照。
- 变量和事件处理函数不会在重渲染中“存活”。每个渲染都有自己的事件处理函数。
- 每个渲染(以及其中的函数)始终“看到”的是 React 提供给这个 渲染的 state 快照。
- 你可以在心中替换事件处理函数中的 state,类似于替换渲染的 JSX。
- 过去创建的事件处理函数拥有的是创建它们的那次渲染中的 state 值。
把一系列 state 更新加入队列
设置组件 state 会把一次重新渲染加入队列。但有时你可能会希望在下次渲染加入队列之前对 state 的值执行多次操作。为此,了解 React 如何批量更新 state 会很有帮助。
React 会对 state 更新进行批处理
在下面的示例中,你可能会认为点击 “+3” 按钮会使计数器递增三次,因为它调用了setNumber(number + 1)三次:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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 值都是固定的,因此无论你调用多少次 setNumber(1),在第一次渲染的事件处理函数内部的 number 值总是 0 :
1 2 3 |
setNumber(0 + 1); setNumber(0 + 1); setNumber(0 + 1); |
但是这里还有另外一个影响因素需要讨论。React 会等到事件处理函数中的所有代码都运行完毕再处理你的 state 更新。这就是为什么重新渲染只会发生在所有这些setNumber()调用之后的原因。
这可能会让你想起餐厅里帮你点菜的服务员。服务员不会在你说第一道菜的时候就跑到厨房!相反,他们会让你把菜点完,让你修改菜品,甚至会帮桌上的其他人点菜。
这让你可以更新多个 state 变量——甚至来自多个组件的 state 变量——而不会触发太多的重新渲染。但这也意味着只有在你的事件处理函数及其中任何代码执行完成之后,UI 才会更新。这种特性也就是批处理,它会使你的 React 应用运行得更快。它还会帮你避免处理只更新了一部分 state 变量的令人困惑的“半成品”渲染。
React 不会跨多个需要刻意触发的事件(如点击)进行批处理——每次点击都是单独处理的。请放心,React 只会在一般来说安全的情况下才进行批处理。这可以确保,例如,如果第一次点击按钮会禁用表单,那么第二次点击就不会再次提交它。
在下次渲染前多次更新同一个 state
这是一个不常见的用例,但是如果你想在下次渲染之前多次更新同一个 state,你可以像setNumber(n => n + 1)这样传入一个根据队列中的前一个 state 计算下一个 state 的函数,而不是像setNumber(number + 1)这样传入下一个 state 值。这是一种告诉 React “用 state 值做某事”而不是仅仅替换它的方法。
现在尝试递增计数器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import { useState } from 'react'; export default function Counter() { const [number, setNumber] = useState(0); return ( <> <h1>{number}</h1> <button onClick={() => { setNumber(n => n + 1); setNumber(n => n + 1); setNumber(n => n + 1); }}>+3</button> </> ) } |
在这里,n => n + 1被称为更新函数。当你将它传递给一个 state 设置函数时:
- React 会将此函数加入队列,以便在事件处理函数中的所有其他代码运行后进行处理。
- 在下一次渲染期间,React 会遍历队列并给你更新之后的最终 state。
1 2 3 |
setNumber(n => n + 1); setNumber(n => n + 1); setNumber(n => n + 1); |
下面是 React 在执行事件处理函数时处理这几行代码的过程:
- setNumber(n => n + 1):n => n + 1 是一个函数。React 将它加入队列。
- setNumber(n => n + 1):n => n + 1 是一个函数。React 将它加入队列。
- setNumber(n => n + 1):n => n + 1 是一个函数。React 将它加入队列。
当你在下次渲染期间调用useState时,React 会遍历队列。之前的numberstate 的值是0,所以这就是 React 作为参数n传递给第一个更新函数的值。然后 React 会获取你上一个更新函数的返回值,并将其作为n传递给下一个更新函数,以此类推:
React 会保存3为最终结果并从useState中返回。
这就是为什么在上面的示例中点击“+3”正确地将值增加“+3”。
如果你在替换 state 后更新 state 会发生什么
这个事件处理函数会怎么样?你认为number在下一次渲染中的值是什么?
1 2 3 4 |
<button onClick={() => { setNumber(number + 5); setNumber(n => n + 1); }}> |
再看下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import { useState } from 'react'; export default function Counter() { const [number, setNumber] = useState(0); return ( <> <h1>{number}</h1> <button onClick={() => { setNumber(number + 5); setNumber(n => n + 1); }}>增加数字</button> </> ) } |
这是事件处理函数告诉 React 要做的事情:
- setNumber(number + 5):number 为 0,所以 setNumber(0 + 5)。React 将 “替换为 5” 添加到其队列中。
- setNumber(n => n + 1):n => n + 1 是一个更新函数。 React 将 该函数 添加到其队列中。
在下一次渲染期间,React 会遍历 state 队列:
React 会保存 6 为最终结果并从 useState 中返回。
注意
你可能已经注意到,setState(x)实际上会像setState(n => x)一样运行,只是没有使用n!
如果你在更新 state 后替换 state 会发生什么
让我们再看一个例子。你认为number在下一次渲染中的值是什么?
1 2 3 4 5 |
<button onClick={() => { setNumber(number + 5); setNumber(n => n + 1); setNumber(42); }}> |
看下面代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import { useState } from 'react'; export default function Counter() { const [number, setNumber] = useState(0); return ( <> <h1>{number}</h1> <button onClick={() => { setNumber(number + 5); setNumber(n => n + 1); setNumber(42); }}>增加数字</button> </> ) } |
以下是 React 在执行事件处理函数时处理这几行代码的过程:
- setNumber(number + 5):number 为 0,所以 setNumber(0 + 5)。React 将 “替换为 5” 添加到其队列中。
- setNumber(n => n + 1):n => n + 1 是一个更新函数。React 将该函数添加到其队列中。
- setNumber(42):React 将 “替换为 42” 添加到其队列中。
在下一次渲染期间,React 会遍历 state 队列:
然后 React 会保存42为最终结果并从useState中返回。
总而言之,以下是你可以考虑传递给setNumberstate 设置函数的内容:
- 一个更新函数(例如:n => n + 1)会被添加到队列中。
- 任何其他的值(例如:数字 5)会导致“替换为 5”被添加到队列中,已经在队列中的内容会被忽略。
事件处理函数执行完成后,React 将触发重新渲染。在重新渲染期间,React 将处理队列。更新函数会在渲染期间执行,因此更新函数必须是纯函数并且只返回结果。不要尝试从它们内部设置 state 或者执行其他副作用。在严格模式下,React 会执行每个更新函数两次(但是丢弃第二个结果)以便帮助你发现错误。
摘要
- 设置 state 不会更改现有渲染中的变量,但会请求一次新的渲染。
- React 会在事件处理函数执行完成之后处理 state 更新。这被称为批处理。
- 要在一个事件中多次更新某些 state,你可以使用 setNumber(n => n + 1) 更新函数。
为什么不能直接修改React的State
参:
https://juejin.cn/post/7033590099992379422
https://www.zhihu.com/question/440916294
https://cloud.tencent.com/developer/article/2001534