Wenzi

React18 源码解析之 useState 的原理

蚊子前端博客
发布于 2022/12/20 00:04
useState()是我们最常用的hooks之一,主要是为了存储数据和更新视图的状态。

我们解析的源码是 React18.1.0 版本,请注意版本号。React 源码学习的 GitHub 仓库地址:https://github.com/wenzi0github/react

useState()是我们最常见的几个 hooks 之一,今天我们来了解下他的用法和源码实现。

1. useState 的使用 #

我们先来 useState() 的用法,我们知道 useState() 返回的第 2 个参数是 dispatch(即 set 方法),用来更新 useState() 的状态值。dispatch 的参数,既可以传入普通数据,也可以传入有返回值的函数:

import { useState } from 'react';

function App() {
  const [count, setCount] = useState(0);
  const [userInfo, setUserInfo] = useState({ name: 'wenzi', age: 24 });

  const handleClickByVal = () => {
    setCount(count + 1);
  };
  const handleClickByCallback = () => {
    setCount(count => count + 1);
  };
  const handleUpdateUserInfo = () => {
    setCount({ ...userInfo, age: userInfo.age + 1 });
  };

  return (
    <div className="App">
      <p>{count}</p>
      <p>
        <button onClick={handleClickByVal}>add by val</button>
      </p>
      <p>
        <button onClick={handleClickByCallback}>add by callback</button>
      </p>
      <p>
        <button onClick={handleUpdateUserInfo}>update userInfo</button>
      </p>
    </div>
  );
}

用过 useState() 的同学,还知道它还有如下的几个特点:

  1. setState()的参数,既可以传入普通数据,也可以传入 callback;在以 callback 的方式传入时,callback 里的参数就是截止到当前最新的 state,使用的是执行 callback()后的返回值;
  2. 传入的数据若是 object 类型,并不会自动和之前的数据进行合并,如上面的userInfo,我们需要手动合并后,再调用 set 方法;

1.1 传参的区别 #

useState()在初始时,或调用 dispatch()时,都有两种传参方式:一种是直接传入数据;一种是以函数 callback 的形式传入,state 的值就是该函数执行后的结果。

function App() {
  // 初始时传入一个callback,现在count的值就是他的返回值,即 Date.now()
  const [count, setCount] = useState(() => {
    return Date.now();
  });
}

这里我们主要关注的是多次调用 setState()时,不同的传参方式,他使用的 state 是不一样的。如

直接使用变量:

// 直接使用变量
function AppData() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1);
    setCount(count + 1);
    setCount(count + 1);
  };

  return (
    <div className="App">
      <button onClick={handleClick}>click me, {count}</button>
    </div>
  );
}

使用 callback 中的变量:

// 使用callback中的变量
function AppCallback() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count => count + 1);
    setCount(count => count + 1);
    setCount(count => count + 1);
  };

  return (
    <div className="App">
      <button onClick={handleClick}>click me, {count}</button>
    </div>
  );
}

点击一次按钮后,这两个组件最终展示的 count 值是不一样的,<AppData /> 中展示的是 1,<AppCallback />中展示的是 3。

为什么会出现这种现象呢?这是因为,在执行setCount(count + 1)时,变量 count 在函数组件的当前生命周期内,它永远是 0,因此即使调用再多的次数也没用。这里我们简化一下,就方便理解了。

function App() {
  const count = 0; // count是一个固定值

  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);

  setTimeout(() => {
    setCount(count + 1);
  }, 1000);
}

对同一次的渲染来说,count 是一个固定值,无论在哪里使用这个值,都是固定的。setCount(count+1)的作用仅仅是把要更新的最新数据记录在了 React 内部,然后等待下次的渲染更新。

setCount(count => count + 1)则不一样,callback 中的 prevState 则是执行到当前语句之前最新的那个 state。因此在执行第 2 条语句前,count 已经变成了 1;同理第 3 条语句。

我们稍后会从源码的层面分析下这种现象。

1.2 获取 setState()更新后的值 #

