知根知底setState

setState做爲react中使用最頻繁的一個API,在這裏簡單分享它的實現機制。
沒錯本文是一篇講源碼的文章,但儘可能避免作代碼的搬運工,根據setState的使用場景進行解析,源碼基於react v16.4.3-alpha.0vue

一,預備知識

1,fiber

網上有不少講解fiber的文章大多在描述fiber的算法。實際上fiber包含數據結構和算法,按照v16以前的版本理解,fiber在源碼中表示虛擬DOM的一個節點react

2,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

  • 點擊一個按鈕觸發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 是在哪一個地方進行賦值的咱們暫時不用關心,只須要知道他被賦值爲classComponentUpdaterbash

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相關的分享。有什麼建議歡迎在下面留言交流。

相關文章
相關標籤/搜索