深入react源码看setState究竟做了什么?

news2024/11/27 12:58:03

前言

在深究 React 的 setState 原理的时候,我们先要考虑一个问题:setState 是异步的吗?
首先以 class component 为例,请看下述代码(demo-0)

class App extends React.Component {
  state = {
    count: 0
  }

  handleCountClick = () => {
    this.setState({
      count: this.state.count + 1
    });
    console.log(this.state.count);
  }

  render() {
    return (
      <div className='app-box'>
        <div onClick={this.handleCountClick}>the count is {this.state.count}</div>
      </div>
    )
  }
}
ReactDOM.render(
    <App />,
  document.getElementById('container')
);

count初始值为 0,当我们触发handleCountClick事件的时候,执行了count + 1操作,并打印了count,此时打印出的count是多少呢?答案不是 1 而是 0

类似的 function component 与 class component 原理一致。现在我们以 function component 为例,请看下述代码 (demo-1)

const App = function () {
  const [count, setCount] = React.useState(0);
  const handleCountClick = () => {
      setCount((count) => {
          return count + 1;
      });
      console.log(count);
  }

  return <div className='app-box'>
    <div onClick={handleCountClick}>the count is {count}</div>
  </div>
}
ReactDOM.render(
    <App />,
  document.getElementById('container')
);

同样的,这里打印出的 count 也为 0

相信大家都知道这个看起来是异步的现象,但他真的是异步的吗?

为什么 setState 看起来是『异步』的?

首先得思考一个问题:如何判断这个函数是否为异步?

最直接的,我们写一个 setTimeout,打个 debugger 试试看

image.png

我们都知道 setTimeout 里的回调函数是异步的,也正如上图所示,chrome 会给 setTimeout 打上一个 async 的标签。

接下来我们 debugger setState 看看

image.png

React.useState 返回的第二个参数实际就是这个 dispatchSetState函数(下文细说)。但正如上图所示,这个函数并没有 async 标签,所以 setState 并不是异步的。

那么抛开这些概念来看,上文中 demo-1 的类似异步的现象是怎么发生的呢?

简单的来说,其步骤如下所示。基于此,我们接下来更深入的看看 React 在这个过程中做了什么

image.png

从 first paint 开始

first paint 就是『首次渲染』,为突出显示,就用英文代替。

  • 这里先简单看一下App往下的 fiber tree 结构。每个 fiber node 还有一个return指向其 parent fiber node,这里就不细说了

我们都知道 React 渲染的时候,得遍历一遍 fiber tree,当走到 App 这个 fiber node 的时候发生了什么呢?

接下来我们看看详细的代码(这里的 workInProgress 就是整在处理的 fiber node,不关心的代码已删除)

  • 首先要注意的是,虽然 App 是一个 FunctionComponent,但是在 first paint 的时候,React 判断其为 IndeterminateComponent
 switch (workInProgress.tag) { // workInProgress.tag === 2
  case IndeterminateComponent:
    {
      return mountIndeterminateComponent(
        current,
        workInProgress,
        workInProgress.type,
        renderLanes
      );
    }
  // ...

  case FunctionComponent:
    { /** ... */}
}
  • 接下来走进这个 mountIndeterminateComponent,里头有个关键的函数 renderWithHooks;而在 renderWithHooks 中,我们会根据组件处于不同的状态,给 ReactCurrentDispatcher.current 挂载不同的 dispatcher 。而在first paint 时,挂载的是HooksDispatcherOnMountInDEV 相关参考视频讲解:进入学习

image.png

 function mountIndeterminateComponent(_current, workInProgress, Component, renderLanes) {
     value = renderWithHooks(
         null,
         workInProgress,
         Component,
         props,
         context,
         renderLanes
     );
 }
 function renderWithHooks() {
     // ...
     if (current !== null && current.memoizedState !== null) {
         // 此时 React 认为组件在更新
         ReactCurrentDispatcher.current = HooksDispatcherOnUpdateInDEV;
     } else if (hookTypesDev !== null) {
         // handle edge case,这里我们不关心
     } else {
         // 此时 React 认为组件为 first paint 阶段
         ReactCurrentDispatcher.current = HooksDispatcherOnMountInDEV;
     }
     // ...
     var children = Component(props, secondArg); // 调用我们的 Component
 }
  • 这个 HooksDispatcherOnMountInDEV 里就是组件 first paint 的时候所用到的各种 hooks,
