我们定义的state看起来和普通的JavaScript变量一样,既可以读也可以写,但是其更像是一个快照,我们的set操作并不会改变已有的变量,而是去触发重绘(re-render)。
1.set操作触发render
我们的直观感受是页面基于用户操作做出实时响应,但是在react中,并不是如此。如下图所示
点击操作并不会去直接修改页面,而是通知react,react去初始化render或者re-render。
看一段代码:
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) {
// ...
}
点击“send”后,setIsSent(true)触发页面重绘:
- 执行onSubmit事件处理程序;
setIsSent(true)将isSent的值置为true并且触发页面重绘;- react基于新的
isSent去重绘该组件
2. Rendering时获取快照
‘Rendering’就是react函数调用你定义的组件,其返回值(你写的JSX)就是关于UI的快照,在render时使用组件内的状态(props 事件控制器 组件内变量)。但是这里的UI快照是响应式的,它会包含一些逻辑,比如当输入时会触发一些特定的事件,react会根据快照的数据去更新UI以达到匹配的目的。所以当 react重新渲染一个组件时,主要流程是:1. react去重新调用定义的函数;2. 函数返回一个新的JSX快照;3. react根据返回的快照更新UI,如下图。
组件内的state同普通函数内的变量不同,它不会在函数执行结束后销毁,它会在react中继续存在(就像是在架子上的一本书一样)。当对该组件再次调用时,react会将特定render的快照返回,该快照内包含着新计算得到的props以及事件控制器,依据该快照的内容更新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>
</>
)
}
点击button按钮时,number的值是多少呢?是3吗?还是说是别的值?首先我们要记住一句话: state的修改仅在下一次render时生效. 点击事件发生以后, 过程如下:
setNumber(number + 1):number当前的值是,0所以本质是setNumber(0 + 1), react准备在下一次render时将原来的0更改为1.setNumber(number + 1):number当前的值是,0所以本质是setNumber(0 + 1), react准备在下一次render时将原来的0更改为1.setNumber(number + 1):number当前的值是,0所以本质是setNumber(0 + 1), react准备在下一次render时将原来的0更改为1.
所以即便我们调用了三次setNumber(number + 1), 但是在事件控制器中number 的值始终是0,只有在下一次render时, 才会将number的值置为1, 也即是三次调用将number的值设为了三次1, 也即是这样:
setNumber(0 + 1);
setNumber(0 + 1);
setNumber(0 + 1);
当再一次点击按钮时, 代码本质变成了这样:
setNumber(1 + 1);
setNumber(1 + 1);
setNumber(1 + 1);
3.状态变化
阅读下面这段代码, 说出浏览器的变化过程:
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, number为5
- 弹出5, number为5 通过我们上述对于快照的定义不难得出, 第一种是对的, 因为代码的本质如下,
setNumber(0 + 5);
alter(0);
并且由于alter的操作会阻塞浏览器重绘, 所以会先弹出0, 后变为5.
那么当我们将alter的操作放到定时器里呢?代码变成这样:
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>
</>
)
}
首先我们要知道javascript里面定时器的作用是什么, 定时器是在一段时间后执行其回调函数, 不要去多想定时器会带来什么影响, 只要记得我们获取的state都是快照, 所以代码运行的本质是:
setNumber(0 + 5);
setTimeout(() => {
alert(0);
}, 3000);
所以当点击按钮后, 数值变为5, 3秒后弹出0. 这里猜测是和任务队列有关, 定时器内的宏任务在同步代码后执行
虽然在我们在alter的时候state的值已经做了修改, 但是react就是通过状态快照来与用户交互的.
state的值永远不会在一次render内被修改, 即便代码内的事件控制函数是非同步的. 虽然在alter之前我们已经调用了setNumber(0 + 5), 但是在react调用我们当前的组件函数时, state内的值被锁死了.
最后, 做一道测验, 点击按钮后, 修改选择器的值以及输入框的内容, 弹出的内容会变吗?
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>
);
}