react的useState源码分析

news2024/11/16 17:56:38

前言

简单说下为什么React选择函数式组件,主要是class组件比较冗余、生命周期函数写法不友好,骚写法多,functional组件更符合React编程思想等等等。更具体的可以拜读dan大神的blog。其中Function components capture the rendered values这句十分精辟的道出函数式组件的优势。

但是在16.8之前react的函数式组件十分羸弱,基本只能作用于纯展示组件,主要因为缺少state和生命周期。本人曾经在hooks出来前负责过纯函数式的react项目,所有状态处理都必须在reducer中进行,所有副作用都在saga中执行,可以说是十分艰辛的经历了。在hooks出来后我在公司的一个小中台项目中使用,落地效果不错,代码量显著减少的同时提升了代码的可读性。因为通过custom hooks可以更好地剥离代码结构,不会像以前类组件那样在cDU等生命周期堆了一大堆逻辑,在命令式代码和声明式代码中有一个良性的边界。

useState在React中是怎么实现的

Hooks take some getting used to — and especially at the boundary of imperative and declarative code.

如果对hooks不太了解的可以先看看这篇文章:前情提要,十分简明的介绍了hooks的核心原理,但是我对useEffect,useRef等钩子的实现比较好奇,所以开始啃起了源码,下面我会结合源码介绍useState的原理。useState具体逻辑分成三部分:mountState,dispatch, updateState

hook的结构

首先的是hooks的结构,hooks是挂载在组件Fiber结点上memoizedState的

//hook的结构
export type Hook = {
  memoizedState: any, //上一次的state
  baseState: any,  //当前state
  baseUpdate: Update<any, any> | null,  // update func
  queue: UpdateQueue<any, any> | null,  //用于缓存多次action
  next: Hook | null, //链表
};

renderWithHooks

在reconciler中处理函数式组件的函数是renderWithHooks,其类型是:

renderWithHooks(
  current: Fiber | null, //当前的fiber结点
  workInProgress: Fiber, 
  Component: any, //jsx中用<>调用的函数
  props: any,
  refOrContext: any,
  nextRenderExpirationTime: ExpirationTime, //需要在什么时候结束
): any

在renderWithHooks,核心流程如下:

//从memoizedState中取出hooks
nextCurrentHook = current !== null ? current.memoizedState : null; 
//判断通过有没有hooks判断是mount还是update,两者的函数不同
ReactCurrentDispatcher.current =
      nextCurrentHook === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;
//执行传入的type函数
let children = Component(props, refOrContext);
//执行完函数后的dispatcher变成只能调用context的
ReactCurrentDispatcher.current = ContextOnlyDispatcher;

return children;

useState构建时流程

mountState

在HooksDispatcherOnMount中,useState调用的是下面的mountState,作用是创建一个新的hook并使用默认值初始化并绑定其触发器,因为useState底层是useReducer,所以数组第二个值返回的是dispatch。

type BasicStateAction<S> = (S => S) | S;

