:::info setState更新状态的使用总结:

  • setState方法并不是组件上的方法,而是React Component组件原型上的方法,之所以我们的组件可以直接使用,是因为继承的Component组件上面的方法。
  • setState是异步更新的。我们并不能在执行完setState之后立马拿到最新的state的结果,因为setState的更新是异步的。
    • 为什么设计为异步的?
      • 可以显著的提高性能。我们在开发中可能会多次调用setState,每次都调用setState进行更新的话,意味着render函数就会被频繁的调用。界面重新渲染,这样,效率是非常低的。
      • 最好的方法是获得多个更新,之后进行批量更新。(将多个更新放入到一个队列中,之后进行批量的更新操作。)
    • 如果同步更新了state,但是还没有执行render函数,那么state和props不能保持同步。
      • state和props不能保持同步的话,在开发中会遇到各种各样的问题。

获取异步更新后的数据的方法:

  • 方法1 向setState函数中传入第二个参数,第二个参数是一个回调函数,我们在回调函数中可以直接获取到最新的state状态。
  • 方法2 当使用setState方法更新状态后,react会重新调用render函数,render函数调用完毕后,react会主动回调组件更新的生命周期钩子 componentDidupdate(),在这个生命周期中,就可以获取最新的state状态。react会先执行生命周期函数,再执行setState中的回调函数。


    setState同步更新的场景:

  • 方案1 将setState更新状态的操作放在定时器中,就表现为同步的操作。

  • 方案2 在原生dom事件中,setState更新状态的操作,就表现为同步的操作。

总结:主要分为两种情况

  • 在组件生命周期或react的合成事件中,setState更新状态表现为异步。
  • 在定时器或者原生dom事件中,setState更新状态表现为同步。

所以,根据不同的上下文、不同的优先级返回不同的结果,有同步也有异步的,所以有时变现为同步的,有时表现为异步的。

setState中数据的合并:

  • 数据合并的api 对象的浅拷贝
    • Object.assign({}, { 组件原始的状态 },{ 组件更新的状态 })。这样的话,我们最新修改的状态就会将以前的状态进行覆盖,但是不会影响没有修改的状态。就是对象的合并操作。 :::

1、setState()的具体使用

  • react更新数据的setState()的原理以及使用?
    • 原理:在事件监听函数里面,当我们需要更新当前组件的状态时,我们就会调用setState()函数,setState()函数就会帮助我们修改组件自身的状态,render函数就会重新渲染。组件就会更新。
    • 底层:setState()方法由父类Component所提供,当我们调用这个函数的时候,react会自动更新组件的状态state,并且重新调用render方法,然后再把render方法所渲染的最新内容显示到页面上。
    • 注意点:当我们要改变组件状态的时候。不能直接使用this.state = “xxx”这种方式来改变。如果这样做react框架内部就没办法知道我们修改了组件的状态,它也就没有办法来更新页面,所以,更新组件的状态(state),一定要使用react框架提供的setState方法,它接受一个对象或者函数作为参数。

1.1 setState方法接收对象作为参数

当传入对象的时候,这个对象表示该组件的新状态,但是只需要传入需要更新的部分就可以了,而不需要传入整个对象。例如现在组件有两个状态name、isLiked:

  1. constructor(props) {
  2. super(props)
  3. this.state = {
  4. name: "Tomy",
  5. isLiked: false
  6. }
  7. }
  8. // 修改组件状态的事件 只需要传入我们需要修改的最新的状态以及最新的状态值
  9. handleClickOnLikeButton() {
  10. this.setState({
  11. isLiked: !this.state.isLiked
  12. })
  13. }

因为点击的时候我们并不需要修改name状态,所以只需要传入isLiked就行了,Tomy还是那个Tomy,而isLiked已经不是那个isLiked了。在react的内部会对组件的新旧状态进行相应合并,在实际开发中,我们只需要更新某一个状态即可。不需要担心组件其他的状态会受到影响。