很多同学在初次使用useState()时,经常会在调用 setState()后,马上就使用更新后的数据。

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

  const getList = () => {
    // console.log(count);
    fetch('https://www.xiabingbao.com', {
      method: 'POST',
      body: JSON.stringify({ count }),
    });
  };

  const handleClick = () => {
    setCount(count + 1);
    console.log(count);

    // 本意是想用更新后的最新count来调用 getList()
    getList();
  };
}

其实我们通过上面第 1.1 节的了解,已经知道此时输出的 count 还是之前的数值 0。那怎么才能使用最新的数据,来做后续的操作呢?

  1. 先计算出最新值,然后同步传给 setCount()和 getList();
  2. 用 useEffect()来监听 count 的变化;

1.2.1 先计算出最新的值 #

我们可以把更新操作放在前面,先得到结果,然后再同步传给 setCount()和 getList()。

const handleClick = () => {
  const newCount = count + 1;
  setCount(newCount);
  getList(newCount);
};

这就得要求我们把函数 getList()改造为传参的形式。

1.2.2 用 useEffect()来监听 count 的变化 #

既然不确定什么时候回拿到最新的值,那我们就监听他的变化,等它更新了之后再进行后续的请求。

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

  useEffect(() => {
    getList();
  }, [count]);

  const handleClick = () => {
    setCount(count + 1);
  };
}

1.3 object 类型的数据不能自动合并 #

之前在类组件中的 state,我们可以只传入需要改动的字段,React 会帮助我们合并:

class App {
  state = {
    name: 'wenzi',
    age: 24,
  };

  handleClick() {
    this.setState({ age: this.state.age + 1 }); // 只传入有改动的字段即可
  }
}

但在函组件的useState()中,传入什么数据,就使用什么数据。若还需要使用之前的部分数据时,就需要我们自己来合并数据了,然后再传给 setState()。

function App() {
  const [userInfo, setUserInfo] = useState({ name: 'wenzi', age: 24 });

  setCount({ ...userInfo, age: userInfo.age + 1 }); // 直接使用state
  setCount(userInfo => ({ ...userInfo, age: userInfo.age + 1 })); // 用callback的方式使用state
}

在类组件中,所有的状态都必须挂载在state上。在函数组件中,我们可以根据情况进行更细粒度的拆分,如 count 何 userInfo 的拆分;如果觉得 userInfo 不够精细,还可以把其中的 name 和 age 再拆分,单独进行控制。

function App() {
  const [name, setName] = useState('wenzi');
  const [age, setAge] = useState(24);
}

React 官方更推荐精细化地拆分控制,一方面是控制起来更方便,若 state 比较复杂,那在每次调用 setState()时,都要手动合并数据(当然,您可以自己实现一个自动合并数据的 hook)。另一方面在后期的维护和扩展上更容易,不必考虑其他属性的影响。

1.4 typescript 的使用 #

在 typescript 环境中,useState()是支持泛型的,state 的类型默认就是初始数据的类型,如:

function App() {
  const [name, setName] = useState('wenzi'); // name 是 string 类型
  const [age, setAge] = useState(24); // age 是 number 类型

  // userInfo 是有多个属性的类型,且已明确了属性,有且只有name和age两个属性,并且这两个属性的类型分别是string和number
  const [userInfo, setUserInfo] = useState({ name: 'wenzi', age: 24 });
}

一些相对复杂的数据类型,或者多种数据类型的组合,我们可以显式地设置 state 的类型。

enum SEX_TYPE {
  MALE = 0,
  FEMALE = 1,
}

interface UserInfoType {
  name: string;
  age: number;
  score?: number;
}

function App() {
  const [name, setName] = useState<string | null>(null); // name 是 string 类型 或 null,并且初始为null
  const [sex, setSex] = useState<SEX_TYPE>(SEX_TYPE.MALE); // sex是枚举类型

  // 显式地明确 userInfo 的各个属性,score可选
  const [userInfo, setUserInfo] = useState<UserInfoType>({ name: 'wenzi', age: 24 });

  // 更复杂的ts类型
  const [userInfo, setUserInfo] = useState<Required<Pick<UserInfoType, 'score'>>>({ score: 96 });
}

