知根知底setState

hailx發表於2019-01-05

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前生命週期)

先補充一個知識點

在第二節原始碼中可以注意到三個全域性變數:isRenderingisWorkingisCommitting

v16中react更新有兩個階段reconciler和commit階段

  • isRendering:開始react更新就為true
  • isWorking:進入reconciler階段就為true、進入commit階段就為true
  • isCommitting:進入commit階段就為true

render前生命週期屬於reconciler階段:isRendering = trueisWorking = true

觸發第二節第五步:

function requestWork(){
    ...
    if (isRendering) {
        return
    }
    ...
}
複製程式碼

render前生命週期不會觸發新的更新,只是將新的update新增到enqueueUpdate尾部,在當前更新任務中處理

4,componentDidUpdate (render後生命週期)

render後生命週期屬於commit階段:isRendering = trueisWorking = trueisCommitting = 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相關的分享。有什麼建議歡迎在下面留言交流。

相關文章