React系列九(深入理解setState)

快来加入我们吧! "小和山的菜鸟们",为前端开发者提供技术相关资讯以及系列基础文章。为更好的用户体验,请您移至我们官网小和山的菜鸟们 ( https://xhs-rookies.com/ ) 进行学习,及时获取最新文章。
"Code tailor" ,如果您对我们文章感兴趣、或是想提一些建议,微信关注 “小和山的菜鸟们” 公众号,与我们取的联系,您也可以在微信上观看我们的文章。每一个建议或是赞同都是对我们极大的鼓励!
前言 这节我们将介绍 ReactsetState ,希望可以帮助大家真正理解 setState
本文会向你介绍以下内容:

  • 如何使用 setState
  • 不能直接修改 State
  • setState()
  • setState 可能是异步更新
  • setState 的合并
如何使用 setState 在介绍 setState 之前,我们先来看一个 setState 的案例,了解一下是如何使用的。
我们来展示一个使用案例,当点击一个 改变文本 的按钮时,修改界面前显示的内容:
React系列九(深入理解setState)
文章图片

案例代码
import React, { Component } from 'react'export default class App extends Component { constructor(props) { super(props)this.state = { message: 'Hello React', } }render() { return ({this.state.message} ) }changeMessage() { this.setState({ message: 'Hello xhs,your message is changed.', }) } }

state 的初始化是在类构造器中去设置的,然后如果想要更改 state ,那就是通过 setState 函数,该函数最主要的就是传入一个对象作为参数,而该对象就是你想要修改的值。上述代码中,当你点击 ChangeMessage 时,就会调用 setState 函数,而 setState 会调用 render 函数,页面就会被重新渲染。
点击按钮之后,重新渲染的效果:
React系列九(深入理解setState)
文章图片

不能直接修改 State 将上面的 changeMessage 方法,改成下面的样子。
changeMessage() { this.state.message = "Hello xhs,your message is changed."; }

点击 ChangeMessage 之后,页面并没有改变,但是打印 state 你会发现,state 中的 message 变了,但是页面不会被重新渲染。
构造函数,是我们唯一可以给 this.state 赋值的地方,而为了可以重新渲染页面,那就只能通过 setState 函数来修改。
所以,我们通过调用 setState 来修改数据:
  • 当我们调用 setState 时,会重新执行 render 函数,根据最新的 State 来创建 ReactElement 对象
  • 再根据最新的 ReactElement 对象,对 DOM 进行修改;
changeMessage() { this.setState({ message: "Hello xhs,your message is changed." }) }

setState()
setState(updater, [callback])

setState() 将对组件 state 的更改排入队列,并通知 React 需要使用更新后的 state 重新渲染此组件及其子组件。这是用于更新用户界面以响应事件处理器和处理服务器数据的主要方式
setState() 视为 请求 而不是立即更新组件的命令。为了更好的感知性能,React 会延迟调用它,然后通过一次传递更新多个组件。React 并不会保证 state 的变更会立即生效。
setState() 并不总是立即更新组件。它会批量推迟更新。这使得在调用 setState() 后立即读取 this.state 成为了隐患。为了消除隐患,请使用 componentDidUpdate 或者 setState 的回调函数(setState(updater, callback)),这两种方式都可以保证在应用更新后触发。
参数一可以有两种形式
1. 接受对象类型
setState(stateChange[, callback])

stateChange 会将传入的对象浅层合并到新的 state 中,例如 让 count +1
this.setState({ count: this.state.count + 1 })

这种形式的 setState() 也是异步的,并且在同一周期内会对多个 setState 进行批处理。例如,如果在同一周期内多次 count + 1,则相当于:
Object.assign( previousState, // 之前的状态 {count: state.count + 1}, {count: state.count + 1}, ... )

后调用的 setState() 将覆盖同一周期内先调用 setState 的值,因此商品数仅增加一次。如果后续状态取决于当前状态,我们建议使用 updater 函数 的形式代替:
this.setState((state) => { return { count: state.count + 1 } })

2. 接受函数参数
【React系列九(深入理解setState)】我们在初始的 state 中并没有一个 count 。但是现在我们有一个需求:那就是添加一个 countstate 中,使用对象作为第一参数,你就会发现这样一个问题。
changeCount () { this.setState({ count: 0 }) // => this.state.count 是 undefined this.setState({ count: this.state.count + 1 }) // => undefined + 1 = NaN this.setState({ count: this.state.count + 2 }) // => NaN + 2 = NaN }

上面的代码的运行结果并不能达到我们的预期,我们希望 count 运行结果是 3 ,可是最后得到的是 NaN。但是这种后续操作依赖前一个 setState 的结果的情况并不罕见。
这里就自然地引出了 setState 的第二种使用方式,可以接受一个函数作为参数。React.js 会把上一个 setState 的结果传入这个函数,你就可以使用该结果进行运算、操作,然后返回一个对象作为更新 state 的对象:
changeCount () { this.setState((prevState) => { return { count: 0 } }) this.setState((prevState) => { return { count: prevState.count + 1 } // 上一个 setState 的返回是 count 为 0,这里需要执行 +1 所以当前返回 1 }) this.setState((prevState) => { return { count: prevState.count + 2 } // 上一个 setState 的返回是 count 为 1,这里需要执行 +2 所以当前返回 3 }) // 最后的结果是 this.state.count 为 3 }

这样就可以达到上述的利用上一次 setState 结果进行运算的效果。
参数二为回调函数
setState() 的第二个参数为可选的回掉函数,它将在 setState 完成合并并重新渲染组件后执行。通常,我们建议使用 componentDidUpdate() 来代替此方式。
我们来给案例中的 changeMessage 函数添加两个打印方式。
changeMessage() { this.setState({ message: "Hello xhs,your message is changed." },() => { console.log('callback result: ',this.state.message); }) console.log('no callback result:',this.state.message); }

我们来看看结果
React系列九(深入理解setState)
文章图片

从图片中我们可以看出
  • setState 外部的时候,并不可以立马拿到我们想要的结果
  • callback 中则返回的是修改之后的结果。
正因为不是立马拿到我们想要的结果,所以这就是我们接下来要讲的 setState 可能是异步更新。
这样我们就可以知道 setState 的第二参数的作用,就是可以确保得到 state 已经修改之后的结果。也就是重新渲染之后执行的内容。
setState 可能是异步更新 我们来看下面的代码:
  • 最终打印结果是 Hello React
  • 可见 setState 是异步的操作,我们并不能在执行完 setState 之后立马拿到最新的 state 的结果
changeMessage() { this.setState({ message: "Hello xhs,your message is changed." }) console.log(this.state.message); // Hello React }

为什么 setState 设计为异步呢?
  • setState 设计为异步其实之前在 GitHub 上也有很多的讨论;
  • React 核心成员(Redux 的作者)Dan Abramov 也有对应的回复,可以参考一下;
我们来对他的回答做一个简单的总结:
  • setState 设计为异步,可以显著的提升性能;
  • 如果每次调用 setState 都进行一次更新,那么意味着 render 函数会被频繁调用,界面重新渲染,这样效率是很低的;
注意: 最好的办法应该是获取到多个更新,之后进行批量更新;
  • 如果同步更新了 state ,但是还没有执行 render 函数,那么 stateprops 不能保持同步;
注意: stateprops 不能保持一致性,会在开发中产生很多的问题;
获取到更新后的值
  • setState 的第二参数,一个回调函数,这个回调函数会在更新后会执行;
changeMessage() { this.setState({ message: "Hello xhs,your message is changed." }, () => { console.log(this.state.message); // Hello xhs,your message is changed. }); }

当然,我们也可以在生命周期函数:
componentDidUpdate(prevProps, provState, snapshot) { console.log(this.state.message); }

setState 一定是异步?
疑惑:setState一定是异步更新的吗?
验证一:在 setTimeout 中的更新:
changeText() { setTimeout(() => { this.setState({ message: "你好啊,小和山" }); console.log(this.state.message); // 你好啊,小和山 }, 0); }

验证二:原生 DOM 事件:
componentDidMount() { const btnEl = document.getElementById("btn"); btnEl.addEventListener('click', () => { this.setState({ message: "你好啊,小和山" }); console.log(this.state.message); // 你好啊,小和山 }) }

其实分成两种情况:
  • 在组件生命周期或 React 合成事件中,setState 是异步;
  • setTimeout 或者原生 dom 事件中,setState 是同步;
setState 的合并 数据的合并
当你调用 setState() 的时候,React 会把你提供的对象合并到当前的 state
state 定义一些数据
this.state = { name: 'xhs', message: 'Hello React', count: 0, }

通过 setState 去修改 state 中的 message ,是不会对 name 产生影响的
changeMessage() { this.setState({ message: "Hello xhs,your message is changed." }); }

多个 setState 会被合并
比如我们还是有一个 count 属性,默认为 0,记录当前的数字:
  • 执行下面的操作之后,最后的答案是 1
  • 多个 state 进行了合并
increment() { this.setState({ count: this.state.count + 1 }); this.setState({ count: this.state.count + 1 }); this.setState({ count: this.state.count + 1 }); }

如何可以做到,让 count 最终变成 3 呢?
increment() { this.setState((state, props) => { return { count: state.count + 1 } })this.setState((state, props) => { return { count: state.count + 1 } })this.setState((state, props) => { return { count: state.count + 1 } }) }

上面的代码就是用到了,setState 接收一个函数作为第一参数的情况,来解决这个问题,就可以很好的得到我们期待的结果。
下节预告 本节我们学习了 ReactSetState 其中的奥秘,在下一个章节我们将继续学习 React 中受控组件和非受控组件的内容,敬请期待!

    推荐阅读