在 ts 中,明确各个变量参数的类型,一个原因是为了避免对其随意的赋值,再一个原因,从类型定义上我们就能知道这个变量的具体类型,或他的属性是什么。

我们在上面已经了解了 useState() 不少的使用方式,这里我们通过源码的角度,来看看为什么出现上面的这些现象。

2 hook 的初始挂载 #

useState() 这个 hook 的大致结构:

useState() 这个 hook 的大致结构

在第一次初始声明 useState(),state 的值就是传入的值,若不传入,则是 undefined。我们再来看下 hook 的结构:

const hook: Hook = {
  memoizedState: null, // 这个hook目前在函数组件中显示的值,初始时,即为传入的数据(若传入的是函数,则为函数执行后的结果)

  /**
   * 该hook所有的set操作开始执行时的初始值,初始挂载时,该值与 memoizedState 相同;
   * 在中间更新过程中,若存在低优先级的set操作,则 baseState 此时为执行到目前set的值
   **/
  baseState: null,

  /**
   * 执行set操作的链表,这里包含了上次遗留下来的所有set操作,和本次将要执行的所有set操作
   **/
  baseQueue: null,

  // 所有的set操作,都会挂载到 queue.pendig 上
  queue: null,

  // 指向到下一个hook的指针
  next: null,
};

注意:我们之前在讲解 hooks 挂载的时候,也讲到过 memoizedState 属性。这两个 memoizedState 属性是不一样的。fiber.memoizedState 是用来挂载 hook 节点链表的;而现在讲解的 hook.memoizedState 是用来挂载该 hook 的数值的。

function mountState<S>(initialState: (() => S) | S): [S, Dispatch<BasicStateAction<S>>] {
  /**
   * 创建一个hook节点,并将其挂载到 currentlyRenderingFiber 链表的最后
   * @type {Hook}
   */
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    // 若传入的是函数,则使用执行该函数后得到的结果
    initialState = initialState();
  }
  /**
   * 设置该 hook 的初始值
   * memoizedState 用来存储当前hook要显示的数据
   * baseState 用来存储执行setState()的初始数据
   **/
  hook.memoizedState = hook.baseState = initialState;

  // 为该 hook 添加一个 queue 结构,用来存放所有的 setState() 操作
  const queue = {
    pending: null,
    interleaved: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer, // 上次render后使用的reducer
    lastRenderedState: initialState, // 上次render后的state
  };
  hook.queue = queue;

  /**
   * 这里用到了 bind() 的偏函数的特性,我们稍后会在下面进行讲解,
   *
   */
  const dispatch = (queue.dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue));
  return [hook.memoizedState, dispatch]; // useState() 返回的数据
}

mountState()的整体流程:

  1. 创建一个 hook 节点,挂载所有初始的数据;
  2. 若 initialState 是函数类型,则使用执行它后的结果;
  3. 执行当前节点的方法是 basicStateReducer() 函数;这里跟我们后续要讲解的 useReducer() 有关系;
  4. 将 hook 节点挂载到函数组件对应的 fiber 节点上;
  5. 返回该 hook 的初始值 和 set 方法;

basicStateReducer() 函数的具体实现:

/**
 * 对当前的 state 执行的基本操作,若传入的不是函数类型,则直接返回该值,
 * 若传入的是函数类型,返回执行该函数的结果
 * @param {S} state 当前节点的state
 * @param {BasicStateAction<S>} action 接下来要对该state执行的操作
 * @returns {S}
 */
function basicStateReducer(state, action) {
  return typeof action === 'function' ? action(state) : action;
}

这个 action 就是我们执行 useState() 里的第 2 个返回值的 set 操作。如:

