React的setState分析

来源:https://www.cnblogs.com/isLiu/archive/2018/01/19/8313063.html
-Advertisement-
Play Games

前端框架層出不窮,不過萬變不離其宗,就是從MVC過渡到MVVM。從數據映射到DOM,angular中用的是watcher對象,vue是觀察者模式,react就是state了。 React通過管理狀態實現對組件的管理,通過this.state()方法更新state。當this.setState()被調 ...


前端框架層出不窮,不過萬變不離其宗,就是從MVC過渡到MVVM。從數據映射到DOM,angular中用的是watcher對象,vue是觀察者模式,react就是state了。

React通過管理狀態實現對組件的管理,通過this.state()方法更新state。當this.setState()被調用的時候,React會重新調用render方法來重新渲染UI。

本文針對React的SetState的源碼來進行解讀,根據陳屹老師的《深入React技術棧》加上自己的理解。

1. setState非同步更新


setState通過一個隊列機制實現state的更新。當執行setState時,會把需要更新的state合併後放入狀態隊列,而不會立刻更新this.state,利用這個隊列機制可以高效的批量的更新state。

// 將新的 state 合併到狀態更新隊列中
var nextState = this._processPendingState(nextProps, nextContext);

// 根據更新隊列和 shouldComponentUpdate 的狀態來判斷是否需要更新組件 var shouldUpdate =
 this._pendingForceUpdate ||
!inst.shouldComponentUpdate || inst.shouldComponentUpdate(nextProps, nextState, nextContext);

如果不通過setState而直接修改this.state的值,就像這樣:this.state.value = 1,那麼該state將不會被放入狀態隊列中,下次調用this.setState並對狀態隊列進行合併時,將會忽略之前直接別修改的state,因此我們應該用setState更新state的值。

2. setState迴圈調用


當調用setState時,實際上會執行enqueueSetState方法,並對partialState以及_pendingStateQueue更新隊列進行合併,最終通過enqueueUpdate執行state更新。

而performUpdateIfNecessary方法獲取_pendingElement、_pendingStateQueue、_pendingForceUpdate,並調用reciveComponent和updateComponent方法進行組件更新。

如果在shouldComponentUpdate或者componentWillUpdate方法中調用setState,此時this._pendingStateQueue != null,則perfromUpdateIfNecessary方法就會調用updateComponent方法進行組件更新,單updateComponent方法又會調用shouldComponentUpdate和componentWillUpdate方法,造成迴圈調用。

接下里我們看看setState的源碼:

// 更新 state
ReactComponent.prototype.setState = function(partialState, callback) {
    this.updater.enqueueSetState(this, partialState);
    if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState');
    } 
};

enqueueSetState: function(publicInstance, partialState) { 
    var internalInstance = getInternalInstanceReadyForUpdate(
         publicInstance,
        'setState' 
    );
    
    if (!internalInstance) { 
    return;
    }
    
    // 更新隊列合併操作
    var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
    
    queue.push(partialState);
    enqueueUpdate(internalInstance); 
},

// 如果存在 _pendingElement、_pendingStateQueue和_pendingForceUpdate,則更新組件 
performUpdateIfNecessary: function(transaction) {
    if (this._pendingElement != null) {
        ReactReconciler.receiveComponent(this, this._pendingElement, transaction, this._context);
    }
    if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
        this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context);
    } 
}

3. setState調用棧


對setState很瞭解是吧?來看看這個:

class Example extends React.Component {
  constructor() {
    super();
    this.state = {
      val: 0
    };
  }
  
  componentDidMount() {
    this.setState({val: this.state.val + 1});
    console.log(this.state.val);    // 第 1 次 log

    this.setState({val: this.state.val + 1});
    console.log(this.state.val);    // 第 2 次 log

    setTimeout(() => {
      this.setState({val: this.state.val + 1});
      console.log(this.state.val);  // 第 3 次 log

      this.setState({val: this.state.val + 1});
      console.log(this.state.val);  // 第 4 次 log
    }, 0);
  }

  render() {
    return null;
  }
};

結果是:0、0、2、3,我第一次也不懂,怎麼回事呢?

下麵是一個簡化的setState調用棧。

我們說過setState最終是通過enqueueUpdate執行state更新,enqueueUpdate代碼如下:

function enqueueUpdate(component) {
    ensureInjected();
    
    // 如果不處於批量更新模式
    if (!batchingStrategy.isBatchingUpdates) {
        batchingStrategy.batchedUpdates(enqueueUpdate, component);
        return; 
    }
    
    // 如果處於批量更新模式,則將該組件保存在 dirtyComponents 中
    dirtyComponents.push(component); 
}

