聊一聊React中虛擬DOM
阿新 • • 發佈:2020-03-19
### 1. 什麼是虛擬 DOM
在 React 中實際上是 render 函式中return 的內容會生成 DOM,return 中的內容由兩部分組成,一部分是 JSX ,另一部分就是 state 中的資料,所以簡單來講,在 React 中 JSX 結合 state 就生成了 DOM。
現在拋開虛擬 DOM 不談,如果讓我們去實現 React 中當資料發生變化時如何操作 DOM 來實現頁面內容的變化,我們會怎樣去實現?
**第一種方案:**
1)JSX + state 生成真實的 DOM,並顯示在頁面上
2)state 發生變化
3)此時 JSX + state 再次結合生成新的真實的 DOM
4)新的 DOM 直接替換掉原來的 DOM
這樣頁面會發生變化,但是生成真實的 DOM 和在頁面上再重新載入新的 DOM 都比較耗效能。
**第二種方案:**
1)JSX + state 生成真實的 DOM,並顯示在頁面上
2)state 發生變化
3)此時 JSX + state 再次結合生成新的真實的 DOM
4)新的 DOM 和原始的 DOM 作對比,找出差異
5)利用找出的差異,替換掉頁面上原始 DOM 的相應部分
此時頁面也會發生變化,和方案一相比多了對比步驟但是隻需要替換掉原始DOM的一部分即可,綜合來說,方案二要優於方案一。
**第三種方案**
1)JSX + state 生成虛擬 DOM(虛擬 DOM 就是一個 JS 物件,用它來描述真實 DOM)
例如下面這段程式碼:
```html
item
```
注意上面的 `div`,`span` 標籤時 JSX 語法,並不是真實的 DOM,這裡是先生成虛擬 DOM ,然後再下一步的時候才用虛擬 DOM 生成真實的 DOM,由 JSX 到真實的 DOM 中間有一個虛擬 DOM。
```js
JSX -> 虛擬DOM(JS物件) -> 真實DOM
```
也就是說,JSX 需要先轉換為 JS 物件,然後再轉換為真實的 DOM。
生成的虛擬 DOM 為
```js
['div',{id: 'abc'}, 'item']
```
虛擬 DOM 的格式為
```js
['標籤名',標籤屬性物件,子標籤]
```
那麼 `item` 是如何轉化為 JS 物件的呢?
實際上在 React 中上面這樣寫就相當於下面這樣寫:
```js
React.createElement('div', {id: 'abc'}, 'item');
```
那麼實際上就算是沒有 JSX 語法通過上面這樣寫也是可以的,但是會非常不方便。
2)用虛擬 DOM 的結構生成真實的 DOM 顯示在頁面上。
3)JSX + state 生成新的虛擬 DOM
4)兩個虛擬 DOM 進行對比,找出差異
5)根據差異直接修改替換頁面上的 DOM
虛擬 DOM 是一個 JS 物件,生成一個虛擬 DOM 比生成一個真實的 DOM 結構要容易省時地多,而且兩個虛擬 DOM(JS 物件) 之間的對比也比較簡單,所以方案三最佳。
React 中使用的也是第三種方案的思想。
### 2. 虛擬 DOM 的優點
那麼虛擬DOM的優點到底有哪些呢?
1)效能提升
這一點通過上面的比較就可以看得出來
2)使得跨端應用得以實現,例如原生應用。
React Native 能夠做原生應用虛擬 DOM 是很重要的一方面,原生應用中是沒有 DOM 這個概念的,DOM 是瀏覽器中存在的,但是有了虛擬 DOM(JS 物件) 之後,在原生應用中就可以將虛擬 DOM(JS 物件) 轉換為一些原生應用中能夠支援的原生元件在原生應用中顯示。
### 3. 虛擬 DOM 的對比
使用虛擬 DOM 時很重要的一個步驟就是兩個虛擬 DOM 之間的比較,那麼怎樣去進行比較呢?
React 中採用 diff 演算法,簡單來說主要有以下三個方面:
1)當短時間內連續呼叫多次 setState 時,React 只會進行一次虛擬 DOM 的比對。
我們知道當 state 或者 props 發生變化時,頁面會發生變化,實際上 props 的變化也是因為父元件 state 的變化,所以當頁面發生變化時實際上是呼叫 setState 導致資料發生變化變化時。當短時間內連續呼叫多次 setState 時,如果每次都進行一次虛擬 DOM 的比對,那麼效能會比較低,反之多次呼叫 setState 只進行一次虛擬 DOM 的比對會提升效能。這也是為什麼 setState 要設定成非同步的原因,因為如果同步的話當執行完一次 setState 時就會發生一次虛擬 DOM 的比對。(同步是順序立即執行,非同步是當所有的同步程式執行完後再執行)
2)在比較虛擬 DOM 時採用逐層同層比較,當上一層出現差異時,那麼下面的各層就不需要再比較了,下面各層的 DOM 都將被新的 DOM 替換。
這樣做看起來,複用性不是很好,因為下面各層有可能會有許多相同的 DOM。但是這樣做會使得比較演算法非常簡單,比較的速度非常快。
3)設定 key 值
假設現在有一個數組 `[a, b, c]` 遍歷每一項顯示在頁面上,現在陣列發生變化將第一項 a 刪掉,如果沒有 key 值,陣列 `[b, c]` 無法和原陣列進行比對,例如 b 到底和原陣列的哪一個進行比較呢?
但是現在假設有了 key 值,原陣列中 a 的 key 值是 a,b 的 key 值是 b,c 的 key 值是 c。刪除 a 之後,通過 key 值,b 的 key 值 b 在原陣列中找到 b,說明 b 沒有發生變化,c 同理也沒有發生變化,但是原陣列中的 a 在新陣列中並沒有找到,說明新陣列中將 a 刪掉了,所以在操作頁面時將 a 刪掉即可。
這裡有一點需要注意的是,key 值一定要選不能變化的,利用陣列的索引來做 key 值就不可取。還是以上面為例進行說明。原陣列的 a 的 key 值是 0,b 的 key 值是 1,c 的 key 值是 2,刪掉 a 後,新陣列的 b 的 key 值是 0,c 的 key 值是 1,經過比對原陣列的 a 和新陣列的 b key 值相同,虛擬 DOM 會認為它們是相同的,沒有差異,但是實際上它們是不