在熟練使用react中泽疆,聽到最多的就是虛擬dom,diff算法等等梯浪,也是面試必問的一個題目瓢娜,這個問題想要弄透徹眠砾,需要深入閱讀源碼,源碼閱讀還是有一定的難度的柒巫。對這個源碼的理解我也是閱讀很多別人的文章來輔助理解的堡掏,希望也能對看到的人有所幫助。
開發(fā)中常常遇到的問題:
1.為何必須引用React
2.自定義的React組件為何必須大寫
3.React如何防止XSS
4.React的Diff算法
5.key在React中的作用
什么是虛擬dom淤井?
在原生的JavaScript程序中布疼,我們直接對DOM進行創(chuàng)建和更改摊趾,而DOM元素通過我們監(jiān)聽的事件和我們的應(yīng)用程序進行通訊币狠。
所謂的virtual dom游两,也就是虛擬節(jié)點。React會先將你的代碼轉(zhuǎn)換成一個JavaScript對象來模擬DOM中的節(jié)點漩绵,然后再通過特定的render方法將其渲染成真實的DOM節(jié)點贱案。這個JavaScript對象就是虛擬DOM。
比如下面一段 html代碼:
<div class="title">
? ? ? <span>Hello world</span>
? ? ? <ul>
? ? ? ? <li>hello</li>
? ? ? ? <li>world</li>
? ? ? </ul>
</div>
在 React可能存儲為這樣的 JS代碼:
const VitrualDom = {
? type: 'div',
? props: { class: 'title' },
? children: [
? ? {
? ? ? type: 'span',
? ? ? children: 'Hello world'
? ? },
? ? {
? ? ? type: 'ul',
? ? ? children: [
? ? ? ? { type: 'ul', children: 'hello' },
? ? ? ? { type: 'ul', children: 'world' }
? ? ? ]
? ? }
? ]
}
當(dāng)我們需要創(chuàng)建或更新元素時止吐,React首先會讓這個VitrualDom對象進行創(chuàng)建和更改宝踪,然后再將VitrualDom對象渲染成真實DOM;
當(dāng)我們需要對DOM進行事件監(jiān)聽時,首先對VitrualDom進行事件監(jiān)聽厉膀,VitrualDom會代理原生的DOM事件從而做出響應(yīng)。
使用虛擬dom的好處是什么?
1.提升開發(fā)效率
使用JavaScript仗哨,我們在編寫應(yīng)用程序時的關(guān)注點在于如何更新DOM。
使用React,你只需要告訴React你想讓視圖處于什么狀態(tài)雏节,React則通過VitrualDom確保DOM與該狀態(tài)相匹配怔锌。你不必自己去完成屬性操作涝涤、事件處理、DOM更新辨宠,React會替你完成這一切。
這讓我們更關(guān)注我們的業(yè)務(wù)邏輯而非DOM操作,這一點即可大大提升我們的開發(fā)效率毡惜。
2.性能提升
直接操作 DOM是非常耗費性能的帕膜,這一點毋庸置疑。VitrualDom的優(yōu)勢在于 React的 Diff算法和批處理策略荒典, React在頁面更新之前刻剥,提前計算好了如何進行更新和渲染 DOM御吞。實際上挟裂,這個計算過程我們在直接操作?DOM時嫩与,也是可以自己判斷和實現(xiàn)的处坪,但是一定會耗費非常多的精力和時間玄帕,而且往往我們自己做的是不如 React好的丧没。所以,在這個過程中?React幫助我們"提升了性能"奸汇。
React基于 VitrualDom自己實現(xiàn)了一套自己的事件機制,自己模擬了事件冒泡和捕獲的過程,采用了事件代理柬采,批量更新等方法礁遣,抹平了各個瀏覽器的事件兼容性問題盈包。
虛擬DOM原理、特性
React組件的渲染流程:
1.使用 React.createElement或 JSX編寫 React組件叛氨,實際上所有的 JSX代碼最后都會轉(zhuǎn)換成 React.createElement(...)寞埠, Babel幫助我們完成了這個轉(zhuǎn)換的過程仁连。因為我們的 JSX 代碼會被 Babel 編譯為React.createElement伍伤,不引入 React 的話就不能使用React.createElement了。
2.createElement函數(shù)對 key和 ref等特殊的 props進行處理劝评,并獲取 defaultProps對默認(rèn) props進行賦值,并且對傳入的孩子節(jié)點進行處理均牢,最終構(gòu)造成一個 ReactElement對象(所謂的虛擬 DOM)徘跪。
3.ReactDOM.render將生成好的虛擬 DOM渲染到指定容器上松邪,其中采用了批處理、事務(wù)等機制并且對特定瀏覽器進行了性能優(yōu)化,最終轉(zhuǎn)換為真實 DOM。
虛擬DOM的組成
即ReactElementelement對象参萄,我們的組件最終會被渲染成下面的結(jié)構(gòu):
type:元素的類型怜奖,可以是原生html類型(字符串)募强,或者自定義組件(函數(shù)或class)
key:組件的唯一標(biāo)識,用于Diff算法崇摄,下面會詳細(xì)介紹
ref:用于訪問原生dom節(jié)點
props:傳入組件的props擎值,chidren是props中的一個屬性,它存儲了當(dāng)前組件的孩子節(jié)點逐抑,可以是數(shù)組(多個孩子節(jié)點)或?qū)ο螅ㄖ挥幸粋€孩子節(jié)點)
owner:當(dāng)前正在構(gòu)建的Component所屬的Component
self:(非生產(chǎn)環(huán)境)指定當(dāng)前位于哪個組件實例
_source:(非生產(chǎn)環(huán)境)指定調(diào)試代碼來自的文件(fileName)和代碼行數(shù)(lineNumber)
自定義的React組件為何必須大寫
我們在 React 中都是寫的 JSX語法鸠儿,從 JSX語法 到頁面上的 真實DOM大概需要經(jīng)歷以下幾個階段:JSX語法 —> 虛擬DOM(JS對象) —> 真實DOM。
因為瀏覽器是無法識別JSX語法的厕氨,因此我們需要通過?babel 對JSX語法進行轉(zhuǎn)義进每,然后才能生成虛擬DOM對象,而原因就是在這里命斧。babel在進行轉(zhuǎn)義JSX語法時田晚,是調(diào)用了 React.createElement() 這個方法,這個方法需要接收三個參數(shù):type, config, children国葬。第一個參數(shù)聲明了這個元素的類型贤徒。
創(chuàng)建自定義組件時沒有首字母大寫,而 babel 在轉(zhuǎn)義時把它當(dāng)成了一個字符串 傳遞汇四;把首字母大寫接奈,babel 在轉(zhuǎn)義時傳遞了一個變量進去。
問題就在這里通孽,如果傳遞的是一個字符串序宦,那么在創(chuàng)建虛擬DOM對象時,React會認(rèn)為這是一個簡單的HTML標(biāo)簽利虫,但是這顯然不是一個簡單的HTML標(biāo)簽挨厚,因此去創(chuàng)建一個不存在的標(biāo)簽肯定是會報錯的。
如果首字母大寫糠惫,那么就會當(dāng)成一個變量傳遞進去疫剃,這個時候React會知道這是一個自定義組件,因此他就不會報錯了硼讽。
防止XSS
ReactElement對象還有一個$$typeof屬性巢价,它是一個Symbol類型的變量Symbol.for('react.element'),當(dāng)環(huán)境不支持Symbol時,$$typeof被賦值為0xeac7壤躲。
這個變量可以防止XSS城菊。如果你的服務(wù)器有一個漏洞,允許用戶存儲任意JSON對象碉克,?而客戶端代碼需要一個字符串凌唬,這可能為你的應(yīng)用程序帶來風(fēng)險。JSON中不能存儲Symbol類型的變量漏麦,而React渲染時會把沒有$$typeof標(biāo)識的組件過濾掉客税。
虛擬DOM事件機制
React自己實現(xiàn)了一套事件機制,其將所有綁定在虛擬 DOM上的事件映射到真正的 DOM事件撕贞,并將所有的事件都代理到 document上更耻,自己模擬了事件冒泡和捕獲的過程,并且進行統(tǒng)一的事件分發(fā)捏膨。
React自己構(gòu)造了合成事件對象SyntheticEvent秧均,這是一個跨瀏覽器原生事件包裝器。?它具有與瀏覽器原生事件相同的接口号涯,包括stopPropagation()和preventDefault()等等目胡,在所有瀏覽器中他們工作方式都相同。這抹平了各個瀏覽器的事件兼容性問題诚隙。
React的diff算法
React將DOM抽象為虛擬DOM, 然后通過新舊虛擬DOM 這兩個對象的差異(Diff算法),最終只把變化的部分重新渲染,提高渲染效率的過程; diff 是通過JS層面的計算讶隐,返回一個patch對象,即補丁對象久又,在通過特定的操作解析patch對象,完成頁面的重新渲染效五。是在render里面進行計算的地消。
(1)什么是調(diào)和?將Virtual DOM樹轉(zhuǎn)換成actual DOM樹的最少操作的過程稱為 調(diào)和 畏妖。
(2)什么是React diff算法脉执?diff算法是調(diào)和的具體實現(xiàn)。
DIFF算法在執(zhí)行時有三個維度戒劫,分別是Tree DIFF半夷、Component DIFF和Element DIFF,執(zhí)行時按順序依次執(zhí)行迅细,它們的差異僅僅因為DIFF粒度不同巫橄、執(zhí)行先后順序不同。diff算法用將O(n^3)復(fù)雜度轉(zhuǎn)化為?O(n)復(fù)雜度茵典。
Tree DIFF是對樹的每一層進行遍歷湘换,如果某組件不存在了,則會直接銷毀。如圖所示彩倚,左邊是舊屬筹我,右邊是新屬,第一層是R組件帆离,一模一樣蔬蕊,不會發(fā)生變化;第二層進入Component DIFF哥谷,同一類型組件繼續(xù)比較下去岸夯,發(fā)現(xiàn)A組件沒有,所以直接刪掉A呼巷、B囱修、C組件;繼續(xù)第三層王悍,重新創(chuàng)建A破镰、B、C組件压储。
如圖所示鲜漩,第一層遍歷完,進行第二層遍歷時集惋,D和G組件是不同類型的組件孕似,不同類型組件直接進行替換,將D刪掉刮刑,再將G重建喉祭。
Element DIFF緊接著以上統(tǒng)一類型組件繼續(xù)比較下去,常見類型就是列表雷绢。同一個列表由舊變新有三種行為泛烙,插入、移動和刪除翘紊,它的比較策略是對于每一個列表指定key蔽氨,先將所有列表遍歷一遍,確定要新增和刪除的帆疟,再確定需要移動的鹉究。如圖所示,第一步將D刪掉踪宠,第二步增加E自赔,再次執(zhí)行時A和B只需要移動位置即可。
總結(jié):
1.React是如何將O(n3) 復(fù)雜度的問題轉(zhuǎn)換成 O(n) 的殴蓬?
????????只進行同級比較
????????不同類的React組件會被當(dāng)做完全不同的DOM結(jié)構(gòu)而被完全替換
????????key prop:開發(fā)人員可以通過給Virtual DOM一個唯一的key屬性暗示React這是同一個DOM結(jié)構(gòu)匿级,反之若key值不同則會被當(dāng)做完全不同的DOM結(jié)構(gòu)蟋滴。
2.React通過分層求異的策略,對tree diff進行算法優(yōu)化痘绎;
3.React通過相同類生成相似樹形結(jié)構(gòu)津函,不同類生成不同樹形結(jié)構(gòu)的策略,對component diff進行算法優(yōu)化孤页。
4.React通過設(shè)置唯一key的策略尔苦,對element diff進行算法優(yōu)化;
5.建議行施,在開發(fā)組件時允坚,保持穩(wěn)定的DOM結(jié)構(gòu)會有助于性能的提升;
6.建議蛾号,在開發(fā)過程中稠项,盡量減少類似將最后一個節(jié)點移動到列表首部的操作,當(dāng)節(jié)點數(shù)量過大或更新操作過于頻繁時鲜结,在一定程度上會影響React的渲染性能展运。