setState

setState

在更新react组件时,我们常常使用setState方法传入一个对象来更新组价的状态。但其实除了状态对象,该方法还允许传入一个状态计算函数。

setState()的参数支持对象和函数两种。

  1. 对象式setState —> this.setState({ showModal: true});
  2. 函数式setState
    1
    2
    3
    4
    5
    this.setState((state, props) => {
    // 当前组件的state和props值作为参数传入,
    // 通过返回一个新的state对象来进行state的更改。
    return newState;
    });

为什么setState要允许传入函数?
我们知道state更新是异步的,多个setState连续调用会被合并成一个批处理更新操作。

1
2
3
4
5
6
7
this.state = {count: 0};
function add() {
this.setState({count: this.state.count+1});
this.setState({count: this.state.count+1});
this.setState({count: this.state.count+1});
}
console.log(this.state.count); //?

上面的代码,处于性能考虑,React不会真的分3次去setState,而是把传入的这3个对象合并成一个,即只会得到一个对象{count: this.state.count+1},对这个对象执行setState操作,导致最终只setState了一次,我们的得到结果是1,而不是3。

函数式setState就是为了解决这个问题滴:
当 React 碰到多次 setState(func)调用时,它会按照调用的顺序把这些函数func放进一个队列。
接下来,React 会依次调用 队列 中的方法,把上一个func中返回的状态传递给当前的方法,从而不断更新state,实现效果上可以认为是一个reduce累加器。

我们改写上面的例子,改用函数式setState将会得到3。

1
2
3
4
5
6
function add() {
this.setState((state) => {count: state.count+1});
this.setState((state) => {count: state.count+1});
this.setState((state) => {count: state.count+1});
}
console.log(this.state.count); //3

从上面的样例,我们知道函数式setState允许传入一个funciton, 这个function返回一个更新状态后的state对象。在设置state之前,这个function都会查询此前一刻的state和props,并返回一个修改后的state对象。但需要注意的是,这里的setState并不会立即改变this.state的值,而是创建一个待执行的变动,在setState((state, props) => newState)后访问this.state,有可能state还未来得及改变,得到一个旧state。可以看下面这个例子。

搞事情

如果混用两种方式,会产生意想不到的效果。举个例子(伪代码)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
this.state = {
count: 0;
}

function add(state, props) {
return {
count: state.count + 1;
};
}

function handle() {
this.setState(add);
this.setState(add);
this.setState({ count: this.state.count + 1});
this.setState(add);
}

handle();

console.log(this.state.count); // ?

你可能期望输出的count是4,但实际输出的是2。
原因是React会依次合并所有setState产生的效果。虽然第2个调用时产生的效果count值为2,但由于第三次调用setState时,采用了对象式setState调用,一下子强行把积攒的state更新效果清空,this.state.count又开始从0算起。因此对象式setState调用会影响到当前队列下此前函数式setState调用积攒的效果,切忌两种方式混用。

一般来讲,按照React组件的生命周期,state的更新会导致新旧虚拟DOM的diff过程,从而导致最终的组件render。我们知道,shouldComponnetUpdate方法返回false时,会强制组件没有render。那这个时候组件的state的值到底改变没有呢,答案是没有。我们可以认为,组件的state只有在render函数重新执行时才会被改变。也就是说setState更新组件state并不是一定的,还需要得到render和shouldComponentUpdate的“承认”。

setState到底是同步的还是异步的

正确的回答是:当前版本下可能同步可能异步。
一般来说,setState在React生命周期以及事件处理函数回调中是异步的,也就是react的批量处理更新。在其它情况下如promise,setTimeout中都是同步执行的。

比如在setTimeout调用setState,会立即render并导致dom更新。

1
2
3
4
5
6
7
8
9
10
11
12
this.state = {
count: 0;
}

function onClick() {
setTimeout(_ => {
this.setState(count: this.state.count + 1) // result: 1
}, 200)
setTimeout(_ => {
this.setState(count: this.state.count + 1) // resut: 2
}, 400)
}

这个问题的实质是React内部根据isBatchingUpdates标记位来判断是直接更新state,还是先加入队列批处理更新。默认值是false,即同步更新。但React在生命钩子函数和事件处理函数中会把这个值修改为true,导致setState不会同步更新state。

既然是当前版本,也就是说未来这种策略可能会改,但是在React16及以下版本依然是这种策略。未来的17版本可能修改为默认所有的更新都采用批处理更新。
更深层次的解释,可以阅读这篇文章或者这一篇

事务transaction(v16已废弃)

将需要执行的函数做一层封装,在执行这个函数之前,先执行initialize和close方法。react15中用这个机制来控制渲染逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
var Transaction = require('./Transaction');

// 事务构造函数,自定义的 Transaction
var MyTransaction = function() {
// 原型中定义的初始化方法
this.reinitializeTransaction();

// do something...
};

Object.assign(MyTransaction.prototype, Transaction.Mixin, {
getTransactionWrappers: function() {
return [{
initialize: function() {
console.log('exec initiallize, before perform');
},
close: function() {
console.log('exec close, after perform');
}
}];
};
});

var transaction = new MyTransaction();
var testMethod = function() {
console.log('test method');
}
transaction.perform(testMethod);

// exec initiallize, before perform
// test method
// exec close, after perform

-------------本文结束 感谢您的阅读-------------