HooksDispatcherOnMountInDEV = {
    // ...
    useState: function (initialState) {
        currentHookNameInDev = 'useState';
        mountHookTypesDev();
        var prevDispatcher = ReactCurrentDispatcher$1.current;
        ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;

        try {
          return mountState(initialState);
        } finally {
          ReactCurrentDispatcher.current = prevDispatcher;
        }
    },
    // ...
}
  • 接下里走进我们的 App(),我们会调用 React.useState,点进去看看,代码如下。这里的 dispatcher 就是上文挂载到 ReactCurrentDispatcher.currentHooksDispatcherOnMountInDEV
function useState(initialState) {
    var dispatcher = resolveDispatcher();
    return dispatcher.useState(initialState);
}
// ...
HooksDispatcherOnMountInDEV = {
    // ...
    useState: function (initialState) {
        currentHookNameInDev = 'useState';
        mountHookTypesDev();
        var prevDispatcher = ReactCurrentDispatcher$1.current;
        ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnMountInDEV;

        try {
          return mountState(initialState);
        } finally {
          ReactCurrentDispatcher$1.current = prevDispatcher;
        }
    },
    // ...
}
  • 这里会调用 mountState 函数
function mountState(initialState) {
    var hook = mountWorkInProgressHook();

    if (typeof initialState === 'function') {
      // $FlowFixMe: Flow doesn't like mixed types
      initialState = initialState();
    }

    hook.memoizedState = hook.baseState = initialState;
    var queue = {
      pending: null,
      interleaved: null,
      lanes: NoLanes,
      dispatch: null,
      lastRenderedReducer: basicStateReducer,
      lastRenderedState: initialState
    };
    hook.queue = queue;
    var dispatch = queue.dispatch = dispatchSetState.bind(null, currentlyRenderingFiber$1, queue);
    return [hook.memoizedState, dispatch];
}
  • 这个函数做了这么几件事情:

    1. 执行 mountWorkInProgressHook 函数:

      function mountWorkInProgressHook() {
         var hook = {
           memoizedState: null,
           baseState: null,
           baseQueue: null,
           queue: null,
           next: null
         };
      
         if (workInProgressHook === null) {
           // This is the first hook in the list
           currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;
         } else {
           // Append to the end of the list
           workInProgressHook = workInProgressHook.next = hook;
         }
      
         return workInProgressHook;
       }
      
      • 创建一个 hook
      • 若无 hook 链,则创建一个 hook 链;若有,则将新建的 hook 加至末尾
      • 将新建的这个 hook 挂载到 workInProgressHook 以及当前 fiber node 的 memoizedState
      • 返回 workInProgressHook,也就是这个新建的 hook
    2. 判断传入的 initialState 是否为一个函数,若是,则调用它并重新赋值给 initialState (在我们的demo-1里是『0』)

    3. initialState 挂到 hook.memoizedState 以及 hook.baseState

    4. hook 上添加一个 queue。这个 queue 有多个属性,其中queue.dispatch 挂载的是一个 dispatchSetState。这里要注意一下这一行代码

var dispatch = queue.dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue);

Function.prototype.bind 的第一个参数都知道是绑 this 的,后面两个就是绑定了 dispatchSetState 所需要的第一个参数(当前fiber)和第二个参数(当前queue)。

这也是为什么虽然 dispatchSetState 本身需要三个参数,但我们使用的时候都是 setState(params),只用传一个参数的原因。

  1. 返回一个数组,也就是我们常见的 React.useState 返回的形式。此时这个 state 是 0
    • 至此为止,React.useState 在 first paint 里做的事儿就完成了,接下来就是正常渲染,展示页面

image.png

触发组件更新

  • 触发组件更新,自然就是点击这个绑定了事件监听的 div,触发 setCount。回忆一下,这个 setCount 就是上文讲述的,暴露出来的 dispatchSetState。并且正如上文所述,我们传进去的参数实际上是 dispatchSetState 的第三个参数 action。(这个函数自然也涉及一些 React 执行优先级的判断,不在本文的讨论范围内就省略了)
