詳解react應(yīng)用中的DOM DIFF算法
前言
對(duì)我們搞前端的來(lái)說(shuō),目前最流行的兩大前端框架毫無(wú)疑問(wèn)當(dāng)屬React和Vue,對(duì)于這兩大框架,想必大家也是再熟悉不過(guò)了。然而,這兩大框架無(wú)一例外的全部放棄使用傳統(tǒng)的DOM技術(shù),卻采用了以JS為基礎(chǔ)的Virtual DOM技術(shù),也可稱(chēng)作虛擬DOM。所以,到底什么是Virtual DOM??jī)纱鬅衢T(mén)框架全部使用Virtual DOM的原因又是什么?接下來(lái)讓我這個(gè)搞前端的人來(lái)好好地為您講解一下DOM DIFF算法的牛之處。
什么是Virtual DOM?
如字面意思所說(shuō),Virtual DOM即 虛擬DOM,它的特點(diǎn)就是利用Javascript來(lái)模擬DOM結(jié)構(gòu),并且將DOM的變化對(duì)比放在JS層中進(jìn)行比較。具體操作如下:
// 普通的HTML DOM結(jié)構(gòu) <ul class="list"> <li class="item">wjy</li> <li class="item">易烊千璽</li> </ul> // 映射出的虛擬DOM { tag:'ul', props:{ class:'list' }, children: [ { tag:'li', props:{ class:'item' }, children: ['易烊千璽'] } ] }
有朋友或許會(huì)有點(diǎn)迷惑,普通的HTML DOM結(jié)構(gòu)不好嗎?通俗易懂,代碼也更加簡(jiǎn)潔,為什么還需要采用嵌套遞歸的虛擬DOM形式呢?其實(shí)這和普通DOM在重新渲染的過(guò)程中非常消耗性能有關(guān),DOM操作看似簡(jiǎn)便,但其實(shí)效率相當(dāng)?shù)?這是因?yàn)槿绻窃谛枰l繁修改的真實(shí)DOM中,看起來(lái)更加復(fù)雜的運(yùn)用JS結(jié)構(gòu)的Virtual DOM效率會(huì)更高.
使用Virtual DOM的原因
DOM 渲染頁(yè)面的操作流程
- 當(dāng)瀏覽器通過(guò)域名從服務(wù)器拿到對(duì)應(yīng)的HTML文件后,瀏覽器首先會(huì)進(jìn)行構(gòu)建DOM樹(shù)和CSSOM樹(shù),關(guān)于樹(shù)的概念,學(xué)過(guò)數(shù)據(jù)結(jié)構(gòu)與算法的同學(xué)或許對(duì)樹(shù)的節(jié)點(diǎn)概念印象深刻。
- 在HTML DOM中,所有的事物都是節(jié)點(diǎn),而DOM是被視為節(jié)點(diǎn)樹(shù)的HTML,并且,各個(gè)節(jié)點(diǎn)之間有著相應(yīng)的層級(jí)關(guān)系。DOM節(jié)點(diǎn)樹(shù)和HTML中的標(biāo)簽一一對(duì)應(yīng),構(gòu)成了DOM樹(shù)
- HTML文件
- HTML DOM樹(shù)
同樣的。在CSS文檔中,所有的元素也皆是節(jié)點(diǎn),與HTML中的標(biāo)簽一一對(duì)應(yīng),構(gòu)成了CSSOM樹(shù) 如下圖所示
警告! 如果在構(gòu)建DOM樹(shù)的過(guò)程中,有遇到JS相關(guān)的內(nèi)容時(shí),DOM樹(shù)的構(gòu)建會(huì)立即停止,這是由于,JS可以對(duì)DOM節(jié)點(diǎn)進(jìn)行操作,瀏覽器為了防止JS會(huì)對(duì)以完成的DOM造成影響,會(huì)阻止DOM樹(shù)的構(gòu)建,以節(jié)約資源。
在DOM樹(shù)和CSSOM樹(shù)不斷構(gòu)建的過(guò)程中,渲染樹(shù)也在逐漸形成,瀏覽器會(huì)根據(jù)所構(gòu)建的渲染樹(shù)進(jìn)行網(wǎng)頁(yè)布局和繪制流程,不斷地進(jìn)行網(wǎng)頁(yè)的搭建。具體操作流程圖如下:
一般而言,對(duì)于頁(yè)面渲染的常規(guī)操作,我們通常是操作DOM,修改并重置innerHTML完成頁(yè)面的渲染,每進(jìn)行一次DOM的更新操作,都會(huì)重新進(jìn)行一次渲染流程,這個(gè)過(guò)程包含著頁(yè)面的重繪和重排。
Virtual DOM的優(yōu)勢(shì)
但是如果對(duì)于大型頁(yè)面項(xiàng)目,或者具有多標(biāo)簽,多屬性的網(wǎng)頁(yè)而言,常規(guī)的DOM操作實(shí)在是太耗時(shí)了,每次的簡(jiǎn)單修改都需要牽動(dòng)大量的DOM節(jié)點(diǎn)的重繪與重排,極大地降低了頁(yè)面渲染效率.于是,當(dāng)前端開(kāi)發(fā)人員面對(duì)DOM瓶頸一籌莫展的時(shí)候,Virtual DOM顯示出了作為輕量級(jí)的JavaScript對(duì)象的極大優(yōu)越性,順利得到了了前端開(kāi)發(fā)者的青睞。
在頁(yè)面進(jìn)行重新渲染的時(shí)候,Virtual DOM進(jìn)行dom diff計(jì)算對(duì)比兩次并發(fā)現(xiàn)其中的差異,只需要修改DOM樹(shù)中不同的部分即可.也可以理解為Virtual DOM做了一個(gè)中間件,先利用JS修改Virtual DOM,在對(duì)比出差異后,將所有的更改加入頁(yè)面的真實(shí)DOM.所以說(shuō),Virtual DOM的最大優(yōu)勢(shì)就在于完全不用像原生DOM,在對(duì)比之后還要進(jìn)行DOM的重建與創(chuàng)造,因?yàn)檫@對(duì)于大型項(xiàng)目的運(yùn)行來(lái)說(shuō)非常消耗性能,開(kāi)銷(xiāo)極大.由此可見(jiàn),不論在什么體量的網(wǎng)頁(yè)中,放棄傳統(tǒng)DOM采用Virtual DOM無(wú)疑是非常高效且絕佳的選擇.
如何將DOM用virtual DOM 來(lái)表示
首先,在vscode中新建一個(gè)dom diff 項(xiàng)目,項(xiàng)目初始化,裝好相應(yīng)組件
由于是DOM樹(shù),所以在將HTML轉(zhuǎn)換成DOM樹(shù)時(shí),要運(yùn)用遞歸的形式,首先創(chuàng)建結(jié)點(diǎn),其次設(shè)置屬性,然后設(shè)置子節(jié)點(diǎn)
<ul class="list"> <li class="item">wjy</li> <li class="item">易烊千璽</li> </ul> // DOM 樹(shù)的表達(dá)轉(zhuǎn)換形式 let virtualDOM = createElement('ul', { class:'list', }, [ createElement('li',{ class:'item' },['wjy']), createElement('li',{ class:'item' },['易烊千璽']), ])
然后,在新建一個(gè)element.js文件進(jìn)行向外輸出,完成頁(yè)面渲染
// 通過(guò)構(gòu)造函數(shù)Element構(gòu)造虛擬DOM節(jié)點(diǎn) class Element { constructor(type,props,children){ this.type = type; this.props = props; this.children =children; } } // const createElement = (type,props,children) => { return new Element(type,props,children); } // 進(jìn)行頁(yè)面渲染 將Virtual Dom轉(zhuǎn)化為真實(shí)DOM const render = (domObj) => { let el = document.createElement(domObj.type); for(let key in domObj.props){ setAttr(el,key,domObj.props[key]); } domObj.children.forEach(child => { child = (child instanceof Element) ? render(child) : document.createTextNode(child); el.appendChild(child); }) return el; } function setAttr(node,key,value){ switch(key){ case 'value': if(node.tagName.toLowerCase() === 'input' || node.tagName.toLowerCase() ==='textarea' ){ node.value = value; }else{ node.setAttribute(key,value) } break; case 'style': // node.setAttribute('style',value) node.style.cssText = value; break; default: node.setAttribute(key,value) break; } } // 將真實(shí)DOM 掛載到制定根節(jié)點(diǎn) const renderDOM = (el,target) => { target.appendChild(el); } // 向外輸出 export { createElement, render, renderDOM }
在控制臺(tái)上得到真實(shí)DOM
頁(yè)面渲染成功!??
DOM DIFF算法
在用戶(hù)進(jìn)行操作更改交互頁(yè)面操作后,虛擬DOM樹(shù)上的節(jié)點(diǎn)會(huì)發(fā)生變化,然而此時(shí)真實(shí)節(jié)點(diǎn)卻沒(méi)有改變,為了使得更改與真實(shí)頁(yè)面同步,我們會(huì)使用DOM DIFF算法找出這兩顆樹(shù)的差異,然后產(chǎn)生差異補(bǔ)丁對(duì)象,再將差異補(bǔ)丁對(duì)象應(yīng)用到真實(shí)的DOM節(jié)點(diǎn)上去,于是完成了頁(yè)面的渲染和更新。
傳統(tǒng)的Diff算法時(shí)間復(fù)雜度達(dá)到了O(n^3),若要滿足每次都可以整體刷新頁(yè)面的目的,這種指數(shù)型的增長(zhǎng)的性能開(kāi)銷(xiāo)是無(wú)法滿足性能要求的,于是,F(xiàn)acebook的工程師對(duì)此進(jìn)行了優(yōu)化,通過(guò)制定diff策略將Diff算法的復(fù)雜度降低到了O(n)
Diff 策略
- DOM節(jié)點(diǎn)跨層級(jí)的操作特別少,所以可以忽略不計(jì)
- 擁有相同類(lèi)的兩個(gè)組件將會(huì)產(chǎn)生相似的樹(shù)形結(jié)構(gòu),擁有不同類(lèi)的兩個(gè)組件將會(huì)產(chǎn)生不同的樹(shù)形結(jié)構(gòu)
- 同一層級(jí)的一組子節(jié)點(diǎn),他們可以通過(guò)uuid進(jìn)行區(qū)分
Diff 粒度
由于DIFF的粒度不同,DIFF算法按照下面的順序依次執(zhí)行
- Tree DIFF
- Component DIFF
- Element DIFF
打補(bǔ)丁
我們根據(jù)diff策略以及react diff中的比對(duì)算法將兩個(gè)虛擬DOM通過(guò)深度優(yōu)先遍歷進(jìn)行比較,如果有差異,就把所遍歷到節(jié)點(diǎn)的索引值所對(duì)應(yīng)的操作存儲(chǔ)起來(lái),也稱(chēng)為補(bǔ)丁對(duì)象(patches)
然后對(duì)真實(shí)的DOM再次經(jīng)過(guò)深度優(yōu)先遍歷,補(bǔ)丁對(duì)象中的索引就會(huì)和DOM相對(duì)應(yīng),我們就完成了DOM的更新操作。
結(jié)語(yǔ)
在互聯(lián)網(wǎng)環(huán)境下,隨時(shí)刷新交互頁(yè)面是我們上網(wǎng)的常規(guī)操作,然而這一簡(jiǎn)單的操作卻是多次算法優(yōu)化的結(jié)果。DOM DIFF的底層原理挺復(fù)雜,如果有感興趣的朋友,可以自行搜索相關(guān)文獻(xiàn),因?yàn)楸疚闹皇菧\析,所以太多方面就不贅述了,如果本文有知識(shí)錯(cuò)漏的地方,也歡迎指正!虛心接收一切合理批評(píng)!??
如果這篇文章有幫助到你對(duì)dom diff算法的理解,也希望您能為我點(diǎn)一個(gè)贊??,答主是剛?cè)腴T(mén)的前端小白,每一個(gè)贊都是我前進(jìn)的動(dòng)力,我會(huì)持續(xù)更新掘金的博客的??
以上就是詳解react應(yīng)用中的DOM DIFF算法的詳細(xì)內(nèi)容,更多關(guān)于react應(yīng)用的DOM DIFF算法的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
React父子組件傳值(組件通信)的實(shí)現(xiàn)方法
本文主要介紹了React父子組件傳值(組件通信)的實(shí)現(xiàn)方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-05-05解決React報(bào)錯(cuò)`value` prop on `input` should&
這篇文章主要為大家介紹了React報(bào)錯(cuò)`value` prop on `input` should not be null解決方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12react實(shí)現(xiàn)頁(yè)面水印效果的全過(guò)程
大家常常關(guān)注的是網(wǎng)站圖片增加水印,而很少關(guān)注頁(yè)面水印,其實(shí)這個(gè)需求也是比較常見(jiàn)的,比如公文系統(tǒng)、合同系統(tǒng)等,這篇文章主要給大家介紹了關(guān)于react實(shí)現(xiàn)頁(yè)面水印效果的相關(guān)資料,需要的朋友可以參考下2021-09-09詳解如何給React-Router添加路由頁(yè)面切換時(shí)的過(guò)渡動(dòng)畫(huà)
這篇文章主要介紹了詳解如何給React-Router添加路由頁(yè)面切換時(shí)的過(guò)渡動(dòng)畫(huà),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-04-04React Native中實(shí)現(xiàn)動(dòng)態(tài)導(dǎo)入的示例代碼
隨著業(yè)務(wù)的發(fā)展,每一個(gè) React Native 應(yīng)用的代碼數(shù)量都在不斷增加。作為一個(gè)前端想到的方案自然就是動(dòng)態(tài)導(dǎo)入(Dynamic import)了,本文介紹了React Native中實(shí)現(xiàn)動(dòng)態(tài)導(dǎo)入的示例代碼,需要的可以參考一下2022-06-06React Form組件的實(shí)現(xiàn)封裝雜談
這篇文章主要介紹了React Form組件的實(shí)現(xiàn)封裝雜談,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-05-05