深入浅出 setState 原理篇

简介: setState() 将对组件 state 的更新排入队列,并通知 React 需要使用更新后的 state 重新渲染此组件及其子组件。

前言



想起自己(2021年) 8 月份面试时,被面试官们问了好几个 setState 的问题,现在想想,虽然回答上问题,但是了解得不深刻。我知道 setState 被设计成“异步”是为了性能,但是涉及到源码解读我就歇菜了;我知道如何让它同步,但是遇到真实的代码情况时,却不知道如何下手。说到底,当时是准备了面经把这些概念记下来,而没有真正理解它


在认识 setState 前,我们问几个常见问题


  • setState 是同步还是异步?


  • 如果是异步,怎么让它同步?


  • 为什么要这样设计?


基本概念和使用



React 的理念之一是 UI=f(data),修改 data 即驱动 UI 变化,那么怎么修改呢?React 提供了一个 API ——setState(类组件的修改方法)


官网介绍:


setState() 将对组件 state 的更新排入队列,并通知 React 需要使用更新后的 state 重新渲染此组件及其子组件。这是用于更新用户界面以响应事件处理器和处理服务器数据的主要方式


为了更好的感知性能,React 会延迟调用它,然后通过一次传递更新多个组件。React 并不会保证 state 的变更会立即生效


setState() 并不总是立即更新组件。它会批量推迟更新。这使得在调用 setState() 后立即读取 this.state 成为了隐患。为了消除隐患,请使用 componentDidUpdate 或者 setState 的回调函数(setState(updater, callback)),这两种方式都可以保证在应用更新后触发


除非 shouldComponentUpdate() 返回 false,否则 setState() 将始终执行重新渲染操作。如果可变对象被使用,且无法在 shouldComponentUpdate() 中实现条件渲染,那么仅在新旧状态不一致调用 setState()可以避免不必要的重新渲染


使用方法


setState(updater, [callback])


参数一为带有形式参数的 updater 函数:


(state, props) => stateChange
// 例如
// this.setState((state, props) => {
//   return {counter: state.counter + props.step};
// });


setState 的第一个参数除了接受函数外,还可以接受对象类型:


setState(stateChange[, callback])
// 例如:this.setState({count: 2})


setState 的第二个参数为可选的回调函数,它将在 setState 完成合并重新渲染组件后执行。通常,我们建议使用 componentDidUpdate 来代替此方法


setState(stateChange[, callback])
// 例如: this.setState({count: 2}, () => {console.log(this.state.count)})


与 setState 回调相比,使用 componentDidUpdate 有什么优势?


stackoverflow 有人问过,也有人回答过:


  • 一致的逻辑


  • 批量更新


  • 什么时候 setState 会比较好?


  • 当外部代码需要等待状态更新时,如 Promise


setState 的特性——批处理



如果在同一周期内对多个 setState 进行处理,例如,在同一周期内多次设置商品数据,相当于:


this.setState({count: state.count + 1});
this.setState({count: state.count + 1});
this.setState({count: state.count + 1});
// === 
Object.assign(
  count,
  {quantity: state.quantity + 1},
  {quantity: state.quantity + 1},
  ...
)


后调的 setState 将覆盖同一周期内先调用 setState 的值


  • setState(stateChange[, callback])


  • setState((state, props) => stateChange[, callback])


setState 必引发更新过程,但不一定会引发 render 被执行,因为 shouldCompomentUpdate 可以返回 false


批处理引发的问题



问题1:连续使用 setState,为什么不能实时改变


state.count = 0;
this.setState({count: state.count + 1}); 
this.setState({count: state.count + 1}); 
this.setState({count: state.count + 1}); 
// state.count === 1,不是 3


因为 this.setState 方法为会进行批处理,后调的 setState 会覆盖统一周期内先调用的 setState 的值,如下图所示:


state.count = 0;
this.setState({count: state.count + 2}); 
this.setState({count: state.count + 3}); 
this.setState({count: state.count + 4}); 
// state.count === 4


问题2:为什么要 setState,而不是直接 this.state.xx = oo?


因为 setState 做的事情不仅仅只是修改了 this.state 的值,另外最重要的是它会触发 React 的更新机制,会进行diff,然后将 patch 部分更新到真实 dom 里


如果你直接 this.state.xx = oo 的话,state 的值确实会改,但是它不会驱动 React 重渲染。setState 能帮助我们更新视图,引发 shouldComponentUpdate、render 等一系列函数的调用。至于批处理,React 会将 setState 的效果放入队列中,在事件结束之后产生一次重新渲染,为的就是把 Virtual DOM 和 DOM 树操作降到最小,用于提高性能


当调用 setState 后,React 的 生命周期函数 会依次顺序执行


  • static getDerivedStateFromProps


  • shouldComponentUpdate


  • render


  • getSnapshotBeforeUpdate


  • componentDidUpdate


问题3:那为什么会出现异步的情况呢?(为什么这么设计?)


因为性能优化。假如每次 setState 都要更新数据,更新过程就要走五个生命周期,走完一轮生命周期再拿 render 函数的结果去做 diff 对比和更新真实 DOM,会很耗时间。所以将每次调用都放一起做一次性处理,能降低对 DOM 的操作,提高应用性能


问题4:那如何在表现出异步的函数里可以准确拿到更新后的 state 呢?


通过第二个参数 setState(partialState, callback) 中的 callback 拿到更新后的结果


