ReactJS底層揭祕

雲棲大講堂發表於2018-06-04
譯者 翻譯章節
Candy Zheng 3、4、5、14、8、9、10、11、12、14
undead25 主頁、介紹、0
Tina92 6、13
HydeSong 1
bambooom 7
ahonn 2

ReactJS 底層揭祕

本文包含 ReactJS 內部工作原理的說明。實際上,我在除錯整個程式碼庫時,將所有的邏輯放在視覺化的流程圖上,對它們進行分析,然後總結和解釋主要的概念和方法。我已經完成了 Stack 版本,現在我在研究下一個版本 —— Fiber。

通過 github-pages 網站來以最佳格式閱讀.

為了讓它變得更好,如果你有任何想法,歡迎隨時提 issue。

每張流程圖都可以通過點選在新的選項卡中開啟,然後通過縮放使它適合閱讀。在單獨的視窗(選項卡)中保留文章和正在閱讀的流程圖,將有助於更容易地匹配文字和程式碼流。

我們將在這裡談論 ReactJS 的兩個版本,老版本使用的是 Stack 協調引擎,新版本使用的是 Fiber(你可能已經知道,React v16 已經正式釋出了)。讓我們先深入地瞭解(目前廣泛使用的)React-Stack 的工作原理,並期待下 React-Fiber 帶來的重大變革。我們使用 React v15.4.2 來解釋“舊版 React”的工作原理。

概覽

整個流程圖分為 15 個部分,讓我們開始學習歷程吧。

介紹

初識流程圖

圖 介紹-0:整體流程

你可以先花點時間看下整體的流程。雖然看起來很複雜,但它實際上只描述了兩個流程:(元件的)掛載和更新。我跳過了解除安裝,因為它是一種“反向掛載”,而且刪除這部分簡化了流程圖。另外,這圖並不是100% 同原始碼匹配,而只是描述架構的主要部分。總體來說,它大概是原始碼的 60%,而另外的 40% 沒有多少視覺價值,為了簡單起見,我省略了那部分。

乍一看,你可能會注意到流程圖中有很多顏色。每個邏輯項(流程圖上的形狀)都以其父模組的顏色高亮顯示。例如,如果是從紅色的 模組 B 呼叫 方法 A,那 方法 A 也是紅色的。以下是流程圖中模組的圖例以及每個檔案的路徑。

圖 介紹-1:模組顏色

讓我們把它們放在一張流程圖中,看看模組之間的依賴關係

圖 介紹-2 模組依賴關係

