React - setState原始碼分析(小白可讀)

amandakelake發表於2018-03-09

一、請先看官方文件

上來先看官方文件中對setState()的定義 英文文件最佳

React英文文件

React中文文件

二、setState()的實踐與問題

先看個最簡單的問題,點選按鈕後,count是加2嗎? 

class NextPage extends Component<Props> {
  static navigatorStyle = {
    tabBarHidden: true
  };

  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  add() {
    this.setState({
      count: this.state.count + 1
    });
    this.setState({
      count: this.state.count + 1
    });
  }

  render() {
    return (
      <View style={styles.container}>
        <TouchableOpacity
          style={styles.addBtn}
          onPress={() => {
            this.add();
          }}
        >
          <Text style={styles.btnText}>點選+2</Text>
        </TouchableOpacity>

        <Text style={styles.commonText}>當前count {this.state.count}</Text>
      </View>
    );
  }
}複製程式碼

結果卻是1

React - setState原始碼分析(小白可讀)


為什麼會只加1?

看官網這句話

setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater, callback)), either of which are guaranteed to fire after the update has been applied. If you need to set the state based on the previous state, read about the updater argument below.

重點是前兩句,翻譯過來就是 setState()並不總是立即更新元件,它可能會進行批處理或者推遲更新。這使得在呼叫setState()之後立即讀取this.state成為一個潛在的隱患。 先直接丟擲點選按鈕加2的正確答案吧,下面兩種方法都OK

this.setState(preState => {
  return {
    count: preState.count + 1
  };
});
this.setState(preState => {
  return {
    count: preState.count + 1
  };
});
複製程式碼

setTimeout(() => {
  this.setState({
    count: this.state.count + 1
  });
  this.setState({
    count: this.state.count + 1
  });
}, 0);
複製程式碼

React - setState原始碼分析(小白可讀)


三、setState原始碼世界

相信能到這裡的同學都知道了setState()是個`既能同步又能非同步`的方法了,那具體什麼時候是同步的,什麼時候是非同步的?

去原始碼裡面看實現是比較靠譜的方式。 

注:這裡說的同步和非同步只是“實現上看起來像同步還是非同步,比如上面答案二setTimeout裡面,看起來就是同步的”,實質上setState()還是非同步的 不管這裡看不看得懂都沒關係了,馬上進入原始碼的世界。 

1、如何快速檢視react原始碼

上react的github倉庫,直接clone下來

react-github倉庫

git clone https://github.com/facebook/react.git
複製程式碼


到目前我看為止,最新的版本是16.2.0,我選了15.6.0的程式碼 

一是為了參考前輩們的分析成果 

二來,我水平有限,如果寫的實在不清晰,同學們還可以參考著其他人的分析文章一起讀,而不至於完全理解不了

如何切換版本? 

1、找到對應版本號

React - setState原始碼分析(小白可讀)

2、複製15.6.0的歷史記錄號 

React - setState原始碼分析(小白可讀)

3、回滾

git reset --hard 911603b
複製程式碼

如圖,成功回滾到15.6.0版本

React - setState原始碼分析(小白可讀)


2、setState入口 => enqueueSetState

核心原則:既然是看原始碼,那當然就不是一行一行的讀程式碼,而是看核心的思想,所以接下來的程式碼都只會放核心程式碼,旁枝末節只提一下或者忽略


setState的入口檔案在src/isomorphic/modern/class/ReactBaseClasses.js

React元件繼承自React.Component,而setState是React.Component的方法,因此對於元件來講setState屬於其原型方法 

ReactComponent.prototype.setState = function(partialState, callback) {
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState');
  }
};
複製程式碼

partialState顧名思義-“部分state”,這取名,大概就是想不影響原來的state的意思吧


當呼叫setState時實際上是呼叫了enqueueSetState方法,我們順藤摸瓜(我用的是vscode的全域性搜尋),找到了這個檔案src/renderers/shared/stack/reconciler/ReactUpdateQueue.js

React - setState原始碼分析(小白可讀)

這個檔案匯出了一個ReactUpdateQueue物件,“react更新佇列”,程式碼名字起的好可以自帶註釋,說的就是這種大作吧,在這裡註冊了enqueueSetState方法


3、enqueueSetState => enqueueUpdate

先看enqueueSetState的定義

  enqueueSetState: function(publicInstance, partialState) {
    var internalInstance = getInternalInstanceReadyForUpdate(
      publicInstance,
      'setState',
    );
	
    var queue =
      internalInstance._pendingStateQueue ||
      (internalInstance._pendingStateQueue = []);
    queue.push(partialState);

    enqueueUpdate(internalInstance);
  },
複製程式碼

這裡只需要關注internalInstance的兩個屬性 

_pendingStateQueue:待更新佇列 

_pendingCallbacks: 更新回撥佇列 