setCount(count + 1); // action 是数值
setCount(count => {
  // action是函数,参数为当前的 count
  console.log('dispatch setCount');
  return count + 1;
});

bind()方法可以基于某个函数返回一个新的函数,并且可以为这个新函数预设初始的参数,然后剩余的参数给到这个新函数。官方文档:bind()的偏函数功能

我们这里暂时先不管这个函数 dispatchSetState() 的作用是什么,目前只关心参数的传递:

function dispatchSetState(fiber: Fiber, queue, action) {}

dispatchSetState() 本身要传入 3 个参数的:

  1. fiber: 当前处理的 fiber 节点
  2. queue: 该 hook 的 queue 结构,用来挂载 setState() 中的操作的;
  3. action: 要执行的操作,即 setState(action)里的 action,可能是数据,也可能是函数;

可是我们在执行 dispatch()(即 setState())时只需要传入一个参数就行了,这就是因为源码中利用到了 bind() 的偏函数功能。

再来看下派生出 dispatch() 的操作:

/**
 * 这里已经提前把当前的 fiber 节点和 hook 的 queue 结构传进去了,
 * 就只留一个 action 参数给dispatch。
 */
const dispatch = (queue.dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue));

可以看到,通过 bind()方法,已经提前把当前的 fiber 节点和 hook 的 queue 结构传进去了,就只留一个 action 参数给 dispatch。在调用dispatch(action时),就是在执行dispatchSetState(fiber, queue, action)

如果不太理解的话,我们再看一个简化后的例子:

/**
 * 设置学生的某学科的分数
 *
 * @param nick 学生姓名
 * @param subject 学科
 * @param score 分数
 */
const setStudentInfo = (nick, subject, score) => {
  console.log(nick, subject, score);
};

// 设置jack的分数
// 已预设了1个参数,剩余的两个参数供新函数设置
const setJackInfo = setStudentInfo.bind(null, 'Jack');
setJackInfo('math', 89); // Jack math 89
setJackInfo('computer', 92); // Jack computer 92

// 已预设了2个参数,剩余的一个参数供新函数设置
const setTomEnglishScore = setStudentInfo.bind(null, 'Tom', 'english');
setTomEnglishScore(97); // Tom english 97

再回到 dispatch(action) 这儿,我们在执行该方法的时候,其实已经预定了前 2 个参数:fiber 和 queue。即 dispath()已经和当前的 fibe 节点强绑定了,执行的操作只会在该 fiber 节点中产生影响。

3. dispatchSetState #

我们使用的 setState()(即源码中的 dispatch)就是 dispatchSetState() 函数派生出来的,执行 useState()的 set 操作,就是执行我们的 dispatchSetState()。

先看下传入的参数:

/**
 * 派生一个 setState(action) 方法,并将传入的 action 存放起来
 * 同一个 useState() 的 setState(action) 方法可能会执行多次,这里会把参数里的 action 均会放到queue.pending的链表中
 * @param {Fiber} fiber 当前的fiber节点
 * @param {UpdateQueue<S, A>} queue
 * @param {A} action 即执行setState()传入的数据,可能是数据,也能是方法,setState(1) 或 setState(prevState => prevState+1);
 */
function dispatchSetState<S, A>(fiber: Fiber, queue: UpdateQueue<S, A>, action: A) {}

dispatchSetState() 已经让提前传入 fiber 和 queue 的两个参数了,用来表示当前处理的是哪个 fiber 节点,action 的操作放到哪个链表中。这样当执行 useState() 中的 set 方法时,就能直接跟当前的 fiber 节点和当前的 hook 进行绑定。

再看下具体的实现:

