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

譯者:Kite
做者:Gethyl George Kurian
原文連接:medium.com/@gethylgeor…react

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

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

因爲在我大量搜索的資料中沒有獲取到一點有用的資料,我最終決定探究 reactreact-dom 的源碼來更好地理解它們的工做原理。算法

可是在咱們開始以前,你有思考過爲何咱們不直接渲染DOM的更新嗎?app

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

DOM 是如何建立的

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

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

由於 DOM 是一個樹形結構,每次DOM 中的某些部分發生變化時,雖然這些變化 已經至關地快了,但它改變的元素不得不通過迴流的步驟,且它的子節點不得不被重繪,所以,若是項目中越多的節點須要經歷迴流/重繪,你的應用就會表現得越慢。ui

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

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

理解 Virtual-DOM

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

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

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

能夠在這個git repo 上找到這個 app 的源碼。這個簡單的計算器界面長這樣:

除了 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 長這樣:

(初始渲染後的 DOM)

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

如今添加兩個數字並點擊「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>
複製代碼

標記組件

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

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

  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 中的樣子:

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

在咱們的例子中,您能夠看到 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 中被從新構建

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

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

有一個重要的點就是這裏是調用組件 render 方法的地方。好比,Calculator.render()

這個 reconciliation 過程一般採用如下步驟:

組件的 render 方法 - 更新Virtual DOM,運行 diff 算法,最後更新 DOM

紅色虛線意味着全部的reconciliation 步驟都將在下一個子節點及子節點中的子節點裏重複。

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

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

所以,你能夠在咱們的示例中看到這個reconciliation是如何像如下這樣進行運做的:

我先跳過前一個<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 期間,只有輸出字段有以下所示的更改和在開發人員控制檯出現繪製閃爍。

僅重繪輸出

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

結論

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

我沒有選擇更復雜的應用程序是由於繪製整個組件樹真的很煩人。:-|

reconciliation 過程就是 React

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

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

相關文章
相關標籤/搜索