你可能知道,React 是為支援多種環境而構建的。

  • 移動端(ReactNative
  • 瀏覽器(ReactDOM
  • 服務端渲染
  • ReactART(使用 React 繪製向量圖形)
  • 其它

因此,一些檔案實際上比上面流程圖中列出的要更大。以下是包含多環境支援的相同的流程圖。

介紹 圖-3 多平臺模組依賴關係

如你所見,有些項似乎翻倍了。這表明它們對每個平臺都有一個獨立的實現。讓我們來看一些簡單例子,例如 ReactEventListener,顯然,不同平臺會有不同的實現。從技術上講,你可以想象,這些依賴於平臺的模組,應該以某種方式注入或連線到當前的邏輯流程中。實際上有很多這樣的注入器,因為它們的用法是標準組合模式的一部分。同樣,為了簡單起見,我選擇忽略它們。

讓我們來學習下常規瀏覽器React DOM 的邏輯流程。這是最常用的平臺,並完全覆蓋了所有 React 的架構設計理念。

程式碼示例

學習框架或者庫的原始碼的最佳方式是什麼?沒錯,研讀並除錯原始碼。那好,我們將要除錯這兩個流程ReactDOM.rendercomponent.setState 這兩者對應了元件的掛載和更新。讓我們來看一下我們能編寫一些什麼樣的程式碼來開始學習。我們需要什麼呢?或許幾個具有簡單渲染的小元件就可以了,因為更容易除錯。

class ChildCmp extends React.Component {
    render() {
        return <div> {this.props.childMessage} </div>
    }
}

class ExampleApplication extends React.Component {
    constructor(props) {
        super(props);
        this.state = {message: `no message`};
    }

    componentWillMount() {
        //...
    }

    componentDidMount() {
        /* setTimeout(()=> {
            this.setState({ message: `timeout state message` });
        }, 1000); */
    }

    shouldComponentUpdate(nextProps, nextState, nextContext) {
        return true;
    }

    componentDidUpdate(prevProps, prevState, prevContext) {
        //...
    }

    componentWillReceiveProps(nextProps) {
        //...
    }

    componentWillUnmount() {
        //...
    }

    onClickHandler() {
        /* this.setState({ message: `click state message` }); */
    }

    render() {
        return <div>
            <button onClick={this.onClickHandler.bind(this)}> set state button </button>
            <ChildCmp childMessage={this.state.message} />
            And some text as well!
        </div>
    }
}

ReactDOM.render(
    <ExampleApplication hello={`world`} />,
    document.getElementById(`container`),
    function() {}
);複製程式碼

我們已經準備好開始學習了。讓我們先來分析流程圖中的第一部分。一個接一個,我們會將它們全部分析完。

第 0 部分

圖 0-0

ReactDOM.render

讓我們從 ReactDOM.render 的呼叫開始。

入口點是 ReactDom.render,我們的應用程式是從這裡開始渲染到 DOM 中的。為了方便除錯,我建立了一個簡單的 <ExampleApplication /> 元件。因此,發生的第一件事就是 JSX 會被轉換成 React 元件。它們是簡單的、直白的物件。具有簡單的結構。它們僅僅展示從本元件渲染中返回的內容,沒有其他了。一些欄位應該是你已經熟悉的,像 props、key 和 ref。屬性型別是指由 JSX 描述的標記物件。所以,在我們的例子中,它就是 ExampleApplication 類,但是它也可以僅僅是 Button 標籤的 button 字串等其他類。另外,在 React 元件建立過程中,它會將 defaultPropsprops 合併(如果顯式宣告瞭),並驗證 propTypes

更多詳細資訊可參考原始碼:srcisomorphicclassicelementReactElement.js

ReactMount

你可以看到一個叫做 ReactMount(01)的模組。它包含元件掛載的邏輯。實際上,在 ReactDOM 裡面沒有邏輯,它只是一個與ReactMount 一起使用的介面,所以當你呼叫 ReactDOM.render 的時候,實際上呼叫了 ReactMount.render。那“掛載”指的是什麼呢?

掛載是初始化 React 元件的過程。該過程通過建立元件所代表的 DOM 元素,並將它們插入到提供的 container 中來實現。

至少原始碼中的註釋是這樣描述的。那這真實的含義是什麼呢?好吧,讓我們想象一下下方的轉換:

圖 0-1 JSX 到 HTML

React 需要將你的元件描述轉換為 HTML 以將其放入到 DOM 中。那怎樣才能做到呢?沒錯,它需要處理所有的屬性、事件監聽、內嵌的元件和邏輯。它需要將你的高階描述(元件)轉換成實際可以放入到網頁中的低階資料(HTML)。這就是真正的掛載過程。

圖 0-2 JXS 到 HTML 2

讓我們繼續深入下去。接下來是有趣的事實時間!是的,讓我們在探索過程中新增一些有趣的東西,讓它變得更“有趣”。

有趣的事實:確保滾動正在監聽(02)

有趣的是,在第一次渲染根元件時,React 初始化滾動監聽並快取滾動值,以便應用程式程式碼可以訪問它們而不觸發重排。實際上,由於瀏覽器渲染機制的不同,一些 DOM 值不是靜態的,因此每次在程式碼中使用它們時都會進行計算。當然,這會影響效能。事實上,這隻影響了不支援pageXpageY 的舊版瀏覽器。React 也試圖優化這一點。可以看到,製作一個執行快速的工具需要使用很多技術,這個滾動就是一個很好的例子。

例項化 React 元件

看下流程圖,在圖中(03)處標明瞭一個建立的例項。在這裡建立一個 <ExampleApplication /> 的例項還為時過早。實際上該處例項化了 TopLevelWrapper(一個 React 內部的類)。讓我們先來看看下面這個流程圖。

圖 0-3 JSX 到 虛擬 DOM

你可以看到有三個部分,JSX 會被轉換為 React 內部三種元件型別中的一種:ReactCompositeComponent(我們自定義的元件),ReactDOMComponent(HTML 標籤)和 ReactDOMTextComponent(文字節點)。我們將略過描述ReactDOMTextComponent 並將重點放在前兩個。

內部元件?這很有趣。你已經聽說過 虛擬 DOM 了吧?虛擬 DOM 是一種 DOM 的表現形式。 React 用虛擬 DOM 進行元件差異計算等過程。該過程中無需直接操作 DOM 。這使得 React 在更新檢視時候更快。但在 React 的原始碼中沒有名為“Virtual DOM”的檔案或者類。這是因為 虛擬DOM 只是一個概念,一種如何操作真實 DOM 的方法。所以,有些人說 虛擬DOM 元素等同於 React 元件,但在我看來,這並不完全正確。我認為虛擬 DOM 指的是這三個類:ReactCompositeComponentReactDOMComponentReactDOMTextComponent。後面你會知道到為什麼。

好了,讓我們在這裡完成例項化過程。我們將建立一個 ReactCompositeComponent 例項,但實際上這並不是因為我們把<ExampleApplication /> 放在了 ReactDOM.render 裡。React 總是從 TopLevelWrapper 開始渲染一棵元件的樹。它幾乎是一個空的包裝器,其 render 方法(元件的 render)隨後將返回 <ExampleApplication />

//src
enderersdomclientReactMount.js#277
TopLevelWrapper.prototype.render = function () {
  return this.props.child;
};複製程式碼

所以,目前為止只有 TopLevelWrapper 被建立了。但是……先看一下一個有趣的事實。

有趣的事實:驗證 DOM 內巢狀

幾乎每次內嵌的元件渲染時,都被一個專門用於進行 HTML 驗證的 validateDOMNesting 模組驗證。DOM 內嵌驗證指的是 子標籤 -> 父標籤 的標籤層級的驗證。例如,如果父標籤是 <select>,則子標籤應該是以下其中一個標籤:optionoptgroup 或者 #text。這些規則實際上是在 html.spec.whatwg.org/multipage/s… 中定義的。你可能已經看到過這個模組是如何工作的,它像這樣報錯:
<div> cannot appear as a descendant of <p> .

小結

讓我們回顧一下上面的內容。再看一下流程圖,然後刪除多餘的不太重要的部分,變成下面這樣:

圖 0-4 簡述

再調整一下間距和對齊:

圖 0-5 簡述和調整

實際上,這就是本部分的所有內容。因此,我們可以從 第 0 部分 中得到重點,並將它用於最終的 mounting 流程中:

圖 0-6 重點

第 1 部分

1.0 第 1 部分(點選檢視大圖)

事務

某一元件例項應該以某種方式連線入React的生態系統,並對該系統產生一些影響。有一個專門的模組名為 ReactUpdates 專職於此。 正如大家所知, React 以塊形式執行更新,這意味著它會收集一些操作然後統一執行。
這樣做更好,因為這樣允許為整個塊只應用一次某些前置條件後置條件,而不是為塊中的每個操作都應用。

什麼真正執行了這些前/後處理?對, 事務!對某些人來說,事務可能是一個新術語,至少對UI方面來說是個新的含義。接下來我們從一個簡單的例子開始再來談一下它。

想象一下 通訊通道。你需要開啟連線,傳送訊息,然後關閉連線。 如果你按這個方式逐個傳送訊息,就要每次傳送訊息的時候建立、關閉連線。不過,你也可以只開啟一次連線,傳送所有掛起的訊息然後關閉連線。

1.1 非常真實的事務示例 (檢視大圖)

好的,讓我們再想想更多抽象的東西。想象一下,在執行操作期間,“傳送訊息”是您要執行的任何操作,“開啟/關閉連線”是預處理/後處理。 然後,再想想一下,你可以分別定義任何 open/close 對,並使用任何方法來使用它們(我們可以將它們命名為 wrapper ,因為事實上每一對都包裝動作方法)。聽起來很酷,不是嗎?

我們回到 React。 事務是 React 中廣泛使用的模式。除了包裝行為外,事務允許應用程式重置事務流,如果某事務已在進行中則阻止同時執行,等等。有很多不同的事務類,它們每個都描述具體的行為,它們都繼承自Transaction 模組。事務類之間的主要區別是具體的事務包裝器的列表的不同。包裝器只是一個包含初始化和關閉方法的物件。

所以,我的想法是

  • 呼叫每個 wrapper.initialize 方法並快取返回結果(可以進一步使用)
  • 呼叫事務方法本身
  • 呼叫每個 wrapper.close 方法

1.2 事務實現 (點選檢視大圖)

我們來看看 React 中的一些其他事務用例

  • 在差分對比更新渲染步驟的前後,保留輸入選取的範圍,即使在發生意外錯誤的情況下也能儲存。
  • 在重排DOM時,停用事件,防止模糊/焦點選中,同時保證事件系統在 DOM 重排後重新啟動。
  • 在 worker 執行緒完成了差分對比更新演算法後,將一組選定的 DOM 變化直接應該用到 UI 主執行緒上。
  • 在渲染新內容後觸發任何收集到的 componentDidUpdate 回撥。

讓我們回到具體案例。

正如我們看到的, React 使用 ReactDefaultBatchingStrategyTransaction (1)。我們前文提到過,事務最重要的是它的包裝器。所以,我們可以看看包裝器,並弄清楚具體被定義的事務。好,這裡有兩個包裝器:FLUSH_BATCHED_UPDATESRESET_BATCHED_UPDATES。我們來看它們的程式碼:

//src
endererssharedstack
econcilerReactDefaultBatchingStrategy.js#19
var RESET_BATCHED_UPDATES = {
      initialize: emptyFunction,
      close: function() {
        ReactDefaultBatchingStrategy.isBatchingUpdates = false;
      },
};

var FLUSH_BATCHED_UPDATES = {
     initialize: emptyFunction,
     close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates),
}

var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];複製程式碼