function dispatchSetState<S, A>(fiber: Fiber, queue: UpdateQueue<S, A>, action: A) {
  /**
   * 获取当前 fiber 更新的优先级,
   * 当前 action 要执行的优先级,就是触发当前fiber更新更新的优先级
   */
  const lane = requestUpdateLane(fiber);

  /**
   * 将 action 操作封装成一个 update节点,用于后续构建链表使用
   */
  const update: Update<S, A> = {
    lane, // 该节点的优先级,即当前fiber的优先级
    action, // 操作,可能直接是数值,也可能是函数
    hasEagerState: false, // 是否是急切状态
    eagerState: null, // 提前计算出结果,便于在render()之前判断是否要触发更新
    next: (null: any), // 指向到下一个节点的指针
  };

  if (isRenderPhaseUpdate(fiber)) {
    /**
     * 是否是渲染阶段的更新,若是,则拼接到 queue.pending 的后面
     */
    enqueueRenderPhaseUpdate(queue, update);
  } else {
    /**
     * 正常执行
     * 将 update 形成单向环形链表,并放到 queue.pending 里
     * 即 hook.queue.pending 里,存放着 update 的数据
     * queue.pending指向到update链表的最后一个元素,next即是第1个元素
     * 示意图: https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/31b3aa9d0f5d4284af1db2c73ea37b9a~tplv-k3u1fbpfcp-zoom-in-crop-mark:1304:0:0:0.awebp
     */
    enqueueUpdate(fiber, queue, update, lane);

    const alternate = fiber.alternate;
    if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) {
      /**
       * 当前组件不存在更新,那么首次触发状态更新时,就能立刻计算出最新状态,进而与当前状态比较。
       * 如果两者一致,则省去了后续render的过程。
       * 可以直接执行当前的action,用来提前判断是否需要当前的函数组件fiber节点
       * 若新的state与现在的state一样,我们可以直接提前退出,
       * 若不相同,则标记该fiber节点是需要更新的;同时计算后的state可以直接用于后面的更新流程,不用再重新计算一次。
       * 根据这文档, https://www.51cto.com/article/703718.html
       * 比如从0更新到1,此后每次的更新都是1,即使是相同的值,也会再次重新渲染一次,因为两棵树上的fiber节点,
       * 在一次更新后,只会有一个fiber节点会消除更新标记,
       * 再更新一次,另一个对应的节点才会消除更新标记;再下一次,就会进入到当前的流程,然后直接return
       */
      const lastRenderedReducer = queue.lastRenderedReducer; // 上次render后的reducer,在mount时即 basicStateReducer
      if (lastRenderedReducer !== null) {
        let prevDispatcher;

        const currentState: S = (queue.lastRenderedState: any); // 上次render后的state,mount时为传入的initialState
        const eagerState = lastRenderedReducer(currentState, action);

        update.hasEagerState = true; // 表示该节点的数据已计算过了
        update.eagerState = eagerState; // 存储计算出来后的数据
        if (is(eagerState, currentState)) {
          // 若这次得到的state与上次的一样,则不再重新渲染
          return;
        }
      }
    }

    const eventTime = requestEventTime();

    /**
     * 将当前的优先级lane和触发时间给到 fiber 和 fiber.alternate,
     * 并以 fiber 的父级节点往上到root所有的节点,将 lane 添加他们的 childLanes 属性中,表示该节点的子节点有更新,
     * 在 commit 阶段就会更新该 fiber 节点
     * 这里面还存在一个任务优先级的调度,我们暂时先不考虑
     */
    const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
    if (root !== null) {
      entangleTransitionUpdate(root, queue, lane);
    }
  }

  markUpdateInDevTools(fiber, lane, action);
}

dispatchSetState()函数主要是做 3 件事情:

  1. 把所有执行的 setState(action) 里的参数 action,全部挂载到链表中;
  2. 若之前没有更新(比如第一次渲染后的更新等),马上计算出新的 state,然后与之前的 state 对比,若没有更新,则直接退出;
  3. 若有更新,则标记该 fiber 节点及所有的父级节点;刚才计算出的新的 state 可以在接下来的更新中使用;

action 通过 update 节点挂载到链表上后:

action挂载到queue上的循环链表

关于为什么要构建循环链表,如何构建循环链表,请参考React18 中的循环链表,先埋坑,后续补充。

