如何理解Virtual DOM

什麼是虛擬DOM

接下來用vdom(Virtual DOM)來簡稱爲虛擬DOM。css

指的是用JS模擬的DOM結構,將DOM變化的對比放在JS層來作。換而言之,虛擬DOM就是JS對象。
以下DOM結構:
<ul id="list">
    <li class="item">Item1</li>
    <li class="item">Item2</li>
</ul>

映射成虛擬DOM就是這樣:html

{
    tag: "ul",
    attrs: {
        id:&emsp;"list"
    },
    children: [
        {
            tag: "li",
            attrs: { className: "item" },
            children: ["Item1"]
        }, {
            tag: "li",
            attrs: { className: "item" },
            children: ["Item2"]
        }
    ]
}

React會去調用render()方法來從新渲染整個組件的UI,可是若是咱們真的去操做這麼大量的DOM,顯然性能是堪憂的。因此React實現了一個Virtual DOM,組件的真實DOM結構和Virtual DOM之間有一個映射的關係,React在虛擬DOM上實現了一個diff算法,當render()去從新渲染組件的時候,diff會找到須要變動的DOM,而後再把修改更新到瀏覽器上面的真實DOM上,因此,React並非渲染了整個DOM樹,Virtual DOM就是JS數據結構,因此 理論上原生的DOM快得多。react

爲何使用 virtual dom

現有一個例子:jquery

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>eg</title>
</head>

<body>
  <div id="box"></div>
  <button id="btn">點擊</button>

  <script src="https://cdn.bootcss.com/jquery/3.2.0/jquery.js"></script>
  <script>
    const data = ['哈哈', '呵呵','嘿嘿']
    //渲染函數
    function render(data) {
      const $box = $('#box');
      $box.html('');
      const $ul = $('<ul>');
      // 重繪一次
      $ul.append($('<li>10</li>'));
      data.forEach(item => {
        //每次進入都重繪
        $ul.append($(`<li>${item}</li>`))
      })
      $box.append($ul);
    }

    $('#btn').click(function () {
      data[1] = data[1] + '';
      render(data);
    });
    render(data)
  </script>
</body>
</html>
這樣點擊按鈕,會有相應的視圖變化,可是你審查如下元素,每次改動以後,ul標籤都得從新建立,也就是說ul下面的每個欄目,無論是數據是否和原來同樣,都得從新渲染,這並非理想中的狀況,當其中的一欄數據和原來同樣,咱們但願這一欄不要從新渲染,由於DOM重繪至關消耗瀏覽器性能。
 
所以咱們採用JS對象模擬的方法,將DOM的比對操做放在JS層,減小瀏覽器沒必要要的重繪,提升效率。
 
固然有人說虛擬DOM並不比真實的DOM快,其實也是有道理的。當上述ul中的每一條數據都改變時,顯然真實的DOM操做更快,由於虛擬DOM還存在js中diff算法的比對過程。因此,上述性能優點僅僅適用於大量數據的渲染而且改變的數據只是一小部分的狀況
virtual dom 基本步驟

①用JS對象構建一顆虛擬DOM樹,而後用虛擬樹構建一顆真實的DOM樹,而後插入到文檔中。
②當狀態變動時,從新構造一顆新的對象樹,而後新樹舊樹進行比較,記錄兩樹差別。
③把步驟2的差別應用到步驟1所構建的真實DOM樹上,視圖就更新了。算法

虛擬DOM的優勢還有:編程

一、函數式的UI編程,即UI = f(data)這種構建UI的模式。瀏覽器

二、能夠將JS對象渲染到瀏覽器DOM之外的環境中,也就是支持了跨平臺開發,好比ReactNative。數據結構

diff算法

React的 virtual dom的性能好也離不開它自己特殊的diff算法。傳統的diff算法時間複雜度達到o(n3),而react的diff算法時間複雜度只是o(n),react的diff能減小到o(n)依靠的是react diff的三大策略。app

傳統diff 對比 react diff
傳統的diff算法追求的是「徹底」以及「最小」,而react diff則是放棄了這兩種追求:
在傳統的diff算法下,對比先後兩個節點,若是發現節點改變了,會繼續去比較節點的子節點,一層一層去對比。就這樣循環遞歸去進行對比,複雜度就達到了o(n3),n是樹的節點數,想象一下若是這棵樹有1000個節點,咱們得執行上十億次比較,這種量級的對比次數,時間基本要用秒來作計數單位了。那麼react到底是如何把複雜度下降到o(n)的呢?dom

React diff 三大策略
策略一(tree diff):Web UI中DOM節點跨層級的移動操做特別少,能夠忽略不計。(DOM結構發生改變-----直接卸載並從新creat)
策略二(component diff):DOM結構同樣-----不會卸載,可是會update
策略三(element diff):全部同一層級的子節點.他們均可以經過key來區分-----同時遵循1.2兩點