所以,你可以看看事務的寫法。此程式碼中事務沒有前置條件。 initialize 方法是空的,但其中一個 close 方法很有趣。它呼叫了ReactUpdates.flushBatchedUpdates。 這意味著什麼? 它實際上對對髒元件的驗證進一步重新渲染。所以,你理解了,對嗎?我們呼叫 mount 方法並將其包裝在這個事務中,因為在 mount 執行後,React 檢查已載入的元件對環境有什麼影響並執行相應的更新。

我們來看看包裝在該事務中的方法。 事實上,它引發了另外一個事務…

第 1 部分我們講完了

我們來回顧一下我們學到的。我們再看一下這種模式,然後去掉冗餘的部分:

1.3 第 1 部分簡化版 (點選檢視大圖)

然後我們適當再調整一下:

1.4 第 1 部分簡化和重構 (點選檢視大圖)

很好,實際上,下面的示意圖就是我們所講的。因此,我們可以理解第 1 部分的本質,並將其畫在最終的 mount(掛載) 方案裡:

1.5 第 1 部分本質(點選檢視大圖)

第二部分

2.0 第二部分

另一個事務

這次我們將討論 ReactReconcileTransaction事務。正如你所知道的,對我們來說主要感興趣的是事務包裝器。其中包括三個包裝器:

//src
enderersdomclientReactReconcileTransaction.js#89
var TRANSACTION_WRAPPERS = [
  SELECTION_RESTORATION,
  EVENT_SUPPRESSION,
  ON_DOM_READY_QUEUEING,
];複製程式碼

我們可以看到,這些包裝器主要用來 保留實際狀態,React 將確保在事務的方法呼叫之前鎖住某些可變值,呼叫完後再釋放它們。舉個例子,範圍選擇(輸入當前選擇的文字)不會被事務的方法執行干擾(在 initialize 時選中並在 close 時恢復)。此外,它阻止因為高階 DOM 操作(例如,臨時從 DOM 中移除文字)而無意間觸發的事件(例如模糊/選中焦點),React在 initialize暫時禁用 ReactBrowserEventEmitter 並在事務執行到 close 時重新啟用。

到這裡,我們已經非常接近元件的掛載了,掛載將會把我們準備好的(HTML)標記插入到 DOM 中。實際上,ReactReconciler.mountComponent 只是一個包裝,更準確的說,它是一箇中介者。它將代理元件模組的掛載方法。這是一個重要的部分,畫個重點。

在實現某些和平臺相關的邏輯時,ReactReconciler 模組總是會被呼叫,例如這個確切的例子。掛載過程在每個平臺上都是不同的,所以 “主模組” 會詢問 ReactReconcilerReactReconciler 知道下一步應該怎麼做。

好的,讓我們將目光移到元件方法 mountComponent 上。這可能是你已經聽說過的方法了。它初始化元件,渲染標記以及註冊事件監聽函式。你看,千辛萬苦我們終於看到了呼叫元件載入。呼叫載入之後,我們應該可以得到可以插入到文件中的 HTML 元素了。

我們完成了 第二部分

讓我們回顧一下這一部分,我們再一次流程圖,然後刪除一些不重要的資訊,它將變成這樣:

2.1 第二部分 簡化

讓我們優化一下排版:

2.2 第二部分 簡化與重構

很好,其實這就是這一部分所發生的一切。我們可以從 第一部分 中取下必要的資訊,然後完善 mounting(掛載) 的流程圖:

2.3 第二部分 必要資訊

第 3 部分

3.0 第 3 部分 (點選檢視大圖)

掛載

componentMount 方法是我們整個系列中極其重要的一個部分。如圖,我們關注 ReactCompositeComponent.mountComponent (1) 方法。

如果你還記得,我曾提到過 元件樹的入口元件TopLevelWrapper 元件 (React 底層內部類)。我們準備掛載它。由於它實際上是一個空的包裝器,除錯起來非常枯燥並且對實際的流程而言沒有任何影響,所以我們跳過這個元件從他的孩子元件開始分析。

把元件掛載到元件樹上的過程就是先掛載父親元件,然後他的孩子元件,然後他的孩子的孩子元件,依次類推。可以肯定,當 TopLevelWrapper 掛載後,他的孩子元件 (用來管理 ExampleApplication 的元件 ReactCompositeComponent) 也會在同一階段注入。

現在我們回到步驟 (1) 觀察這個方法的內部實現,有一些重要行為會發生,接下來讓我們深入研究這些重要行為。

給例項賦值 updater

transaction.getUpdateQueue() 方法返回的 updater 見圖中(2), 實際上就是 ReactUpdateQueue 模組。 為什麼要在這裡賦值一個 updater 呢?因為我們正在研究的類 ReactCompositeComponent 是一個全平臺的共用的類,但是 updater 卻依賴於平臺環境有不同的實現,所以我們在這裡根據不同的平臺動態的將它賦值給例項。

然而,我們現在並不馬上需要這個 updater,但是你要記住它是非常重要的,因為它很快就會應用於非常知名的元件內更新方法 setState

事實上在這個過程中,不僅僅 updater 被賦值給例項,元件例項(你的自定義元件)也獲得了繼承的 props, context, 和 refs

觀察以下的程式碼:

// src
endererssharedstack
econcilerReactCompositeComponent.js#255
// 這些應該在構造方法裡賦值,但是為了
// 使類的抽象更簡單,我們在它之後賦值。
inst.props = publicProps;
inst.context = publicContext;
inst.refs = emptyObject;
inst.updater = updateQueue;複製程式碼

因此,你才可以通過一個例項從你的程式碼中獲得 props,比如 this.props

建立 ExampleApplication 例項

通過呼叫步驟 (3) 的方法 _constructComponent 然後經過幾個構造方法的作用後,最終建立了 new ExampleApplication()。這就是我們程式碼中構造方法第一次被執行的時機,當然也是我們的程式碼第一次實際接觸到 React 的生態系統,很棒。

執行首次掛載

接著我們研究步驟 (4),第一個即將發生的行為是 componentWillMount(當然僅當它被定義時) 的呼叫。這是我們遇到的第一個生命週期鉤子函式。當然,在下面一點你會看到 componentDidMount 函式, 只不過這時由於它不能馬上執行,而是被注入了一個事務佇列中,在很後面執行。他會在掛載系列操作執行完畢後執行。當然你也可能在 componentWillMount 內部呼叫 setState,在這種情況下 state 會被重新計算但此時不會呼叫 render。(這是合理的,因為這時候元件還沒有被掛載)

官方文件的解釋也證明這一點:

componentWillMount() 在掛載執行之前執行,他會在 render() 之前被呼叫,因此在這個過程中設定元件狀態不會觸發重繪。