onHandleClick() {
    this.setState(
        {
            count: this.state.count + 1,
        },
        () => {
            console.log("点击之后的回调", this.state.count); // 最新值
        }
    );
}


或者可以直接给 state 传递函数来表现出同步的情况


this.setState(state => {
 console.log("函数模式", state.count);
 return { count: state.count + 1 };
});


执行原理



首先先了解三种渲染模式


  • legacy 模式:ReactDOM.render(<App />, rootNode) 。这是当前 React app 使用的方式。当前没有计划删除本模式,但是这个模式可能不支持新功能


  • blocking 模式:ReactDOM.createBlockingRoot(rootNode).render(<App />) 。目前正在实验中,作为迁移到 concurrent 模式的第一个步骤


  • concurrent 模式 :ReactDOM.createRoot(rootNode).render(<App />)。目前在实验中,未来稳定之后,打算作为 React 的模式开发模式。这个模式开启了所有的新功能


  • 拥有不同的优先级,更新的过程可以被打断


在 legacy 模式下,在 React 的 setState 函数实现中,会根据一个变量isBatchingUpdates 判断是直接更新 this.state 还是放到队列中回头再说,而 isBatchingUpdates 默认是 false,也就表示 setState 会同步更新 this.state,但是,有一个函数 batchedUpdates,这个函数会把 isBatchingUpdates 修改为 true,而当 React 在调用事件处理函数之前就会调用这个 batchedUpdates,造成的后果,就是由 React 控制的事件处理过程 setState 不会同步更新 this.state


像 addEventListener 绑定的原生事件、setTimeout/setInterval 会走同步,除此之外,也就是 React 控制的事件处理 setState 会异步


而 concurrent 模式都是异步,这也是未来 React 18 的默认模式


总结



首先,我们总结下关键知识点


  • setState 不会立即改变 React 组件中 state 的值


  • setState 通过引发一次组件的更新过程来引发重新绘制


  • 多次 setState 函数调用产生的效果会合并(批处理)


其次,回答一下文章开头的问题(第二第三问题在文中已经回答)


setState 是同步还是异步?


  • 代码同步,渲染看模式


  • legacy模式,非原生事件、setTimeout/setInterval 的情况下为异步;addEventListener 绑定原生事件、setTimeout/setInterval 时会同步


  • concurrent 模式:异步


image.png


参考资料



  • setState:这个API设计到底怎么样


  • setState为什么不会同步更新组件状态


  • setState何时同步更新状态


  • 浅入深出setState(上篇)


  • 浅入深出setState(下篇)


  • 重新认识 React 的 setState


  • 你真的理解setState吗?



  • React 中 setState 是一个宏任务还是微任务?


  • What is the advantage of using componentDidUpdate over the setState callback?


  • 深入学习:何时以及为什么 setState() 会批量执行?


  • 深入:为什么不直接更新 this.state



相关文章
|
存储 缓存 JavaScript
深入浅出 RxJS 核心原理(响应式编程篇)
在最近的项目中,我们面临了一个需求:监听异步数据的更新,并及时通知相关的组件模块进行相应的处理。传统的事件监听和回调函数方式可能无法满足我们的需求,因此决定采用响应式编程的方法来解决这个问题。在实现过程中发现 RxJS 这个响应式编程库,可以很高效、可维护地实现数据的监听和组件通知。
373 0
深入浅出 RxJS 核心原理(响应式编程篇)
|
存储 前端开发 JavaScript
AntV X6源码探究简析
AntV是蚂蚁金服全新一代数据可视化解决方案,其中X6主要用于解决图编辑领域相关的解决方案,其是一款图编辑引擎,内置了一下编辑器所需的功能及组件等,本文旨在通过简要分析x6源码来对图编辑领域的一些底层引擎进行一个大致了解,同时也为团队中需要进行基于X6编辑引擎进行构建的图编辑器提供一些侧面了解,在碰到问题时可以较快的找到问题点。
400 0
|
4月前
|
存储 缓存 异构计算
PixiJS源码分析系列:第二章 渲染在哪里开始?
PixiJS源码分析系列:第二章 渲染在哪里开始?
|
存储 前端开发 JavaScript
新手也可以读懂的 React18 源码分析
打造全网最简单,新手也可以看懂的 React 18 源码分析系列。共同学习 React 设计思想,提升编码能力,轻松应对前端面试
283 0
新手也可以读懂的 React18 源码分析
|
JavaScript 前端开发 中间件
Redux 原理探秘
Redux 是一个非常不错的状态管理库,和 Vuex 不同的是 Redux 并不和 React 强绑定,你甚至可以在 Vue 中使用 Redux。当初的目标是创建一个状态管理库,来提供最简化 API。
136 0
|
设计模式 JavaScript 前端开发
✨从响应式讲起,Observable:穿个马甲你就不认识啦?(附实战)
✨从响应式讲起,Observable:穿个马甲你就不认识啦?(附实战)
|
前端开发 JavaScript UED
搞懂Vue3中的异步组件,看这篇就够了
当我们的项目达到一定的规模时,对于某些组件来说,我们并不希望一开始全部加载,而是需要的时候进行加载;这样的做得目的可以很好的提高用户体验
1477 0
搞懂Vue3中的异步组件,看这篇就够了
|
前端开发
react实战笔记102:setstate得执行流程1
react实战笔记102:setstate得执行流程1
112 0
react实战笔记102:setstate得执行流程1
|
前端开发
react实战笔记102:setstate得执行流程2
react实战笔记102:setstate得执行流程2
91 0
react实战笔记102:setstate得执行流程2