注意,scheduleUpdateOnFiber()函数,仅仅是用来标记该 fiber 有更新需要处理,而并不会立刻重新执行函数组件。

这里有个重要的优化操作,就是若在该 fiber 节点中的 useState()时,之前没有更新(之前 fiber 节点为空或前几次都没更新),则这次的计算不受之前更新的影响

4. updateState #

当函数组件二次渲染时,可能会进入到 updateState() 里的逻辑。而 updateState() 实际上执行的是 updateReducer()。

/**
 * useState()的更新阶段
 * 传入要更新的值initialState,并返回新的[state, setState]
 * @param initialState
 * @returns {[(*|S), Dispatch<S>]}
 */
function updateState<S>(initialState: (() => S) | S): [S, Dispatch<BasicStateAction<S>>] {
  return updateReducer(basicStateReducer, (initialState: any));
}

这也说明了 updateState() 和 updateReducer() 执行的逻辑是一样的,只不过 updateState 已经默认指定了第 1 个参数,为 basicStateReducer(),而执行 useReducer() 时的 udateReducer,我们可以自己实现一个 reducer。这里我们暂时不展开对 useReducer() 的 hook 的讲解。

5. updateReducer #

在 updateReducer() 中,很大一部分的内容是用来对不同优先级的 set 的调度,和任务链表的拼接。

因为对同一个 useState() 的 hook 来讲,不是所有的 set 操作都要同时一起执行的。因为不同的 set 操作可能有不同的优先级,比如有的在异步的数据请求后才执行的,有的是放在定时器中执行的。React 会根据不同的优先级,来挑选出当前符合优先级的任务来执行。那么也就会有优先级不足的任务留到下次的渲染时执行。

updateReducer() 的代码比较长,我们主要分为三部分来讲解:

  1. 把上次遗留下来的低优先级任务(如果有的话)与当前的任务拼接(这里不对当前任务进行优先级的区分,会在第 2 步进行区分)到 baseQueue 属性上;
  2. 遍历 baseQueue 属性上所有的任务,若符合当前优先级的,则执行该 update 节点;若不符合,则将此节点到最后的所有节点都存储起来,便于下次渲染遍历,并将到此刻计算出的 state 作为下次更新时的基准 state(在 React 内部,下次渲染的初始 state,可能并不是当前页面展示的那个 state,只有所有的任务都满足优先级完成执行后,两者才是一样的);
  3. 遍历完所有可以执行的任务后,得到一个新的 newState,然后判断与之前的 state 是否一样,若不一样,则标记该 fiber 节点需要更新,并返回新的 newState 和 dispatch 方法。

源码会比较长,这里我们直接看下他的结构:

function updateReducer<S, I, A>(reducer: (S, A) => S, initialArg: I, init?: I => S): [S, Dispatch<A>] {
  if (pendingQueue !== null) {
    // 若上次更新时,有遗留下来的低优先级任务;同时当前也有要更新的任务,
    // 则将当前跟新的任务拼接到上次遗留任务的后面
    // 然后放到 baseQueue 中
  }

  if (baseQueue !== null) {
    // 当前次的更新时,更新链表不为空,那就得检查是否有可以在本地更新时要执行的任务

    do {
      if (!isSubsetOfLanes(renderLanes, updateLane)) {
        // 当前任务不满足优先级,存储起来,方便下次更新时使用
      } else {
        // 若任务优先级足够,则执行该任务;
        // 但若此时已经有低优先级的任务,为保证下次更新跳过这些任务,
        // 也会将这些任务存储起来
      }
    } while (update !== null && update !== first);

    if (newBaseQueueLast === null) {
      // 所有的任务都符合优先级,都执行完了,则下次更新时的初始值,就是上面do-while后得到的 newState 的值。
    } else {
      // 若有低优先级的任务,则将链表的最后一个节点的next指向到头结点,形成单向环形链表
    }

    if (!is(newState, hook.memoizedState)) {
      // 若新产生的 newState 跟之前的值不一样,则标记该fiber节点需要更新
      markWorkInProgressReceivedUpdate();
    }

    hook.memoizedState = newState; // 整个update链表执行完,得到的newState,用于本次渲染时使用
    hook.baseState = newBaseState; // 下次执行链表时的初始值
    hook.baseQueue = newBaseQueueLast; // 新的update链表,可能为空

    queue.lastRenderedState = newState; // 将本次的state存储为上次rendered后的值
  }

  /**
   * 返回最新的state
   */
  const dispatch: Dispatch<A> = (queue.dispatch: any);
  return [hook.memoizedState, dispatch];
}