觀察以下的程式碼,進一步驗證:

// src
endererssharedstack
econcilerReactCompositeComponent.js#476
if (inst.componentWillMount) {
    //..
    inst.componentWillMount();

    // 當掛載時, 在 `componentWillMount` 中呼叫的 `setState` 會執行並改變狀態
    // `this._pendingStateQueue` 不會觸發重渲染
    if (this._pendingStateQueue) {
        inst.state = this._processPendingState(inst.props, inst.context);
    }
}複製程式碼

確實如此,但是當 state 被重新計算完成後,會呼叫我們在元件中申明的 render 方法。再一次接觸 “我們的” 程式碼。

接下來下一步就會建立一個 React 的元件的例項。然後呢?我們已經看見過步驟 (5) this._instantiateReactComponent 的呼叫了,對嗎?是的。在那個時候它為我們的 ExampleApplication 元件例項化了 ReactCompositeComponent,現在我們準備基於它的 render 方法獲得的元素作為它的孩子建立 VDOM (虛擬 DOM) 例項。在我們的例子中,render 方法返回了一個div,所以準確的 VDOM 元素是一個ReactDOMElement。當該例項被建立後,我們會再次呼叫 ReactReconciler.mountComponent,但是這次我們傳入剛剛新建立的 ReactDOMComponent 例項作為internalInstance

然後繼續呼叫此類中的 mountComponent 方法,這樣遞迴往下…

好,第 3 部分我們講完了

我們來回顧一下我們學到的。我們再看一下這種模式,然後去掉冗餘的部分:

3.1 第 3 部分簡化版 (點選檢視大圖)

讓我們適度在調整一下:

3.2 第 3 部分簡化和重構 (點選檢視大圖)

很好,實際上,下面的示意圖就是我們所講的。因此,我們可以理解第 3 部分的本質,並將其用於最終的 mount 方案:

3.3 第 3 部分本質 (點選檢視大圖)

第 4 部分

4.0 第 4 部分 (點選檢視大圖)

子元素掛載

已經入迷了對嗎? 讓我們接續研究 mount 方法。

如果步驟 (1) 的 _tag 包含一個複雜的標籤,比如 videoformtextarea 等等,這些就需要更進一步的封裝,對每個媒體事件需要綁上更多事件監聽器,比如給 audio 標籤增加 volumechange 事件監聽,或者像 selecttextarea 等標籤只需要封裝一些瀏覽器原生行為。

我們有很多封裝器幹這事,比如 ReactDOMSelectReactDOMTextarea 位於原始碼 (src
enderersdomclientwrappersfolder) 中。本文例子中只有簡單的 div 標籤。

Props 驗證

接下來要講解的驗證方法是為了確保內部 props 被設定正確,不然它就會丟擲異常。舉個例子,如果設定了 props.dangerouslySetInnerHTML (經常在我們需要基於一個字串插入 HTML 時使用),但是它的物件健值 __html 忘記設定,那麼將會丟擲下面的異常:

props.dangerouslySetInnerHTML must be in the form {__html: ...}. Please visit fb.me/react-invar… for more information.

(props.dangerouslySetInnerHTML 必須符合 {__html: ...}的形式)

建立 HTML 元素

接著, document.createElement 方法會建立真實的 HTML 元素,例項出真實的 HTML div,在這一步之前我們只能用虛擬的表現形式表達,而現在你第一次能實際看到它了。

好,第 4 部分我們講完了

我們來回顧一下我們學到的。我們再看一下這種模式,然後去掉冗餘的部分:

4.1 第 4 部分簡化版 (點選檢視大圖)

讓我們適度在調整一下:

4.2 第 4 部分簡化和重構 (點選檢視大圖)

很好,實際上,下面的示意圖就是我們所講的。因此,我們可以理解第 4 部分的本質,並將其用於最終的 mount 方案:

4.3 第 4 部分本質 (點選檢視大圖)

第 5 部分

5.0 第 5 部分(點選檢視大圖)

更新 DOM 屬性

這圖片看上去有點複雜?這裡主要講的是如何高效的把diff作用到新老 props 上。我們來看一下原始碼對這塊的程式碼註釋:

差分對比更新演算法通過探測屬性的差異並更新需要更新的 DOM。該方法可能是效能優化上唯一且極其重要的一環。

這個方法實際上有兩個迴圈。第一個迴圈遍歷前一個 props,後一個迴圈遍歷下一個 props。在我們的掛載場景下,lastProps (前一個) 是空的。(很明顯這是第一次給我們的 props 賦值),但是我們還是來看看這裡發生了什麼。

lastprops 迴圈

第一步,我們檢查 nextProps 物件是不是包含相同的 prop 值,如果相等的話,我們就跳過那個值,因為它之後會在 nextProps 迴圈中處理。然後我們重置樣式的值,刪除事件監聽器 (如果監聽器之前設定過的話),然後去除 DOM 屬性名以及 DOM 屬性值。對於屬性們,只要我們確定它們不是 RESERVED_PROPS 中的一員,而是實際的 prop,例如 children 或者 dangerouslySetInnerHTML

nextprops 迴圈

該迴圈中,第一步檢查 prop 是不是變化了,也就是檢查下一個值是不是和老的值不同。如果相同,我們不做任何處理。對於 styles(你也許已經注意到我們會區別對待它)我們更新從lastProp 到現在變化的部分值。然後我們新增事件監聽器(比如 onClick 等等)。讓我們更深入的分析它。

其中很重要的一點是,縱觀 React app,所有的工作都會傳入一個名叫 syntetic 的事件。沒有一個例外。它其實是一些封裝器來優化效率的。下一個重要部分是我們處理事件監聽器的中介控制模組 EventPluginHub (位於原始碼中src
endererssharedstackeventEventPluginHub.js
)。它包含一個 listenerBank 的對映來快取並管控所有的監聽器。我們準備好了新增我們自己的事件監聽器,但是不是現在。這裡的關鍵在於我們應該在元件和 DOM 元素已經準備好處理事件的時候才增加監聽器。看上去在這裡我們執行遲了。也你許會問,我們如何知道 DOM 已經準備好了?很好,這就引出了下一個問題!你是否還記得我們曾把 transaction 傳遞給每個方法和呼叫?這就對了,我們那樣做就是因為在這種場景它可以很好的幫助我們。讓我們從程式碼中尋找佐證:

//src
enderersdomsharedReactDOMComponent.js#222
transaction.getReactMountReady().enqueue(putListener, {
    inst: inst,
    registrationName: registrationName,
    listener: listener,
});複製程式碼

在處理完事件監聽器,我們開始設定 DOM 屬性名和 DOM 屬性值。就像之前說的一樣,對於屬性們,我們確定他們不是 RESERVED_PROPS 中的一員,而是實際的 prop,例如 children 或者 dangerouslySetInnerHTML

在處理前一個和下一個 props 的時候,我們會計算 styleUpdates 的配置並且現在把它傳遞給 CSSPropertyOperations 模組。

