探究React中setState的执行机制.md 7.97 KB
Newer Older
fang's avatar
fang committed
1
---
caihaifei's avatar
caihaifei committed
2 3 4 5
title: 探究React中setState的执行机制              
date: 2022/3/31 18:00:00
updated: 2022/3/28 09:42:24
tags:                    
fang's avatar
fang committed
6
- 前端
caihaifei's avatar
caihaifei committed
7 8 9
- JavaScript 
            
author: 方玉新         
fang's avatar
fang committed
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
---

### 相关的一些问题
1. `setState` 是同步还是异步的,为什么有时可以里见到更新的结果而有时又不行?
 
1.1 钩子函数和React合成事件中的 `setState`

假如有两个组件

```javascript
componentDidMount(){
  console.log('这是父组件componentDidMount');
}
render(){
  return (
    <div>
      <SetState1></SetState1>
      <SetState></SetState>
    </div>
  )
}
```
组件内部放入同样的代码,并在`SetState1`中的`componentDidMount`中添加一段同步延时代码,打印延时时间

```javascript
componentWillUpdate(){
  console.log('componentWillUpdate');
}
componentDidUpdate(){
  console.log('componentDidUpdate');
}
componentDidMount(){
  console.log('SetState调用setState');
  this.setState({index:this.state.index +1});
  console.log('state',this.state.index);
  console.log('SetState调用setState');
  this.setState({index:this.state.index+1});
  console.log('state', this.state.index);
}
```
说明:
* 1. 调用 `setState` 不会立即更新
* 2. 所有组件使用的是同一套更新机制,当所有组件`didmount`后,父组件`didmount`,然后执行更新
* 3. 更新时会把每个组件的更新合并,每个组件只会触发一次更新的生命周期。

caihaifei's avatar
caihaifei committed
55 56

<!-- more -->
fang's avatar
fang committed
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
1.2 异步函数和原生事件中的 `setstate`
`setTimeout` 中调用 `setState`(例子和在浏览器原生事件以及接口回调中执行效果相同)
```javascript
componentDidMount(){
  setTimeout(()=>{console.log('调用setState');  
  this.setState({index:this.state.index+1})
  console.log('state',this.state.index);
  console.log('调用setState');
  this.setState({index:this.state.index +1})
  console.log('state',this.state.index);},0);
}
```
根据执行结果可以得出:
* 1. 在父组件 `didmount` 后执行
* 2. 调用 `setState` 同步更新

2. 为什么有时连续两次 `setState` 只有一次生效
分别执行以下代码:
```javascript
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);})
}

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);
  })
}
```
执行结果:
```javascript
1
1
2
2
```
说明:
* 1. 直接传递对象的 `setstate` 会被合并成一次
* 2. 使用函数传递 `state` 不会被合并


### setState执行过程

##### 1. 流程图

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

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

### 总结

##### 1. 钩子函数和合成事件中:
在 react的生命周期和合成事件中, react仍然处于他的更新机制中,这时 isBranchUpdate为true。
按照上述过程,这时无论调用多少次 setState,都会不会执行更新,而是将要更新的 state存入 _pendingStateQueue,将要更新的组件存入 dirtyComponent。
当上一次更新机制执行完毕,以生命周期为例,所有组件,即最顶层组件 didmount后会将 isBranchUpdate设置为false。这时将执行之前累积的 setState。

##### 2. 异步函数和原生事件中
由执行机制看, setState本身并不是异步的,而是如果在调用 setState时,如果 react正处于更新过程,当前更新会被暂存,等上一次更新执行后在执行,这个过程给人一种异步的假象。
在生命周期,根据JS的异步机制,会将异步函数先暂存,等所有同步代码执行完毕后在执行,这时上一次更新过程已经执行完毕, isBranchUpdate被设置为false,根据上面的流程,这时再调用 setState即可立即执行更新,拿到更新结果。

##### 3. partialState合并机制
我们看下流程中 _processPendingState的代码,这个函数是用来合并 state暂存队列的,最后返回一个合并后的 state。
```javascript
_processPendingState : function(props,context)
{
var inst = this._instance;

var queue = this._pendingStateQueue;
var replace = this._pendingReplaceState;

this._pendingReplaceState = false;
this._pendingStateQueue = null;

if(!queue)
{     
return inst.state;
}
if(replace && queue.length === 1)
{
  return queue[0];
}
    
var nextState = _assign({}, replace ? queue[0] : inst.state);    
for(var i = replace ? 1 : 0 ; i < queue.length;i++)
{
  var partial = queue[i];
  _assign(nextState, typeof partial === 'function' ? partial.call(inst,nextState,props,context) : partial);
}
return nextState;
}
```
我们只需要关注下面这段代码:
```javascript
_assign(nextState,typeof partial === 'function' ? partial.call(inst,nextState,props,context) : partial);
```
如果传入的是对象,很明显会被合并成一次:
```javascript
Object.assign(
  nextState,
  {index: state.index+1},
  {index: state.index+1},
)
```
如果传入的是函数,函数的参数`preState`是前一次合并后的结果,所以计算结果是准确的。

##### 4. componentDidMount调用 setstate
> 在componentDidMount()中,你 可以立即调用setState()。它将会触发一次额外的渲染,但是它将在浏览器刷新屏幕之前发生。这保证了在此情况下即使render()将会调用两次,用户也不会看到中间状态。谨慎使用这一模式,因为它常导致性能问题。在大多数情况下,你可以 在constructor()中使用赋值初始状态来代替。然而,有些情况下必须这样,比如像模态框和工具提示框。这时,你需要先测量这些DOM节点,才能渲染依赖尺寸或者位置的某些东西。

以上是官方文档的说明,不推荐直接在 `componentDidMount` 直接调用 `setState`,由上面的分析: `componentDidMount` 本身处于一次更新中,我们又调用了一次 `setState`,就会在未来再进行一次 `render`,造成不必要的性能浪费,大多数情况可以设置初始值来搞定。

当然在 `componentDidMount` 我们可以调用接口,再回调中去修改 `state`,这是正确的做法。

当state初始值依赖dom属性时,在 `componentDidMount``setState`是无法避免的。

##### 5. componentWillUpdate componentDidUpdate
这两个生命周期中不能调用 `setState`
由上面的流程图很容易发现,在它们里面调用 `setState`会造成死循环,导致程序崩溃。

##### 6. 推荐使用方式
在调用 `setState`时使用函数传递 `state`值,在回调函数中获取最新更新后的 `state`