關於React setState的實現原理(一)


前言

首先在學習react的時候就對setSate的實現有比較濃厚的興趣,那么對於下邊的代碼,可以快速回答嗎?

class Root extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }
  componentDidMount() {
    let me = this;
    me.setState({
      count: me.state.count + 1
    });
    console.log(me.state.count);    // 打印
    me.setState({
      count: me.state.count + 1
    });
    console.log(me.state.count);    // 打印
    setTimeout(function(){
     me.setState({
       count: me.state.count + 1
     });
     console.log(me.state.count);   // 打印
    }, 0);
    setTimeout(function(){
     me.setState({
       count: me.state.count + 1
     });
     console.log(me.state.count);   // 打印
    }, 0);
  }
  render() {
    return (
      <h1>{this.state.count}</h1>
    )
  }
}

這段代碼大家可能在很多地方看見過,結果是讓你匪夷所思的0,0,2,3。 大部分人相信都不知道其中的原因,首先肯定會問:

  • 為什么前兩次為零,而加上setTimeout就可以打印出來?
  • 為什么setTimeout打印出不同的結果?

那么請你接下來向下看,我首先說一下Batch Updata(批量更新)。如下圖:

什么事Batch Update 

在一些MV*框架中,就是將一段時間內對model的修改批量更新到view的機制。比如那前端比較火的React、vue為例。

在React中,我們在componentDidMount生命周期連續調用SetState:

componentDidMount () {
  this.setState({ foo: 1 })
  this.setState({ foo: 2 })
  this.setState({ foo: 3 })
}

 

在沒有Batch Update的情況下,上面的操作會導致三次組件渲染,但是使用Batch Update機制下時間上只運行了一次渲染。componentDidMount中三次對model的操作被優化為一次view更新,

不必要的Vitual Dom計算被忽略,從而提高了框架的效率。

Batch Update的實現

我們想到的可能就是數據結構中的棧和隊列,比較一下還是使用一個queue來保存update,並在合適的時機對這個queue進行flush操作。那么現在有兩個問題:

  1. 什么時候創建這個queue
  2. 什么時候對這個queue進行flush

那么我們要對Reac和Vue的源碼進行分析,首先React:React中的Batch Update是通過Transaction(事務)來實現的。在React源碼關於Transaction的部分可以用一幅畫解釋:

 Transaction對一個函數進行包裝,讓React有機會在一個函數執行前和執行后運行特定的邏輯,從而完成對整個Batch Update流程的控制。

簡單的說就是在要執行的函數中用事務包裹起來,在函數執行前加入initialize階段,函數執行,最后執行close階段。那么Batch Update中

在事件initialize階段,一個update queue被創建。在事件中調用setState方法時,狀態不會被立即調用,而是被push進Update queue中。

函數執行結束調用事件的close階段,Update queue會被flush,這事新的狀態才會被應用到組件上並開始后續的Virtual DOM更新,biff算法來對

model更新。

對比於React,Vue實現Batch update就簡單多了:直接借助JS中的Event Loop。(參考阮老師的http://www.ruanyifeng.com/blog/2013/10/event_loop.html)

Vue中的核心代碼就僅僅20多行,如下:

// https://github.com/vuejs/vue/blob/dev/src/core/observer/scheduler.js#L122-L148
/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 */
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}

 

當model被修改時,對應的watcher會被推入Update queue, 與此同時還會在異步隊列中添加一個task用於flush當前的Update queue。

這樣一來,當前的task中的其他watcher會被推進同一個Update queue中。當前task執行結束后,異步隊列下一個task執行,update queue

會被 flush,並進行后續的更新操作。

為了讓 flush 動作能在當前 Task 結束后盡可能早的開始,Vue 會優先嘗試將任務 micro-task 隊列,具體來說,在瀏覽器環境中 Vue 會優

先嘗試使用 MutationObserver API 或 Promise,如果兩者都不可用,則 fallback 到 setTimeout。

對比兩個框架可以發現 React 基於 Transition 實現的 Batch Query 是一個不依賴語言特性的通用模式,因此有更穩定可控的表現,但缺點

是無法完全覆蓋所有情況,例如對於如下代碼:

 

componentDidMount () {
  setTimeout(_ => {
    this.setState({ foo: 1 })
    this.setState({ foo: 2 })
    this.setState({ foo: 3 })
  }, 0)
}

由於 setTimeout 的回調函數「不受 React 控制」,其中的 setState 就無法得到優化,最終會導致 render 函數執行三次。

而 Vue 的實現則對語言特性乃至運行環境有很強的依賴,但可以更好的覆蓋各種情況:只要是在同一個 task 中的修改都可以進行 Batch Update 優化。

總結一下:

 

React 在這里的更新和事務機制使用比較通用的處理方式。

 

比如默認第一次應用初始化的時候是一次事務的進行,在用戶交互的時候是一次新的事務開始,會在同一次同步事務中標記 batchUpdate=true,這樣的做法是不破壞使用者的代碼。

 

然后如果是 Ajax,setTimeout 等要離開主線程進行異步操作的時候會脫離當前 UI 的事務,這時候再進入此次處理的時候 batchUpdate=false,所以才會 setState 幾次就 render 幾次。

 

Vue 的策略雖然在機制上雷同,但是從根本上來講是一種延遲的批量更新機制。

 

Angular 在這里也處理得很巧妙,利用 zone.js 對 task 進行攔截,對 JS 現有場景進行 AOP,這樣就成功的橋接了代碼。

 

React 的事務是純粹的 IO 模型的適配。

 

 那么Batch Update介紹到這里 ,在下一篇我們將參考React源碼來分析setState的實現過程。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM