2021-10-31

setState

一.setState产生原因

在React中,数据是自顶向下单向流动的,即从父组件到子组件。这样组件之间的关系变得简单并且可预测。

state和props是React组件中最重要的改建,如果顶层组件初始化props, 那么React会向下遍历整颗组件树,重新尝试渲染所有的子组件。而state只关心每个组件自己内部的状态,这些状态只能在组件内部改变。

当组件内部使用内置方法setState时,该组件就会尝试重新渲染,因为我们改变了内部状态,组件需要更新。

二.setState作用

通过setState来告知React数据已经发生了变化。

三.为什么不能用this.state = {}来修改state?

setState通过队列机制实现state更新, React也正是通过状态队列实现了setState的异步更新,避免频繁的重复更新state

当执行setState时,会将需要更新的state合并后放入状态队列,不会立即更新。

如果直接修改this.state就不会被放入状态队列。

当下次调用setState并对状态队列合并时, 会忽略掉之前修改的state,造成错误。

四.setState延迟更新

setState延迟更新的原因在于—state是异步执行的,只有render发生变化的时候才触发this.setState()。
想要在不更新render的情况下立即执行(同步执行),需要在setState后加上一个function函数。

// 代码
this.setState({
   myState: 'doubi'
   }, function() {
     // stateFunction是需要立即用到
     this.stateFunction()
   })

五.setState为什么是异步的

Rudux作者Dan Abramov的回答

总结:

setState设计为异步, 可以显著的提升性能;

  1. 如果每次调用setState都进行一次更新, 那么意味着render函数会被频繁的调用, 界面重新渲染, 这样效率很低;
  2. react16 引入了 Fiber 架构,Fiber
    中对任务进行了划分和优先级的分类,优先处理优先级比较高的任务。页面的响应就是一个优先级比较高任务,所以如果setState是同步,那么更新一次就要更新一次页面,就会阻塞到页面的响应。最好的办法应该是获取到多个更新,
    之后进行批量更新, 放入到一个队列里, 进行一个合并, 批量更新;
  3. 如果同步更新了state, 但是还没有执行render函数, 那么state和props不能保持同步;
    state和props不能保持一致性, 会在开发中产生很多的问题;

六.为什么有时连续两次setState只有一次生效

setState()会合并

  componentDidMount() {
    this.setState({ index: this.state.index + 1 }, () => {
      console.log(this.state.index);
    })
    this.setState({ index: this.state.index + 1 }, () => {
      console.log(this.state.index);
    })
  }
	结果
	1
	1
    componentDidMount() {
    this.setState((preState) => ({ index: preState.index + 1 }), () => {
      console.log(this.state.index);
    })
    this.setState(preState => ({ index: preState.index + 1 }), () => {
      console.log(this.state.index);
    })
  }
  结果
  2
  2

1.直接传递对象的setstate会被合并成一次
2.使用函数传递state不会被合并

七.流程图

2021-10-31

2021-10-31
2021-10-31

名词解释

partialState:setState传入的第一个参数,对象或函数
_pendingStateQueue:当前组件等待执行更新的state队列
isBatchingUpdates:react用于标识当前是否处于批量更新状态,所有组件公用
dirtyComponent:当前所有处于待更新状态的组件队列
transcation:react的事务机制,在被事务调用的方法外包装n个waper对象,并一次执行:waper.init、被调用方法、waper.close
FLUSH_BATCHED_UPDATES:用于执行更新的waper,发起组件更新。只有一个close方法

执行过程

对照上面流程图的文字说明,大概可分为以下几步:

1.将setState传入的partialState参数存储在当前组件实例的state暂存队列中。
2.判断当前React是否处于批量更新状态,如果是,将当前组件加入待更新的组件队列中。
3.如果未处于批量更新状态,将批量更新状态标识设置为true,用事务再次调用前一步方法,保证当前组件加入到了待更新组件队列中。
4.调用事务的waper方法,遍历待更新组件队列依次执行更新。
5.执行生命周期componentWillReceiveProps。
6.将组件的state暂存队列中的state进行合并,获得最终要更新的state对象,并将队列置为空。
7.执行生命周期componentShouldUpdate,根据返回值判断是否要继续更新。
8.执行生命周期componentWillUpdate。
9.执行真正的更新,render。
10.执行生命周期componentDidUpdate。

八.不是所有的setState 都是异步的

onClick = () => {
    this.setState({ count: this.state.count + 1 })
  console.log(this.state)
  setTimeout(() => {
    this.setState({ count: this.state.count + 1 })
    console.log(this.state)
  }, 0)
}
会打印出0,2