很好,我們已經完成了更新屬性這一部分,讓我們繼續

好, 第 5 部分我們講完了

我們來回顧一下我們學到的。我們再看一下這種模式,然後去掉冗餘的部分:

5.1 第 5 部分簡化版 (點選檢視大圖)

然後我們適當再調整一下:

5.2 第 5 部分簡化和重構 (點選檢視大圖)

很好,實際上,下面的示意圖就是我們所講的。因此,我們可以理解第 5 部分的本質,並將其用於最終的 mounting 方案:

5.3 第 5 部分 本質 (點選檢視大圖)

第 6 部分

6.0 第 6 部分(點選檢視大圖)

建立最初的子元件

好像元件本身已經建立完成了,現在我們可以繼續建立它的子元件了。這個分為以下兩步:(1)子元件應該由(this.mountChildren)載入,(2)並與它的父級通過(DOMLazyTree.queueChild)連線。我們來討論一下子元件的掛載。

有一個單獨的 ReactMultiChild (src
endererssharedstack
econcilerReactMultiChild.js
) 模組來操作子元件。我們來檢視一下 mountChildren 方法。它包括兩個主要任務。首先我們初始化子元件(使用 ReactChildReconciler)並載入他們。這裡到底是什麼子元件呢?它可能是一個簡單的 HTML 標籤或者一個其他自定義的元件。為了處理 HTML,我們需要初始化 ReactDOMComponent,對於自定義元件,我們使用 ReactCompositeComponent。載入流程也是依賴於子元件是什麼型別。

再一次

如果你還在閱讀這篇文章,那麼現在可能是再一次闡述和整理整個過程的時候了。現在我們休息一下,重新整理下物件的順序。

6.1 所有載入圖示(點選檢視大圖)

1) 在React 中使用 ReactCompositeComponent 例項化你的自定義元件(通過使用像componentWillMount 這類的元件生命週期鉤子)並載入它。

2) 在載入過程中,首先會建立一個你自定義元件的例項(呼叫構造器函式)。

3) 然後,呼叫該元件的渲染函式(舉個簡單的例子,渲染返回的 div)並且 React.createElement 來建立 React 元素。它可以直接被呼叫或者通過Babel解析JSX後來替換渲染中的標籤。但是,它可能不是我們所需要的,看看接下來是什麼。

4) 我們對於 div 需要一個 DOM 元件。所以,在例項化過程中,我們從元素-物件(上文提到過)出發建立 ReactDOMComponent 的例項。

5) 然後,我們需要載入 DOM 元件。這實際上就意味者我們建立 DOM 元素,並載入了事件監聽等。

6) 然後,我們處理我們的DOM元件的直接子元件。我們建立它們的例項並且載入它們。根據子元件的是什麼(自定義元件或只是HTML標籤),我們分別跳轉到步驟1)或步驟5)。然後再一次處理所有的內嵌元素。

載入過程就是這個。就像你看到的一樣非常直接。

載入基本完成。下一步是 componentDidMount 方法。大功告成。

好的,我們已經完成了第 6 部分

讓我們概括一下我們怎麼到這裡的。再一次看一下示例圖,然後移除掉冗餘的不那麼重要的部分,它就變成了這樣:

6.2 第 6 部分 簡化(點選檢視大圖)

我們也應該儘可能的修改空格和對齊方式:

6.3 第 6 部分 簡化和重構(點選檢視大圖)

很好。實際上它就是這兒所發生的一切。我們可以從第 6 部分中獲得基本精髓,並將其用於最終的“載入”圖表:

6.4 第 6 部分本質 (點選檢視大圖)

第七部分

7.0 第七部分(可點選檢視大圖)

回到開始的地方

在執行載入後,我們就準備好了可以插入文件的 HTML 元素。實際上生成的是 markup,但是無論 mountComponent 是如何命名的,它們並非等同於 HTML 標記。它是一種包括子節點、節點(也就是實際 DOM 節點)等的資料結構。但是,我們最終將 HTML 元素放入在 ReactDOM.render 的呼叫中指定的容器中。在將其新增到 DOM 中時,React 會清除容器中的所有內容。DOMLazyTree 是一個對樹形結構執行一些操作的工具類,也是我們在使用 DOM 時實際在做的事。

最後一件事是 parentNode.insertBefore(tree.node),其中 parentNode 是容器 div 節點,而 tree.node 實際上是 ExampleAppliication 的 div 節點。很好,載入建立的 HTML 元素終於被插入到文件中了。

那麼,這就是所有?並未如此。也許你還記得,mount 的呼叫被包裝到一個事務中。這意味著我們需要關閉這個事務。讓我們來看看我們的 close 包裝。多數情況下,我們應該恢復一些被鎖定的行為,例如 ReactInputSelection.restoreSelection()ReactBrowserEventEmitter.setEnabled(previouslyEnabled),而且我們也需要使用 this.reactMountReady.notifyAll 來通知我們之前在 transaction.reactMountReady 中新增的所有回撥函式。其中之一就是我們最喜歡的 componentDidMount,它將在 close 中被觸發。

現在你對“元件已載入”的意思有了清晰的瞭解。恭喜!

還有一個事務需要關閉

實際上,不止一個事務需要關閉。我們忘記了另一個用來包裝 ReactMount.batchedMountComponentIntoNode 的事務。我們也需要關閉它。

這裡我們需要檢查將處理 dirtyComponents 的包裝器 ReactUpdates.flushBatchedUpdates。聽起來很有趣嗎?那是好訊息還是壞訊息。我們只做了第一次載入,所以我們還沒有髒元件。這意味著它是一個空置的呼叫。因此,我們可以關閉這個事務,並說批量策略更新已完成。

好的,我們已經完成了第 7 部分

讓我們回顧一下我們是如何到達這裡的。首先再看一下整體流程,然後去除多餘的不太重要的部分,它就變成了:

7.1 第 7 部分 簡化(點選檢視大圖)

我們也應該修改空格和對齊:

7.2 第 7 部分 簡化並重構(點選檢視大圖)

其實這就是這裡發生的所有。我們可以從第 7 部分中的重要部分來組成最終的 mounting 流程:

7.3 第 7 部分 基本價值(點選檢視大圖)

完成!其實我們完成了載入。讓我們來看看下圖吧!

7.4 Mounting 過程(點選檢視大圖)

第 8 部分

8.0 Part 8 (點選檢視大圖)

this.setState

我們已經學習了掛載的工作原理,現在從另一個角度來學習。嗯,比如 setState 方法,其實也很簡單。

首先,為什麼我們可以在自己的元件中呼叫 setState 方法呢?很明顯我們的元件繼承自 ReactComponent,這個類我們可以很方便的在 React 原始碼中找到。

//srcisomorphicmodernclassReactComponent.js#68
this.updater.enqueueSetState(this, partialState)複製程式碼

