React之虛擬DOM的實現(xiàn)原理
React虛擬DOM機(jī)制
- 虛擬DOM本質(zhì)上是JavaScript對象,是對真實DOM的抽象
- 狀態(tài)變更時,記錄新樹和舊樹的差異
- 最后把差異更新到真正的dom中
React引入了虛擬DOM(Virtual DOM)的機(jī)制:在瀏覽器端用Javascript實現(xiàn)了一套DOM API。
基于React進(jìn)行開發(fā)時所有的DOM構(gòu)造都是通過虛擬DOM進(jìn)行,每當(dāng)數(shù)據(jù)變化時,React都會重新構(gòu)建整個DOM樹,然后React將當(dāng)前整個DOM樹和上一次的DOM樹進(jìn)行對比,得到DOM結(jié)構(gòu)的區(qū)別,然后僅僅將需要變化的部分進(jìn)行實際的瀏覽器DOM更新。
而且React能夠批量處理虛擬DOM的刷新,在一個事件循環(huán)(Event Loop)內(nèi)的兩次數(shù)據(jù)變化會被合并,例如你連續(xù)的先將節(jié)點內(nèi)容從A變成B,然后又從B變成A,React會認(rèn)為UI不發(fā)生任何變化。
盡管每一次都需要構(gòu)造完整的虛擬DOM樹,但是因為虛擬DOM是內(nèi)存數(shù)據(jù),性能是極高的,而對實際DOM進(jìn)行操作的僅僅是Diff部分,因而能達(dá)到提高性能的目的。這樣,在保證性能的同時,開發(fā)者將不再需要關(guān)注某個數(shù)據(jù)的變化如何更新到一個或多個具體的DOM元素,而只需要關(guān)心在任意一個數(shù)據(jù)狀態(tài)下,整個界面是如何Render的。
總之一句話:根據(jù) React 的設(shè)計,所有的 DOM 變動,都先在虛擬 DOM 上發(fā)生,然后再將實際發(fā)生變動的部分,反映在真實 DOM上
React diff 算法
diff 算法作為Virtual DOM的加速器,其算法的改進(jìn)優(yōu)化是React整個界面渲染的基礎(chǔ)和性能的保障,同時也是React源碼中最神秘的,最不可思議的部分。
1. 傳統(tǒng) diff 算法
計算一棵樹形結(jié)構(gòu)轉(zhuǎn)換為另一棵樹形結(jié)構(gòu)需要最少步驟,如果使用傳統(tǒng)的diff算法通過循環(huán)遞歸遍歷節(jié)點進(jìn)行對比,其復(fù)雜度要達(dá)到O(n^3),其中n是節(jié)點總數(shù),效率十分低下,假設(shè)我們要展示1000個節(jié)點,那么我們就要依次執(zhí)行上十億次的比較。
下面附上一則簡單的傳統(tǒng)diff算法:
let result = []; // 比較葉子節(jié)點 const diffLeafs = function (beforeLeaf, afterLeaf) { // 獲取較大節(jié)點樹的長度 let count = Math.max(beforeLeaf.children.length, afterLeaf.children.length); // 循環(huán)遍歷 for (let i = 0; i < count; i++) { const beforeTag = beforeLeaf.children[i]; const afterTag = afterLeaf.children[i]; // 添加 afterTag 節(jié)點 if (beforeTag === undefined) { result.push({ type: "add", element: afterTag }); // 刪除 beforeTag 節(jié)點 } else if (afterTag === undefined) { result.push({ type: "remove", element: beforeTag }); // 節(jié)點名改變時,刪除 beforeTag 節(jié)點,添加 afterTag 節(jié)點 } else if (beforeTag.tagName !== afterTag.tagName) { result.push({ type: "remove", element: beforeTag }); result.push({ type: "add", element: afterTag }); // 節(jié)點不變而內(nèi)容改變時,改變節(jié)點 } else if (beforeTag.innerHTML !== afterTag.innerHTML) { if (beforeTag.children.length === 0) { result.push({ type: "changed", beforeElement: beforeTag, afterElement: afterTag, html: afterTag.innerHTML }); } else { // 遞歸比較 diffLeafs(beforeTag, afterTag); } } } return result; }
2. react diff 算法
1. diff 策略
下面介紹一下react diff算法的3個策略
- Web UI 中DOM節(jié)點跨層級的移動操作特別少,可以忽略不計
- 擁有相同類的兩個組件將會生成相似的樹形結(jié)構(gòu),擁有不同類的兩個組件將會生成不同的樹形結(jié)構(gòu)。
- 對于同一層級的一組子節(jié)點,它們可以通過唯一id進(jìn)行區(qū)分。
對于以上三個策略,react 分別對 tree diff, component diff, element diff 進(jìn)行算法優(yōu)化。
2. tree diff
基于策略一,WebUI中DOM節(jié)點跨層級的移動操作少的可以忽略不計,React對Virtual DOM樹進(jìn)行層級控制,只會對相同層級的DOM節(jié)點進(jìn)行比較,即同一個父元素下的所有子節(jié)點,當(dāng)發(fā)現(xiàn)節(jié)點已經(jīng)不存在了,則會刪除掉該節(jié)點下所有的子節(jié)點,不會再進(jìn)行比較。這樣只需要對DOM樹進(jìn)行一次遍歷,就可以完成整個樹的比較。復(fù)雜度變?yōu)镺(n);
疑問:當(dāng)我們的DOM節(jié)點進(jìn)行跨層級操作時,diff 會有怎么樣的表現(xiàn)呢?
如下圖所示,A節(jié)點及其子節(jié)點被整個移動到D節(jié)點下面去,由于React只會簡單的考慮同級節(jié)點的位置變換,而對于不同層級的節(jié)點,只有創(chuàng)建和刪除操作,所以當(dāng)根節(jié)點發(fā)現(xiàn)A節(jié)點消失了,就會刪除A節(jié)點及其子節(jié)點,當(dāng)D發(fā)現(xiàn)多了一個子節(jié)點A,就會創(chuàng)建新的A作為其子節(jié)點。
此時,diff的執(zhí)行情況是:
createA-->createB-->createC-->deleteA
由此可以發(fā)現(xiàn),當(dāng)出現(xiàn)節(jié)點跨層級移動時,并不會出現(xiàn)想象中的移動操作,而是會進(jìn)行刪除,重新創(chuàng)建的動作,這是一種很影響React性能的操作。因此官方也不建議進(jìn)行DOM節(jié)點跨層級的操作。
3. component diff
- React是基于組件構(gòu)建應(yīng)用的,對于組件間的比較所采用的策略也是非常簡潔和高效的。
- 如果是同一個類型的組件,則按照原策略進(jìn)行Virtual DOM比較。
- 如果不是同一類型的組件,則將其判斷為dirty component,從而替換整個組價下的所有子節(jié)點。
- 如果是同一個類型的組件,有可能經(jīng)過一輪Virtual DOM比較下來,并沒有發(fā)生變化。如果我們能夠提前確切知道這一點,那么就可以省下大量的diff運算時間。因此,React允許用戶通過shouldComponentUpdate()來判斷該組件是否需要進(jìn)行diff算法分析。
如下圖所示,當(dāng)組件D變?yōu)榻M件G時,即使這兩個組件結(jié)構(gòu)相似,一旦React判斷D和G是不用類型的組件,就不會比較兩者的結(jié)構(gòu),而是直接刪除組件D,重新創(chuàng)建組件G及其子節(jié)點。
雖然當(dāng)兩個組件是不同類型但結(jié)構(gòu)相似時,進(jìn)行diff算法分析會影響性能,但是畢竟不同類型的組件存在相似DOM樹的情況在實際開發(fā)過程中很少出現(xiàn),因此這種極端因素很難在實際開發(fā)過程中造成重大影響。
4. element diff
當(dāng)節(jié)點屬于同一層級時,diff提供了3種節(jié)點操作,分別為INSERT_MARKUP(插入),MOVE_EXISTING(移動),REMOVE_NODE(刪除)。
INSERT_MARKUP
:新的組件類型不在舊集合中,即全新的節(jié)點,需要對新節(jié)點進(jìn)行插入操作。MOVE_EXISTING
:舊集合中有新組件類型,且element是可更新的類型,這時候就需要做移動操作,可以復(fù)用以前的DOM節(jié)點。REMOVE_NODE
:舊組件類型,在新集合里也有,但對應(yīng)的element不同則不能直接復(fù)用和更新,需要執(zhí)行刪除操作,或者舊組件不在新集合里的,也需要執(zhí)行刪除操作。
總結(jié)
通過diff策略,將算法從O(n^3)簡化為O(n)。
分層求異,對tree diff進(jìn)行優(yōu)化。
分組件求異,相同類生成相似樹形結(jié)構(gòu)、不同類生成不同樹形結(jié)構(gòu),對component diff進(jìn)行優(yōu)化。
設(shè)置key,對element diff進(jìn)行優(yōu)化。
盡量保持穩(wěn)定的DOM結(jié)構(gòu)、避免將最后一個節(jié)點移動到列表首部、避免節(jié)點數(shù)量過大或更新過于頻繁。
最后
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
- Vue響應(yīng)式原理與虛擬DOM實現(xiàn)步驟詳細(xì)講解
- Vue中簡單的虛擬DOM是什么樣
- Vue虛擬dom被創(chuàng)建的方法
- Vue源碼分析之虛擬DOM詳解
- vue 虛擬DOM快速入門
- react中的虛擬dom和diff算法詳解
- vue 虛擬DOM的原理
- 詳解操作虛擬dom模擬react視圖渲染
- 淺談React的最大亮點之虛擬DOM
- React?JSX深入淺出理解
- React jsx轉(zhuǎn)換與createElement使用超詳細(xì)講解
- React詳細(xì)講解JSX和組件的使用
- React基礎(chǔ)-JSX的本質(zhì)-虛擬DOM的創(chuàng)建過程實例分析
相關(guān)文章
ReactNative點擊事件.bind(this)操作分析
這篇文章主要為大家介紹了ReactNative點擊事件.bind(this)操作分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-11-11React實現(xiàn)Excel文件的導(dǎo)出與在線預(yù)覽功能
這篇文章主要為大家詳細(xì)介紹了如何利用?React?18?的強(qiáng)大功能,演示如何使用?React?18?編寫?Excel?文件的導(dǎo)出與在線預(yù)覽功能,需要的小伙伴可以參考下2023-12-12react性能優(yōu)化達(dá)到最大化的方法 immutable.js使用的必要性
這篇文章主要為大家詳細(xì)介紹了react性能優(yōu)化達(dá)到最大化的方法,一步一步優(yōu)化react性能的過程,告訴大家使用immutable.js的必要性,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-03-03React教程之Props驗證的具體用法(Props Validation)
這篇文章主要介紹了React教程之Props驗證的具體用法(Props Validation),非常具有實用價值,需要的朋友可以參考下2017-09-09React入門教程之Hello World以及環(huán)境搭建詳解
Facebook 為了開發(fā)一套更好更適合自己的JavaScript MVC 框架,所以產(chǎn)生了react。后來反響很好,所以于2013年5月開源。下面這篇文章主要給大家介紹了關(guān)于React入門教程之Hello World以及環(huán)境搭建的相關(guān)資料,需要的朋友可以參考借鑒。2017-07-07