1.2 setState方法接受函数作为参数

  • 需要注意的是,当我们调用setState的时候,React并不会马上修改state。而是把这个对象放到一个更新队列里面,稍后才会从队列中把新的状态提取出来合并到state当中,然后再触发组件更新。这里需要注意,下面的代码:
    1. constructor(props) {
    2. super(props)
    3. this.state = {
    4. isLiked: true
    5. }
    6. }
    7. // 处理事件的函数
    8. handleClickOnLikeButton() {
    9. // 原始状态
    10. console.log(this.state.isLiked) // 结果 true
    11. // 修改状态
    12. this.setState({
    13. isLiked: !this.state.isLiked
    14. })
    15. // 最后的状态
    16. console.log(this.state.isLiked) // 结果 true
    17. }
    上面代码中,两次打印的结果都是true,即使我们中间已经setState过一次了。这并不是什么bug,只是react的setState把我们传进去的状态缓存起来,稍后才会帮我们更新到state上去,所以我们获取的还是原来的isLiked的值。这说明setState更新状态是一个异步的操作。
    所以,如果想要在setState之后使用新的state来作后续运算就做不到了,例如:
    1. // 构造器函数
    2. constructor(props) {
    3. super(props)
    4. this.state = {
    5. count: 0
    6. }
    7. }
    8. // 事件处理函数
    9. handleClickOnLikeButton() {
    10. // 第一次更新状态
    11. this.setState({
    12. count: 0 // 初始化的值
    13. })
    14. // 第二次更新状态
    15. this.setState({ // this.state.count还是undefined
    16. count: this.state.count + 1 // undefined + 1 === NaN 这里的=表示的是等于的意思 不是赋值操作
    17. })
    18. // 第三次更新状态
    19. this.setState({
    20. count: this.state.count + 2 // NaN + 2 === NaN 这里=表示等于的意思 不是赋值
    21. })
    22. }
    上面代码的运行结果并不能打到我们的预期,我们希望count的结果是3,可是最后得到的却是NaN。这种后续操作依赖前一个setState的结果的情况并不罕见。
    在这里就自然地引出了setState的第二种使用方式,可以接受一个函数作为参数。react.js会把上一次setState的结果传入这个函数,我们就可以使用这个函数进行运算、操作,然后返回一个对象更新state的对象:
    具体的写法:
    1. // 事件处理的函数
    2. handleClickOnLikeButton() {
    3. // 第一次更新状态 第一次的默认参数是整个state状态数据(react内部帮我们做的)
    4. this.setState((prevState) => {
    5. // 返回新的对象
    6. return { count: 0 }
    7. })
    8. // 第二次更新状态 这里的参数是上一个修改状态的函数的返回值
    9. this.setState((prevState) => { // 这里的状态已经修改
    10. return { count: prevState.count + 1 }
    11. })
    12. // 第三次修改状态
    13. this.setState((prevState) => { // 这里的状态也已经修改
    14. return { count: prevState.count + 2 }
    15. })
    16. // 最后的结果是 this.state.count 为 3
    17. }
    这样的话,可以达到上述的利用上一次setState的运算结果作为下一次运算的条件进行下面的计算,并将本次运行的结果的返回值作为下一次运算的参数,依次类推。

    1.3 setState()对参数的合并处理

  • setState参数的合并
    • 上面我们进行了3次setState,但是实际上组件只会渲染1次,而不是三次;这是因为在react的内部会把javascript事件循环中的事件放在消息队列中,消息队列中的同一消息中的setState都会进行合并后再重新渲染组件。
    • 深层的原理不需要纠结,在使用react的时候,并不需要担心多次进行setState会带来性能问题。

2、setState的使用原理

setState方法的来源:setState方法在我们的组件中可以直接进行使用,但是在组件内却没有进行声明或者定义,为什么可以直接使用呢?是因为我们定义组件的时候继承了Component父组件,setState方法是在父组件中定义的,我们直接继承过来的,所以可以直接使用。

2.1 setState是异步更新的

  1. // 在组件内使用 setState是进行异步更新的
  2. export default class App extends Component {
  3. constructor() {
  4. props()
  5. this.state = {
  6. counter: 0
  7. }
  8. }
  9. // 修改数据
  10. increment() {
  11. this.setState({
  12. counter: this.state.counter + 1
  13. })
  14. // 异步的 先执行同步的代码 结果为0 但是界面的数据可以进行修改
  15. console.log(this.state.counter)
  16. }
  17. }

setState()方法设计为异步的原因:

  1. setState设计为异步,可以显著的提高性能:
    • 如果每次调用setState都进行一次更新,那么意味着render函数会频繁被调用,界面重新渲染,这样效率是很低的;
    • 最好的办法应该是获取到多个更新,之后进行批量更新。
  2. 如果同步更新了state,但是还没有执行render函数,那么state和props不能保持同步。
    • state和props不能保持一致性,会在开发中产生很多的问题。

2.2 如何获取异步的结果

2.2.1 方式1: 利用回调函数来获取异步更新后的数据

原理:setState(更新的state,回调函数) 回调函数等到更新结束后执行,内部进行回调

  1. // 事件函数
  2. change() {
  3. this.setState({
  4. counter: this.state.counter + 1
  5. }, () => {
  6. console.log(this.state.counter) // 这里的数据是正确的 数据修改之后的回调函数
  7. })
  8. }

2.2.2 方式2: 利用组件的生命周期函数componentDidUpdate()函数获取异步更新后的数据