function dispatchSetState(fiber, queue, action) {
    var update = {
      lane: lane,
      action: action,
      hasEagerState: false,
      eagerState: null,
      next: null
    };
    enqueueUpdate(fiber, queue, update);
}
  • dispatchSetState 做了这么几件事

    1. 创建一个 update,把我们传入的 action 放进去

    2. 进入 enqueueUpdate 函数:

      • queue上无 update 链,则在 queue 上以 刚创建的 update 为头节点构建 update
      • queue上有 update 链,则在该链的末尾添加这个 刚创建的 update
    function enqueueUpdate(fiber, queue, update, lane) {
      var pending = queue.pending;
    
      if (pending === null) {
        // This is the first update. Create a circular list.    update.next = update;
      } else {
        update.next = pending.next;
        pending.next = update;
      }
    
      queue.pending = update;
    
      var lastRenderedReducer = queue.lastRenderedReducer;
      var currentState = queue.lastRenderedState;
      var eagerState = lastRenderedReducer(currentState, action);
      update.hasEagerState = true;
      update.eagerState = eagerState;
    }
    
    1. 根据 queue 上的各个参数(reducer、上次计算出的 state)计算出 eagerState,并挂载到当前 update
  • 到此,我们实际上更新完 state了,这个新的 state 挂载到哪儿了呢?在 fiber.memoizedState.queue.pending 上。注意:

    1. fiber 即为当前的遍历到的 fiber node;
    2. pending 是一个环状链表
  • 此时我们打印进行打印,但这里打印的还是 first paint 里返回出来的 state,也就是 0

更新、渲染 fiber tree

现在我们更新完 state,要开始跟新 fiber tree 了,进行最后的渲染。逻辑在 performSyncWorkOnRoot 函数里,同样的,不关心的逻辑我们省略

function performSyncWorkOnRoot(root) {
    var exitStatus = renderRootSync(root, lanes);
}
  • 同样的我们先看一眼 fiber tree 更新过程中 与 useState 相关的整个流程图

image.png

  • 首先我们走进 renderRootSync,这个函数作用是遍历一遍 fiber tree,当遍历的 App时,此时的类型为 FunctionComponent。还是我们前文所说的熟悉的步骤,走进 renderWithHooks。注意此时 React 认为该组件在更新了,所以给 dispatcher 挂载的就是 HooksDispatcherOnUpdateInDEV
function renderWithHooks(current, workInProgress, Component, props, secondArg, nextRenderLanes) {
    var children = Component(props, secondArg);
}
  • 我们再次走进 App,这里又要再次调用 React.useState
const App = function () {
      const [count, setCount] = React.useState(0);
      const handleCountClick = () => {
        setCount(count + 1);
      }

      return <div className='app-box'>
        <div onClick={handleCountClick}>the count is {count}</div>
      </div>
}
  • 与之前不同的是,这次所使用的 dispatchHooksDispatcherOnUpdateInDEV。那么这个 dispatch 下的 useState 具体做了什么呢?
useState: function (initialState) {
        currentHookNameInDev = 'useState';
        updateHookTypesDev();
        var prevDispatcher = ReactCurrentDispatcher$1.current;
        ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV;

        try {
          return updateState(initialState);
        } finally {
          ReactCurrentDispatcher$1.current = prevDispatcher;
        }
}
  • 可以看到大致都差不多,唯一不同的是,这里调用的是 updateState,而之前是 mountState
function updateState(initialState) {
    return updateReducer(basicStateReducer);
}
function updateReducer(reducer, initialArg, init) {
  var first = baseQueue.next;
  var newState = current.baseState;

  do {
    // 遍历更新 newState
    update = update.next;
  } while (update !== null && update !== first);

  hook.memoizedState = newState;
  queue.lastRenderedState = newState;
  return [hook.memoizedState, dispatch];
}
  • 这里又调用了 updateReducer,其中代码很多不一一展示,关键步骤就是:

    1. 遍历我们之前挂载到 fiber.memoizedState.queue.pending 上的环状链表,并得到最后的 newState

    2. 更新 hookqueue 上的相关属性,也就是将最新的这个 state 记录下来,这样下次更新的时候可以这次为基础再去更新

    3. 返回一个数组,形式为 [state, setState]此时这个 state 即为计算后的 newState,其值为 1

  • 接下来就走进 commitRootImpl 进行最后的渲染了,这不是本文的重点就不展开了,里头涉及 useEffect 等钩子函数的调用逻辑。

  • 最后看一眼整个详细的流程图

usestate.png

写在最后

上文只是描述了一个最简单的 React.useState 使用场景,各位可以根据本文配合源码,进行以下两个尝试:

Q1. 多个 state 的时候有什么变化?例如以下场景时:

const App = () => {
    const [count, setCount] = React.useState(0);
    const [str, setStr] = React.useState('');
    // ...
}

A1. 将会构建一个上文所提到的 hook

Q2. 对同个 state 多次调用 setState 时有什么变化?例如以下场景:

const App = () => {
      const [count, setCount] = React.useState(0);
      const handleCountClick = () => {
        setCount(count + 1);
        setCount(count + 2);
      }
      return <div className='app-box'>
        <div onClick={handleCountClick}>the count is {count}</div>
      </div>
}

A2. 将会构建一个上文所提到的 update

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

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

相关文章

LQ0272 矩形运算【计算几何】

题目来源&#xff1a;蓝桥杯2012初赛 Java A组H题 题目描述 在编写图形界面软件的时候&#xff0c;经常会遇到处理两个矩形的关系。 如图 1 所示&#xff0c;矩形的交集指的是&#xff1a;两个矩形重叠区的矩形&#xff0c;当然也可能不存在&#xff08;参看图 2 &#xff09…

Linux下C语言UDP协议通信实践

UDP和TCP协议一样&#xff0c;都是传输层协议。是无连接的&#xff0c;不安全的&#xff0c;报式传输层协议&#xff0c;通信过程默认也是阻塞的。其通信特点主要如下&#xff1a; &#xff08;1&#xff09;不需要建立连接 &#xff0c;所以不需要进行 connect () 操作 &…

国产操作系统之凝思磐石安装

一、凝思磐石操作系统简介 安全性是凝思磐石安全服务平台软件V2.4的主要特征&#xff0c;充分结合凝思科技独有技术与国内外多项安全标准的要求&#xff0c;在操作系统和应用程序的各个层次进行安全增强&#xff0c;使系统成为安全的有机整体。主要安全机制有以下几点&#xff…

高校房产管理现状及数图互通解决方案?

高校拥有大量的房产土地资源、公共设施、公有住房等&#xff0c;是高校开展各类教学、科研的基础场所&#xff0c;也是学校国有资产不可缺少的一部分。但是在管理过程中&#xff0c;存在着较多的困难与问题&#xff1a; 1.房地产的有效利用率不高 2.房地产管理信息化速度较慢…

异常解决!针对文件I/O写入操作时FileNotFoundException异常的修复

一. 异常问题 I/O对于在Java学习路上的初学者来说&#xff0c;简直就是一道长长的坎儿。我们在使用I/O的过程中&#xff0c;会有许多的异常需要处理&#xff0c;并且其中还带有一系列的验证逻辑在其中。如果在学习过程中&#xff0c;我们的基础打得不牢靠&#xff0c;那么可能…

如何在Xshell上运行一个C文件?

1.设置XTP服务器配置 按下图点击&#xff0c;创建一个新的连接&#xff1a; 如图所示&#xff0c;创建成功 2.Xshell界面创建 如图界面&#xff0c;就说明成功了 3.在电脑任意位置保存一个txt文件&#xff0c;注意扩展名要改成.c 4.在Xftp中导入刚写好的.c文件 如图&#xff…

pom报红

若是从其他地方导入的新工程&#xff0c;右边 maven 管理工具中 dependencies 若出现红色波浪线。通常用以下方式尝试解决。 第一步 0.导入外部项目&#xff0c;先配置jdk。 1.&#xff08;检查maven仓库配置是否正确&#xff09;File --> settings 输入maven&#xff0…

爆料,前华为微服务专家纯手打500页落地架构实战笔记,已开源

开篇 微服务是现代系统中非常受关注的“焦点”&#xff0c;越来越多地分布式系统都纷纷采用微服务的设计理念来演进其架构模型。随着微服务应用规模的增长&#xff0c;治理膨胀的系统会越来越困难。比起微服务设计和拆分&#xff0c;如何能够更好地治理大规模的微服务&#xf…

LeetCode每日一题——813. 最大平均值和的分组

LeetCode每日一题系列 题目&#xff1a;813. 最大平均值和的分组 难度&#xff1a;普通 文章目录LeetCode每日一题系列题目示例思路题解题目 给定数组 nums 和一个整数 k 。我们将给定的数组 nums 分成 最多 k 个相邻的非空子数组 。 分数 由每个子数组内的平均值的总和构成…

制作一个简单HTML西安旅游网页(HTML+CSS)