我們發現,這裡有一些 updater 介面。什麼是 updater 呢?在講解掛載過程時我們講過,在 mountComponent 過程中,例項會接受一個 ReactUpdateQueue(src
endererssharedstack
econcilerReactUpdateQueue.js
) 的引用作為 updater 屬性。

很好,我們現在深入研究步驟 (1) 的 enqueueSetState。首先,它會往步驟 (2) 的 _pendingStateQueue (來自於內部例項。注意,這裡我們說的外部例項是指使用者的元件 ExampleApplication,而內部例項則掛載過程中建立的 ReactCompositeComponent) 注入 partialState (這裡的 partialState 就是指給 this.setState 傳遞的物件)。然後,執行 enqueueUpdate,這個過程會檢查更新是否已經在進展中,如果是則把我們的元件注入到 dirtyComponents 列表中,如果不是則先初始化開啟更新事務,然後把元件注入到 dirtyComponents 列表。

總結一下,每個元件都有自己的一組處於等待的”狀態“的列表,當你在一次事務中呼叫 setState 方法,其實只是把那個狀態物件注入一個佇列裡,它會在之後一個一個依次被合併到元件 state 中。呼叫此setState方法同時,你的元件也會被新增進 dirtyComponents 列表。也許你很好奇 dirtyComponents 是如何工作的,這就是另一個研究重點。

好, 第 8 部分我們講完了

我們來回顧一下我們學到的。我們再看一下這種模式,然後去掉冗餘的部分:

8.1 第 8 部分簡化版 (點選檢視大圖)

讓我們適度在調整一下:

8.2 第 8 部分簡化和重構 (點選檢視大圖)

很好,實際上,下面的示意圖就是我們所講的。因此,我們可以理解第 8 部分的本質,並將其用於最終的 updating 方案:

8.3 Part 8 本質 (點選檢視大圖)

第 9 部分

9.0 第 9 部分(點選檢視大圖)

繼續研究 setState

根據流程圖我們發現,有很多方式來觸發 setState。可以直接通過使用者互動觸發,也可能只是隱含在方法裡觸發。我們舉兩個例子:第一種情況下,它由使用者的滑鼠點選事件觸發。而第二種情況,例如在 componentDidMount 裡通過 setTimeout 呼叫來觸發。

那麼這兩種方式有什麼差異呢?如果你還記得 React 的更新過程是批量化進行的,這就意味著他先會收集這些更新操作,然後一起處理。當滑鼠事件觸發後,會被頂層先處理,然後經過多層封裝器的作用,這個批更新操作才會開始。過程中你會發現,只有當步驟 (1) 的 ReactEventListenerenabled 的狀態才會觸發更新。然而你還記得在元件掛載過程中,ReactReconcileTransaction 中的一個封裝器會使它 disabled 來確保掛載的安全。那麼 setTimeout 案例是怎樣的呢?這個也很簡單,在把元件丟進 dirtyComponents 列表前,React會確保事務已經開始,那麼,之後他應該會被關閉,然後一起處理列表中的元件。

就像你所知道的那樣,React 有實現很多 “syntetic事件”,一些 “語法糖”,實際上包裹著原生事件。隨後,他會表現為我們很熟悉的原生事件。你可以看下面的程式碼註釋:

實驗過程為了更方便和除錯工具整合,我們模擬一個真實瀏覽器事件

var fakeNode = document.createElement(`react`);

ReactErrorUtils.invokeGuardedCallback = function (name, func, a) {
      var boundFunc = func.bind(null, a);
      var evtType = `react-` + name;

      fakeNode.addEventListener(evtType, boundFunc, false);

      var evt = document.createEvent(`Event`);
      evt.initEvent(evtType, false, false);

      fakeNode.dispatchEvent(evt);
      fakeNode.removeEventListener(evtType, boundFunc, false);
};複製程式碼

好,回到我們的更新,讓我們總結一下,整個過程是:

  1. 呼叫 setState
  2. 如果批處理事務沒有開啟,則開啟
  3. 把受影響的元件新增入 dirtyComponents 列表
  4. 在呼叫 ReactUpdates.flushBatchedUpdates的同時關閉事務, 並處理在所有 dirtyComponents 列表中的元件

9.1 setState 執行過程 (點選檢視大圖)

好,第 9 部分我們講完了

我們來回顧一下我們學到的。我們再看一下這種模式,然後去掉冗餘的部分:

9.2 第 9 部分簡化版 (點選檢視大圖)

然後我們適當再調整一下:

9.3 第 9 部分簡化和重構 (點選檢視大圖)

很好,實際上,下面的示意圖就是我們所講的。因此,我們可以理解第 9 部分的本質,並將其用於最終的 updating 方案:

9.4 第 9 部分本質 (點選檢視大圖)

第 10 部分

10.0 第十部分 (點選檢視大圖)

髒元件

就像流程圖所示那樣,React 會遍歷步驟 (1) 的 dirtyComponents,並且通過事務呼叫步驟 (2) 的 ReactUpdates.runBatchedUpdates。事務? 又是一個新的事務,它怎麼工作呢,我們一起來看。

這個事務的型別是 ReactUpdatesFlushTransaction,之前我們也說過,我們需要通過事務包裝器來理解事務具體幹什麼。以下是從程式碼註釋中獲得的啟示:

ReactUpdatesFlushTransaction 的封裝器組會清空 dirtyComponents 陣列,並且執行 mount-ready 處理器組壓入佇列的更新 (mount-ready 處理器是指那些在 mount 成功後觸發的生命週期函式。例如 componentDidUpdate)

但是,不管怎樣,我們需要證實它。現在有兩個 wrappersNESTED_UPDATESUPDATE_QUEUEING。在初始化的過程中,我們存下步驟 (3) 的 dirtyComponentsLength。然後觀察下面的 close 處,React 在更新過程中會不斷檢查對比 dirtyComponentsLength,當一批髒元件變更了,我們把它們從中陣列中移出並再次執行 flushBatchedUpdates。 你看, 這裡並沒有什麼黑魔法,每一步都清晰簡單。

然而… 一個神奇的時刻出現了。ReactUpdatesFlushTransaction 複寫了 Transaction.perform 方法。因為它實際上是從 ReactReconcileTransaction (在掛載的過程中應用到的事務,用來保障應用 state 的安全) 中獲得的行為。因此在 ReactUpdatesFlushTransaction.perform 方法裡,ReactReconcileTransaction 也被使用到,這個事務方法實際上又被封裝了一次。

因此,從技術角度看,它可能形如:

[NESTED_UPDATES, UPDATE_QUEUEING].initialize()
[SELECTION_RESTORATION, EVENT_SUPPRESSION, ON_DOM_READY_QUEUEING].initialize()

method -> ReactUpdates.runBatchedUpdates

[SELECTION_RESTORATION, EVENT_SUPPRESSION, ON_DOM_READY_QUEUEING].close()
[NESTED_UPDATES, UPDATE_QUEUEING].close()複製程式碼

