setState可以說是React中使用頻率最高的一個函數了,我們都知道,React是通過管理狀態來實現對組件的管理的,當this.setState()被調用的時候,React會重新調用render方法來重新渲染UI
但實際使用的時候,我們會發現,有時候我們setState之后,並沒有立刻生效,例如我們看一下以下的示例代碼
class Test extends Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
this.setState({count: this.state.count + 1});
console.log(this.state.count); // 輸出0
this.setState({count: this.state.count + 1});
console.log(this.state.count); // 輸出0
setTimeout(() => {
this.setState({count: this.state.count + 1});
console.log(this.state.count); // 輸出2
this.setState({count: this.state.count + 1});
console.log(this.state.count); // 輸出3
}, 0);
}
render() {
return <div> test </div>;
}
}
開發過程中我們會發現,在componentDidMount方法中,我們調用setState之后state的值並沒有立即改變,但如果我們在setTimeOut里面調用,我們卻能立刻就能獲得更新,原因就在於react中的使用了基於事務(傳送門,關於事務原理的解析)的異步更新機制,但對於這個異步的理解,又跟ajax的異步有所不同,因為畢竟react是一個js框架,所有的操作都是單線程的,所有的操作,都得按順序來,那么它具體是怎么實現的呢?
我們都知道,對於dom的操作對性能的損耗是非常嚴重的,所以react為了提高整體的渲染性能,會將一次渲染周期中的state進行合並,在這個渲染周期中你對所有setState的所有調用都會被合並起來之后,再一次性的渲染,這樣可以避免頻繁的調用setState導致頻繁的操作dom,提高渲染性能。具體的實現方面,可以簡單的理解為react中存在一個狀態變量isBatchingUpdates,當處於渲染周期開始時,這個變量會被設置成true,渲染周期結束時,會被設置成false,react會根據這個狀態變量,當出在渲染周期中時,僅僅只是將當前的改變緩存起來,等到渲染周期結束時,再一次性的全部render,,具體的流程可以參照下面的流程圖
現在,我們回到最開始的問題,為什么一開始在componentDidMount中直接執行setState會無法立刻得到更新呢,原因就在於,我們在componentDidMount中其實處於首次渲染的事務當中,這次事務的渲染尚未完成,而首次渲染的時候會將isBatchingUpdates設置為true,這是我們在componentDidMount中調用setState,react會發現當前事務尚未完成,只會直接將修改后的state放入到dirtyComponents中,等待最終渲染周期完成時,將所有的state進行合並,一次性render。而當我們放在setTimeOut里面的時候,setTimeOut會將操作放到執行隊列的最后方,也就是說會等待渲染周期結束之后再進行setState,這個時候狀態變量已經被重置回來了,所以此時我們的每一次setState都會立刻生效
接下來,我們從源碼的角度來看看setState是怎么操作的
function enqueueUpdate(component) {
ensureInjected();
//不在渲染周期中
if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
//渲染周期中,直接緩存state等待下一步更新
dirtyComponents.push(component);
}
這個邏輯跟上圖的邏輯是一樣的,當我們調用setState函數的時候,實際上最終會調用到enqueueUpdate函數,整體邏輯上面已經分析過了,就不再贅述,接下來看看setState是如何通過事務來進行渲染的,也就是batchingStrategy.batchedUpdates到底做了些什么,往下走之前,如果對事務不了解建議先看看這篇文章(傳送門,關於事務原理的解析)
var ReactUpdates = require('ReactUpdates');
var Transaction = require('Transaction');
var emptyFunction = require('emptyFunction');
//第二個wrapper
var RESET_BATCHED_UPDATES = {
initialize: emptyFunction,
close: function() {
ReactDefaultBatchingStrategy.isBatchingUpdates = false;
},
};
//第一個wrapper
var FLUSH_BATCHED_UPDATES = {
initialize: emptyFunction,
close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates),
};
//wrapper列表
var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];
//事務構造函數
function ReactDefaultBatchingStrategyTransaction() {
//原型中定義的初始化方法
this.reinitializeTransaction();
}
//繼承原型
Object.assign(
ReactDefaultBatchingStrategyTransaction.prototype,
Transaction.Mixin,
{
getTransactionWrappers: function() {
return TRANSACTION_WRAPPERS;
},
}
);
//新建一個事務
var transaction = new ReactDefaultBatchingStrategyTransaction();
var ReactDefaultBatchingStrategy = {
isBatchingUpdates: false,
batchedUpdates: function(callback, a, b, c, d, e) {
var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
ReactDefaultBatchingStrategy.isBatchingUpdates = true;
// The code is written this way to avoid extra allocations
if (alreadyBatchingUpdates) {
callback(a, b, c, d, e);
} else {
//在這個地方調用事務,callback是從外部傳入的方法
transaction.perform(callback, null, a, b, c, d, e);
}
},
};
上面這個就是react渲染所使用的事務,react就是用這個事務來處理setState引起的渲染,根據我們剛剛的解釋,我們可以看到,事務開始時就把isBatchingUpdates設置成了true,防止在一次渲染周期中重復渲染,我們還可以看到這個事務定義了兩個wrapper,其出口方法close分別用於執行渲染和設置狀態變量,而執行渲染的FLUSH_BATCHED_UPDATES 要先於執行設置狀態變量的RESET_BATCHED_UPDATES ,也就是說,執行渲染之后,才會通過RESET_BATCHED_UPDATES的close方法執行這句代碼
ReactDefaultBatchingStrategy.isBatchingUpdates = false;
之后整個渲染周期結束。這時候當我們執行setState的時候,重新進入一個新的渲染周期
那么,問題來了,當我們在渲染周期中執行了setState之后,我們要如何獲取到最新的state的值呢,setTimeOut是一個方案,但是不太優雅,有沒有其他方法呢,我們注意到,setState提供了一個回調函數,我們只需要在回調里面獲取更新后的state即可,像這樣
componentDidMount() {
this.setState({count: this.state.count + 1},()=>{
console.log(this.state.count);//該是啥就是是啥
}));
}