function mountState<S>(
  initialState: (() => S) | S,
){
  const hook = mountWorkInProgressHook();
//如果入参是func则会调用,但是不提供参数,带参数的需要包一层
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
//上一个state和基本(当前)state都初始化
  hook.memoizedState = hook.baseState = initialState;
  const queue = (hook.queue = {
    last: null,
    dispatch: null,
    eagerReducer: basicStateReducer, // useState使用基础reducer
    eagerState: (initialState: any),
  });
//返回触发器
  const dispatch: Dispatch<
    //useState底层是useReducer,所以type是BasicStateAction
 (queue.dispatch = (dispatchAction.bind(
    null,
    //绑定当前fiber结点和queue
    ((currentlyRenderingFiber: any): Fiber),
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}

mountWorkInProgressHook

这个函数是mountState时调用的构建hook的方法,在初始化完毕后会连接到当前hook.next(如果有的话)

function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,
    baseState: null,
    queue: null,
    baseUpdate: null,
    next: null,
  };
  if (workInProgressHook === null) {
    // 列表中的第一个hook
    firstWorkInProgressHook = workInProgressHook = hook;
  } else {
    // 添加到列表的末尾
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

dispatch分发函数

在上面我们提到,useState底层是useReducer,所以返回的第二个参数是dispatch函数,其中的设计十分巧妙。

假设我们有以下代码:

相关参考视频讲解:进入学习

const [data, setData] = React.useState(0)
setData('first')
setData('second')
setData('third')

在第一次setData后, hooks的结构如上图

在第二次setData后, hooks的结构如上图

在第三次setData后, hooks的结构如上图

在正常情况下,是不会在dispatcher中触发reducer而是将action存入update中在updateState中再执行,但是如果在react没有重渲染需求的前提下是会提前计算state即eagerState。作为性能优化的一环。

function dispatchAction<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
) {
  const alternate = fiber.alternate;
   {
    flushPassiveEffects();
    //获取当前时间并计算可用时间
    const currentTime = requestCurrentTime();
    const expirationTime = computeExpirationForFiber(currentTime, fiber);

    const update: Update<S, A> = {
      expirationTime,
      action,
      eagerReducer: null,
      eagerState: null,
      next: null,
    };
    //下面的代码就是为了构建queue.last是最新的更新,然后last.next开始是每一次的action
    // 取出last
    const last = queue.last;
    if (last === null) {
      // 自圆
      update.next = update;
    } else {
      const first = last.next;
      if (first !== null) {

        update.next = first;
      }
      last.next = update;
    }
    queue.last = update;

    if (
      fiber.expirationTime === NoWork &&
      (alternate === null || alternate.expirationTime === NoWork)
    ) {
      // 当前队列为空,我们可以在进入render阶段前提前计算出下一个状态。如果新的状态和当前状态相同,则可以退出重渲染
      const lastRenderedReducer = queue.lastRenderedReducer; // 上次更新完后的reducer
      if (lastRenderedReducer !== null) {
        let prevDispatcher;
        if (__DEV__) {
          prevDispatcher = ReactCurrentDispatcher.current;  // 暂存dispatcher
          ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
        }
        try {
          const currentState: S = (queue.lastRenderedState: any);
          // 计算下次state
          const eagerState = lastRenderedReducer(currentState, action);
          // 在update对象中存储预计算的完整状态和reducer,如果在进入render阶段前reducer没有变化那么可以服用eagerState而不用重新再次调用reducer
          update.eagerReducer = lastRenderedReducer;
          update.eagerState = eagerState;
          if (is(eagerState, currentState)) {
            // 在后续的时间中,如果这个组件因别的原因被重渲染且在那时reducer更变后,仍有可能重建这次更新
            return;
          }
        } catch (error) {
          // Suppress the error. It will throw again in the render phase.
        } finally {
          if (__DEV__) {
            ReactCurrentDispatcher.current = prevDispatcher;
          }
        }
      }
    }
    scheduleWork(fiber, expirationTime);
  }
}

useState更新时流程

updateReducer

因为useState底层是useReducer,所以在更新时的流程(即重渲染组件后)是调用updateReducer的。

function updateState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  return updateReducer(basicStateReducer, (initialState: any));
}

所以其reducer十分简单

function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
  return typeof action === 'function' ? action(state) : action;
}

我们先把复杂情况抛开,跑通updateReducer流程

function updateReducer(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
){
  // 获取当前hook,queue
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;

  queue.lastRenderedReducer = reducer;

  // action队列的最后一个更新
  const last = queue.last;
  // 最后一个更新是基本状态
  const baseUpdate = hook.baseUpdate;
  const baseState = hook.baseState;

  // 找到第一个没处理的更新
  let first;
  if (baseUpdate !== null) {
    if (last !== null) {
      // 第一次更新时,队列是一个自圆queue.last.next = queue.first。当第一次update提交后,baseUpdate不再为空即可跳出队列
      last.next = null;
    }
    first = baseUpdate.next;
  } else {
    first = last !== null ? last.next : null;
  }
  if (first !== null) {
    let newState = baseState;
    let newBaseState = null;
    let newBaseUpdate = null;
    let prevUpdate = baseUpdate;
    let update = first;
    let didSkip = false;
    do {
      const updateExpirationTime = update.expirationTime;
      if (updateExpirationTime < renderExpirationTime) {
        // 优先级不足,跳过这次更新,如果这是第一次跳过更新,上一个update/state是newBaseupdate/state
        if (!didSkip) {
          didSkip = true;
          newBaseUpdate = prevUpdate;
          newBaseState = newState;
        }
        // 更新优先级
        if (updateExpirationTime > remainingExpirationTime) {
          remainingExpirationTime = updateExpirationTime;
        }
      } else {
        // 处理更新
        if (update.eagerReducer === reducer) {
          // 如果更新被提前处理了且reducer跟当前reducer匹配,可以复用eagerState
          newState = ((update.eagerState: any): S);
        } else {
          // 循环调用reducer
          const action = update.action;
          newState = reducer(newState, action);
        }
      }
      prevUpdate = update;
      update = update.next;
    } while (update !== null && update !== first);

    if (!didSkip) {
      newBaseUpdate = prevUpdate;
      newBaseState = newState;
    }

    // 只有在前后state变了才会标记
    if (!is(newState, hook.memoizedState)) {
      markWorkInProgressReceivedUpdate();
    }
    hook.memoizedState = newState;
    hook.baseUpdate = newBaseUpdate;
    hook.baseState = newBaseState;
    queue.lastRenderedState = newState;
  }

  const dispatch: Dispatch<A> = (queue.dispatch: any);
  return [hook.memoizedState, dispatch];
}
export function markWorkInProgressReceivedUpdate() {
  didReceiveUpdate = true;
}

