react源碼分析之-setState是異步仍是同步?

本文轉載自:微信公衆號 方凳雅集
寫在前面的話react

setState是React很重要的模塊, 社區中也有不少分析文章,大多強調setState是異步更新,但有些文章分析又說某些狀況下是同步更新,那究竟是同步仍是異步呢,這篇文章仍是基於15.x進行的分析,16.x的分析等後面用機會再分享。算法

咱們看一下React官網對setState的說明:數組

16d389ee3e331424?w=1492&h=598&f=png&s=288459

官網也沒說setState究竟是同步仍是異步,只是說React不保證setState以後可以當即拿到改變後的結果。瀏覽器

咱們先看一個經典例子微信

Demoapp

// demo.js
class Demo extends PureComponent {
state={dom

count: 0,

}
componentDidMount() {異步

console.log('pre state', this.state.count);
this.setState({
  count: this.state.count + 1
});
console.log('next state', this.state.count);

//測試setTimeout
setTimeout(() => {
  console.log('setTimeout pre state', this.state.count);
  this.setState({
    count: this.state.count + 1
  });
  console.log('setTimeout next state', this.state.count);
}, 0);

}async

onClick = (event) => {函數

// 測試合成函數中setState
console.log(`${event.type} pre state`, this.state.count);
this.setState({
  count: this.state.count + 1
});
console.log(`${event.type} next state`, this.state.count);

}

render() {

return <button onClick={this.onClick}>count+1</button>

}
}

這裏有三種方法調用setState:

在componentDidMount中直接調用setState;
在componentDidMount的setTimeout方法裏調用setState;
在dom中綁定onClick(React的合成函數:抹平不一樣瀏覽器和端的差別)直接調用setState;

16d389f6bfee02ec?w=1440&h=274&f=png&s=116850

從控制檯打印出來的結果看,方法1和3直接調用setState是異步的,而方法2中setTimeout調用setState證實了同步,到底爲何呢?這兩種調用方式有什麼區別嘛?接下來咱們從源碼進行分析。

源碼分析

setState入口函數

//ReactComponent.js
ReactComponent.prototype.setState = function (partialState, callback) {
!(typeof partialState === 'object' || typeof partialState === 'function' || partialState == null) ? "development" !== 'production' ? invariant(false, 'setState(...): takes an object of state variables to update or a ' + 'function which returns an object of state variables.') : invariant(false) : undefined;
if ("development" !== 'production') {

"development" !== 'production' ? warning(partialState != null, 'setState(...): You passed an undefined or null state object; ' + 'instead, use forceUpdate().') : undefined;

}

this.updater.enqueueSetState(this, partialState);
if (callback) {

this.updater.enqueueCallback(this, callback);

}
};

//ReactUpdateQueue.js
enqueueSetState: function(publicInstance, partialState) {
// 根據 this.setState 中的 this 拿到內部實例, 也就是組件實例
var internalInstance = getInternalInstanceReadyForUpdate(

publicInstance,
'setState'

);

if (!internalInstance) {

return;

}

//取得組件實例的_pendingStateQueue
var queue =

internalInstance._pendingStateQueue ||
(internalInstance._pendingStateQueue = []);

//將partial state存到_pendingStateQueue
queue.push(partialState);
//喚起enqueueUpdate
enqueueUpdate(internalInstance);
};
...

function enqueueUpdate(internalInstance) {
ReactUpdates.enqueueUpdate(internalInstance);
}

在setState函數中調用enqueueSetState, 拿到內部組件實例, 而後把要更新的partial state存到其_pendingStateQueue中,至此,setState調用方法執行結束,接下來是setState調用以後的動做。

調用 setState 後發生了什麼?

setState調用以後執行方法enqueueUpdate

//ReactUpdates.js
function enqueueUpdate(component) {
//注入默認策略,開啓ReactReconcileTransaction事務
ensureInjected();

// 若是沒有開啓batch(或當前batch已結束)就開啓一次batch再執行, 這一般發生在異步回調中調用 setState
//batchingStrategy:批量更新策略,經過事務的方式實現state的批量更新
if (!batchingStrategy.isBatchingUpdates) {

batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;

}
// 若是batch已經開啓,則將該組件保存在 dirtyComponents 中存儲更新
dirtyComponents.push(component);
}

上面demo對setState三次調用結果之因此不一樣,應該是這裏的判斷邏輯致使的:

1和3的調用走的是isBatchingUpdates === true分支,沒有執行更新操做;
2的setTimeout走的是isBatchingUpdates === false分支,執行更新;

isBatchingUpdates是事務batchingStrategy的一個標記,若是爲true,把當前調用setState的組件放入dirtyComponents數組中,作存儲處理,不會當即更新,若是爲false,將enqueueUpdate做爲參數傳入batchedUpdates方法中,在batchedUpdates中執行更新操做。

但是事務batchingStrategy究竟是作什麼的呢?batchedUpdates又作了什麼處理?咱們看一下它的源碼

//ReactDefaultBatchingStrategy.js
var transaction = new ReactDefaultBatchingStrategyTransaction();// 實例化事務

var ReactDefaultBatchingStrategy = {
isBatchingUpdates: false,

batchedUpdates: function(callback, a, b, c, d, e) {

var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
// 開啓一次batch
ReactDefaultBatchingStrategy.isBatchingUpdates = true;

if (alreadyBatchingUpdates) {
  callback(a, b, c, d, e);
} else {
  // 啓動事務, 將callback放進事務裏執行
  transaction.perform(callback, null, a, b, c, d, e);
}

},
};
//說明:這裏使用到了事務transaction,簡單來講,transaction就是將須要執行的方法使用 wrapper 封裝起來,
//再經過事務提供的 perform 方法執行。而在 perform 以前,先執行全部 wrapper 中的 initialize 方法,
//執行完 perform 以後(即執行method 方法後)再執行全部的 close 方法。
//一組 initialize 及 close 方法稱爲一個 wrapper。事務支持多個 wrapper 疊加,嵌套,
//若是當前事務中引入了另外一個事務B,則會在事務B完成以後再回到當前事務中執行close方法。

(上面涉及到了事務,事務的具體分析有興趣能夠看文章最後)

ReactDefaultBatchingStrategy就是一個批量更新策略事務, isBatchingUpdates默認是false,而batchedUpdates方法被調用時纔會將屬性isBatchingUpdates設置爲true,代表目前處於批量更新流中;但是上面demo中1和3執行到判斷邏輯以前源碼分析中沒見到有batchedUpdates方法調用,那batchedUpdates何時被調用的呢?

全局搜索React中調用batchedUpdates的地方不少,分析後發現與更新流程相關的只有兩個地方:

// ReactMount.js
_renderNewRootComponent: function(nextElement,container,shouldReuseMarkup,context) {

...
// 實例化組件
var componentInstance = instantiateReactComponent(nextElement, null);
//初始渲染是同步的,但在渲染期間發生的任何更新,在componentWillMount或componentDidMount中,將根據當前的批處理策略進行批處理
ReactUpdates.batchedUpdates(
  batchedMountComponentIntoNode,
  componentInstance,
  container,
  shouldReuseMarkup,
  context
);

...
},
// ReactEventListener.js
dispatchEvent: function (topLevelType, nativeEvent) {

...
try {
  // 處理事件
  ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
} finally {
  TopLevelCallbackBookKeeping.release(bookKeeping);
}

}

第一種狀況,是在首次渲染組件時調用batchedUpdates,開啓一次batch。由於組件在渲染的過程當中, 會依順序調用各類生命週期函數, 開發者極可能在生命週期函數中(如componentWillMount或者componentDidMount)調用setState. 所以, 開啓一次batch就是要存儲更新(放入dirtyComponents), 而後在事務結束時批量更新. 這樣以來, 在初始渲染流程中, 任何setState都會生效, 用戶看到的始終是最新的狀態
第二種狀況,若是在組件上綁定了事件,在綁定事件中頗有可能觸發setState,因此爲了存儲更新(dirtyComponents),須要開啓批量更新策略。在回調函數被調用以前, React事件系統中的dispatchEvent函數負責事件的分發, 在dispatchEvent中啓動了事務, 開啓了一次batch, 隨後調用了回調函數. 這樣一來, 在事件的監聽函數中調用的setState就會生效.
這裏借用《深刻REACT技術棧》文章裏的一個在componentDidMount中setState的調用棧圖例

16d389feede7f455?w=730&h=544&f=png&s=199597

圖例中代表,ReactDefaultBatchingStrategy.batchedUpdates在ReactMount.