我們之後會回到這個事務,再次理解它是如何幫助我們的。但是現在,讓我們來看步驟 (2) ReactUpdates.runBatchedUpdates (src
endererssharedstack
econcilerReactUpdates.js#125
)。

我們要做的第一件事就是給 dirtyComponets 排序,我們來看步驟 (4)。怎麼排序呢?通過 mount order (當例項掛載時元件獲得的序列整數),這將意味著父元件 (先掛載) 會被先更新,然後是子元件,然後往下以此類推。

下一步我們提升批號 updateBatchNumber,批號是一個類似當前差分對比更新狀態的 ID。
程式碼註釋中提到:

‘任何在差分對比更新過程中壓入佇列的更新必須在整個批處理結束後執行。 否則, 如果 dirtyComponents 為[A, B]。 其中 A 有孩子 B 和 C, 那麼如果 C 的渲染壓入一個更新給 B,則 B 可能在一個批次中更新兩次 (由於 B 已經更新了,我們應該跳過它,而唯一能感知的方法就是檢查批號)。’

這將避免重複更新同一個元件。

非常好,最終我們遍歷 dirtyComponents 並傳遞其每個元件給步驟 (5) 的 ReactReconciler.performUpdateIfNecessary,這也是 ReactCompositeComponent 例項裡呼叫 performUpdateIfNecessary 的地方。然後,我們將繼續研究 ReactCompositeComponent 程式碼以及它的 updateComponent 方法,在那裡我們會發現更多有趣的事,讓我們繼續深入研究。

好, 第 10 部分我們講完了

我們來回顧一下我們學到的。我們再看一下這種模式,然後去掉冗餘的部分:

10.1 第 10 部分簡化版 (點選檢視大圖)

讓我們適度調整一下:

10.2 第 10 部分重構與簡化 (點選檢視大圖)

很好,實際上,下面的示意圖就是我們所講的。因此,我們可以理解第 10 部分的本質,並將其用於最終的 updating 方案:

10.3 第 10 部分 本質 (點選檢視大圖)

第 11 部分

11.0 第 11 部分(點選檢視大圖)

更新元件方法

原始碼中的註釋是這樣介紹這個方法的:

對一個已經掛載後的元件執行再更新操作的時候,componentWillReceiveProps 以及 shouldComponentUpdate 方法會被呼叫,然後 (假定這個更新有效) 呼叫其他更新中其餘的生命週期鉤子方法,並且需要變化的 DOM 也會被更新。預設情況下這個過程會使用 React 的渲染和差分對比更新演算法。對於一些複雜的實現,客戶可能希望重寫這步驟。

很好… 聽起來很合理。

首先我們會去檢查步驟 (1) 的 props 是否改變了,原理上講,updateComponent 方法會在 setState 方法被呼叫或者 props 變化這兩種情況下使用。如果 props 確實改變了,那麼生命週期函式componentWillReceiveProps 就會被執行. 接著, React 會根據 pending state queue (指我們之前設定的partialState 佇列,現在可能形如 [{ message: “click state message” }]) 重新計算步驟 (2) 的 nextState。當然在只有 props 更新的情況下, state 是不會受到影響的。

很好,下一步,我們把 shouldUpdate 初始化為步驟 (3) 的 true。這裡可以看出即使shouldComponentUpdate 沒有申明,元件也會按照此預設行為更新。然後檢查一下 force update的狀態,因為我們也可以在元件裡呼叫forceUpdate 方法,不管stateprops是不是變化,都強制更新。當然,React 的官方文件不推薦這樣的實踐。在使用 forceUpdate 的情況下,元件將會被持久化的更新,否則,shouldUpdate 將會是 shouldComponentUpdate 的返回結果。如果 shouldUpdate 為否,元件不應該更新時,React 依然會設定新的 props and state, 不過會跳過更新的餘下部分。

好, 第 11 部分我們講完了

我們來回顧一下我們學到的。我們再看一下這種模式,然後去掉冗餘的部分:

11.1 第 11 部分簡化版 (點選檢視大圖)

然後我們適當再調整一下:

11.2 第 11 部分簡化和重構 (點選檢視大圖)

很好,實際上,下面的示意圖就是我們所講的。因此,我們可以理解第 11 部分的本質,並將其用於最終的 updating 方案:

11.3 第 11 部分本質 (點選檢視大圖)

第 12 部分

12.0 第 12 部分(點選檢視大圖)

當元件確實需要更新…

現在我們已經到更新行為的開始點,此時應該先呼叫步驟 (1) 的 componentWillUpdate (當然必須宣告過) 的生命週期鉤子。然後重繪元件並且把另一個知名的方法 componentDidUpdate 的呼叫壓入佇列 (推遲是因為它應該在更新操作結束後執行)。那怎麼重繪呢?實際上這時候會呼叫元件的 render 方法,並且相應的更新 DOM。所以第一步,呼叫例項 (ExampleApplication) 中步驟 (2) 的 render 方法, 並且儲存更新的結果 (這裡會返回 React 元素)。然後我們會和之前已經渲染的元素對比並決策出哪些 DOM 應該被更新。

這個部分是 React 殺手級別的功能,它避免冗餘的 DOM 更新,只更新我們需要的部分以提高效能。

我們來看原始碼對步驟 (3) 的 shouldUpdateReactComponent 方法的註釋:

決定現有例項的更新是部分更新,還是被移除還是被一個新的例項替換

因此,通俗點講,這個方法會檢測這個元素是否應該被徹底的替換, 在徹底替換掉情況下,舊的部分需要先被 unmounted(解除安裝),然後從 render 獲取的新的部分應該被掛載,然後把掛載後獲得的元素替換現有的。這個方法還會檢測是否一個元素可以被部分更新。徹底替換元素的主要條件是當一個新的元素是空元素 (意即被 render 邏輯移除了)。或者它的標籤不同,比如原先是一個 div,然而是現在是其它的標籤了。讓我們來看以下程式碼,表達的非常清晰。

///src/renderers/shared/shared/shouldUpdateReactComponent.js#25

function shouldUpdateReactComponent(prevElement, nextElement) {
    var prevEmpty = prevElement === null || prevElement === false;
    var nextEmpty = nextElement === null || nextElement === false;
    if (prevEmpty || nextEmpty) {
        return prevEmpty === nextEmpty;
    }

    var prevType = typeof prevElement;
    var nextType = typeof nextElement;
    if (prevType === `string` || prevType === `number`) {
        return (nextType === `string` || nextType === `number`);
    } else {
        return (
            nextType === `object` &&
            prevElement.type === nextElement.type &&
            prevElement.key === nextElement.key
        );
    }
}複製程式碼

很好,實際上我們的 ExampleApplication 例項僅僅更新了 state 屬性,並沒有怎麼影響 render。到現在我們可以進入下一個場景,update 後的反應。

好, 第 12 部分我們講完了

我們來回顧一下我們學到的。我們再看一下這種模式,然後去掉冗餘的部分:

第 12 部分簡化版 (點選檢視大圖)

然後我們適當再調整一下:

12.2 第 12 部分簡化和重構 (點選檢視大圖)

