setState做爲react中使用最頻繁的一個API,在這裏簡單分享它的實現機制。
沒錯本文是一篇講源碼的文章,但儘可能避免作代碼的搬運工,根據setState的使用場景進行解析,源碼基於react v16.4.3-alpha.0vue
網上有不少講解fiber的文章大多在描述fiber的算法。實際上fiber包含數據結構和算法,按照v16以前的版本理解,fiber在源碼中表示虛擬DOM的一個節點react
對react有必定了解的同窗確定知道react封裝了一套本身的事件系統,<div onClick={handleClick}></div>
並非像vue同樣調用addEventListener綁定事件到對應的節點上,而是經過事件委託的方式綁定到document上了
接下來咱們簡單來看實現過程:git
// 獲取任意一個經過react渲染獲得的DOM節點
const someElement = document.getElementById('#someId')
// 打印節點元素
console.dir(someElement)
// 任何一個經過react渲染獲得的DOM節點都會有`__reactEventHandlers****`這個屬性
console.dir(someElement.__reactEventHandlers****)
// __reactEventHandlers中能夠找到在JSX中爲這個標籤添加的事件屬性
const onClick = someElement.__reactEventHandlers****.onClick
複製代碼
有了上面的知識咱們看一下react事件系統的簡易過程github
__reactEventHandlers
// 僞代碼
documnet.addEventListener('click', function(event){
const target = event.target
const onClick = traget.__reactEventHandlers*****.onClick
// isBatchingUpdates全局變量後面會具體講解到
var previousIsBatchingUpdates = isBatchingUpdates;
isBatchingUpdates = true;
try {
// 執行事件回調
return onClick(event);
} finally {
isBatchingUpdates = previousIsBatchingUpdates;
performSyncWork()
}
})
複製代碼
這裏只是簡要描述,實際實現要複雜不少算法
在這裏能夠看源碼瀏覽器
Component.prototype.setState = function(partialState, callback) {
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
複製代碼
this.updater 是在哪一個地方進行賦值的咱們暫時不用關心,只須要知道他被賦值爲classComponentUpdater
bash
在這裏能夠看源碼 咱們只需關心生成了update,插入到update隊列,而後調用scheduleWork數據結構
// 僞代碼
const classComponentUpdater = {
...
enqueueSetState(inst, payload, callback) {
const update = createUpdate(expirationTime);
// setState(payload, callback);
update.payload = payload;
update.callback = callback;
// 插入到update隊列
enqueueUpdate(fiber, update);
scheduleWork(fiber, expirationTime);
},
...
複製代碼
在這裏能夠看源 這一步咱們只需關心下面的這一段邏輯異步
// isWorking、isCommitting是所有變量,在後面咱們會具體分析到
if (
!isWorking ||
isCommitting ||
nextRoot !== root
) {
const rootExpirationTime = root.expirationTime;
requestWork(root, rootExpirationTime);
}
複製代碼
function requestWork(root, expirationTime) {
// 將根節點添加到調度任務中
addRootToSchedule(root, expirationTime)
// isRendering是全局變量,在後面咱們會具體分析到
if (isRendering) {
return;
}
// isBatchingUpdates、isUnbatchingUpdates是全局變量
// 在第一節瞭解react事件時有對他們進行從新賦值
if (isBatchingUpdates) {
if (isUnbatchingUpdates) {
....
performWorkOnRoot(root, Sync, false);
}
return;
}
if (expirationTime === Sync) {
performSyncWork();
} else {
scheduleCallbackWithExpirationTime(root, expirationTime);
}
}
複製代碼
handleClick(){
this.setState({
name: '吳彥祖'
})
console.log(this.state.name) // >> 狗蛋
this.setState({
age: '18'
})
console.log(this.state.age) // >> 40
}
複製代碼
第一節中瞭解到在執行事件回調handleClick前isBatchingUpdates = true
,滾動到看第二節的源碼過程,最終在第五步requestWork
中會執行數據結構和算法
function requestWork(){
...
if (isBatchingUpdates) {
return
}
...
}
複製代碼
第一個setState也就到此爲止被return
,接着執行第二個setState一樣到這一步爲止。
如今咱們能知道什麼呢?
在交互事件中的setState每次執行只是建立了一個新的
update
,而後添加到enqueueUpdate
,setState並無直接觸發react的update
再回頭看第一節中react的事件過程,當handleClick
執行後會立馬調用performWork
開始react的update過程
理一下整個過程,交互事件中的由於
isBatchingUpdates = true
會先收集全部的update到enqueueUpdate
中,交互事件回調執行完後再調用performWork
一次更新全部的state
從源碼能夠看到這整個過程對瀏覽器來講都是同步的,一步一步順序執行;對於開發者來講,執行setState後由於要進行批處理操做,而延後了react的更新
在1中咱們知道由於isBatchingUpdates = true
的緣由執行setState後沒法直接拿到新的state,若是咱們能夠避免isBatchingUpdates的問題結果又會怎樣
handleClick(){
setTimeout(() => {
this.setState({
name: '吳彥祖'
})
console.log(this.state.name) // >> 吳彥祖
this.setState({
age: '18'
})
console.log(this.state.age) // >> 18
})
}
複製代碼
經過setTimeout執行setState
,也就沒有react的事件系統什麼事了, isBatchingUpdates
的默認爲false,看第二節第五步,每次setState都會執行performSyncWork
觸發react的update,因此每次調用setState緊接着咱們就能拿到最新的state
經過在setTimeout中執行setState咱們達到了setState是同步的效果,固然經過setInterval、Promise也能達到一樣的效果。
在第二節源碼中能夠注意到三個全局變量:isRendering
、isWorking
、isCommitting
v16中react更新有兩個階段reconciler和commit階段
render前生命週期屬於reconciler階段:isRendering = true
、isWorking = true
觸發第二節第五步:
function requestWork(){
...
if (isRendering) {
return
}
...
}
複製代碼
render前生命週期不會觸發新的更新,只是將新的
update
添加到enqueueUpdate
尾部,在當前更新任務中處理
render後生命週期屬於commit階段:isRendering = true
、isWorking = true
、isCommitting = true
一樣會觸發第二節第五步:
function requestWork(){
addRootToSchedule(root, expirationTime)
if (isRendering) {
return
}
...
}
複製代碼
render後生命週期不會當即觸發新的更新,固然也不會在本次更新任務中處理,這裏咱們注意有一個
addRootToSchedule(root, expirationTime)
,將新的更新做爲下一個更新任務
例:
修改
name
觸發componentDidUpdate()
在componentDidUpdate
修改age
過程: 修改
name
開始react的update過程完成reconciler和commit階段,由於任務中還有一個修改age
的任務,再次開始react的update過程完成reconciler和commit階段
注意:在componentDidUpdate使用setState可能會形成死循環
react本人用的不是不少,結合官方文檔暫時只能想到上述四種場景。爲僅講解setState文中刻意省略了fiber相關的過程,後面有機會會有fiber相關的分享。有什麼建議歡迎在下面留言交流。