[譯] Virtual Dom 和 Diff 演算法在 React 中是如何工作的?

16slowly發表於2019-01-31

譯者:Kite
作者:Gethyl George Kurian
原文連結:medium.com/@gethylgeor…

文章已經過時,基於 react v16 的將會近期發出,此文僅供參考

我曾經嘗試去深層而清晰地去理解 Virtual-DOM 的工作原理,也一直在尋找可以更詳細地解釋其工作細節的資料。

由於在我大量搜尋的資料中沒有獲取到一點有用的資料,我最終決定探究 reactreact-dom 的原始碼來更好地理解它們的工作原理。

但是在我們開始之前,你有思考過為什麼我們不直接渲染DOM的更新嗎?

接下來的一節中,我將介紹 DOM 是如何建立的,以及讓你瞭解為什麼 React 一開始就建立了 Virtual-DOM

DOM 是如何建立的

[譯] Virtual Dom 和 Diff 演算法在 React 中是如何工作的?

(圖片來自 Mozilla - https://developer.mozilla.org/en-US/docs/Introduction_to_Layout_in_Mozilla)

我不會說太多關於 DOM 是如何建立且是如何繪製到螢幕上的,但可以查閱這裡這裡去理解將整個 HTML 轉換成 DOM 以及繪製到螢幕的步驟。

因為 DOM 是一個樹形結構,每次DOM 中的某些部分發生變化時,雖然這些變化 已經相當地快了,但它改變的元素不得不經過迴流的步驟,且它的子節點不得不被重繪,因此,如果專案中越多的節點需要經歷迴流/重繪,你的應用就會表現得越慢。

什麼是 Virtual-DOM ? 它嘗試去最小化迴流/重繪步驟,從而在大型且複雜的專案中得到更好的效能。

接下來一節中將會解釋更多有關於Virtual-DOM 如何工作的細節。

理解 Virtual-DOM

既然你已經瞭解了 DOM 是如何構建的,那現在就讓我們去更多地瞭解一下 Virtual-DOM吧。

在這裡,我會先用一個小型的 app 去解釋 virtual dom 是如何工作的,這樣,你可以容易地去看到它的工作過程。

我不會深入到最初渲染的工作細節,僅關注重新渲染時所發生的事情,這將幫助你去理解 virtual domdiff 演算法是如何工作的,一旦你理解了這個過程,理解初始的渲染就變得很簡單:)。

可以在這個git repo 上找到這個 app 的原始碼。這個簡單的計算器介面長這樣:

[譯] Virtual Dom 和 Diff 演算法在 React 中是如何工作的?

除了 Main.jsCalculator.js之外,在這個 repo 中的其他檔案都可以不用關心。

// Calculator.js
import React from "react"
import ReactDOM from "react-dom"

export default class Calculator extends React.Component{
	constructor(props) {
		super(props);
		this.state = {output: ""};
	}

	render(){
		let IntegerA,IntegerB,IntegerC;
		

		return(
			<div className="container">						
				<h2>using React</h2>
				<div>Input 1: 
					<input type="text" placeholder="Input 1" ref="input1"></input>
				</div>
				<div>Input 2 :
					<input type="text" placeholder="Input 2" ref="input2"></input>
				</div>
				<div>
					<button id="add" onClick={ () => {
						IntegerA = parseInt(ReactDOM.findDOMNode(this.refs.input1).value)
						IntegerB = parseInt(ReactDOM.findDOMNode(this.refs.input2).value)
						IntegerC = IntegerA+IntegerB
						this.setState({output:IntegerC})
					  }
					}>Add</button>
					
					<button id="subtract" onClick={ () => {
						IntegerA = parseInt(ReactDOM.findDOMNode(this.refs.input1).value)
						IntegerB = parseInt(ReactDOM.findDOMNode(this.refs.input2).value)
						IntegerC = IntegerA-IntegerB
						this.setState({output:IntegerC})

					  }
					}>Subtract</button>
				</div>
				<div>
					<hr/>
					<h2>Output: {this.state.output}</h2>
				</div>
				
			</div>
		);
	}
}
複製程式碼
// Main.js
import React from "react";
import Calculator from "./Calculator"

