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

1、請先看官方文檔

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

React英文文檔前端

React中文文檔react

2、setState()的實踐與問題

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

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>
    );
  }
}複製代碼

結果倒是1github



爲何會只加1?
數組

看官網這句話
bash

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
app

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);
複製代碼



3、setState源碼世界

相信能到這裏的同窗都知道了setState()是個`既能同步又能異步`的方法了,那具體何時是同步的,何時是異步的?異步

去源碼裏面看實現是比較靠譜的方式。  函數

注:這裏說的同步和異步只是「實現上看起來像同步仍是異步,好比上面答案二setTimeout裏面,看起來就是同步的」,實質上setState()仍是異步的 無論這裏看不看得懂都不要緊了,立刻進入源碼的世界。 

一、如何快速查看react源碼

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

react-github倉庫

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


到目前我看爲止,最新的版本是16.2.0,我選了15.6.0的代碼 

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

二來,我水平有限,若是寫的實在不清晰,同窗們還能夠參考着其餘人的分析文章一塊兒讀,而不至於徹底理解不了

如何切換版本? 

一、找到對應版本號


二、複製15.6.0的歷史記錄號 


三、回滾

git reset --hard 911603b
複製代碼

如圖,成功回滾到15.6.0版本



二、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


這個文件導出了一個ReactUpdateQueue對象,「react更新隊列」,代碼名字起的好能夠自帶註釋,說的就是這種大做吧,在這裏註冊了enqueueSetState方法


三、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方法


定義以下

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中一圖 



四、核心: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 實現的

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


五、事務

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



簡單地說,一個所謂的 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
複製代碼


六、核心分析: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);
複製代碼

一、首先,點擊事件的處理自己就是在一個大的事務中(這個記着就好),isBatchingUpdates已是true了  


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


三、在setState執行的時候isBatchingUpdates已是true了,setState作的就是將更新都統一push到dirtyComponents數組中; 


四、在事務結束的時候才經過 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。


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

var transaction = new ReactDefaultBatchingStrategyTransaction();
複製代碼

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


到這裏,咱們會發現,前面全部的隊列、batchUpdate等等都是爲了來到事務的這一步,前面都只是批收集的工做,到這裏才真正的完成了批更新的操做。


七、再回到最初的題目

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博客】

相關文章
相關標籤/搜索