6. 总结 #

这篇文章耽误的时间比较长,不过也总算是把 useState() 的源码顺下来了。了解完源码的实现后,我们再来看几个关于它的相关问题。

6.1 多次调用 useState() 中的 dispatch 方法,会产生多次渲染吗? #

这个要看情况来具体分析。针对相同优先级的操作,即使有多个 useState(),或执行多次的 dispatch()方法,也仅会引起一次的组件渲染。

如下面的代码,虽然有 2 个 useState(),且各自的 dispatch()方法也执行了多次,但这些执行的优先级是相同的,则 React 内部会将其合并到一起执行,然后再一起更新渲染。

function App() {
  const [count, setCount] = useState(0);
  const [random, setRandom] = useState(0);

  const handleClick = () => {
    setCount(count => count + 1);
    setCount(count => count + 1);
    setRandom(Math.random());
    setRandom(Math.random());
  };

  console.log('refresh', Math.random());

  return (
    <div>
      <p>
        App, {count}, {random}
      </p>
      <button onClick={handleClick}>click me</button>
    </div>
  );
}

每次点击按钮时,只会输出一次refresh

6.2 props 发生变动时,useState()中的数据会变吗? #

不会。

虽然 props 的变动,会导致组件的重新刷新,但 useState()中的数据并不会发生变动,即使 useState()用了 props 中的数据作为初始值。这是因为 state 值的变动,只受 dispatch() 的影响。

若想在 props 变动时,重新调整 state 的值,可以用 useEffect() 来监听 props 的变动:

function App(props) {
  const [count, setCount] = useState(props.count);

  useEffect(() => {
    // props 中的 count 属性发生变动时,重新赋值
    setCount(props.count);
  }, [props.count]);
}

6.3 直接修改 state 的值,会怎样? #

state 作为函数组件中的一个变量,当然可以直接修改它的值,然而这并不会 React 组件的重新渲染,页面上的数据也不会更新。唯一有影响的,就是后续要使用该变量的地方,会使用到新数据。但若其他 useState() 导致了组件的刷新,刚才变量的值,若是基本类型(比如数字、字符串等),会重置为修改之前的值;若是复杂类型,基于 js 的 对象引用 特性,也会同步修改 React 内部存储的数据,但不会引起视图的变化。

function App() {
  let [count, setCount] = useState(0);
  const [user, setUser] = useState({ age: 20 });
  const [random, setRandom] = useState(0);

  // 直接修改state的值
  const handleStateDirectly = () => {
    count = 2;
    user.age = 23;

    // 值可以改变,但视图并不会更新
    console.log(count, user);
  };

  // 修改random,用来刷新组件
  const handeRandom = () => {
    setRandom(Math.random());
  };

  return (
    <div className="App">
      <p>{count}</p>
      <p>{user.age}</p>
      <p>
        <button onClick={handleStateDirectly}>change state directly</button>
      </p>
      <p>
        <button onClick={handeRandom}>set random</button>
      </p>
    </div>
  );
}

在上面的样例中,直接修改 state 的值,也是可以修改的,但这种方式并不会引起视图的刷新;而通过其他 hook 的正常赋值后,相应的 state 也会发生不一样的变化,count 会重置为 0,而 user 则会变为 { age: 23}。

因此,千万不要直接修改 state 的值,否则会收到意想不到的惊喜。

标签:reacthooks
阅读(5607)
Simple Empty
No data