React setState后发生了什么?

React Version | v16.8.6 组件类型 | Hooks Components

一、序言

在开发中,我们只知道useState返回的set,执行后值发生变化的话,就会触发组件重新渲染。但为什么呢?当时我脑海浮现了几个疑问:

  1. React为何使用了useState后就可以保存value,不管函数执行了多少次。
  2. setState后,value怎么被更新的,形象点就是setState(prev ⇒ return prev + 1)时,prev参数到底从哪里来的?
  3. setState后,React从哪里开始更新的,为什么React能知道要从这里开始更新?

带着这几个疑问,开始翻阅源码跟资料,希望下文能有所启发。

二、useState源码分析

React源码 | 源码地址 React项目本地配置 | vscode 配置调试react源码 - 掘金

代码定位

在React中搜useState 可以看到在最顶层的const React = { ..., useState} ,这里便是我们平常使用时 import { useState } from 'react'; 所引入的函数,顺着这个代码,可以发现最终指向三个DispatcherContextOnlyDispatcherHooksDispatcherOnMountHooksDispatcherOnUpdate,忽略用于边界情况异常上下文的Dispatcher,最终分为两类:HooksDispatcherOnMountHooksDispatcherOnUpdate

可以看到不同状态下DispatcheruseState 是不同的,分为mountStateupdateState

源码分析

组件初始化时调用的useState -> mountState,会初始化该hooks的上下文。而React的hooks,就是将一个存储了 值+更新回调 的链表。具体看Hooks的结构:

typescript 复制代码
export type Hook = {
  memoizedState: any,

  baseState: any,
  baseUpdate: Update<any, any> | null,
  queue: UpdateQueue<any, any> | null,

  next: Hook | null,
};

分析源码可以看到,memoizedState便是存储的hooks对应的值,这也是函数组件执行多次,state仍可以保存状态的原因。

但这里会有一个新的疑问,既然Hook节点是一个链表的全局变量,那函数组件在运行时怎么知道当前useState对应的Hook节点是谁呢?以及最初问题2?

关于这些,需要分别看state跟set如何获取和更新的,这里简化下useState的执行逻辑:

  1. 初始化Hook节点:

    1. 创建Hook节点,挂载到workInProgressHook
    2. 初始化state值
    3. 包装set并挂载到对应Fiber节点上:这里会初始化UpdateQueue,这是当前Hook节点的更新链
  2. set state:上面初始化的时候提到包装set,具体代码看mountState → dispatchAction,包装的set后,每次set(action)执行时,都会将action包装成一个Update节点推入Hook节点的UpdateQueue中,然后调用scheduleWork触发更新渲染。所以可以看到,在set时,正常来讲其实是不会实时更新state值的。

  3. get/update state:可以在updateState-> updateReducer 里看到,这里有比较复杂的更新逻辑,简化下逻辑:

    1. 取出对应Hook节点
    2. 不断应用上次set(action) :将上次通过set(action) 包装到update里的更新链全都取出,然后执行一遍,用于更新state(代码

三、运行机制

对整个useState运行机制进行归纳:

  1. 组件初始化,通过mountState 初始化Hook节点并挂载到Hooks链表上,这也是为何所有的hook api不允许在return 之后定义。
  2. 组件执行 set(action) ,初始化时对 dispatchAction 柯里化的set,将action操作封装成Update节点挂载到Hook.update这个 UpdateQueue 上,并通过scheduleWork 触发新一轮更新渲染。
  3. 组件重新渲染,也就是第二次执行useState时,会将Hook.update的所有更新操作出栈并执行,这将得到新的memoizedState,而这时state就是最新值了,所以setState的回调是在下次渲染时生效的。

四、题外话

了解了useState的机制,其实不难发现其跟useReducer基本没啥差别,甚至可以看到useState基本都是基于useReducer封装的,那我们如何通过useReducer也来封装一个useState呢?

typescript 复制代码
type FCAction<S> = (newState: S) => S;
type SetState<S> = (newState: S | FCAction<S>) => void;
function useState<S>(initialState: (() => S) | S): [S, SetState<S>] {
    const [state, dispatch] = useReducer<(state: S, newState: S) => S>(
        (state, newState) => newState, 
        typeof initialState === "function" 
            ? (initialState as () => S)() 
            : initialState
    );

    const setState: SetState<S> = (newState) => {
        if (typeof newState === "function") {
            dispatch((newState as FCAction<S>)(state));
        } else {
            dispatch(newState);
        }
    };

    return [state, setState];
}
相关推荐
emojiwoo11 小时前
【前端基础知识系列六】React 项目基本框架及常见文件夹作用总结(图文版)
前端·react.js·前端框架
Bug改不动了13 小时前
React Native 与 UniApp 对比
react native·react.js·uni-app
然我14 小时前
React 16.8:不止 Hooks 那么简单,这才是真正的划时代更新 🚀
前端·react.js·前端框架
OEC小胖胖14 小时前
【React Hooks】封装的艺术:如何编写高质量的 React 自-定义 Hooks
前端·react.js·前端框架·web
404_Not_Found1115 小时前
用 react + ts 实现我的第一个 todoList
react.js
木春17 小时前
React入门:构建你的第一个应用
前端·react.js
吃奥特曼的饼干19 小时前
React useEffect 清理函数:别让依赖数组坑了你!
前端·react.js
随笔记20 小时前
react中函数式组件和类组件有什么区别?新建的react项目用函数式组件还是类组件?
前端·react.js·typescript
emojiwoo20 小时前
React 状态管理:useState 与 useDatePersistentState 深度对比
前端·javascript·react.js
D11_20 小时前
【React】JSX基础
前端·react.js·前端框架