setState作為react中使用最頻繁的一個API,在這裡簡單分享它的實現機制。
沒錯本文是一篇講原始碼的文章,但儘量避免做程式碼的搬運工,根據setState的使用場景進行解析,原始碼基於react v16.4.3-alpha.0
一,預備知識
1,fiber
網上有很多講解fiber的文章大多在描述fiber的演算法。實際上fiber包含資料結構和演算法,按照v16之前的版本理解,fiber在原始碼中表示虛擬DOM的一個節點
2,react的事件系統
對react有一定了解的同學肯定知道react封裝了一套自己的事件系統,<div onClick={handleClick}></div>
並不是像vue一樣呼叫addEventListener繫結事件到對應的節點上,而是通過事件委託的方式繫結到document上了
接下來我們簡單來看實現過程:
// 獲取任意一個通過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事件系統的簡易過程
- 點選一個按鈕觸發document上的click事件
- 獲得事件物件event
- 通過event.target可以知道是點選的那個按鈕
- 拿到按鈕上面的
__reactEventHandlers
- 然後就有了onClick
// 虛擬碼
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()
}
})
複製程式碼
這裡只是簡要描述,實際實現要複雜很多
二,走一遍原始碼
1,setState實現
Component.prototype.setState = function(partialState, callback) {
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
複製程式碼
2,this.updater
this.updater 是在哪個地方進行賦值的我們暫時不用關心,只需要知道他被賦值為classComponentUpdater
3,classComponentUpdater
在這裡可以看原始碼 我們只需關心生成了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);
},
...
複製程式碼
4,scheduleWork
在這裡可以看源 這一步我們只需關心下面的這一段邏輯
// isWorking、isCommitting是全部變數,在後面我們會具體分析到
if (
!isWorking ||
isCommitting ||
nextRoot !== root
) {
const rootExpirationTime = root.expirationTime;
requestWork(root, rootExpirationTime);
}
複製程式碼
5,requestWork
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);
}
}
複製程式碼
好了,要了解setState的過程,追蹤到這五步就可以了,下面會結合具體場景來對這整個過程具體分析
三,使用場景
1,互動事件
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是非同步的?
從原始碼可以看到這整個過程對瀏覽器來說都是同步的,一步一步順序執行;對於開發者來說,執行setState後因為要進行批處理操作,而延後了react的更新
2,setTimeout、setInterval、Promise中
在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也能達到同樣的效果。
3,componentWillUpdate (render前生命週期)
先補充一個知識點
在第二節原始碼中可以注意到三個全域性變數:isRendering
、isWorking
、isCommitting
v16中react更新有兩個階段reconciler和commit階段
- isRendering:開始react更新就為true
- isWorking:進入reconciler階段就為true、進入commit階段就為true
- isCommitting:進入commit階段就為true
render前生命週期屬於reconciler階段:isRendering = true
、isWorking = true
觸發第二節第五步:
function requestWork(){
...
if (isRendering) {
return
}
...
}
複製程式碼
render前生命週期不會觸發新的更新,只是將新的
update
新增到enqueueUpdate
尾部,在當前更新任務中處理
4,componentDidUpdate (render後生命週期)
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相關的分享。有什麼建議歡迎在下面留言交流。