如果_pendingStateQueue的值為null,將其賦值為空陣列[],並將partialState放入待更新state佇列_pendingStateQueue,最後執行enqueueUpdate(internalInstance)

接下來看enqueueUpdate

function enqueueUpdate(internalInstance) {
  ReactUpdates.enqueueUpdate(internalInstance);
}
複製程式碼

它執行的是ReactUpdates的enqueueUpdate方法

var ReactUpdates = require('ReactUpdates');
複製程式碼

這個檔案剛好就在旁邊src/renderers/shared/stack/reconciler/ReactUpdates.js

找到enqueueUpdate方法

React - setState原始碼分析(小白可讀)

定義如下

function enqueueUpdate(component) {
  ensureInjected();

  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }

  dirtyComponents.push(component);
  if (component._updateBatchNumber == null) {
    component._updateBatchNumber = updateBatchNumber + 1;
  }
}
複製程式碼


這段程式碼對於理解setState非常重要

if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }
dirtyComponents.push(component);
複製程式碼

判斷batchingStrategy.isBatchingUpdates batchingStrategy是批量更新策略,isBatchingUpdates表示是否處於批量更新過程,開始預設值為false


上面這句話的意思是: 

如果處於批量更新模式,也就是isBatchingUpdates為true時,不進行state的更新操作,而是將需要更新的component新增到dirtyComponents陣列中;

如果不處於批量更新模式,對所有佇列中的更新執行batchedUpdates方法,往下看下去就知道是用事務的方式批量的進行component的更新,事務在下面。

借用《深入React技術棧》Page167中一圖 

React - setState原始碼分析(小白可讀)


4、核心:batchedUpdates => 呼叫transaction

那batchingStrategy.isBatchingUpdates又是怎麼回事呢?看來它才是關鍵

但是,batchingStrategy 物件並不好找,它是通過 injection 方法注入的,一番尋找,發現了 batchingStrategy 就是 ReactDefaultBatchingStrategy。 

src/renderers/shared/stack/reconciler/ReactDefaultBatchingStrategy.js具體怎麼找檔案,又屬於另一個範疇了,我們今天只專注 setState,其他的容後再說吧 


相信部分同學看到這裡已經有些迷糊了,沒關係,再堅持一下,旁枝末節先不管,只知道我們找到了核心方法batchedUpdates,馬上要勝利了,別放棄(我第一次看也是這樣熬過來的,一遍不行就兩遍,大不了看多幾遍又如何)


先看批量更新策略-batchingStrategy,它到底是什麼

var ReactDefaultBatchingStrategy = {
  isBatchingUpdates: false,

  batchedUpdates: function(callback, a, b, c, d, e) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;

    ReactDefaultBatchingStrategy.isBatchingUpdates = true;

    if (alreadyBatchingUpdates) {
      return callback(a, b, c, d, e);
    } else {
      return transaction.perform(callback, null, a, b, c, d, e);
    }
  },
};

module.exports = ReactDefaultBatchingStrategy;
複製程式碼

終於找到了,isBatchingUpdates屬性和batchedUpdates方法


如果isBatchingUpdates為true,當前正處於更新事務狀態中,則將Component存入dirtyComponent中, 否則呼叫batchedUpdates處理,發起一個transaction.perform()

注:所有的 batchUpdate 功能都是通過執行各種 transaction 實現的

這是事務的概念,先了解一下事務吧


5、事務

這一段就直接引用書本里面的概念吧,《深入React技術棧》Page169

React - setState原始碼分析(小白可讀)


簡單地說,一個所謂的 Transaction 就是將需要執行的 method 使用 wrapper 封裝起來,再通過 Transaction 提供的 perform 方法執行。而在 perform 之前,先執行所有 wrapper 中的 initialize 方法;perform 完成之後(即 method 執行後)再執行所有的 close 方法。一組 initialize 及 close 方法稱為一個 wrapper,從上面的示例圖中可以看出 Transaction 支援多個 wrapper 疊加。


具體到實現上,React 中的 Transaction 提供了一個 Mixin 方便其它模組實現自己需要的事務。而要使用 Transaction 的模組,除了需要把 Transaction 的 Mixin 混入自己的事務實現中外,還需要額外實現一個抽象的 getTransactionWrappers 介面。這個介面是 Transaction 用來獲取所有需要封裝的前置方法(initialize)和收尾方法(close)的,因此它需要返回一個陣列的物件,每個物件分別有 key 為 initialize 和 close 的方法。 


下面這段程式碼應該能幫助理解

var Transaction = require('./Transaction');

// 我們自己定義的 Transaction
var MyTransaction = function() {
  // do sth.
};

Object.assign(MyTransaction.prototype, Transaction.Mixin, {
  getTransactionWrappers: function() {
    return [{
      initialize: function() {
        console.log('before method perform');
      },
      close: function() {
        console.log('after method perform');
      }
    }];
  };
});

