添加交互- State as a Snapshot

138 阅读4分钟

我们定义的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)触发页面重绘:

  1. 执行onSubmit事件处理程序;
  2. setIsSent(true)isSent的值置为true并且触发页面重绘;
  3. 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时生效. 点击事件发生以后, 过程如下:

  1. setNumber(number + 1): number 当前的值是,0 所以本质是setNumber(0 + 1), react准备在下一次render时将原来的0更改为1.
  2. setNumber(number + 1): number 当前的值是,0 所以本质是setNumber(0 + 1), react准备在下一次render时将原来的0更改为1.
  3. 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>
  );
}