原理:在组件的数据发生更新后,组件的生命周期函数componentDidUpdate会进行相应的回调

  1. // 触发事件
  2. handleChange() {
  3. this.setState({
  4. counter: this.state.counter + 1
  5. })
  6. }
  7. // 在生命周期函数中进行获取更新后的状态数据
  8. componentDidUpdate() {
  9. // 输出结果 这里的结果也是正确的
  10. console.log(this.state.counter)
  11. }

上面两种方法执行的先后顺序是: 先执行组件的生命周期函数,再执行更新数据的回调函数

2.3 setState()方法实现同步更新

2.3.1 情况1 将setState函数放入定时器中:

  1. // 将修改的事件放入定时器中执行
  2. handleBtnClick() {
  3. // 定时器 同步的效果
  4. setTimeout(() => {
  5. this.setState({
  6. counter: this.state.counter + 1
  7. })
  8. console.log(this.state.counter) // 同步的效果
  9. }, 0)
  10. }

2.3.2 情况2 不使用react的合成事件,使用原生DOM事件进行监听:

  1. // 使用原生的DOM监听事件 来修改状态
  2. // JSX support
  3. <button id="btn">测试按钮</button>
  4. // 组件挂载完毕
  5. componentDidMount() {
  6. document.getElementById("btn").addEventListener("click", () => {
  7. // 修改组件的状态
  8. this.serState({
  9. counter: this.state.counter + 1
  10. })
  11. console.log(this.state.counter) // 这里是同步的 数据更新后的结果
  12. })
  13. }

知识点:

  • 一般在react的合成事件和组件的生命周期函数中,setState状态数据的更新是异步的。
  • 在定时器和原生的DOM事件中,setState状态数据的更新是同步的。

2.4 setState数据的合并

原理:使用ES6数据的合并 Object.assign({}, this.state, { 新传入的对象 } )

  1. // setState数据的合并
  2. export default class App extends Component {
  3. constructor(props) {
  4. super(props)
  5. // 定义组件的状态
  6. this.state = {
  7. name: "coderweiwei",
  8. age: 123456
  9. }
  10. }
  11. render() {
  12. return (
  13. <div>
  14. <button onClick={ () => this.btnClick() }>修改name<button>
  15. </div>
  16. )
  17. }
  18. // 事件的回调
  19. btnClick() {
  20. // 修改组件的状态
  21. this.setState({
  22. name: "修改后的数据"
  23. })
  24. }
  25. }

在上述的案例中,当我们点击按钮修改的是组件name的属性值,但是我们的setState传入的却是一个对象,会不会覆盖组件原有的状态呢?答案是不会。
在React中是这样处理的,使用Object.assign({}, this.state, { setState传入的对象 }),在这里进行了数据的合并,并且放入一个新的对象里面,当我们修改的数据在后面,会覆盖组件原始的状态state中的属性,并修改属性的值。即我们传入的值已经是最新的值了。 :::warning setState的原理总结:

  • setState只在合成事件和钩子函数中是“异步”的,在原生事件和setTimeout中是同步的。

  • setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形成所谓的“异步”,当然可以通过第二个参数setState(partialState,callback)中的callback拿到更新后的结果。

  • setState的批量更新优化建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout中不会批量更新,在“异步”中如果对同一个值进行多次setState,setState的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时setState多个不同的值,在更新时会对其进行合并批量更新。 :::

2.5 diff算法的详解

:::info diff算法的详细解析:

  • react的更新流程:react在props或者state发生改变时,会调用react的render方法。会创建一颗不同的树。

react需要基于这两颗不同的树之间的差别来判断如何有效的更新UI:

  • 如果一棵树参考另外一棵树进行完全比较更新,那么即使是最先进的算法。该算法的复杂度为o(n3)的数量级,其中n是树中元素的数量。
  • 如果在react中使用了该算法,那么展示1000个元素所需要执行的计算量将在十亿的量级范围。
  • 这个开销太过昂贵,react的更新效率会变得非常低效。

于是,rect对这个算法进行了优化,将其优化成了O(n),如何优化的呢?

  • 同层次节点之间相互比较,不会跨节点进行比较;
  • 不同类型的节点,产生不用的树结构;
  • 开发中,可以通过key来指定那些节点在不同渲染下保持稳定。

情况一:对比不同类型的元素

情况二:对比同一类型的元素

  • 当比对两个相同类型的react元素时,react会保留dom节点,仅对比及更新有改变的属性。
  • 比如下面代码的更改:
    • 通过比对这两个元素,react知道只需要修改DOM元素上的className属性:
    • 比如下面代码的更改:
      • 当更新style属性时,react仅更新有所变更的属性。
      • 通过比对这两个元素,react知道只需要DOM元素上的color样式,无需修稿fontWeight。
  • 如果是同类型的组件元素
    • 组件会保持不变,react会更新该组件的props,并且会调用componentWillReceiveProps()和componentWillUpdate()方法。
    • 下一步,调用render()方法,diff算法将在之前的结果以及新的结果中进行递归。