export default class Layout extends React.Component{
	render(){	

		return(
			<div>
			        <h1>Basic Calculator</h1>
				 <Calculator/>
			</div>
		);
	}
}
複製程式碼

初始載入時產生的 DOM 長這樣:

[譯] Virtual Dom 和 Diff 演算法在 React 中是如何工作的?

(初始渲染後的 DOM)

下面是 React 內部構建的上述 DOM 樹的結構:

[譯] Virtual Dom 和 Diff 演算法在 React 中是如何工作的?

現在新增兩個數字並點選「Add」按鈕去更深入的理解

為了去理解 Diff 演算法是如何工作及reconciliation 如何排程 virtual-dom 到真實的DOM 的,在這個計算器中,我將輸入 100 和 50 並點選「Add」按鈕,期待輸出 150:

輸入1: 100
輸入2: 50

輸出: 150
複製程式碼

那麼,當你按下「Add」按鈕時,發生了什麼?

在我們的例子中,當點選了「Add」按鈕,我們 set 了一個包含有輸出值 150 的 state:

// Calculator.js
 <button id="add" onClick={() => {
        IntegerA = parseInt(ReactDOM.findDOMNode(this.refs.input1).value);
        IntegerB = parseInt(ReactDOM.findDOMNode(this.refs.input2).value);
        IntegerC = IntegerA+IntegerB;
        this.setState({output:IntegerC});
      }}>Add</button>
複製程式碼

標記元件

(注: 將發生變化的元件)

[譯] Virtual Dom 和 Diff 演算法在 React 中是如何工作的?

首先,讓我們理解第一步,一個元件是如何被標記的:

  1. 所有的 DOM 事件監聽器都被包裹在 React 自定義的事件監聽器中,因此,當點選「Add」按鈕時,這個點選事件被髮送到 react 的事件監聽器,從而執行上面程式碼中你所看到的匿名函式

  2. 在匿名函式中,我們調取 this.setState 方法得到了一個新的 state 值。

  3. 這個 setState() 方法將如以下幾行程式碼一樣,依次標記元件。

// ReactUpdates.js  - enqueueUpdate(component) function
dirtyComponents.push(component);
複製程式碼

你是否在思考為什麼 react 不直接標記這個 button, 而是標記整個元件?好了,這是因為你用了this.setState() 來調取 setState 方法,而這個 this 指向的就是這個 Calculator 元件

  1. 所以現在,我們的 Calculator 元件被標記了,讓我們看看接下來又將發生什麼。

遍歷元件的生命週期

很好!現在這個元件被標記了,那麼接下來會發生什麼呢?接下來是更新 virtual dom,然後使用diff 演算法做 reconciliation 並更新真實的 DOM

在我們進行下一步之前,熟悉元件生命週期的不同之處是非常重要的

以下是我們的 Calculator 元件在 react 中的樣子:

[譯] Virtual Dom 和 Diff 演算法在 React 中是如何工作的?

Calculator Wrapper

以下是這個元件被更新的步驟:

  1. 這是通過 react 執行批量更新而更新的;

  2. 在批量更新中,它會檢查是否元件被標記,然後開始更新。

 //ReactUpdates.js
