譯者:Kite
做者:Gethyl George Kurian
原文連接:medium.com/@gethylgeor…react
文章已通過時,基於 react v16 的將會近期發出,此文僅供參考 git
我曾經嘗試去深層而清晰地去理解 Virtual-DOM
的工做原理,也一直在尋找能夠更詳細地解釋其工做細節的資料。github
因爲在我大量搜索的資料中沒有獲取到一點有用的資料,我最終決定探究 react
和 react-dom
的源碼來更好地理解它們的工做原理。算法
可是在咱們開始以前,你有思考過爲何咱們不直接渲染
DOM
的更新嗎?app
接下來的一節中,我將介紹 DOM
是如何建立的,以及讓你瞭解爲何 React
一開始就建立了 Virtual-DOM
dom
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 dom
與diff
算法是如何工做的,一旦你理解了這個過程,理解初始的渲染就變得很簡單:)。
能夠在這個git repo 上找到這個 app 的源碼。這個簡單的計算器界面長這樣:
除了 Main.js
和 Calculator.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 樹的結構:
爲了去理解 Diff
算法是如何工做及reconciliation
如何調度 virtual-dom
到真實的DOM
的,在這個計算器中,我將輸入 100 和 50 並點擊「Add」按鈕,期待輸出 150:
輸入1: 100
輸入2: 50
輸出: 150
複製代碼
在咱們的例子中,當點擊了「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>
複製代碼
(注: 將發生變化的組件)
首先,讓咱們理解第一步,一個組件是如何被標記的:
全部的 DOM
事件監聽器都被包裹在 React
自定義的事件監聽器中,所以,當點擊「Add」按鈕時,這個點擊事件被髮送到 react 的事件監聽器,從而執行上面代碼中你所看到的匿名函數
在匿名函數中,咱們調取 this.setState
方法獲得了一個新的 state 值。
這個 setState()
方法將如如下幾行代碼同樣,依次標記組件。
// ReactUpdates.js - enqueueUpdate(component) function
dirtyComponents.push(component);
複製代碼
你是否在思考爲何 react 不直接標記這個 button, 而是標記整個組件?好了,這是由於你用了
this.setState()
來調取setState
方法,而這個 this 指向的就是這個 Calculator 組件
很好!如今這個組件被標記了,那麼接下來會發生什麼呢?接下來是更新 virtual dom
,而後使用diff
算法作 reconciliation
並更新真實的 DOM
在咱們進行下一步以前,熟悉組件生命週期的不一樣之處是很是重要的
如下是咱們的 Calculator 組件在 react
中的樣子:
Calculator Wrapper
如下是這個組件被更新的步驟:
這是經過 react
運行批量更新而更新的;
在批量更新中,它會檢查是否組件被標記,而後開始更新。
//ReactUpdates.js
var flushBatchedUpdates = function () {
while (dirtyComponents.length || asapEnqueued) {
if (dirtyComponents.length) {
var transaction = ReactUpdatesFlushTransaction.getPooled();
transaction.perform(runBatchedUpdates, null, transaction);
複製代碼
forceUpdate
。if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context);
複製代碼
在咱們的例子中,您能夠看到 this._pendingStateQueue
在具備新輸出狀態的計算器包裝器裏
首先,它會檢查咱們是否使用了componentWillReceiveProps()
,若是咱們使用了,則容許使用收到的 props
更新 state
。
接下來,react
會檢查咱們在組件裏是否使用了 shouldComponentUpdate()
,若是咱們使用了,咱們能夠檢查一個組件是否須要根據它的 state
或 props
的改變而從新渲染。
當你知道不須要從新渲染組件時,請使用此方案,從而提升性能
componentWillUpdate()
, render()
, 最後是 componentDidUpdate()
從第 4,5 和 6 步, 咱們只使用
render()
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
)