renderNewRootComponent中被調用,依次倒推,最後發如今組件首次渲染時就會經過injectBatchingStrategy()方法注入ReactDefaultBatchingStrategy(這部分有興趣能夠看一下ReactDefaultInjection.js源碼),而且在ReactMount.render中觸發
renderNewRootComponent函數,調用batchedUpdates將isBatchingUpdates設置爲了true,因此componentDidMount的執行都是在一個大的事務ReactDefaultBatchingStrategyTransaction中。

這就解釋了在componentDidMount中調用setState並不會當即更新state,由於正處於一個這個大的事務中,isBatchingUpdates此時爲true,因此只會放入dirtyComponents中等待稍後更新。

state何時批量更新呢?

追蹤代碼後我畫了一個組件初次渲染和setState後簡單的事務啓動和執行的順序:

16d38a084d0357b4?w=1456&h=1506&f=png&s=205295

從上面的圖中能夠看到,ReactDefaultBatchingStrategy就是一個批量更新策略事務,控制了批量策略的生命週期。看一下ReactDefaultBatchingStrategy源碼分析一下事務中執行了什麼:

// ReactDefaultBatchingStrategy.js
var RESET_BATCHED_UPDATES = {
initialize: emptyFunction,
close: function() {

ReactDefaultBatchingStrategy.isBatchingUpdates = false;

},
};

var FLUSH_BATCHED_UPDATES = {
initialize: emptyFunction,
close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates),
};

var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];

在事務的close階段執行了flushBatchedUpdates函數,flushBatchedUpdates執行完以後再將ReactDefaultBatchingStrategy.isBatchingUpdates重置爲false,表示此次batch更新結束。
flushBatchedUpdates函數啓動ReactUpdatesFlushTransaction事務,這個事務開啓了批量更新,執行runBatchedUpdates對dirtyComponents循環處理。

怎麼批量更新的呢?

批量更新flushBatchedUpdates中,看一下源碼

// ReactUpdates.js
var flushBatchedUpdates = function() {
// 開啓批量更新
while (dirtyComponents.length || asapEnqueued) {

if (dirtyComponents.length) {
  var transaction = ReactUpdatesFlushTransaction.getPooled();
  transaction.perform(runBatchedUpdates, null, transaction);
  ReactUpdatesFlushTransaction.release(transaction);
}
// 批量處理callback
if (asapEnqueued) {
  asapEnqueued = false;
  var queue = asapCallbackQueue;
  asapCallbackQueue = CallbackQueue.getPooled();
  queue.notifyAll();
  CallbackQueue.release(queue);
}

}
};

flushBatchedUpdates開啓事務ReactUpdatesFlushTransaction, 執行runBatchedUpdates,

// ReactUpdates.js
function runBatchedUpdates(transaction) {
var len = transaction.dirtyComponentsLength;
// 排序保證父組件優於子組件更新
dirtyComponents.sort(mountOrderComparator);
// 遍歷dirtyComponents
for (var i = 0; i < len; i++) {

var component = dirtyComponents[i];
var callbacks = component._pendingCallbacks;
component._pendingCallbacks = null;
// 執行更新操做
ReactReconciler.performUpdateIfNecessary(
  component,
  transaction.reconcileTransaction
);
// 存儲callbacks
if (callbacks) {
  for (var j = 0; j < callbacks.length; j++) {
    transaction.callbackQueue.enqueue(
      callbacks[j],
      component.getPublicInstance()
    );
  }
}

}
}

接下來就是ReactReconciler調用組件實例的performUpdateIfNecessary方法,這裏只分析ReacrCompositeComponent實例,若是接收了props,就會調用receiveComponent方法,在該方法裏調用updateComponent方法;若是有新的要更新的狀態(_pendingStateQueue不爲空)也會直接調用updateComponent來更新

// ReactCompositeComponent.js
performUpdateIfNecessary: function(transaction) {

if (this._pendingElement != null) {
  ReactReconciler.receiveComponent(
    this,
    this._pendingElement || this._currentElement,
    transaction,
    this._context
  );
}
// 待更新state隊列不爲空或者_pendingForceUpdate爲true
if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
  this.updateComponent(
    transaction,
    this._currentElement,
    this._currentElement,
    this._context,
    this._context
  );
}

},

調用組件實例中的updateComponent,這塊代碼是組件更新機制的核心,負責管理生命週期中的componentWillReceiveProps、shouldComponentUpdate、componentWillUpdate、render 和 componentDidUpdate;

這段代碼比較多,集中在ReactCompositeComponent.js文件中,

若是不想看源碼能夠直接看後面的代碼流程圖