后记

作为系列的第一篇文章,我选择了最常用的hooks开始,抛开提前计算及与react-reconciler的互动,整个流程是十分清晰易懂的。mount的时候构建钩子,触发dispatch时按序插入update。updateState的时候再按序触发reducer。可以说就是一个简单的redux。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/31527.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JWT有状态登陆与无状态登陆

单点登录与JWT JWT 全称&#xff1a; Json Web Token 。作用&#xff1a; JWT 的作用是 用户授权(Authorization) &#xff0c;而不是用户的身份认证(Authentication) 。用户认证 指的是使用用户名、密码来验证当前用户的身份&#xff0c;即用户登录。用户授权 指用户登录成功后…

「从零单排canal 07」 parser模块源码解析

本文将对canal的binlog订阅模块parser进行分析。 parser模块(绿色部分)在整个系统中的角色如下图所示&#xff0c;用来订阅binlog事件&#xff0c;然后通过sink投递到store. parser模块应该来说是整个项目里面比较复杂的模块&#xff0c;代码非常多。 因此&#xff0c;本文根…

Nmap爆破MySQL弱口令漏洞:解决报错Accounts: No valid accounts found

nmap工具不仅仅能扫描&#xff0c;也可以暴力破解mysql&#xff0c;ftp&#xff0c;telnet等服务。 看到这里不要怀疑&#xff0c;在kali系统中查一下到底支持哪些暴力破解功能&#xff0c;命令如下 ls /usr/share/nmap/scripts |grep brute.nse 查询结果为 afp-brute.nse …

镜像底层原理详解和基于Docker file创建镜像

目录 一、镜像底层原理 1.联合文件系统(UnionFS) 2.镜像加载原理 3.为什么Docker里的centos的大小才200M? 二、Dockerfile 1.简介 2.Dockerfile操作常用命令 &#xff08;1&#xff09;FORM 镜像 &#xff08;2&#xff09;MAINTAINER 维护人信息 &#xff08;3&…

Vue的devtools安装教程

devtools是一个便于开发者调试Vue代码的插件 先确保你已经安装了node.js 点击此处去github上拉取工具包 安装yarn&#xff08;用npm在打包的时候会失败&#xff0c;使用yarn可以打包成功&#xff09; ① cmd输入&#xff1a;npm install -g yarn 全局安装yarn包管理工具   …

Nature Plants|植物基因组测序20年回顾与展望:三代HiFi基因组时代

2021年11月29日&#xff0c;美国密歇根州立大学在《Nature Plants》期刊在线发表题为“Representation and participation across 20 years of plant genomesequencing”综述&#xff0c;系统阐述了在过去的20年间&#xff0c;对陆地植物基因组学组装质量、已测序物种的分类和地…

Java的几大常用类

一、Object类 超类、基类&#xff0c;所有类的直接或者间接父类&#xff0c;位于继承树的最顶层。 任何类&#xff0c;如果没有写 extends 显示继承某个类&#xff0c;都直接默认继承 Object 类&#xff0c;否则为间接继承。 Object 类中所定义的方法&#xff0c;是所有对象…

DeeplabV3实战:基于tensorflow搭建DeeplabV3实现语义分割任务

任务描述: 语义分割是一种典型的计算机视觉问题,其是将一些图像作为输入并将它们转换为具有突出显示的感兴趣区域的掩模,即图像中的每个像素根据其所属的感兴趣对象被分配类别。如下图中左图所示,其语义是人骑自行车,语义分割的结果如右图所示,粉红色代表人,绿色代表自行…

狂神的MySQL(1)