情况三:对子节点进行递归

  • 在默认条件下,当递归dom元素的子节点时,reac会遍历两个子元素的列表;当产生差异时,生成一个mutation。
    • 我们来看一下在最后插入一条数据的情况
      • 前面的两个比较是完全相同的,所以不会产生mutation
      • 最后一个比较,产生一个mutation,将其插入到新的dom树中即可。
    • 但是当我们是在中间插入一条数据:
      • react将会对每一个子元素产生一个mutation,而不是保持元素的位置不变。
      • 这种低效的比较方式会带来一定的性能问题。 :::

2.6 遍历列表的时候加key值的作用

:::info 在遍历列表的时候,总会有一个警告,让我们加入一个key属性:

  • 方式1: 在最后的位置插入数据

    • 这种情况,有无key意义不大
  • 方式2: 在前面插入数据

    • 这种做法,在没有key的情况下,所有的li都需要进行修改
  • 当子元素(这里的li元素)拥有key值的时,react使用key来匹配原有树上的子元素以及最新树上的子元素

    • 在下面这种场景下,key为111和222的元素仅仅进行位移,不需要进行任何的修改。
    • 将key为333的元素插入到最前面的位置即可。
  • key值的注意事项

    • key值应该是唯一的
    • key值不要使用随机数(随机数载下一次的render时,会重新生成一个数字)
    • 使用index作为key,对性能是没有优化的。 :::

2.7 react的渲染机制

:::info 在react中,数据是单向流动的,当我们使用setState来更新组件的状态,我们的类组件将会重新调用我们的render方法,调用render方法,会重新构建dom树,dom树会进行diff算法的比较,确保获取最小的更新。经过这样的流程,我们的组件就会重新渲染、更新了。 :::

3、性能优化之shouldComponentUpdate生命周期

在我们对组件的状态进行更新的时候,使用更新状态的方法this.setState({}),当使用这个方式更新状态的时候,组件本身会自动调用render方法,当render方法执行的时候,那么意味着组件本身及其子组件也会随之更新,但是当我们只是修改了组件本身的数据,我们的子组件全部都要重新进行渲染,这是非常消耗性能的一件事。因此,可以考虑是否我们自己控制我们组件本身是否进行渲染,更改了数据是否需要更新视图层,如果需要,则重新渲染,如果不需要则不进行重新渲染。我们引出shouldComponentUpdate函数

  1. # 在组件的生命周期函数中进行声明 默认返回true 就是更新重新调用render函数 如果返回false 则不需要调用render函数
  2. shouldComponentUpdate(nextProps, nextState) {
  3. // nextProps 传入最新的props 可以与上一次的props进行比较
  4. // nextState 传入最新的state 也可以与上一次的state进行比较 如果与上一次的值不相同,则需要进行重新渲染,如果值相同,则不需要进从重新渲染
  5. if (this.state.counter !== nextState.counter) {
  6. return true
  7. }
  8. // 默认返回false
  9. return false
  10. }

4、PureComponent纯组件的使用

在上述的案例中,我们可以看出如果组件每一个状态改变的时候我们都进行相应的判断,那么这个代码量是非常庞大,而且是臃肿的。所以,react给了我们一种新的解决方案, 纯组件PureComponent, 该组件在我们修改状态的时候会自动进行相应的判断,判断我们修改的状态与原始的状态是否相等, 对里面的对象会进行浅层比较,如果相等, 不会调用render函数, 如果不相等, 则会调用render函数, 重新更新状态, 更新界面。

  1. // 在写类组件的时候 只需要让我们自己的组件 继承PureComponent组件即可
  2. import React, { PureComponet } from 'react'
  3. class App extends PureComponent {
  4. render() {}
  5. }
  6. // 原理:
  7. shouldComponentUpdate函数、PureComponentMemo函数在函数定义的内部都进行一次浅层比较shallowEqual(),只进行浅层的比较,判断上一次的props与与下一次的props,上一次的state与下一次的state是否相等,如果不相等,返回true,否则返回false

5、memo在函数式组件上的使用

上述的PureComponent只能在类组件中使用, 不能使用在函数式组件上面, 而函数式组件的解决方案就是使用memo高阶组件(实际上是一个函数)来代替类组件的PureComponent。

  1. // 引入memo高阶组件
  2. import React, { memo } from "react"
  3. // 方式1 普通函数 直接向memo高阶组件中传入一个组件函数即可 memo函数内部会进行处理
  4. const MemeApp = memo(functiuon App() {
  5. return (<div>测试组件</div>)
  6. })
  7. // 传入箭头函数
  8. const MemoApp = memo(() => {
  9. return (<div>测试组件</div>)
  10. })

6、react的更新机制

react的渲染流程

08 setState更新数据的原理 - 图2

react的更新流程:

08 setState更新数据的原理 - 图3