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];
}
相关推荐
@ 前端小白14 小时前
封装倒计时自定义react hook
前端·javascript·react.js
咔咔库奇14 小时前
【react】Redux的设计思想与工作原理
前端·react.js·前端框架
汤圆真的好可爱16 小时前
关于新手学习React的一些忠告
前端·学习·react.js
screct_demo17 小时前
详细讲一下React中Redux的持久化存储(Redux-persist)
前端·react.js·前端框架
夫琅禾费米线19 小时前
React Router 用法概览
前端·javascript·react.js·前端框架
市民中心的蟋蟀19 小时前
第九章 Refs:从存储数据到指令式API 上
前端·javascript·react.js
爱喝奶茶的企鹅19 小时前
WebSocket 基础入门:协议原理与实现
react.js
outstanding木槿1 天前
react中实现拖拽排序
前端·javascript·react.js
zerotower1 天前
nextjs 使用react-hook-form和zod实现登录表单验证和提交
react.js·next.js
傻小胖2 天前
axios和fetch的实现原理以及区别,与XMLHttpRequest的关系,并结合react封装统一请求示例
前端·javascript·react.js·前端框架