react源码层探究setState作用

目录
  • 前言
  • 为什么setState看起来是异步的
  • 从first paint开始
  • 触发组件更新
  • 更新渲染fiber tree
  • 写在最后

前言

在深究 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 试试看

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

接下来我们 debugger setState 看看

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

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

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

从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

 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];
}

这个函数做了这么几件事情:

执行 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

判断传入的 initialState 是否为一个函数,若是,则调用它并重新赋值给 initialState (在我们的demo-1里是『0』)

initialState 挂到 hook.memoizedState 以及 hook.baseState

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),只用传一个参数的原因。

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

触发组件更新

要触发组件更新,自然就是点击这个绑定了事件监听的 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 做了这么几件事

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

进入 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;
}
  • 根据 queue 上的各个参数(reducer、上次计算出的 state)计算出 eagerState,并挂载到当前 update

到此,我们实际上更新完 state了,这个新的 state 挂载到哪儿了呢?在 fiber.memoizedState.queue.pending 上。注意:

  • fiber 即为当前的遍历到的 fiber node;
  • pending 是一个环状链表

此时我们打印进行打印,但这里打印的还是 first paint 里返回出来的 state,也就是 0

更新渲染fiber tree

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

function performSyncWorkOnRoot(root) {
    var exitStatus = renderRootSync(root, lanes);
}

同样的我们先看一眼 fiber tree 更新过程中 与 useState 相关的整个流程图

首先我们走进 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,其中代码很多不一一展示,关键步骤就是:

  • 遍历我们之前挂载到 fiber.memoizedState.queue.pending 上的环状链表,并得到最后的 newState
  • 更新 hookqueue 上的相关属性,也就是将最新的这个 state 记录下来,这样下次更新的时候可以这次为基础再去更新
  • 返回一个数组,形式为 [state, setState],此时这个 state 即为计算后的 newState,其值为 1

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

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

写在最后

上文只是描述了一个最简单的 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

