react新手的困惑——state hooks 是同步还是异步

1,267 阅读3分钟

场景

这是我刚开始用 react 的时候,在一个上传组件中遇到一个疑惑,如下伪代码:

beforeUpload(file) {
  return new Promise<void>(async (resolve, reject) => {
    try {
      ...
      file.md5 = await md5File(file);

      // 合并参数
      const formData = new FormData();
      ...

      setUploadUrl(uploadUrl);
      setUploadData(formData);

      resolve(file);
    } catch (err) {
      reject();
    }
  });
},

一开始以为,此处的 setter 是异步的,所以担心在后面操作的时候,获取不到最新的值。然而,在下面操作的时候,却发现拿到的是最新值,不免心生疑惑,难道 setState 不是异步的吗?于是开始搜搜搜~

探索

在函数式组件中,我们会这样定义状态:

const [count, setCount] = useState(0)

这时候,如果我们在同步函数或者在异步回调中调用 setCount 后,打印 count,都是旧值。往往这时候,就会给人错觉:setState 是异步的。

const [count, setCount] = useState(0);

// 直接调用
const handleUpdate = () => {
  setCount(1);
  console.log(count); // 0
};

// 放在setTimeout回调中
const handleAsyncUpdate = () => {
  setTimeout(() => {
    setCount(1);
    console.log(count); // 0
  });
};

其实,上述现象的原因不只是”异步“这么简单,原因还有以下两点:

  1. setState 里的逻辑其实是同步的,但是,调用 setState 时,react 会对这一系列的 setter 做合并处理,异步更新该函数式组件对应的 hooks 链表里面的值,然后触发重渲染(re-renders),从这个角度上来说,setState 确实是一个"异步"操作;
  2. 函数式的 capture-value 特性决定了 console.log(count) 语句打印的始终是一个只存在于当前帧的常量,所以无论 setState 是同步还是异步的,实际上这里都一定会打印出旧值。

那么,该怎么判断 state hooks 是同步还是”异步“呢?可以通过判断 state 更新时,dom 会不会同步更新,此处通过 render 次数观察:

import { useState } from "react";

const Example = () => {
  console.log("do render >>");
  
  const [num1, setNum1] = useState(0);
  const [num2, setNum2] = useState(0);

  const update = () => {
    console.log("update start");
    setNum1(1);
    console.log("update num1");
    setNum2(2);
    console.log("update num2");
  };

  const asyncUpdate = () => {
    console.log("-----------------------------");
    setTimeout(() => {
      console.log("async update start");
      setNum1(4);
      console.log("async update num1:", num1);
      setNum2(5);
      console.log("async update num2:", num2);
    });
  };

  return (
    <div>
      <p>num1 = {num1}</p>
      <p>num2 = {num2}</p>
      <button onClick={update}>update</button>
      <button onClick={asyncUpdate}>asyncUpdate</button>
    </div>
  );
};

export default Example;

点击 update 按钮,可以观察到一下打印内容:

update start
update num1
update num2
do render >>

点击更新按钮(update),调用 setNum1setNum2,根据打印内容可判断,这两个 setter 不是同步执行,若同步执行,在调用完 setState 后,会立刻 render,打印内容应该如下:

update start
do render >>
update num1
do render >>
update num2

接下来点击 asyncUpdate 按钮,输出以下打印内容

-----------------------------
async update start
do render >>
async update num1: 1
do render >>
async update num2: 2

可以看出此处是同步执行的。

结论

  1. 只要进入了 react 的调度流程,那就是异步的;只要你没有进入 react 的调度流程,那就是同步的。
  2. 什么东西不会进入 react 的调度流程? setTimeoutsetInterval 、直接在 DOM 上绑定原生事件、Promise 的回调等,这些都不会走 React 的调度流程。在这种情况下调用 setState ,那这次 setState 就是同步的。 否则就是异步的。
  3. setState 同步执行的情况下, DOM 也会被同步更新,也就意味着如果多次 setState ,会导致多次更新,这是毫无意义并且浪费性能的。

回到开头,为什么我能够获取到最新值呢?在开头中,我使用了 await,而 await 后面的逻辑,相当于在 .then 回调里的逻辑,所以,结论2 就是答案啦~