虛擬DOM樹分層比較(tree diff)
兩棵樹只會對同一層次的節點進行比較,忽略DOM節點跨層級的移動操做。React只會對相同顏色方框內的DOM節點進行比較,即同一個父節點下的全部子節點。當發現節點已經不存在,則該節點及其子節點會被徹底刪除掉,不會用於進一步的比較。這樣只須要對樹進行一次遍歷,便能完成整個DOM樹的比較。由此一來,最直接的提高就是複雜度變爲線型增加而不是原先的指數增加。

Diff

可是若是DOM節點出現了跨層級操做,diff會如何處理?

跨層級移動節點

就好比上圖,A節點及其子節點進行移動掛到另外一個DOM下時,React是不會機智的判斷出子樹僅僅是發生了移動,而是會直接銷燬,並從新建立這個子樹,而後再掛在到目標DOM上。實際上,React官方也並不推薦咱們作出跨層級的騷操做。因此咱們能夠從中悟出一個道理:就是咱們本身在實現組件的時候,一個穩定的DOM結構是有助於咱們的性能提高的。

組件間的比較(component diff)
查閱的網上的不少資料,發現寫的都比較難懂,根據我本身的理解,其實最核心的策略仍是看結構是否發生改變。React是基於組件構建應用的,對於組件間的比較所採用的策略也是很是簡潔和高效的。
若是是同一個類型的組件,則按照原策略進行Virtual DOM比較。
若是不是同一類型的組件,則將其判斷爲dirty component,從而替換整個組價下的全部子節點。
若是是同一個類型的組件,有可能通過一輪Virtual DOM比較下來,並無發生變化。若是咱們可以提早確切知道這一點,那麼就能夠省下大量的diff運算時間。所以,React容許用戶經過shouldComponentUpdate()來判斷該組件是否須要進行diff算法分析。

組件對比

如上圖所示,當組件D變爲組件G時,哪怕這兩個組件結構類似,一旦React判斷D和G是不用類型的組件,就不會比較二者的結構,而是直接刪除組件D,從新建立組件G及其子節點。也就是說,若是當兩個組件是不一樣類型但結構類似時,其實進行diff算法分析會影響性能,可是畢竟不一樣類型的組件存在類似DOM樹的狀況在實際開發過程當中不多出現,所以這種極端因素很難在實際開發過程當中形成重大影響。

元素間的比較(element diff)
當節點處於同一層級的時候,react diff 提供了三種節點操做:插入、刪除、移動。

操做 描述
插入 新節點不存在於老集合當中,即全新的節點,就會執行插入操做
移動 新節點在老集合中存在,而且只作了位置上的更新,就會複用以前的節點,作移動操做(依賴於Key)
刪除 新節點在老集合中存在,但節點作出了更改不能直接複用,作出刪除操做

 

簡單先看個例子:

移除插入

看上面的例子,得知,老集合包含節點 A、B、C、D,更新以後的新集合包括節點: B、A、D、C,而後diff算法對新老集合進行差別檢測,發現B不等於A,而後就會建立B而後插入,並刪除A節點,以此類推,建立並插入 A、D、C,而後移除B、C、D。
可是這些節點其實都沒有發生改變,僅僅是位置上發生了變化,卻要進行一大堆的繁瑣低效的建立插入刪除等操做,React說:「這樣下去不行的,咱們不如。。。」,因而React容許開發者對同一層級的同組子節點增長一個惟一的Key進行標識。

Key的做用

相信大部分剛開始接觸react的時候,都看到過這樣的警告:

控制檯警告

這是因爲咱們在循環渲染列表時候(map)時候忘記標記key值報的警告,既然是警告,就說明即便沒有key的狀況下也不會影響程序執行的正確性.其實這個key的存在與否只會影響diff算法的複雜度,也就是說你不加上Key就會像上面的例子同樣暴力渲染,加了Key以後,React就能夠作出移動的操做了,看例子:

加上Key

和上面的例子是同樣的,只不過每一個節點都加上了惟一的key值,經過這個Key值發現新老集合裏面其實所有都是相同的元素,只不過位置發生了改變。所以就無需進行節點的建立、插入、刪除等操做了,只須要將老集合當中節點的位置進行移動就能夠了。React給出的diff結果爲:B、D不作操做,A、C進行移動操做。react是如何判斷誰該移動,誰該不動的呢?

 

react源碼邏輯梳理

react會去循環整個新的集合:

①重新集合中取到B,而後去舊集合中判斷是否存在相同的B,確認B存在後,再去判斷是否要移動:
B在舊集合中的index = 1,有一個遊標叫作lastindex。默認lastindex = 0,而後會把舊集合的index和遊標做對比來判斷是否須要移動,若是index < lastindex ,那麼就作移動操做,在這裏B的index = 1,不知足於 index < lastindex,因此就不作移動操做,而後遊標lastindex更新,取(index, lastindex) 的較大值,這裏就是lastindex = 1

②而後遍歷到A,A在老集合中的index = 0,此時的遊標lastindex = 1,知足index < lastindex,因此對A須要移動到對應的位置,此時lastindex = max(index, lastindex) = 1