//ReactCompositeComponent.js
updateComponent: function(

transaction,
prevParentElement,
nextParentElement,
prevUnmaskedContext,
nextUnmaskedContext

) {

var inst = this._instance;

var nextContext = this._context === nextUnmaskedContext ?
  inst.context :
  this._processContext(nextUnmaskedContext);
var nextProps;

// Distinguish between a props update versus a simple state update
if (prevParentElement === nextParentElement) {
  // Skip checking prop types again -- we don't read inst.props to avoid
  // warning for DOM component props in this upgrade
  nextProps = nextParentElement.props;
} else {
  nextProps = this._processProps(nextParentElement.props);
  // 若是有接收新的props,執行componentWillReceiveProps 方法,
  if (inst.componentWillReceiveProps) {
    inst.componentWillReceiveProps(nextProps, nextContext);
  }
}
// 合併props
var nextState = this._processPendingState(nextProps, nextContext);
// 執行shouldComponentUpdate判斷是否須要更新  
var shouldUpdate =
  this._pendingForceUpdate ||
  !inst.shouldComponentUpdate ||
  inst.shouldComponentUpdate(nextProps, nextState, nextContext);
...
// 若是須要更新執行_performComponentUpdate,不然只將當前的props和state保存下來,不作更新
if (shouldUpdate) {
  this._pendingForceUpdate = false;
  // Will set `this.props`, `this.state` and `this.context`.
  this._performComponentUpdate(
    nextParentElement,
    nextProps,
    nextState,
    nextContext,
    transaction,
    nextUnmaskedContext
  );
} else {
  this._currentElement = nextParentElement;
  this._context = nextUnmaskedContext;
  inst.props = nextProps;
  inst.state = nextState;
  inst.context = nextContext;
}

},
...
// 執行componentWillUpdate
_performComponentUpdate: function(

nextElement,
nextProps,
nextState,
nextContext,
transaction,
unmaskedContext

) {

var inst = this._instance;

var hasComponentDidUpdate = Boolean(inst.componentDidUpdate);
var prevProps;
var prevState;
var prevContext;
if (hasComponentDidUpdate) {
  prevProps = inst.props;
  prevState = inst.state;
  prevContext = inst.context;
}

if (inst.componentWillUpdate) {
  inst.componentWillUpdate(nextProps, nextState, nextContext);
}

this._currentElement = nextElement;
this._context = unmaskedContext;
inst.props = nextProps;
inst.state = nextState;
inst.context = nextContext;

this._updateRenderedComponent(transaction, unmaskedContext);

if (hasComponentDidUpdate) {
  transaction.getReactMountReady().enqueue(
    inst.componentDidUpdate.bind(inst, prevProps, prevState, prevContext),
    inst
  );
}

}
// 執行unmountComponent,_instantiateReactComponent, mountComponent、render
_updateRenderedComponent: function(transaction, context) {

var prevComponentInstance = this._renderedComponent;
var prevRenderedElement = prevComponentInstance._currentElement;
var nextRenderedElement = this._renderValidatedComponent();
// 若是prevRenderedElement, nextRenderedElement相等只執行receiveComponent
if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) {
  ReactReconciler.receiveComponent(
    prevComponentInstance,
    nextRenderedElement,
    transaction,
    this._processChildContext(context)
  );
} else {
  // prevRenderedElement, nextRenderedElement不相等,則執行舊組件的unmountComponent
  var oldNativeNode = ReactReconciler.getNativeNode(prevComponentInstance);
  ReactReconciler.unmountComponent(prevComponentInstance);
  this._renderedNodeType = ReactNodeTypes.getType(nextRenderedElement);
  // 組件實例化_instantiateReactComponent
  this._renderedComponent = this._instantiateReactComponent(
    nextRenderedElement
  );
  // 組件掛載
  var nextMarkup = ReactReconciler.mountComponent(
    this._renderedComponent,
    transaction,
    this._nativeParent,
    this._nativeContainerInfo,
    this._processChildContext(context)
  );
  // 新組件替換舊組件
  this._replaceNodeWithMarkup(oldNativeNode, nextMarkup);
}

},

updateComponent流程圖

16d38a1e5de6b65f?w=1492&h=1258&f=png&s=392472

demo擴展

上面分析了一個很經典的demo,下面看一下原生事件和async事件中setState調用後的表現。

綁定原生事件,調用setState