如果isBatchingUpdates也就是處於批量更新模式,就把當前調用了this.setState的組件放入dirtyComponents數組中。否則的話就不是批量更新模式,則對隊列中的所有更新執行batchedUpdates方法。例子中的4次console之所以不同,這裡的邏輯判斷起了關鍵作用。

那batchingStrategy呢?其實他就是一個簡單的對象:

var ReactDefaultBatchingStrategy = { 
    isBatchingUpdates: false,
    
    batchedUpdates: function(callback, a, b, c, d, e) {
        var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
        ReactDefaultBatchingStrategy.isBatchingUpdates = true;
        
        if (alreadyBatchingUpdates) { 
            callback(a, b, c, d, e);
        } else {
            transaction.perform(callback, null, a, b, c, d, e);
        } 
    },
}

註意,batchedUpdates中有個transaction.perform調用,transaction,下麵說。

4. transaction


有人看到這裡,transaction,不就是事務嗎?保證數據一致性要用到的,然後說了幾條事務的特性,什麼原子性、穩定性,但是抱歉,這裡的事務和SQL里的事務不一樣,我個人理解這個事務有點類似於中間件,為什麼叫事務,不知道。

可以畫一張圖理解一下:

事務就是將需要執行的方法用wrapper封裝起來,再通過事務提供的perform方法執行。而再perform之前,先執行所wrapeer中的initialize方法,執行完需要執行的方法後,再執行close方法。一組initialize和close方法稱為一個wrapper,事務支持多個wrapper疊加。

React里涉及到很多高階函數,個人理解這個事務也就是一個高階函數嘛,也就是中間件的思想。

5. 解密setState


剛說了事務,那事務是怎麼導致前面所述的setState的各種不同的輸出呢?

很顯然,我們可以將4次setState簡單規成兩類,componentDidMount是一類,setTimeOut中的又是一類,因為這兩次在不同的調用棧中執行。

我們先看看在componentDidMount中setState的調用棧:

再看看在setTimeOut中的調用棧:

顯然,在componentDidMount中調用setState的調用棧更加複雜,那我們重點看看在componentDidMount中的調用棧,發現了batchedUpdates方法,原來在setState調用之前,就已經處於batchedUpdates執行的事務之中了。

那batchedUpdates方法是誰調用的呢?我們再往上追溯一層,原來是ReactMount.js中的_renderNewRootComponent方法。也就是說,整個將React組件渲染到DOM的過程就處於一個大的事務中了。

接下來就可以理解了,因為在componentDidMount中調用setState時,batchingStrategy的isBatchingUpdates已經被設置為true了,所以兩次setState的結果並沒有立即生效,而是被放進了dirtyComponents中。這也解釋了兩次列印this.state.val都是0的原因,因為新的state還沒被應用到組件中。

在看setTimeOut中的兩次setState,因為沒有前置的batchedUpdate調用,所以batchingStrategy的isBatchingUpdates標誌位是false,也就導致了新的state馬上生效,沒有走到dirtyComponents分支。也就是說,setTimeOut中的第一次執行,setState時,this.state.val為1,而setState完成後列印時this.state.val變成了2。第二次的setState同理。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 主鍵: @tableName --表名 @id 表對應的id SELECT SYSCOLUMNS.name FROM SYSCOLUMNS,SYSOBJECTS,SYSINDEXES,SYSINDEXKEYS WHERE SYSCOLUMNS.id = object_id(@tableName) A ...
  • 學習目標 資料庫管理員(DBA)任務和工具 Oracle軟體安裝 Oracle Grid Infrastructure安裝 Oracle DB安裝 Oracle DB管理員的任務 設計、實施和維護Oracle資料庫任務: 1.評估資料庫伺服器硬體 2.制定Oracle軟體安裝計劃 3.制定資料庫和安 ...
  • 一:需求簡介 之前boss提出一個需求,運行在廣告機上的app,需要完成自動升級的功能,廣告機是非觸摸屏的,不能通過手動點擊,所以app必須做到自動下載,自動安裝升級,並且安裝完成後,app還要繼續運行,最好不藉助其它app來實現以上功能。 二:實現思路 實現這個功能第一個想到的方法就是靜默安裝,由 ...
  • 原文鏈接:一句代碼載入網路圖片到ImageView——Android Picasso 在這裡介紹一個Android框架:Picasso。 picasso是Square公司開源的一個Android圖形緩存庫,地址http://square.github.io/picasso/,可以實現圖片下載和緩存功 ...
  • 這是一個點擊之後反應超時的ANR 初步判斷是系統和服務占用資源太多,引起原生設置的ANR在原生設置“語言和輸入法”界面點擊返回鍵是在10:24:52.563,原生設置是在10:24:52.723結束,公司設置是在10:24:57.238才收到onConfigurationChanged回調,快5秒了 ...
  • 。。。求期末不掛。。。 今天完成了第四題,邏輯稍微有點糾纏,但還好問題不是很多 邏輯其實挺複雜的,也可能我基礎太差,還有就是沒來得及寫註釋,很傷。。。 這裡糾結了蠻久的,一直搞不懂這個remove的用法是什麼,好像我裡面沒寫DOM節點的數值也可以刪除。。。 其實想那個刪除對應數值的節點的時候也卡了很 ...
  • 1, 轉義字元 轉義字元:用於表示網頁中的特殊字元 XHTML不直接輸入符號,建議使用轉義字元。 &nbsp 空格; &copy 版權; &reg 註冊商標 如果輸入連續的空格要使用&的轉義字元&(&amp;)nbsp,即&amp;nbsp; 2, 水平線 <Hr> 水平線 <hr width="5 ...
  • xss攻擊(跨站腳本) 是網站應用程式的安全泄露攻擊,是代碼註入的一種。它允許惡意用戶將代碼註入到網頁上,其他用戶在觀看網頁時就會受到影響。 攻擊原理 其特點是不對伺服器端造成任何傷害,而是通過一些正常的站內交互途徑,例如發佈評論,提交含有 JavaScript 的內容文本。這時伺服器端如果沒有過濾 ...