&#x1f468;‍&#x1f393;学生HTML静态网页基础水平制作&#x1f469;‍&#x1f393;&#xff0c;页面排版干净简洁。使用HTMLCSS页面布局设计,web大学生网页设计作业源码&#xff0c;这是一个不错的旅游网页制作&#xff0c;画面精明&#xff0c;排版整洁&#xff0c;内容…

Java以form-data(表单)的形式调用第三方接口

Java以form-data&#xff08;表单&#xff09;的形式调用第三方接口前言本文目标用到的类工具类及测试信息工具类代码测试信息测试代码测试结果遇到的问题getContentLength()的滥用调用的错误慎用请求输出流flush()方法未写入标识调用错误总结前言 之前写的调用第三方接口&…

【机器学习】推荐系统

推荐系统的工作原理 推荐模型如何进行推荐将取决于您拥有的数据类型。如果您只拥有过去发生的交互数据&#xff0c;您可能有兴趣使用协作过滤。如果您有描述用户及其与之交互过的物品的数据&#xff08;例如&#xff0c;用户的年龄、餐厅的菜系、电影的平均评价&#xff09;&a…

Windows线程 信号量 CreateSemaphore创建信号量、RelaseSemaphore设置信号量

信号量 相关问题 类似于事件&#xff08;作用类似&#xff09;&#xff0c;解决通知的相关问题。但提供一个计数器&#xff0c;可以设置次数。 信号量的使用 1.创建信号量 HANDLE CreateSemaphore( LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,//参数作废&#xff0c;基本用N…

临近期末,这些题不来看看吗?(下)

目录 1、打印1~100之间所有3的倍数&#xff08;三种方法&#xff09; 2、写出3给整数从大到小输出 3、给定两个数&#xff0c;求这两个数的最大公约数 4、 递归实现n的k次方 5、写一个递归函数DigitSum(n),输入一个非负整数&#xff0c;返回组成它的数字之和 6、编写一个…

二叉树模板套题——相同的树的应用

文章目录力扣100. 相同的树递归展开图力扣572. 另一棵树的子树递归展开图力扣101. 对称二叉树递归展开图力扣100. 相同的树 给你两棵二叉树的根节点 p 和 q &#xff0c;编写一个函数来检验这两棵树是否相同。 如果两个树在结构上相同&#xff0c;并且节点具有相同的值&#xf…

四、nginx正向代理

一、正向代理 解释&#xff1a;正向代理指的是客户端通过访问目标服务端&#xff0c;再由目标服务端来转发流量访问互联网 结构图如下&#xff1a; 好处&#xff1a;这样做的好处是&#xff0c;当客户端通过服务端访问互联网某个网站时&#xff0c;该网站获得的IP地址是服务…

STEAM上的一款电路模拟神器 — CRUMB Circuit Simulator

摘要&#xff1a;这几天在逛steam商店时&#xff0c;发现了一款有意思的电路仿真软件CRUMB Circuit Simulator&#xff08;CRUMB电路模拟器&#xff09;&#xff0c;觉得挺有意思的&#xff0c;就下载了玩了一下。 这款模拟电路软件的东西不多&#xff0c;基础的元器件都有&…

关于赚钱这件事,必须做到「金钱场」、「认知场」和「人脉场」三场统一

每周末&#xff0c;我会将我付费星球内的精华文章&#xff0c;在每周六或周日以付费文章的方式在公众号分享给大家&#xff0c;如果你不想加入我的星球&#xff0c;还想看的话&#xff0c;可以在这里付费看。当然&#xff0c;加入星球会更划算&#xff0c;因为星球内内容更多&a…

ArcGIS基础:如何在大量数据里挑选随机样本(创建随机点工具)

【创建随机点】工具位于【采样】工具下&#xff0c;如下所示&#xff1a; 假如我们有一个需求&#xff0c;要在很多数据里随机选择10个数据&#xff0c;就可以使用该工具。 假如我这里有全国的县级数据&#xff0c;我想要在里面随机抽选10个县城。 原始数据如下&#xff1a; …

IPSec 基础介绍

IPSec是IETF&#xff08;Internet Engineering Task Force&#xff09;制定的一组开放的网络安全协议。它并不是一个单独的协议&#xff0c;而是一系列为IP网络提供安全性的协议和服务的集合&#xff0c;包括认证头AH&#xff08;Authentication Header&#xff09;和封装安全载…