很好,實際上,下面的示意圖就是我們所講的。因此,我們可以理解第 12 部分的本質,並將其用於最終的 updating 方案:

12.3 第 12 部分本質 (點選檢視大圖)

第 13 部分

13.0 第 13 部分(點選檢視大圖)

接收元件(更精確的下一個元素)

通過 ReactReconciler.receiveComponent,React 實際上從 ReactDOMComponent 呼叫 receiveComponent 並傳遞給下一個元素。在 DOM 元件例項上重新分配並呼叫 update 方法。updateComponent 方法實際上主要是兩步: 基於 prevnext 的屬性,更新 DOM 屬性和 DOM 元素的子節點。好在我們已經分析了 _updateDOMProperties(src
enderersdomsharedReactDOMComponent.js#946
) 方法。就像你記得的那樣,這個方法大部分處理了 HTML 元素的屬性和特質,計算樣式以及處理事件監聽等。剩下的就是 _updateDOMChildren(src
enderersdomsharedReactDOMComponent.js#1076
) 方法了。

好了,我們已經完成了第 13 部分。好短的一章。

讓我們概括一下我們怎麼到這裡的。再看一下這張圖,然後移除掉冗餘的不那麼重要的部分,它就變成了這樣:

13.1 第 13 部分 簡化(點選檢視大圖)

我們也應該儘可能的修改空格和對齊方式:

13.2 第 13 部分 簡化和重構(點選檢視大圖)

很好。實際上它就是這兒所發生的一切。我們可以從第 13 部分中獲得基本價值,並將其用於最終的“更新”圖表:

13.3 第 13 部分本質(點選檢視大圖)

第 14 部分

14.0 第 14 部分(點選檢視大圖)

最後一章!

在發起子元件更新操作時會有很多屬性影響子元件內容。這裡有幾種可能的情況,不過其實就只有兩大主要情況。即子元件是不是 “複雜”。這裡的複雜的含義是,它們是 React 元件,React 應當通過它們不斷遞迴直到觸及內容層,或者,該子元件只是簡單資料型別,比如字串、數字。

這個判斷條件就是步驟 (1) 的 nextProps.children 的型別,在我們的情形中,ExampleApplication 有三個孩子 button, ChildCmptext string

很好,現在讓我們來看它的工作原理。

首先,在首次迭代時,我們分析 ExampleApplication children。很明顯可以看出子元件的型別不是 “純內容型別”,因此情況為 “複雜” 情況。然後我們一層層往下遞迴,每層都會判斷 children 的型別。順便說一下,步驟 (2) 的 shouldUpdateReactComponent 判斷條件可能讓你有些困惑,它看上去是在驗證更新與否,但實際上它會檢查型別是更新還是刪除與建立(為了簡化流程我們跳過此條件為否的情形,假定是更新)。當然接下來我們對比新舊子元件,如果有孩子被移除,我們也會去除掛載元件,並把它移除。

14.1 Children 更新 (點選檢視大圖)

在第二輪迭代時,我們分析 button,這是一個很簡單的案例,由於它僅包含一個標題文字 set state button,它的孩子只是一個字串。因此我們對比一下之前和現在的內容。很好,這些文字並沒有變化,因此我們不需要更新 button?這非常的合理,因此所謂的 “虛擬 DOM”,現在聽上去也不是那麼的抽象,React 維護了一個對 DOM 的內部表達物件,並且在需要的時候更改真實 DOM,這樣取得了很不錯的效能。因此我想你應該已經瞭解了這個設計模式。那我們接著來更新 ChildCmp,然後它的孩子也到達我們可以更新的最底層。可以看到在這層的內容已經被修改了,當時我們通過 clicksetState 的呼叫,this.props.message 已經更新成 `click state message 了。

//... 
onClickHandler() {
    this.setState({ message: `click state message` });
}

render() {
    return <div>
        <button onClick={this.onClickHandler.bind(this)}>set state button</button>
        <ChildCmp childMessage={this.state.message} />
//...複製程式碼

從這裡可以看出已經可以更新元素的內容,事實上也就是替換它。那麼真正的行為是怎樣的呢,其實它會生成一個“配置物件”並且其配置的動作會被相應地應用。在我們的場景下這個文字的更新操作可能形如:

{
  afterNode: null,
  content: "click state message",
  fromIndex: null,
  fromNode: null,
  toIndex: null,
  type: "TEXT_CONTENT"
}複製程式碼

我們可以看到很多欄位是空,因為文字更新是比較簡單的。但是它有很多屬性欄位,因為當你移動節點就會比僅僅更新字串要複雜得多。我們來看這部分的原始碼加深理解。

//src
enderersdomclientutilsDOMChildrenOperations.js#172
processUpdates: function(parentNode, updates) {
    for (var k = 0; k < updates.length; k++) {
      var update = updates[k];

      switch (update.type) {
        case `INSERT_MARKUP`:
          insertLazyTreeChildAt(
            parentNode,
            update.content,
            getNodeAfter(parentNode, update.afterNode)
          );
          break;
        case `MOVE_EXISTING`:
          moveChild(
            parentNode,
            update.fromNode,
            getNodeAfter(parentNode, update.afterNode)
          );
          break;
        case `SET_MARKUP`:
          setInnerHTML(
            parentNode,
            update.content
          );
          break;
        case `TEXT_CONTENT`:
          setTextContent(
            parentNode,
            update.content
          );
          break;
        case `REMOVE_NODE`:
          removeChild(parentNode, update.fromNode);
          break;
      }
    }
  }複製程式碼

在我們的情況下,更新型別是 TEXT_CONTENT,因此實際上這是最後一步,我們呼叫步驟 (3) 的 setTextContent 方法並且更新 HTML 節點(從真實 DOM 中操作)。

非常好!內容已經被更新,介面上也做了重繪。我們還有什麼遺忘的嗎?讓我們結束更新!這些事都做完了,我們的元件生命週期鉤子函式 componentDidUpdate 會被呼叫。這樣的延遲迴調是怎麼呼叫的呢?實際上就是通過事務的封裝器。如果你還記得,髒元件的更新會被 ReactUpdatesFlushTransaction 封裝器修飾,並且其中的一個封裝器實際上包含了 this.callbackQueue.notifyAll() 邏輯,所以它回撥用 componentDidUpdate。很好,現在看上去我們已經講完了全部內容。

好, 第 14 部分我們講完了

我們來回顧一下我們學到的。我們再看一下這種模式,然後去掉冗餘的部分:

14.2 第 14 部分簡化板 (點選檢視大圖)

然後我們適當再調整一下:

14.3 第 14 簡化和重構 (點選檢視大圖)

很好,實際上,下面的示意圖就是我們所講的。因此,我們可以理解第 14 部分的本質,並將其用於最終的 updating 方案:

14.4 第 14 部分 本質 (點選檢視大圖)

我們已經完成了更新操作的學習,讓我們重頭整理一下。

14.5 更新 (點選檢視大圖)

相關文章