一周排行
    -Advertisement-
    Play Games
  • 前言 在我們開發過程中基本上不可或缺的用到一些敏感機密數據,比如SQL伺服器的連接串或者是OAuth2的Secret等,這些敏感數據在代碼中是不太安全的,我們不應該在源代碼中存儲密碼和其他的敏感數據,一種推薦的方式是通過Asp.Net Core的機密管理器。 機密管理器 在 ASP.NET Core ...
  • 新改進提供的Taurus Rpc 功能,可以簡化微服務間的調用,同時可以不用再手動輸出模塊名稱,或調用路徑,包括負載均衡,這一切,由框架實現並提供了。新的Taurus Rpc 功能,將使得服務間的調用,更加輕鬆、簡約、高效。 ...
  • 順序棧的介面程式 目錄順序棧的介面程式頭文件創建順序棧入棧出棧利用棧將10進位轉16進位數驗證 頭文件 #include <stdio.h> #include <stdbool.h> #include <stdlib.h> 創建順序棧 // 指的是順序棧中的元素的數據類型,用戶可以根據需要進行修改 ...
  • 前言 整理這個官方翻譯的系列,原因是網上大部分的 tomcat 版本比較舊,此版本為 v11 最新的版本。 開源項目 從零手寫實現 tomcat minicat 別稱【嗅虎】心有猛虎,輕嗅薔薇。 系列文章 web server apache tomcat11-01-官方文檔入門介紹 web serv ...
  • C總結與剖析:關鍵字篇 -- <<C語言深度解剖>> 目錄C總結與剖析:關鍵字篇 -- <<C語言深度解剖>>程式的本質:二進位文件變數1.變數:記憶體上的某個位置開闢的空間2.變數的初始化3.為什麼要有變數4.局部變數與全局變數5.變數的大小由類型決定6.任何一個變數,記憶體賦值都是從低地址開始往高地 ...
  • 如果讓你來做一個有狀態流式應用的故障恢復,你會如何來做呢? 單機和多機會遇到什麼不同的問題? Flink Checkpoint 是做什麼用的?原理是什麼? ...
  • C++ 多級繼承 多級繼承是一種面向對象編程(OOP)特性,允許一個類從多個基類繼承屬性和方法。它使代碼更易於組織和維護,並促進代碼重用。 多級繼承的語法 在 C++ 中,使用 : 符號來指定繼承關係。多級繼承的語法如下: class DerivedClass : public BaseClass1 ...
  • 前言 什麼是SpringCloud? Spring Cloud 是一系列框架的有序集合,它利用 Spring Boot 的開發便利性簡化了分散式系統的開發,比如服務註冊、服務發現、網關、路由、鏈路追蹤等。Spring Cloud 並不是重覆造輪子,而是將市面上開發得比較好的模塊集成進去,進行封裝,從 ...
  • class_template 類模板和函數模板的定義和使用類似,我們已經進行了介紹。有時,有兩個或多個類,其功能是相同的,僅僅是數據類型不同。類模板用於實現類所需數據的類型參數化 template<class NameType, class AgeType> class Person { publi ...
  • 目錄system v IPC簡介共用記憶體需要用到的函數介面shmget函數--獲取對象IDshmat函數--獲得映射空間shmctl函數--釋放資源共用記憶體實現思路註意 system v IPC簡介 消息隊列、共用記憶體和信號量統稱為system v IPC(進程間通信機制),V是羅馬數字5,是UNI ...