01、什么是数据库&#xff0c;为什么要学习数据库 javaEE&#xff1a;企业级java开发 Web 前端&#xff08;页面&#xff1a;展示&#xff0c;数据&#xff01;&#xff09; 后台&#xff08;连接点&#xff1a;连接数据库JDBC&#xff0c;连接前端&#xff08;控制&#xf…

web前端-javascript-运算符(介绍说明,算术运算符、+、-、*、/、%,隐式类型转换、转换为String、转换为Number)

文章目录运算符1. 操作符说明1.1. 通过运算符可以对一个或多个值进行运算,并获取运算结果1.2. 它会将该值的类型以字符串的形式返回2. 算数运算符2.1. 当对非 Number 的值进行运算时&#xff0c;会将这些值转换为 Number 然后再运算2.2. 2.3 -2.4 \*2.5 /2.6 %3. 隐式类型转换3…

思科配置VLAN间单臂路由

思科配置VLAN间单臂路由 为什么要配置单臂路由 路由器上链接不同的VLAN的物理接口数量有限&#xff0c;随着VLAN增加端口很快就被耗尽&#xff0c;然而VLAN中继允许单个路由器物理接口接多个VLAN的流量&#xff0c;即有了单臂路由技术。 示例拓扑 本文以下图所示拓扑为例配…

mac误删除文件恢复,mac文件丢失如何找回

mac误删除文件恢复&#xff1f;相信不少朋友一开始遇到这个问题没太放在心上&#xff0c;还以为是自己误删除了&#xff0c;想着以后删除文件的时候尽量小心点&#xff0c;直到发现重要文件被删除后没有经过废纸篓&#xff0c;而被直接删除了&#xff0c;这才引起重视&#xff…

java项目-第160期ssm大学生校园兼职系统_ssm毕业设计_计算机毕业设计

java项目-第160期ssm大学生校园兼职系统_ssm毕业设计_计算机毕业设计 【源码请到资源专栏下载】 今天分享的项目是《ssm大学生校园兼职系统》 该项目分为3个角色&#xff0c;管理员、学生、企业。 学生角色可以访问前台&#xff0c;查看企业信息、招聘信息进行应聘。 企业角色…

Android Material Design之NavigationView(五)

先上图 使用说明 NavigationView与DrawerLayout 成对出现,经常被用作侧滑菜单,因此当我们使用 NavigationView时布局文件的根节点一定是DrawerLayout 属性 NavigationView 属性描述android:id控件idandroid:layout_width控件的长度android:layout_height控件的高度android…

关于web前端大作业的HTML网页设计——我的班级网页HTML+CSS+JavaScript

&#x1f389;精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业…

Flink CDC 新一代数据集成框架

前言&#xff1a; 主要讲解了技术原理&#xff0c;入门与生产实践&#xff0c;主要功能&#xff1a;全增量一体化数据集成、实时数据入库入仓、最详细的教程。Flink CDC 是Apache Flink的一个重要组件&#xff0c;主要使用了CDC技术从各种数据库中获取变更流并接入到Flink中&a…

神经网络训练不起来,怎么办?

从optimization的角度&#xff0c;Cross-entropy比Mean Square Error更加适合用在分类上。使用Cross-entropy这个Loss function的时候&#xff0c;pytorch自动帮你把Soft-max加到你的Network的最后一层。 Optimization Critical Point是Saddle Point还是Local Point&#xff1…

【蓝桥杯web】第十四届蓝桥杯(Web应用开发)模拟赛1期-大学组

数据类型检测 请看这篇数据类型检测 渐变色背景生成器 html <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta http-equiv"X-UA-Compatible" content"IEedge" /><meta name&…

华为云安全亮相世界互联网大会

近日&#xff0c;以“共建网络世界 共创数字未来——携手构建网络空间命运共同体”为主题的世界互联网大会在浙江乌镇盛大举行。华为云安全专家团队携全新的安全运营2.0和数据安全解决方案亮相峰会现场&#xff0c;重点展示华为云在安全运营和数据方案的最新实践成果&#xff0…

小啊呜产品读书笔记001:《邱岳的产品手记-11》第21讲 产品案例分析:Fabulous的精致养成

小啊呜产品读书笔记001&#xff1a;《邱岳的产品手记-11》第21讲 产品案例分析&#xff1a;Fabulous的精致养成一、今日阅读计划二、泛读&知识摘录第21讲 产品案例分析&#xff1a;Fabulous的精致养成三、头脑风暴四、思考叮嘟&#xff01;这里是小啊呜的产品进阶读书笔记整…