到此这篇关于react源码层探究setState作用的文章就介绍到这了,更多相关react setState内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • React 中 setState 的异步操作案例详解

    目录 前言 React 中的 setState 为什么需要异步操作? 什么时候setState会进行同步操作? 前言 在使用state的时候, 如果我们企图直接修改state中的某一个值之后直接打印(使用)他,就会发现,他其实并没有改变. 就像下面的例子,企图通过点击事件之后就使用修改之后的state的值,但是会发state中的并没有被立即修改,还是原先的值,我们都知道那是因为 setState就相当于是一个异步操作,不能立即被修改. import React, { Component } fr

  • React this.setState方法使用原理分析介绍

    目录 摘要 1.异步的setState 2.多个setState方法 3.手动实现mySetState 摘要 这一篇文章,主要是简单的实现一下this.setState方法,为了实现该方法,就要知道this.setState方法具有什么特点. 首先在React组件中,我们先定义一个state和setState方法: myState = { value: 0 } mySetState = ( changeState ) =>{ this.setState( this.myState ) } 这里可

  • React 中的 setState 是同步还是异步

    setState 是同步还是异步?肯定是异步的呀. 确定么?那看一下这段代码会打印什么: import { Component } from 'react'; class Dong extends Component { constructor() { super(); this.state = { count: 0 } } componentDidMount() { setTimeout(() => { this.setState({ count: 1 }); console.log(this

  • React中setState同步异步场景的使用

    目录 setState同步异步场景 描述 原理 保证内部数据统一 启用并发更新 参考 setState同步异步场景 React通过this.state来访问state,通过this.setState()方法来更新state,当this.setState()方法被调用的时候,React会重新调用render方法来重新渲染UI.相比较于在使用Hooks完成组件下所需要的心智负担,setState就是在使用class完成组件下所需要的心智负担,当然所谓的心智负担也许叫做所必须的基础知识更加合适一些.

  • React 组件中的state和setState()你知道多少

    目录 state的基本使用 setState()修改状态 解决方法: 总结 state的基本使用 状态(state)即数据,是组件内部的私有数据,只能在组件内部使用 state的值是对象,可以通过this.state来获取状态. setState()修改状态 状态是可变的,可以通过this.setState({要修改的数据})来改变状态 注意:跟vue语法不同,不要直接修改state中的值,这时错误的! //正确 this.setState({ count:this.state.count+1

  • 关于React中setState同步或异步问题的理解

    目录 1. setState同步?异步? 2. 表现为异步 1. React 合成事件 2. 生命周期函数 3. 表现为同步 1. 原生事件 2. setTimeout 4. setState的第二个参数 1. setState同步?异步? 在 React 的类式组件中,我们可以使用setState方法更新state状态.但有些时候使用setState之后,得不到最新的数据. 其实 React 中setState本身执行的过程和代码是同步的,只是因为 React 框架本身的性能优化机制而导致的.

  • react源码层探究setState作用

    目录 前言 为什么setState看起来是异步的 从first paint开始 触发组件更新 更新渲染fiber tree 写在最后 前言 在深究 React 的 setState 原理的时候,我们先要考虑一个问题:setState 是异步的吗? 首先以 class component 为例,请看下述代码(demo-0) class App extends React.Component { state = { count: 0 } handleCountClick = () => { this

  • react源码层分析协调与调度

    目录 requestEventTime requestUpdateLane findUpdateLane lanePriority LanePriority createUpdate enqueueUpdate 总结 协调与调度 reconciler流程 同步任务类型执行机制 异步任务类型执行机制 shouldYield performUnitOfWork beginWork completeUnitOfWork scheduler流程 performWorkUntilDeadline 总结 r

  • react源码层深入刨析babel解析jsx实现

    目录 jsx v16.x及以前版本 v17及之后版本 ReactElement React.createElement ReactElement React.Component 总结 经过多年的发展,React已经更新了大版本16.17.18,本系列主要讲的是 version:17.0.2,在讲这个版本之前,我们先看一看在babel的编译下,每个大版本之下会有什么样的变化. jsx <div className='box'> <h1 className='title' style={{'

  • React源码state计算流程和优先级实例解析

    目录 setState执行之后会发生什么 根据组件实例获取其 Fiber 节点 创建update对象 将Update对象关联到Fiber节点的updateQueue属性 发起调度 processUpdateQueue做了什么 变量解释 构造本轮更新的 updateQueue 更新 workInProgress 节点 总结 update对象丢失问题 为什么会丢失 如何解决 state计算的连续性 问题现象 如何解决 setState执行之后会发生什么 setState 执行之后,会执行一个叫 en

  • react源码合成事件深入解析

    目录 引言 导火线 事件委托 合成事件特点 React 事件系统 事件注册 enqueuePutListener() listenTo() trapCapturedEvent 与 trapBubbledEvent 事件存储 事件分发 事件执行 构造合成事件 批处理 引言 温馨提示: 下边是对React合成事件的源码阅读,全文有点长,但是!如果你真的想知道这不为人知的背后内幕,那一定要耐心看下去! 最近在做一个功能,然后不小心踩到了 React 合成事件 的坑,好奇心的驱使,去看了 React 官

  • 详细聊聊React源码中的位运算技巧

    目录 前言 几个常用位运算 按位与(&) 按位或(|) 按位非(-) 标记状态 优先级计算 总结 前言 这两年有不少朋友和我吐槽React源码,比如: 调度器为什么用小顶堆这种数据结构,直接用数组不行? 源码里各种单向链表.环状链表,直接用数组不行? 源码里各种位运算,有必要么? 作为业务依赖的框架,为了提升一点点运行时性能,React从不吝惜将源码写的很复杂. 在涉及状态.标记位.优先级操作的地方大量使用了位运算. 本文会讲解其中比较有代表性的部分.学到之后,当遇到类似场景时露一手,你就是业务

  • React 源码调试方式

    目录 正文 断点调试 搜索定位 Chrome Devtools 调试 sourcemap npm 下载react包 插件注释 调试 React 最初源码 关联 react 源码项目 总结 正文 什么?调试 React 源码还有优雅和不优雅之分? 别着急,我们先来听个故事: 东东是一名前端工程师,主要用 React 技术栈,用了多年之后想深入一下,所以最近开始看 React 源码. 断点调试 他把 react 和 react-dom 包下载了下来,在项目里引入,开发服务跑起来后,打开 Chrome

  • Android Handler源码深入探究

    1.android 消息循环有4个重要的类Handler.Message.Looper.MessageQueue handler 用来发送.处理消息. Message 是消息的载体. MessageQueue 是一个消息队列,既然是队列,就有入队.出队的处理. Looper 创建一个消息循环.不断的从MessageQueue中读取消息.并分发给相应的Handler进行处理. 2.我们都知道main函数是Java程序的入口,android程序也不例外. android App的唯一入口就是Acti

  • Kotlin coroutineContext源码层深入分析

    目录 1.CoroutineContext 2.Element的作用 3.CoroutineContext相关的操作符原理解析 3.1.什么类型的集合 3.2.如何管理 Element combinedContext 4.验证以及总结 1.CoroutineContext 表示一个元素或者是元素集合的接口.它有一个Key(索引)的Element实例集合,每一个Element的实例也是一个CoroutineContext,即集合中每个元素也是集合. 如下图所示,CoroutineContext的常

  • 使用VScode 插件debugger for chrome 调试react源码的方法

    代码调试,是我们前端日常工作中不可或缺的能力了吧! 在面向dom开发的时代,我们开发时直接在chrome里打断点是很方便的. 但是,当我们面向组件开发时(react),浏览器拿到的是我们编译过后的代码,还想在浏览器里打断点几乎是不可能的了. 场景 那怎么办,方法总是比困难多!愚蠢的我想到了console/debugger!!一直在使用,虽然很不方便(打印太多实在太乱!上线还要配置删除掉),但是我竟然使用了很久(这真是一个糟糕的编码习惯吧).直到今天,我想研究一下react源码,需要断点的地方有很

随机推荐