React 中的 setState 并不是严格意义上的异步函数。
他是通过队列的延迟执行实现的。使用 isBatchingUpdates 判断当前的setState 是加入到更新队列还是更新页面。当 isBatchingUpdates=ture 是加入更新队列,否则执行更新。

React 使用 isBatchingUpdates 来判断是否加入更新队列。那么为什么在 setTimeout 事件中 isBatchingUpdates 值为 false ? 原因就是在React中,对HTML的原生事件做了一次封装叫做合成事件。所以在React自己的生命周期和合成事件中,可以控制 isBatchingUdates 的值,可以根据值来判断是否更新页面。而在宿主环境提供的原生事件中(即非合成事件),无法将 isBatchingUpdates 的值置为 false,所以就会立即执行更新。

所以setState 并不是有同步的场景,而是在特殊的场景下不受React 的控制

  1. 钩子函数和合成事件中: 在react的生命周期和合成事件中,react仍然处于他的更新机制中,这时isBranchUpdate为true。

    按照上述过程,这时无论调用多少次setState,都会不会执行更新,而是将要更新的state存入_pendingStateQueue,将要更新的组件存入dirtyComponent。

    当上一次更新机制执行完毕,以生命周期为例,所有组件,即最顶层组件didmount后会将isBranchUpdate设置为false。这时将执行之前累积的setState。

  2. 异步函数和原生事件中
    由执行机制看,setState本身并不是异步的,而是如果在调用setState时,如果react正处于更新过程,当前更新会被暂存,等上一次更新执行后在执行,这个过程给人一种异步的假象。

    在生命周期,根据JS的异步机制,会将异步函数先暂存,等所有同步代码执行完毕后在执行,这时上一次更新过程已经执行完毕,isBranchUpdate被设置为false,根据上面的流程,这时再调用setState即可立即执行更新,拿到更新结果。

两类setState

1、批量更新类:即react内部的执行函数,执行setState的执行逻辑,都是批量更新处理,其中包括:react内部事件(合成事件)和生命周期;

2、非批量更新类:即上面两种情况以外的情况,经常见到的:原生事件、setTimeout、fetch等等;

先说明两个概念:

1、事务:
可以理解为,一个正常的函数外层又被包裹了一层。这层包裹处理,包括一个或多个的函数执行前的处理函数(initialize函数)、一个和多个函数执行后的处理函数(close函数);React很多的逻辑处理,都使用了事务的概念;

2、合成事件和原生事件的关系和区别:

区别:原生事件就是addEventListener写法的事件!而合成事件,就是直接书写react中的onClick、onChange等;

关系:合成事件可以理解为react对原生事件的包裹封装;原生事件相当于上面事务概念中的正常的函数,而经过包装处理形成的事务,就是react中的合成事件。

在组件生命周期或React合成事件中, setState是异步
在setTimeout或者原生dom事件中, setState是同步

九.总结

在合成事件和生命周期中

在react的生命周期和合成事件中,react仍然处于他的更新机制中,这时isBranchUpdate为true。

按照上述过程,这时无论调用多少次setState,都会不会执行更新,而是将要更新的state存入_pendingStateQueue,将要更新的组件存入dirtyComponent。

当上一次更新机制执行完毕,以生命周期为例,所有组件,即最顶层组件didmount后会将isBranchUpdate设置为false。这时将执行之前累积的setState。

在原生事件和异步函数中

由执行机制看,setState本身并不是异步的,而是如果在调用setState时,如果react正处于更新过程,当前更新会被暂存,等上一次更新执行后在执行,这个过程给人一种异步的假象。

在生命周期,根据JS的异步机制,会将异步函数先暂存,等所有同步代码执行完毕后在执行,这时上一次更新过程已经执行完毕,isBranchUpdate被设置为false,根据上面的流程,这时再调用setState即可立即发起更新,拿到更新结果。

partialState合并机制

如果传入的是对象,很明显会被合并成一次:

Object.assign(
  nextState,
  {index: state.index+ 1},
  {index: state.index+ 1}
)
1
2
3
4
5

如果传入的是函数,函数的参数preState是前一次合并后的结果,所以计算结果是准确的

关于callback

我们知道setState可以给第二个参数传递一个函数,用作回调函数。这个回调函数在批量更新下也是会进行收集,收集的时间点和state一样。收集之后会在后续的组件reRender之后进行统一执行。

引用文章
引用文章
引用文章
引用文章

上一篇:CSS3中的弹性流体盒模型技术详解


下一篇:setState(API)的异步和同步问题