接下來用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: "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
現有一個例子: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>
①用JS對象構建一顆虛擬DOM樹,而後用虛擬樹構建一顆真實的DOM樹,而後插入到文檔中。
②當狀態變動時,從新構造一顆新的對象樹,而後新樹舊樹進行比較,記錄兩樹差別。
③把步驟2的差別應用到步驟1所構建的真實DOM樹上,視圖就更新了。算法
虛擬DOM的優勢還有:編程
一、函數式的UI編程,即UI = f(data)這種構建UI的模式。瀏覽器
二、能夠將JS對象渲染到瀏覽器DOM之外的環境中,也就是支持了跨平臺開發,好比ReactNative。數據結構
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樹的比較。由此一來,最直接的提高就是複雜度變爲線型增加而不是原先的指數增加。
可是若是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進行標識。
相信大部分剛開始接觸react的時候,都看到過這樣的警告:
這是因爲咱們在循環渲染列表時候(map)時候忘記標記key值報的警告,既然是警告,就說明即便沒有key的狀況下也不會影響程序執行的正確性.其實這個key的存在與否只會影響diff算法的複雜度,也就是說你不加上Key就會像上面的例子同樣暴力渲染,加了Key以後,React就能夠作出移動的操做了,看例子:
和上面的例子是同樣的,只不過每一個節點都加上了惟一的key值,經過這個Key值發現新老集合裏面其實所有都是相同的元素,只不過位置發生了改變。所以就無需進行節點的建立、插入、刪除等操做了,只須要將老集合當中節點的位置進行移動就能夠了。React給出的diff結果爲:B、D不作操做,A、C進行移動操做。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也存在一些問題,和須要優化的地方,看下面的例子:
在上面的這個例子,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 的渲染性能。
咱們再寫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