③而後遍歷到D,D在老集合中的index = 3,此時遊標lastindex = 1,不知足index < lastindex,因此D保持不動。lastindex = max(index, lastindex) = 3

④而後遍歷到C,C在老集合中的index = 2,此時遊標lastindex = 3,知足 index < lastindex,因此C移動到對應位置。C以後沒有節點了,diff就結束了

以上主要分析新老集合中節點相同但位置不一樣的情景,僅對節點進行位置移動的狀況,若是新集合中有新加入的節點且老集合存在須要刪除的節點,那麼 React diff 又是如何對比運做的呢?

既有刪除,也有添加

和第一種情景基本是一致的,react仍是去循環整個新的集合:
①不贅述了,和上面的第一步是同樣的,B不作移動,lastindex = 1

②新集合取得E,發現舊集合中不存在,則建立E並放在新集合對應的位置,lastindex = 1

③遍歷到C,不知足index < lastindex,C不動,lastindex = 2

④遍歷到A,知足index < lastindex,A移動到對應位置,lastindex = 2

⑤當完成新集合中全部節點 diff 時,最後還須要對老集合進行循環遍歷,判斷是否存在新集合中沒有但老集合中仍存在的節點,發現存在這樣的節點 D,所以刪除節點 D,到此 diff 所有完成

可是 react diff也存在一些問題,和須要優化的地方,看下面的例子:

react-diff不足的地方

在上面的這個例子,A、B、C、D都沒有變化,僅僅是D的位置發生了改變。看上面的圖咱們就知道react並無把D的位置移動到頭部,而是把 A、B、C分別移動到D的後面了,經過前面的兩個例子,咱們也大概知道,爲何會發生這樣的狀況了:
由於D節點在老集合裏面的index 是最大的,使得A、B、C三個節點都會 index < lastindex,從而致使A、B、C都會去作移動操做。因此在開發過程當中,儘可能減小相似將最後一個節點移動到列表首部的操做,當節點數量過大或更新操做過於頻繁時,在必定程度上會影響 React 的渲染性能。

三句箴言

因此通過這麼一分析react diff的三大策略,咱們可以在開發中更加進一步的提升react的渲染效率。
箴言一:在開發組件時,保持穩定的 DOM 結構會有助於性能的提高;
箴言二:使用 shouldComponentUpdate()方法節省diff的開銷
箴言三:在開發過程當中,儘可能減小相似將最後一個節點移動到列表首部的操做,當節點數量過大或更新操做過於頻繁時,在必定程度上會影響 React 的渲染性能。

爲何不推薦使用index做爲Key

咱們再寫react的時候,當咱們作map循環的時候,當咱們沒有一個惟一id來標識每一項item的時候,咱們可能會選擇使用index,官網不推薦咱們使用index做爲key,經過上面的知識背景,咱們其實能夠知道爲何使用index會致使一些問題:

看下面的一個場景吧:

    data.map((item, index) => {
        return <li key={index}>{item}</li> 
    })

上面這樣寫會存在很大的坑,好比看下面的例子:

class App extends Component{
    constructor(props) {
        super(props)
        this.state = {
            list: [{id: 1,val: 'A'}, {id: 2, val: 'B'}, {id: 3, val: 'C'}]
        }
    }

    click() {
        this.state.list.reverse()
        this.setState({})
    }
    render() {
        return (
            <ul>
                {
                    this.state.list.map((item, index) => {
                        return (
                            <Li key={index} val={item.val}></Li>
                        )
                    })
                }
                 <button onClick={this.click.bind(this)}>Reverse</button>
            </ul>
        )
    }
}

class Li extends Component{
    constructor(props) {
        super(props)
    }
    componentDidMount() {
        console.log('===mount===')
    }
    componentWillUpdate() {
        console.log('===update====')
    }
    render() {
        return (
            <li>
                {this.props.val}
                <input type="text"></input>
            </li>
        )
    }
}

效果圖

咱們在三個輸入框裏面,依次輸入1,2,3,點擊Reverse按鈕,按照咱們的預期,這時候頁面應該渲染成3,2,1,可是實際上,順序依然仍是1,2,3,再看控制檯裏面,確實是打印了===update===,證實數據確實是更新了的。那麼爲何會發生這種事情,咱們能夠分析一下:

咱們能夠看下這個圖就明白了:

在

就像咱們以前所說,react會經過key去老集合中找,是否有相同的元素,react發現新老key都是一致的,他會認爲是同一個組件,因此input框內的值沒有倒敘。咱們只須要乖乖的把做爲,就能夠解決這個現象了。

還有存在一點隱藏的(性能問題):

當咱們對數據有 刪除、添加 等操做時。咱們所遍歷的index,就會有所變化,這種狀況下diff算法對新老集合進行差別檢測,發現key值有變化而後就會從新渲染,
咱們只須要乖乖的把做爲,這樣就只會對key值有變化的進行重繪,就能夠解決這種性能問題了。idkeyid(或者其餘惟一標識)key
相關文章
相關標籤/搜索