class Button extends PureComponent {
state={

count: 0,
val: 0

}
componentDidMount() {

// 測試原生方法:手動綁定mousedown事件
console.log('mousedown pre state', this.state.count);
ReactDOM.findDOMNode(this).addEventListener(
  "mousedown",
  this.onClick.bind(this)
);
console.log('mousedown pre state', this.state.count);

}

onClick(event) {

console.log(`${event.type} pre state`, this.state.count);
this.setState({
  count: this.state.count + 1
});
console.log(`${event.type} next state`, this.state.count);

}

render() {

return <button onClick={this.onClick.bind(this)}>count+1</button>

}
}

控制檯

16d38a22d5a642d0?w=1492&h=145&f=png&s=31302

async函數和sleep函數

class Button extends PureComponent {
state={

count: 0,
val: 0

}
async componentDidMount() {

// 測試async函數中setState
for(let i = 0; i < 1; i++){
  console.log('sleep pre state', this.state.count);
  await sleep(0);
  this.setState({
    count: this.state.count + 1
  });
  console.log('sleep next state', this.state.count);
}

}

asyncClick = () => {

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

}

async onClick(event) {

const type = event.type;
console.log(`${type} pre state`, this.state.count);
await this.asyncClick();
console.log(`${type} next state`, this.state.count);

}

render() {

return <button onClick={this.onClick.bind(this)}>count+1</button>

}
}

控制檯

16d38a27dcd3287d?w=1492&h=230&f=png&s=85003

結論

setState在生命週期函數和合成函數中都是異步更新。
setState在steTimeout、原生事件和async函數中都是同步更新。
每次更新不表明都會觸發render,若是render內容與newState有關聯,則會觸發,不然即使setState屢次也不會render
若是newState內容與render有依賴關係,就不建議同步更新,由於每次render都會完整的執行一次批量更新流程(只是dirtyComponets長度爲1,stateQueue也只有該組件的newState),調用一次diff算法,這樣會影響React性能。
若是沒有必須同步渲染的理由,不建議使用同步,會影響react渲染性能

總結

React整個更新機制到處包含着事務,總的來講,組件的更新機制依靠事務進行批量更新;

一次batch(批量)的生命週期就是從ReactDefaultBatchingStrategy事務perform以前(調用ReactUpdates.batchUpdates)到這個事務的最後一個close方法調用後結束;
事務啓動後, 遇到 setState 則將 partial state 存到組件實例的_pendingStateQueue上, 而後將這個組件存到dirtyComponents 數組中, 等到 ReactDefaultBatchingStrategy事務結束時調用runBatchedUpdates批量更新全部組件;
組件的更新是遞歸的, 三種不一樣類型的組件都有本身的updateComponent方法來決定本身的組件如何更新, 其中 ReactDOMComponent 會採用diff算法對比子元素中最小的變化, 再批量處理.
生命週期函數和合成函數中調用setState表現異步更新,是由於組件初始化和調用合成函數時都會觸發ReactDefaultBatchingStrategy事務的batchUpdates方法,將批量更新標記設置爲true,因此後面的setState都會存儲到dirtyComponents中,執行批量更新以後再將標誌設置爲false;
setTimeout、原生事件和async函數中調用setState表現同步更新,是由於遇到這些函數時不會觸發
ReactDefaultBatchingStrategy事務的batchUpdates方法,因此批量更新標記依舊時false,因此表現爲同步。

補充:transaction事務介紹

React 的事務機制比較簡單,包括三個階段,initialize、perform和close,而且事務之間支持疊加。

事務提供了一個 mixin 方法供其餘模塊實現本身須要的事務。而要使用事務的模塊,除了須要把 mixin 混入本身的事務實現中外,還要額外實現一個抽象的 getTransactionWrappers 接口。這個接口用來獲取全部須要封裝的前置方法(initialize)和收尾方法(close),

所以它須要返回一個數組的對象,每一個對象分別有 key 爲 initialize 和 close 的方法。

這裏看一個《深刻React技術棧》文章中的例子就比較好理解了

var Transaction = require('./Transaction');
// 咱們本身定義的事務
var MyTransaction = function() {
// ... };
Object.assign(MyTransaction.prototype, Transaction.Mixin, { getTransactionWrappers: function() {
return [{

initialize: function() {
console.log('before method perform'); },
close: function() {
console.log('after method perform');
}

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

// 打印的結果以下:// before method perform // test// after method perform

相關文章
相關標籤/搜索