var flushBatchedUpdates = function () {
  while (dirtyComponents.length || asapEnqueued) {
    if (dirtyComponents.length) {
      var transaction = ReactUpdatesFlushTransaction.getPooled();
      transaction.perform(runBatchedUpdates, null, transaction);
複製程式碼
  1. 接下來,它會檢查是否存在必須更新的待處理狀態或是否發出了forceUpdate
if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
      this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context);
複製程式碼

[譯] Virtual Dom 和 Diff 演算法在 React 中是如何工作的?

在我們的例子中,您可以看到 this._pendingStateQueue 在具有新輸出狀態的計算器包裝器裡

  1. 首先,它會檢查我們是否使用了componentWillReceiveProps(),如果我們使用了,則允許使用收到的 props 更新 state

  2. 接下來,react 會檢查我們在元件裡是否使用了 shouldComponentUpdate() ,如果我們使用了,我們可以檢查一個元件是否需要根據它的 stateprops 的改變而重新渲染。

當你知道不需要重新渲染元件時,請使用此方案,從而提高效能

  1. 接下來的步驟依次是 componentWillUpdate(), render(), 最後是 componentDidUpdate()

從第 4,5 和 6 步, 我們只使用 render()

  1. 現在,讓我們深入看看 render() 期間發生了什麼?

渲染即是 Virtual-DOM 比較差異並重新構建

渲染元件 - 更新 Virtual-DOM, 執行diff 演算法並更新到真實的DOM

在我們的例子中,所有在這個元件裡的元素都會在 Virtual-DOM 中被重新構建

[譯] Virtual Dom 和 Diff 演算法在 React 中是如何工作的?

它會檢查相鄰已渲染的元素是否具有相同的型別和鍵,然後協調這個型別與鍵匹配的元件。

 var prevRenderedElement = this._renderedComponent._currentElement;
 //Calculator.render() method is called and the element is build.
 var nextRenderedElement = this._instance.render(); 
複製程式碼

有一個重要的點就是這裡是呼叫元件 render 方法的地方。比如,Calculator.render()

這個 reconciliation 過程通常採用以下步驟:

[譯] Virtual Dom 和 Diff 演算法在 React 中是如何工作的?

元件的 render 方法 - 更新Virtual DOM,執行 diff 演算法,最後更新 DOM

紅色虛線意味著所有的reconciliation 步驟都將在下一個子節點及子節點中的子節點裡重複。

上述的流程圖總結了 Virtual DOM 是如何更新實際 DOM 的。

我可能在知情或不知情的情況下錯過了幾個步驟,但此圖表涵蓋了大部分關鍵步驟。

因此,你可以在我們的示例中看到這個reconciliation是如何像以下這樣進行運作的:

[譯] Virtual Dom 和 Diff 演算法在 React 中是如何工作的?

我先跳過前一個<div>reconciliation ,引導你看看 DOM 變成 Output:150 的更新步驟,

  • Reconciliation 從這個元件的類名為 "container" 的<div> 開始
  • 它的孩子是一個包含了輸出的<div>, 因此,react 將從這個子節點開始reconciliation
  • 現在這個子節點擁有了子節點 <hr><h2>
  • 所以 react 將為 <hr> 執行reconciliation
  • 接下來,它將從 <h2>reconciliation 開始,因為它有自己的子節點,即輸出和 state 的輸出,它將開始對這兩個進行reconciliation
  • 第一個輸出文字經過了reconciliation,因為它沒有任何變化,所以 DOM 沒有什麼需要改變。
  • 接下來,來自 state 的輸出經過reconciliation,因為我們現在有了一個新值,即 150,react 會更新真實的 DOM。 ...

真實 DOM 的渲染

我們的例子中,在 reconciliation 期間,只有輸出欄位有如下所示的更改和在開發人員控制檯出現繪製閃爍。

[譯] Virtual Dom 和 Diff 演算法在 React 中是如何工作的?

僅重繪輸出

以及在真實 DOM上更新的元件樹

[譯] Virtual Dom 和 Diff 演算法在 React 中是如何工作的?

結論

結論雖然這個例子非常簡單,但它可以讓你基本瞭解react 內部所發生的事情。

我沒有選擇更復雜的應用程式是因為繪製整個元件樹真的很煩人。:-|

reconciliation 過程就是 React

  • 比較前一個的內部例項與下一個內部例項
  • 更新內部例項 Virtual DOM(JavaScript 物件) 中的元件樹結構。
  • 僅更新存在實際變化的節點及其子節點的真實 DOM

(注: 作者文中的 react 版本是 v15.4.1)

相關文章