原文地址:Under-the-hood-ReactJSjavascript
原文做者:Bogdan-Lyashenkohtml
譯文出自:掘金翻譯計劃java
本文永久連接:github.com/xitu/gold-m…node
譯者組:react
譯者 翻譯章節 Candy Zheng 三、四、五、1四、八、九、十、十一、十二、14 undead25 主頁、介紹、0 Tina92 六、13 HydeSong 1 bambooom 7 ahonn 2
校對 / 語句優化:laalaguergit
整合長文: Candy Zhenggithub
本文包含 ReactJS 內部工做原理的說明。實際上,我在調試整個代碼庫時,將全部的邏輯放在可視化的流程圖上,對它們進行分析,而後總結和解釋主要的概念和方法。我已經完成了 Stack 版本,如今我在研究下一個版本 —— Fiber。算法
經過 github-pages 網站來以最佳格式閱讀.設計模式
爲了讓它變得更好,若是你有任何想法,歡迎隨時提 issue。數組
每張流程圖均可以經過點擊在新的選項卡中打開,而後經過縮放使它適合閱讀。在單獨的窗口(選項卡)中保留文章和正在閱讀的流程圖,將有助於更容易地匹配文本和代碼流。
咱們將在這裏談論 ReactJS 的兩個版本,老版本使用的是 Stack 協調引擎,新版本使用的是 Fiber(你可能已經知道,React v16 已經正式發佈了)。讓咱們先深刻地瞭解(目前普遍使用的)React-Stack 的工做原理,並期待下 React-Fiber 帶來的重大變革。咱們使用 React v15.4.2 來解釋「舊版 React」的工做原理。
整個流程圖分爲 15 個部分,讓咱們開始學習歷程吧。
你能夠先花點時間看下總體的流程。雖然看起來很複雜,但它實際上只描述了兩個流程:(組件的)掛載和更新。我跳過了卸載,由於它是一種「反向掛載」,並且刪除這部分簡化了流程圖。另外,這圖並非100% 同源代碼匹配,而只是描述架構的主要部分。整體來講,它大概是源代碼的 60%,而另外的 40% 沒有多少視覺價值,爲了簡單起見,我省略了那部分。
乍一看,你可能會注意到流程圖中有不少顏色。每一個邏輯項(流程圖上的形狀)都以其父模塊的顏色高亮顯示。例如,若是是從紅色的 模塊 B
調用 方法 A
,那 方法 A
也是紅色的。如下是流程圖中模塊的圖例以及每一個文件的路徑。
讓咱們把它們放在一張流程圖中,看看模塊之間的依賴關係。
你可能知道,React 是爲支持多種環境而構建的。
所以,一些文件實際上比上面流程圖中列出的要更大。如下是包含多環境支持的相同的流程圖。
如你所見,有些項彷佛翻倍了。這代表它們對每一個平臺都有一個獨立的實現。讓咱們來看一些簡單例子,例如 ReactEventListener,顯然,不一樣平臺會有不一樣的實現。從技術上講,你能夠想象,這些依賴於平臺的模塊,應該以某種方式注入或鏈接到當前的邏輯流程中。實際上有不少這樣的注入器,由於它們的用法是標準組合模式的一部分。一樣,爲了簡單起見,我選擇忽略它們。
讓咱們來學習下常規瀏覽器中 React DOM 的邏輯流程。這是最經常使用的平臺,並徹底覆蓋了全部 React 的架構設計理念。
學習框架或者庫的源碼的最佳方式是什麼?沒錯,研讀並調試源碼。那好,咱們將要調試這兩個流程:ReactDOM.render 和 component.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() {}
);複製代碼
咱們已經準備好開始學習了。讓咱們先來分析流程圖中的第一部分。一個接一個,咱們會將它們所有分析完。
讓咱們從 ReactDOM.render 的調用開始。
入口點是 ReactDom.render,咱們的應用程序是從這裏開始渲染到 DOM 中的。爲了方便調試,我建立了一個簡單的 <ExampleApplication />
組件。所以,發生的第一件事就是 JSX 會被轉換成 React 組件。它們是簡單的、直白的對象。具備簡單的結構。它們僅僅展現從本組件渲染中返回的內容,沒有其餘了。一些字段應該是你已經熟悉的,像 props、key 和 ref。屬性類型是指由 JSX 描述的標記對象。因此,在咱們的例子中,它就是 ExampleApplication
類,可是它也能夠僅僅是 Button 標籤的 button
字符串等其餘類。另外,在 React 組件建立過程當中,它會將 defaultProps
與 props
合併(若是顯式聲明瞭),並驗證 propTypes
。
更多詳細信息可參考源碼:src\isomorphic\classic\element\ReactElement.js
。
你能夠看到一個叫作 ReactMount
(01)的模塊。它包含組件掛載的邏輯。實際上,在 ReactDOM
裏面沒有邏輯,它只是一個與ReactMount
一塊兒使用的接口,因此當你調用 ReactDOM.render
的時候,實際上調用了 ReactMount.render
。那「掛載」指的是什麼呢?
掛載是初始化 React 組件的過程。該過程經過建立組件所表明的 DOM 元素,並將它們插入到提供的
container
中來實現。
至少源碼中的註釋是這樣描述的。那這真實的含義是什麼呢?好吧,讓咱們想象一下下方的轉換:
React 須要將你的組件描述轉換爲 HTML 以將其放入到 DOM 中。那怎樣才能作到呢?沒錯,它須要處理全部的屬性、事件監聽、內嵌的組件和邏輯。它須要將你的高階描述(組件)轉換成實際能夠放入到網頁中的低階數據(HTML)。這就是真正的掛載過程。
讓咱們繼續深刻下去。接下來是有趣的事實時間!是的,讓咱們在探索過程當中添加一些有趣的東西,讓它變得更「有趣」。
有趣的事實:確保滾動正在監聽(02)
有趣的是,在第一次渲染根組件時,React 初始化滾動監聽並緩存滾動值,以便應用程序代碼能夠訪問它們而不觸發重排。實際上,因爲瀏覽器渲染機制的不一樣,一些 DOM 值不是靜態的,所以每次在代碼中使用它們時都會進行計算。固然,這會影響性能。事實上,這隻影響了不支持
pageX
和pageY
的舊版瀏覽器。React 也試圖優化這一點。能夠看到,製做一個運行快速的工具須要使用不少技術,這個滾動就是一個很好的例子。
看下流程圖,在圖中(03)處標明瞭一個建立的實例。在這裏建立一個 <ExampleApplication />
的實例還爲時過早。實際上該處實例化了 TopLevelWrapper
(一個 React 內部的類)。讓咱們先來看看下面這個流程圖。
你能夠看到有三個部分,JSX 會被轉換爲 React 內部三種組件類型中的一種:ReactCompositeComponent
(咱們自定義的組件),ReactDOMComponent
(HTML 標籤)和 ReactDOMTextComponent
(文本節點)。咱們將略過描述ReactDOMTextComponent
並將重點放在前兩個。
內部組件?這頗有趣。你已經據說過 虛擬 DOM 了吧?虛擬 DOM 是一種 DOM 的表現形式。 React 用虛擬 DOM 進行組件差別計算等過程。該過程當中無需直接操做 DOM 。這使得 React 在更新視圖時候更快。但在 React 的源碼中沒有名爲「Virtual DOM」的文件或者類。這是由於 虛擬DOM 只是一個概念,一種如何操做真實 DOM 的方法。因此,有些人說 虛擬DOM 元素等同於 React 組件,但在我看來,這並不徹底正確。我認爲虛擬 DOM 指的是這三個類:ReactCompositeComponent
、ReactDOMComponent
和 ReactDOMTextComponent
。後面你會知道到爲何。
好了,讓咱們在這裏完成實例化過程。咱們將建立一個 ReactCompositeComponent
實例,但實際上這並非由於咱們把<ExampleApplication />
放在了 ReactDOM.render
裏。React 老是從 TopLevelWrapper
開始渲染一棵組件的樹。它幾乎是一個空的包裝器,其 render
方法(組件的 render)隨後將返回 <ExampleApplication />
。
//src\renderers\dom\client\ReactMount.js#277
TopLevelWrapper.prototype.render = function () {
return this.props.child;
};複製代碼
因此,目前爲止只有 TopLevelWrapper
被建立了。可是……先看一下一個有趣的事實。
有趣的事實:驗證 DOM 內嵌套
幾乎每次內嵌的組件渲染時,都被一個專門用於進行 HTML 驗證的
validateDOMNesting
模塊驗證。DOM 內嵌驗證指的是子標籤 -> 父標籤
的標籤層級的驗證。例如,若是父標籤是<select>
,則子標籤應該是如下其中一個標籤:option
、optgroup
或者#text
。這些規則其實是在 html.spec.whatwg.org/multipage/s… 中定義的。你可能已經看到過這個模塊是如何工做的,它像這樣報錯:
<div> cannot appear as a descendant of <p> .
讓咱們回顧一下上面的內容。再看一下流程圖,而後刪除多餘的不過重要的部分,變成下面這樣:
再調整一下間距和對齊:
實際上,這就是本部分的全部內容。所以,咱們能夠從 第 0 部分 中獲得重點,並將它用於最終的 mounting
流程中:
1.0 第 1 部分(點擊查看大圖)
某一組件實例應該以某種方式鏈接入React的生態系統,並對該系統產生一些影響。有一個專門的模塊名爲 ReactUpdates
專職於此。 正如你們所知, React 以塊形式執行更新,這意味着它會收集一些操做而後統一執行。
這樣作更好,由於這樣容許爲整個塊只應用一次某些前置條件和後置條件,而不是爲塊中的每一個操做都應用。
什麼真正執行了這些前/後處理?對, 事務!對某些人來講,事務多是一個新術語,至少對UI方面來講是個新的含義。接下來咱們從一個簡單的例子開始再來談一下它。
想象一下 通訊信道
。你須要開啓鏈接,發送消息,而後關閉鏈接。 若是你按這個方式逐個發送消息,就要每次發送消息的時候創建、關閉鏈接。不過,你也能夠只開啓一次鏈接,發送全部掛起的消息而後關閉鏈接。
1.1 很是真實的事務示例 (查看大圖)
好的,讓咱們再想一想更多抽象的東西。想象一下,在執行操做期間,「發送消息」是您要執行的任何操做,「打開/關閉鏈接」是預處理/後處理。 而後,再想一想一下,你能夠分別定義任何 open/close 對,並使用任何方法來使用它們(咱們能夠將它們命名爲 wrapper
,由於事實上每一對都包裝動做方法)。聽起來很酷,不是嗎?
咱們回到 React。 事務是 React 中普遍使用的模式。除了包裝行爲外,事務容許應用程序重置事務流,若是某事務已在進行中則阻止同時執行,等等。有不少不一樣的事務類,它們每一個都描述具體的行爲,它們都繼承自Transaction
模塊。事務類之間的主要區別是具體的事務包裝器的列表的不一樣。包裝器只是一個包含初始化和關閉方法的對象。
因此,個人想法是:
1.2 事務實現 (點擊查看大圖)
咱們來看看 React 中的一些其餘事務用例:
componentDidUpdate
回調。讓咱們回到具體案例。
正如咱們看到的, React 使用 ReactDefaultBatchingStrategyTransaction
(1)。咱們前文提到過,事務最重要的是它的包裝器。因此,咱們能夠看看包裝器,並弄清楚具體被定義的事務。好,這裏有兩個包裝器:FLUSH_BATCHED_UPDATES
,RESET_BATCHED_UPDATES
。咱們來看它們的代碼:
//\src\renderers\shared\stack\reconciler\ReactDefaultBatchingStrategy.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.3 第 1 部分簡化版 (點擊查看大圖)
而後咱們適當再調整一下:
1.4 第 1 部分簡化和重構 (點擊查看大圖)
很好,實際上,下面的示意圖就是咱們所講的。所以,咱們能夠理解第 1 部分的本質,並將其畫在最終的 mount
(掛載) 方案裏:
1.5 第 1 部分本質(點擊查看大圖)
2.0 第二部分
此次咱們將討論 ReactReconcileTransaction
事務。正如你所知道的,對咱們來講主要感興趣的是事務包裝器。其中包括三個包裝器:
//\src\renderers\dom\client\ReactReconcileTransaction.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
模塊老是會被調用,例如這個確切的例子。掛載過程在每一個平臺上都是不一樣的,因此 「主模塊」 會詢問ReactReconciler
,ReactReconciler
知道下一步應該怎麼作。
好的,讓咱們將目光移到組件方法 mountComponent
上。這多是你已經據說過的方法了。它初始化組件,渲染標記以及註冊事件監聽函數。你看,千辛萬苦咱們終於看到了調用組件加載。調用加載以後,咱們應該能夠獲得能夠插入到文檔中的 HTML 元素了。
讓咱們回顧一下這一部分,咱們再一次流程圖,而後刪除一些不重要的信息,它將變成這樣:
2.1 第二部分 簡化
讓咱們優化一下排版:
2.2 第二部分 簡化與重構
很好,其實這就是這一部分所發生的一切。咱們能夠從 第一部分 中取下必要的信息,而後完善 mounting
(掛載) 的流程圖:
2.3 第二部分 必要信息
3.0 第 3 部分 (點擊查看大圖)
componentMount
方法是咱們整個系列中極其重要的一個部分。如圖,咱們關注 ReactCompositeComponent.mountComponent
(1) 方法。
若是你還記得,我曾提到過 組件樹的入口組件 是 TopLevelWrapper
組件 (React 底層內部類)。咱們準備掛載它。因爲它其實是一個空的包裝器,調試起來很是枯燥而且對實際的流程而言沒有任何影響,因此咱們跳過這個組件從他的孩子組件開始分析。
把組件掛載到組件樹上的過程就是先掛載父親組件,而後他的孩子組件,而後他的孩子的孩子組件,依次類推。能夠確定,當 TopLevelWrapper
掛載後,他的孩子組件 (用來管理 ExampleApplication
的組件 ReactCompositeComponent
) 也會在同一階段注入。
如今咱們回到步驟 (1) 觀察這個方法的內部實現,有一些重要行爲會發生,接下來讓咱們深刻研究這些重要行爲。
從 transaction.getUpdateQueue()
方法返回的 updater
見圖中(2), 實際上就是 ReactUpdateQueue
模塊。 爲何要在這裏賦值一個 updater
呢?由於咱們正在研究的類 ReactCompositeComponent
是一個全平臺的共用的類,可是 updater
卻依賴於平臺環境有不一樣的實現,因此咱們在這裏根據不一樣的平臺動態的將它賦值給實例。
然而,咱們如今並不立刻須要這個 updater
,可是你要記住它是很是重要的,由於它很快就會應用於很是知名的組件內更新方法 setState
。
事實上在這個過程當中,不只僅 updater
被賦值給實例,組件實例(你的自定義組件)也得到了繼承的 props
, context
, 和 refs
。
觀察如下的代碼:
// \src\renderers\shared\stack\reconciler\ReactCompositeComponent.js#255
// 這些應該在構造方法裏賦值,可是爲了
// 使類的抽象更簡單,咱們在它以後賦值。
inst.props = publicProps;
inst.context = publicContext;
inst.refs = emptyObject;
inst.updater = updateQueue;複製代碼
所以,你才能夠經過一個實例從你的代碼中得到 props
,好比 this.props
。
經過調用步驟 (3) 的方法 _constructComponent
而後通過幾個構造方法的做用後,最終建立了 new ExampleApplication()
。這就是咱們代碼中構造方法第一次被執行的時機,固然也是咱們的代碼第一次實際接觸到 React 的生態系統,很棒。
接着咱們研究步驟 (4),第一個即將發生的行爲是 componentWillMount
(固然僅當它被定義時) 的調用。這是咱們遇到的第一個生命週期鉤子函數。固然,在下面一點你會看到 componentDidMount
函數, 只不過這時因爲它不能立刻執行,而是被注入了一個事務隊列中,在很後面執行。他會在掛載系列操做執行完畢後執行。固然你也可能在 componentWillMount
內部調用 setState
,在這種狀況下 state
會被從新計算但此時不會調用 render
。(這是合理的,由於這時候組件尚未被掛載)
官方文檔的解釋也證實這一點:
componentWillMount()
在掛載執行以前執行,他會在render()
以前被調用,所以在這個過程當中設置組件狀態不會觸發重繪。
觀察如下的代碼,進一步驗證:
// \src\renderers\shared\stack\reconciler\ReactCompositeComponent.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.1 第 3 部分簡化版 (點擊查看大圖)
讓咱們適度在調整一下:
3.2 第 3 部分簡化和重構 (點擊查看大圖)
很好,實際上,下面的示意圖就是咱們所講的。所以,咱們能夠理解第 3 部分的本質,並將其用於最終的 mount
方案:
3.3 第 3 部分本質 (點擊查看大圖)
4.0 第 4 部分 (點擊查看大圖)
已經入迷了對嗎? 讓咱們接續研究 mount
方法。
若是步驟 (1) 的 _tag
包含一個複雜的標籤,好比 video
、form
、 textarea
等等,這些就須要更進一步的封裝,對每一個媒體事件須要綁上更多事件監聽器,好比給 audio
標籤增長 volumechange
事件監聽,或者像 select
、textarea
等標籤只須要封裝一些瀏覽器原生行爲。
咱們有不少封裝器幹這事,好比 ReactDOMSelect
和 ReactDOMTextarea
位於源碼 (src\renderers\dom\client\wrappers\folder) 中。本文例子中只有簡單的 div
標籤。
接下來要講解的驗證方法是爲了確保內部 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: ...}
的形式)
接着, document.createElement
方法會建立真實的 HTML 元素,實例出真實的 HTML div
,在這一步以前咱們只能用虛擬的表現形式表達,而如今你第一次能實際看到它了。
咱們來回顧一下咱們學到的。咱們再看一下這種模式,而後去掉冗餘的部分:
4.1 第 4 部分簡化版 (點擊查看大圖)
讓咱們適度在調整一下:
4.2 第 4 部分簡化和重構 (點擊查看大圖)
很好,實際上,下面的示意圖就是咱們所講的。所以,咱們能夠理解第 4 部分的本質,並將其用於最終的 mount
方案:
4.3 第 4 部分本質 (點擊查看大圖)
5.0 第 5 部分(點擊查看大圖)
這圖片看上去有點複雜?這裏主要講的是如何高效的把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\renderers\shared\stack\event\EventPluginHub.js
)。它包含一個 listenerBank
的映射來緩存並管控全部的監聽器。咱們準備好了添加咱們本身的事件監聽器,可是不是如今。這裏的關鍵在於咱們應該在組件和 DOM 元素已經準備好處理事件的時候才增長監聽器。看上去在這裏咱們執行遲了。也你許會問,咱們如何知道 DOM 已經準備好了?很好,這就引出了下一個問題!你是否還記得咱們曾把 transaction
傳遞給每一個方法和調用?這就對了,咱們那樣作就是由於在這種場景它能夠很好的幫助咱們。讓咱們從代碼中尋找佐證:
//src\renderers\dom\shared\ReactDOMComponent.js#222
transaction.getReactMountReady().enqueue(putListener, {
inst: inst,
registrationName: registrationName,
listener: listener,
});複製代碼
在處理完事件監聽器,咱們開始設置 DOM 屬性名和 DOM 屬性值。就像以前說的同樣,對於屬性們,咱們肯定他們不是 RESERVED_PROPS
中的一員,而是實際的 prop
,例如 children
或者 dangerouslySetInnerHTML
。
在處理前一個和下一個 props 的時候,咱們會計算 styleUpdates
的配置而且如今把它傳遞給 CSSPropertyOperations
模塊。
很好,咱們已經完成了更新屬性這一部分,讓咱們繼續
咱們來回顧一下咱們學到的。咱們再看一下這種模式,而後去掉冗餘的部分:
5.1 第 5 部分簡化版 (點擊查看大圖)
而後咱們適當再調整一下:
5.2 第 5 部分簡化和重構 (點擊查看大圖)
很好,實際上,下面的示意圖就是咱們所講的。所以,咱們能夠理解第 5 部分的本質,並將其用於最終的 mounting
方案:
5.3 第 5 部分 本質 (點擊查看大圖)
6.0 第 6 部分(點擊查看大圖)
好像組件自己已經建立完成了,如今咱們能夠繼續建立它的子組件了。這個分爲如下兩步:(1)子組件應該由(this.mountChildren
)加載,(2)並與它的父級經過(DOMLazyTree.queueChild
)鏈接。咱們來討論一會兒組件的掛載。
有一個單獨的 ReactMultiChild
(src\renderers\shared\stack\reconciler\ReactMultiChild.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.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.1 第 7 部分 簡化(點擊查看大圖)
咱們也應該修改空格和對齊:
7.2 第 7 部分 簡化並重構(點擊查看大圖)
其實這就是這裏發生的全部。咱們能夠從第 7 部分中的重要部分來組成最終的 mounting
流程:
7.3 第 7 部分 基本價值(點擊查看大圖)
完成!其實咱們完成了加載。讓咱們來看看下圖吧!
7.4 Mounting 過程(點擊查看大圖)
8.0 Part 8 (點擊查看大圖)
this.setState
咱們已經學習了掛載的工做原理,如今從另外一個角度來學習。嗯,好比 setState
方法,其實也很簡單。
首先,爲何咱們能夠在本身的組件中調用 setState
方法呢?很明顯咱們的組件繼承自 ReactComponent
,這個類咱們能夠很方便的在 React 源碼中找到。
//src\isomorphic\modern\class\ReactComponent.js#68
this.updater.enqueueSetState(this, partialState)複製代碼
咱們發現,這裏有一些 updater
接口。什麼是 updater
呢?在講解掛載過程時咱們講過,在 mountComponent
過程當中,實例會接受一個 ReactUpdateQueue
(src\renderers\shared\stack\reconciler\ReactUpdateQueue.js
) 的引用做爲 updater
屬性。
很好,咱們如今深刻研究步驟 (1) 的 enqueueSetState
。首先,它會往步驟 (2) 的 _pendingStateQueue
(來自於內部實例。注意,這裏咱們說的外部實例是指用戶的組件 ExampleApplication
,而內部實例則掛載過程當中建立的 ReactCompositeComponent
) 注入 partialState
(這裏的 partialState
就是指給 this.setState
傳遞的對象)。而後,執行 enqueueUpdate
,這個過程會檢查更新是否已經在進展中,若是是則把咱們的組件注入到 dirtyComponents
列表中,若是不是則先初始化打開更新事務,而後把組件注入到 dirtyComponents
列表。
總結一下,每一個組件都有本身的一組處於等待的」狀態「的列表,當你在一次事務中調用 setState
方法,其實只是把那個狀態對象注入一個隊列裏,它會在以後一個一個依次被合併到組件 state
中。調用此setState
方法同時,你的組件也會被添加進 dirtyComponents
列表。也許你很好奇 dirtyComponents
是如何工做的,這就是另外一個研究重點。
咱們來回顧一下咱們學到的。咱們再看一下這種模式,而後去掉冗餘的部分:
8.1 第 8 部分簡化版 (點擊查看大圖)
讓咱們適度在調整一下:
8.2 第 8 部分簡化和重構 (點擊查看大圖)
很好,實際上,下面的示意圖就是咱們所講的。所以,咱們能夠理解第 8 部分的本質,並將其用於最終的 updating
方案:
8.3 Part 8 本質 (點擊查看大圖)
9.0 第 9 部分(點擊查看大圖)
setState
根據流程圖咱們發現,有不少方式來觸發 setState
。能夠直接經過用戶交互觸發,也可能只是隱含在方法裏觸發。咱們舉兩個例子:第一種狀況下,它由用戶的鼠標點擊事件觸發。而第二種狀況,例如在 componentDidMount
裏經過 setTimeout
調用來觸發。
那麼這兩種方式有什麼差別呢?若是你還記得 React 的更新過程是批量化進行的,這就意味着他先會收集這些更新操做,而後一塊兒處理。當鼠標事件觸發後,會被頂層先處理,而後通過多層封裝器的做用,這個批更新操做纔會開始。過程當中你會發現,只有當步驟 (1) 的 ReactEventListener
是 enabled
的狀態纔會觸發更新。然而你還記得在組件掛載過程當中,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);
};複製代碼
好,回到咱們的更新,讓咱們總結一下,整個過程是:
dirtyComponents
列表ReactUpdates.flushBatchedUpdates
的同時關閉事務, 並處理在全部 dirtyComponents
列表中的組件9.1 setState
執行過程 (點擊查看大圖)
咱們來回顧一下咱們學到的。咱們再看一下這種模式,而後去掉冗餘的部分:
9.2 第 9 部分簡化版 (點擊查看大圖)
而後咱們適當再調整一下:
9.3 第 9 部分簡化和重構 (點擊查看大圖)
很好,實際上,下面的示意圖就是咱們所講的。所以,咱們能夠理解第 9 部分的本質,並將其用於最終的 updating
方案:
9.4 第 9 部分本質 (點擊查看大圖)
10.0 第十部分 (點擊查看大圖)
就像流程圖所示那樣,React 會遍歷步驟 (1) 的 dirtyComponents
,而且經過事務調用步驟 (2) 的 ReactUpdates.runBatchedUpdates
。事務? 又是一個新的事務,它怎麼工做呢,咱們一塊兒來看。
這個事務的類型是 ReactUpdatesFlushTransaction
,以前咱們也說過,咱們須要經過事務包裝器來理解事務具體幹什麼。如下是從代碼註釋中得到的啓示:
ReactUpdatesFlushTransaction 的封裝器組會清空 dirtyComponents 數組,而且執行 mount-ready 處理器組壓入隊列的更新 (mount-ready 處理器是指那些在 mount 成功後觸發的生命週期函數。例如
componentDidUpdate
)
可是,無論怎樣,咱們須要證明它。如今有兩個 wrappers
: NESTED_UPDATES
和 UPDATE_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\renderers\shared\stack\reconciler\ReactUpdates.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.1 第 10 部分簡化版 (點擊查看大圖)
讓咱們適度調整一下:
10.2 第 10 部分重構與簡化 (點擊查看大圖)
很好,實際上,下面的示意圖就是咱們所講的。所以,咱們能夠理解第 10 部分的本質,並將其用於最終的 updating
方案:
10.3 第 10 部分 本質 (點擊查看大圖)
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
方法,無論state
和props
是否是變化,都強制更新。固然,React 的官方文檔不推薦這樣的實踐。在使用 forceUpdate
的狀況下,組件將會被持久化的更新,不然,shouldUpdate
將會是 shouldComponentUpdate
的返回結果。若是 shouldUpdate
爲否,組件不該該更新時,React 依然會設置新的 props
and state
, 不過會跳過更新的餘下部分。
咱們來回顧一下咱們學到的。咱們再看一下這種模式,而後去掉冗餘的部分:
11.1 第 11 部分簡化版 (點擊查看大圖)
而後咱們適當再調整一下:
11.2 第 11 部分簡化和重構 (點擊查看大圖)
很好,實際上,下面的示意圖就是咱們所講的。所以,咱們能夠理解第 11 部分的本質,並將其用於最終的 updating
方案:
11.3 第 11 部分本質 (點擊查看大圖)
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.2 第 12 部分簡化和重構 (點擊查看大圖)
很好,實際上,下面的示意圖就是咱們所講的。所以,咱們能夠理解第 12 部分的本質,並將其用於最終的 updating
方案:
12.3 第 12 部分本質 (點擊查看大圖)
13.0 第 13 部分(點擊查看大圖)
經過 ReactReconciler.receiveComponent
,React 實際上從 ReactDOMComponent
調用 receiveComponent
並傳遞給下一個元素。在 DOM 組件實例上從新分配並調用 update 方法。updateComponent
方法實際上主要是兩步: 基於 prev
和 next
的屬性,更新 DOM 屬性和 DOM 元素的子節點。好在咱們已經分析了 _updateDOMProperties
(src\renderers\dom\shared\ReactDOMComponent.js#946
) 方法。就像你記得的那樣,這個方法大部分處理了 HTML 元素的屬性和特質,計算樣式以及處理事件監聽等。剩下的就是 _updateDOMChildren
(src\renderers\dom\shared\ReactDOMComponent.js#1076
) 方法了。
讓咱們歸納一下咱們怎麼到這裏的。再看一下這張圖,而後移除掉冗餘的不那麼重要的部分,它就變成了這樣:
13.1 第 13 部分 簡化(點擊查看大圖)
咱們也應該儘量的修改空格和對齊方式:
13.2 第 13 部分 簡化和重構(點擊查看大圖)
很好。實際上它就是這兒所發生的一切。咱們能夠從第 13 部分中得到基本價值,並將其用於最終的「更新」圖表:
13.3 第 13 部分本質(點擊查看大圖)
14.0 第 14 部分(點擊查看大圖)
在發起子組件更新操做時會有不少屬性影響子組件內容。這裏有幾種可能的狀況,不過其實就只有兩大主要狀況。即子組件是否是 「複雜」。這裏的複雜的含義是,它們是 React 組件,React 應當經過它們不斷遞歸直到觸及內容層,或者,該子組件只是簡單數據類型,好比字符串、數字。
這個判斷條件就是步驟 (1) 的 nextProps.children
的類型,在咱們的情形中,ExampleApplication
有三個孩子 button
, ChildCmp
和 text string
。
很好,如今讓咱們來看它的工做原理。
首先,在首次迭代時,咱們分析 ExampleApplication children
。很明顯能夠看出子組件的類型不是 「純內容類型」,所以狀況爲 「複雜」 狀況。而後咱們一層層往下遞歸,每層都會判斷 children 的類型。順便說一下,步驟 (2) 的 shouldUpdateReactComponent
判斷條件可能讓你有些困惑,它看上去是在驗證更新與否,但實際上它會檢查類型是更新仍是刪除與建立(爲了簡化流程咱們跳過此條件爲否的情形,假定是更新)。固然接下來咱們對比新舊子組件,若是有孩子被移除,咱們也會去除掛載組件,並把它移除。
14.1 Children 更新 (點擊查看大圖)
在第二輪迭代時,咱們分析 button
,這是一個很簡單的案例,因爲它僅包含一個標題文字 set state button
,它的孩子只是一個字符串。所以咱們對比一下以前和如今的內容。很好,這些文字並無變化,所以咱們不須要更新 button
?這很是的合理,所以所謂的 「虛擬 DOM」,如今聽上去也不是那麼的抽象,React 維護了一個對 DOM 的內部表達對象,而且在須要的時候更改真實 DOM,這樣取得了很不錯的性能。所以我想你應該已經瞭解了這個設計模式。那咱們接着來更新 ChildCmp
,而後它的孩子也到達咱們能夠更新的最底層。能夠看到在這層的內容已經被修改了,當時咱們經過 click
和 setState
的調用,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\renderers\dom\client\utils\DOMChildrenOperations.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.2 第 14 部分簡化板 (點擊查看大圖)
而後咱們適當再調整一下:
14.3 第 14 簡化和重構 (點擊查看大圖)
很好,實際上,下面的示意圖就是咱們所講的。所以,咱們能夠理解第 14 部分的本質,並將其用於最終的 updating
方案:
14.4 第 14 部分 本質 (點擊查看大圖)
咱們已經完成了更新操做的學習,讓咱們重頭整理一下。
14.5 更新 (點擊查看大圖)