从源码中看 React 中 setState 是异步还是同步?

相信很多小伙伴面试的时候,都经历过这么一个很是 “无聊” 的问题?但是,也没办法,就是很多面试官在面试的时候喜欢问。为什么说这个问题很无聊呢?因为我们在实际的开发中,根本就可以不用依赖这个特性,不需要管他是异步执行还是同步执行的。因为在如果你需要获取更新后的值,React 本来就提供了方法给开发者去获取。 Class Component 中,我们可以在生命周期 ComponentDidUpdate 中获取到更新后的值,而对于 Functional Component,我们也可以在 useEffect 的回调中去处理获取更新后state的逻辑操作。所以,setState 是同步还是异步,好像也不会影响我们正常的开发。不过,既然聊到了这个,我们不妨试着从不同的角度来看一下setStateReact 中到底是怎么执行的?

React 中的模式

相信熟悉 React 的小伙伴肯定是知道,React 是有多种模式的。如果你还不知道的话,那… 也可以考虑去 React 官网好好学习学习。

原因: 官网有介绍到 React 中是有以下三种模式的,而至于为什么会有这么多重模式嘛,官网上也有详细的说明。大致的原因就是因为版本的更新迭代,而官方又不想直接放弃对之前版本的维护,所以就会出现多个模式啦。

  • legacy 模式: ReactDOM.render(, rootNode)。这是当前 React app 使用的方式。当前没有计划删除本模式,但是这个模式可能不支持这些新功能。
  • blocking 模式: ReactDOM.createBlockingRoot(rootNode).render()。目前正在实验中。作为迁移到 concurrent 模式的第一个步骤。
  • concurrent 模式: ReactDOM.createRoot(rootNode).render()。目前在实验中,未来稳定之后,打算作为 React 的默认开发模式。这个模式开启了所有的新功能。

不是说 setState 吗?怎么又扯到模式上去了呢?聪明的小伙伴们肯定已经猜到了,因为setState 的表现在不同的模式下,表现是有些差异的。因为在不同的模式下,我们创建的更新会有不同的优先级,并且更新的过程也是可以被打断的。下面我们一起来看看,到底都有哪些差异吧!

Legacy 模式

说明:由于blocking 模式和concurrent 模式都还处于实验中,我也不是很了解,所以就不做过多的解释

我们通过 ReactDOM.render() 方法创建的应用,它的模式就是Legacy,在Legacy 模式下触发的更新,它的状态的更新是异步的。Talk is cheap, show you code~~

# index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'

const rootElement = document.getElementById("root")
ReactDOM.render(<App />, rootElement)
# App.js
import React, { Component } from 'react'
export default class App extends Component {
	state = {
		count: 0
	}
	updateCount = () => {
		console.log(this.state.count)
		this.setState({ num: this.state.num+1 })
		console.log(this.state.count)
	}
	render() {
		return <p onClick={this.updateCount}>{this.state.count}</p>
	}
}

上面的代码相信小伙伴们都是不能再熟悉了。很多小伙伴开发的时候就是基于这种模式的,经常是 setState 之后就想要拿(这种多半是习惯了 Vue 开发的)。但是结果,我相信小伙伴们都是很清楚的了,不管是 setState 之前,还是之后,我们拿到的都是更新之前的数据。而造成这一现象的原因是什么呢?

原因

在React 中有一个性能优化的做法 – batchedUpdates,我们都知道,setState 之后是要触发页面的更新或者重新渲染的,那你要是在很短的时间内连续的触发,React 就要连续的去做更新,这就很像我们做输入框搜索的节流防抖的那种效果,我把你几次 setState合并到一起,一块去做更新,不就可以节省很多不必要的性能浪费吗?

解决方法

可以把 this.setState() 方法的执行放到 setTimeout 中就可以啦。
这时候可能就有小伙伴要问了,为什么放在setTimeout 中,它就能同步执行了呢?那我们就要聊一聊源码了。在 Reactreact-reconciler 中的 ReactFiberWorkLoop.old.js 的文件中,有一个batchedUpdates 的方法。有兴趣的小伙伴也可以去 github 拉以下 React 源码回来研究研究。

export function batchedUpdates<A, R>(fn: A => R, a: A): R {
  // 保存开始的状态
  const prevExecutionContext = executionContext;
  // 在这里它把 executionContext 附加上 BatchedContext 的这个标志
  // 如果带有 BatchedContext 这个标志,React 就会把它当成批处理,去异步更新。
  // 如果我们的setState是异步执行的,当我们执行setState的时候,就不会存在BatchedContext了(executionContext是全局变量)
  executionContext |= BatchedContext;
  try {
    // 这里的fn,其实就是上面那个计数器代码中的updataCount
    return fn(a);
  } finally {
  // 这行完fn 以后,他又把 executionContext 放会到原来的状态
    executionContext = prevExecutionContext;
    if (executionContext === NoContext) {
      // Flush the immediate callbacks that were scheduled during this batch
      resetRenderTimer();
      flushSyncCallbackQueue();
    }
  }
}

每次调度更新的时候,React 都会调用一个函数 scheduleUpdateOnFiber,在scheduleUpdateOnFiber中,有这么一段逻辑。就是说,如果 executionContextNoContext,React就会执行 flushSyncCallbackQueue(),也就是同步的执行更新。

if (lane === SyncLane) {
   if (
     (executionContext & LegacyUnbatchedContext) !== NoContext &&
     (executionContext & (RenderContext | CommitContext)) === NoContext
   ) {
     schedulePendingInteractions(root, lane);
     performSyncWorkOnRoot(root);
   } else {
     ensureRootIsScheduled(root, eventTime);
     schedulePendingInteractions(root, lane);
     // 将同步执行代码
     if (executionContext === NoContext) {
       resetRenderTimer();
       flushSyncCallbackQueue();
     }
   }
 }

其实说到这个,我想小伙伴们就应该清楚了setState 在react 中是怎么执行的了吧。如果还不明白的话,可以静下心来,再认真的阅读一次,相信你也能够很清楚的理解setState的。

补充

最后,做个小小的补充,如果是在 concurrent 模式 下,也就是使用 ReactDOM.unstable_createRoot(rootElement).render(<App />) 创建应用的话,setTimeout 就不会生效了。因为在上面的代码我们可以看到,要走到 (executionContext === NoContext) 的一个大前提就是 (lane === SyncLane) ,也就是说,当前更新的优先级是同步的优先级,而我们通过 ReactDOM.render(<App />, rootElement) 创建的应用所触发的更新都是同步的优先级。不过 concurrent 模式 现在还处在实验阶段,一般很少有人用。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值