var transaction = new MyTransaction();
var testMethod = function() {
  console.log('test');
}
transaction.perform(testMethod);

// before method perform
// test
// after method perform
複製程式碼


6、核心分析:batchingStrategy 批量更新策略

回到batchingStrategy:批量更新策略,再看看它的程式碼實現

var ReactDefaultBatchingStrategy = {
  isBatchingUpdates: false,

  batchedUpdates: function(callback, a, b, c, d, e) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;

    ReactDefaultBatchingStrategy.isBatchingUpdates = true;

    if (alreadyBatchingUpdates) {
      return callback(a, b, c, d, e);
    } else {
      return transaction.perform(callback, null, a, b, c, d, e);
    }
  },
};
複製程式碼


可以看到isBatchingUpdates的初始值是false的,在呼叫batchedUpdates方法的時候會將isBatchingUpdates變數設定為true。然後根據設定之前的isBatchingUpdates的值來執行不同的流程


還記得上面說的很重要的那段程式碼嗎

if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }
dirtyComponents.push(component);
複製程式碼

1、首先,點選事件的處理本身就是在一個大的事務中(這個記著就好),isBatchingUpdates已經是true了  


2、呼叫setState()時,呼叫了ReactUpdates.batchedUpdates用事務的方式進行事件的處理  


3、在setState執行的時候isBatchingUpdates已經是true了,setState做的就是將更新都統一push到dirtyComponents陣列中; 


4、在事務結束的時候才通過 ReactUpdates.flushBatchedUpdates 方法將所有的臨時 state merge 並計算出最新的 props 及 state,然後將批量執行關閉結束事務。


到這裡我並沒有順著ReactUpdates.flushBatchedUpdates方法講下去,這部分涉及到渲染和Virtual Dom的內容,反正你知道它是拿來執行渲染的就行了。 

到這裡為止,setState的核心概念已經比較清楚了,再往下的內容,暫時先知道就行了,不然展開來講一環扣一環太雜了,我們做事情要把握核心。 


到這裡不知道有沒有同學想起一個問題 ?

isBatchingUpdates 標誌位在 batchedUpdates 發起的時候被置為 true ,那什麼時候被複位為false的呢?

還記得上面的事務的close方法嗎,同一個檔案src/renderers/shared/stack/reconciler/ReactDefaultBatchingStrategy.js

// 定義復位 wrapper
var RESET_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: function () {
    ReactDefaultBatchingStrategy.isBatchingUpdates = false;
  }
};

// 定義批更新 wrapper
var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
};

var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];

function ReactDefaultBatchingStrategyTransaction() {
  this.reinitializeTransaction();
}

_assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction, {
  getTransactionWrappers: function () {
    return TRANSACTION_WRAPPERS;
  }
});
複製程式碼

相信眼尖的同學已經看到了,close的時候復位,把isBatchingUpdates設定為false。

React - setState原始碼分析(小白可讀)

Object.assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction, {
  getTransactionWrappers: function() {
    return TRANSACTION_WRAPPERS;
  },
});

var transaction = new ReactDefaultBatchingStrategyTransaction();
複製程式碼

通過原型合併,事務的close 方法,將在 enqueueUpdate 執行結束後,先把 isBatchingUpdates 復位,再發起一個 DOM 的批更新  


到這裡,我們會發現,前面所有的佇列、batchUpdate等等都是為了來到事務的這一步,前面都只是批收集的工作,到這裡才真正的完成了批更新的操作。


7、再回到最初的題目

add() {
    this.setState({
      count: this.state.count + 1
    });
    this.setState({
      count: this.state.count + 1
    });
  }
複製程式碼

setTimeout(() => {
  this.setState({
    count: this.state.count + 1
  });
  this.setState({
    count: this.state.count + 1
  });
}, 0);
複製程式碼


第一種情況,在執行第一個setState時,本身已經處於一個點選事件觸發的這個大事務中,已經觸發了一個batchedUpdates,isBatchingUpdates為true,所以兩個setState都會被批量更新,這時候屬於非同步過程,this.state並沒有立即改變,執行setState只是相當於把partialState(前面說的部分state)傳入dirtyComponents,最後在事務的close階段執行flushBatchedUpdates去重新渲染。


第二種情況,有了setTimeout,兩次setState都會在點選事件觸發的大事務中的批量更新batchedUpdates結束之後再執行,所以他們會觸發兩次批量更新batchedUpdates,也就會執行兩個事務和函式flushBatchedUpdates,就相當於同步更新的過程了。


後話

感謝您耐心看到這裡,希望有所收穫!

如果不是很忙的話,麻煩點個star⭐【Github部落格傳送門】,舉手之勞,卻是對作者莫大的鼓勵。

我在學習過程中喜歡做記錄,分享的是自己在前端之路上的一些積累和思考,希望能跟大家一起交流與進步,更多文章請看【amandakelake的Github部落格】



相關文章