一、 JSX
眾所周知形入,React 使用 JSX 來替代常規(guī)的 JavaScript。JSX 是一個看起來很像 XML 的 JavaScript 語法擴展缝左,其本質(zhì)是 createElement()方法的語法糖 (語法糖:更加直觀亿遂、簡潔、友好)渺杉。
JSX 代碼會經(jīng)過babel-loader 會解析為 React.createElement()嵌套對象蛇数。React.createElement() 創(chuàng)建的就是一個虛擬DOM結(jié)構(gòu)。
二是越、 虛擬DOM
通過React.createElement()創(chuàng)建的虛擬DOM描述了DOM樹的結(jié)構(gòu)耳舅,其本質(zhì)是一個輕量級的javaScript對象。該JS對象包含如下屬性:
- type:元素的類型倚评,可以是原生html類型(字符串)浦徊,或者自定義組件(函數(shù)或class)
- key:組件的唯一標識,用于Diff算法天梧,之后會詳細介紹
- ref:用于訪問原生dom節(jié)點
-? props:傳入組件的props盔性,children是props中的一個屬性,它存儲了當前組件的子節(jié)點呢岗,可以是數(shù)組(多個子節(jié)點)或?qū)ο螅ㄖ挥幸粋€子節(jié)點)
- owner:當前正在構(gòu)建的Component所屬的Component
- self:(非生產(chǎn)環(huán)境)指定當前位于哪個組件實例
-? _source:(非生產(chǎn)環(huán)境)指定調(diào)試代碼來自的文件(fileName)和代碼行數(shù)(lineNumber)
為更好理解冕香,下面我們來看一組轉(zhuǎn)換流程。
存在JSX代碼如下:
const element = (
? <div className="title">
? ? <span>Hello JSX</span>
? ? <ul>
? ? ? <li>test1</li>
? ? ? <li>test2</li>
? ? </ul>
? </div>
);
通過babel-loader 解析后:
const element = React.createElement(
? "div",
? { className: "title" },
? React.createElement("span", null, "Hello JSX"),
? React.createElement(
? ? "ul",
? ? null,
? ? React.createElement("li", null, "test1"),
? ? React.createElement("li", null, "test2")
? )
);
轉(zhuǎn)換成虛擬Dom后后豫,會變成如下JS代碼(為方便查看悉尾,刪除部分不必要屬性)
const element = {
? type: "div",
? props: { class: "title" },
? children: [
? ? { type: "span", children: "Hello JSX" },
? ? {
? ? ? type: "ul",
? ? ? children: [
? ? ? ? { type: "li", children: "test1" },
? ? ? ? { type: "li", children: "test2" },
? ? ? ],
? ? },
? ],
};
完整的虛擬Dom代碼如下:
由此可見,虛擬DOM就是JS對象挫酿。最后构眯,ReactDom.render 將生成好的虛擬DOM渲染到指定容器上,其中采用了批處理饭豹,事務等機制鸵赖,并且對特定瀏覽器進行了性能優(yōu)化务漩,最終轉(zhuǎn)換為真實DOM。
1. 為什么需要虛擬DOM呢?
- 提高性能
我們都知道它褪,每次DOM操作會引起重繪或者回流饵骨,頻繁的真實DOM的修改會觸發(fā)多次的排版和重繪,相當耗性能茫打。
虛擬DOM可以提高性能居触,不是說不操作DOM,而是減少操作真實DOM的次數(shù)老赤。即當狀態(tài)/數(shù)據(jù)改變時轮洋,React會自動更新虛擬DOM,產(chǎn)生一個新的虛擬DOM樹抬旺。通過diff算法對新舊虛擬DOM進行比較弊予,找出最小的有變化的部分,將這個變化的部分Patch(即需要修改的部分)加入隊列开财,最終汉柒,批量的更新這些Patch到真實DOM上,以減少重繪和回流责鳍,從而達到性能優(yōu)化的目的碾褂。
此外,React還提供了componentShouldUpdate生命周期來讓開發(fā)者手動控制減少數(shù)據(jù)變化后不必要的虛擬dom對比历葛,提升性能和渲染效率正塌。
- 跨瀏覽器兼容
React 基于 虛擬DOM自己實現(xiàn)了一套事件機制,自己模擬了事件冒泡和捕獲的過程恤溶,采用了事件代理乓诽,批量更新等方法,抹平了各個瀏覽器的事件兼容性宏娄。
- 跨平臺兼容
虛擬DOM為React帶來了跨平臺渲染的能力问裕,以React-native為例子,React根據(jù)虛擬DOM畫出相應平臺的UI.
- 提高開發(fā)效率
三孵坚、 Diff算法
傳統(tǒng)的diff算法是使用遞歸循環(huán)對節(jié)點進行依次對比粮宛,即使在最前沿的算法中 將前后兩棵樹完全比對的算法的復雜程度為 O(n^3),其中 n 是樹中元素的數(shù)量卖宠。 如果在React中使用了該算法巍杈,那么展示1000個元素所需要執(zhí)行的計算量將在十億的量級范圍。 這個開銷實在是太過高昂扛伍。
為了提高性能筷畦,React同時維護著兩棵虛擬DOM樹:一棵為當前的DOM結(jié)構(gòu)(舊虛擬DOM),另一棵為React狀態(tài)變更后生成的DOM結(jié)構(gòu)(新虛擬DOM)。React通過比較這兩棵樹的差異鳖宾,決定是否需要修改DOM結(jié)構(gòu)吼砂,以及如何修改。這種算法稱作**React 的 Diff算法**
React的 Diff算法會幫助我們計算出虛擬DOM 中真正發(fā)生變化的部分鼎文,并且只針對該部分進行實際的DOM操作渔肩,而不是對整個頁面進行重新渲染。為了降低算法復雜度拇惋,React的 Diff算法提出三種策略:
- 針對同一層級的節(jié)點進行比較周偎。即如果一個DOM節(jié)點在前后兩次更新中跨越了層級,那么React不會嘗試復用它(因為跨層級的DOM移動操作特別少撑帖,可以忽略不計)蓉坎。
- 不同類型的元素會產(chǎn)生出不同的樹。即相同類的兩個組件將會生成相似的樹形結(jié)構(gòu)胡嘿,不同類的兩個組件將會生成不同的樹形結(jié)構(gòu)蛉艾。如果元素由div變?yōu)閜,React會銷毀div及其子孫節(jié)點灶平,并新建p及其子孫節(jié)點伺通。
- 同一層級的一組子節(jié)點,可以通過唯一的id區(qū)分(key)
基于以上三個策略逢享,React 的 Diff算法分別對Tree Diff、Component Diff以及Element diff進行了算法優(yōu)化吴藻。
?1. Tree Diff
基于第一個策略瞒爬,React只會對同一層次的節(jié)點進行比較,即如果父節(jié)點不同沟堡,React將不會再去對比子節(jié)點侧但。因為不同的組件DOM結(jié)構(gòu)會不相同,所以就沒有必要再去對比子節(jié)點了航罗。這樣就只需要遍歷一次禀横,就能完成對整個DOM樹的比較,進而提高了對比的效率粥血,把事件復雜度降低為O(n).
React對于不同層級的節(jié)點柏锄,只有創(chuàng)建和刪除操作。如圖所示复亏,如果A節(jié)點整個被移動到D節(jié)點下趾娃,當根節(jié)點發(fā)現(xiàn)子節(jié)點中A不見了,就會直接銷毀A缔御;而當D發(fā)現(xiàn)自己多了一個子節(jié)點A抬闷,則會創(chuàng)建一個新的A作為子節(jié)點。
因此對于這種結(jié)構(gòu)的轉(zhuǎn)變的實際操作是:
A.destroy();
A = new A();
A.append(new B());
A.append(new C());
D.append(A);
由于React 的Diff 算法沒有針對跨層級的DOM移動操作進行深入比較耕突,對于節(jié)點跨層級移動時笤成,只是進行簡單的創(chuàng)建和刪除评架。這會影響 React 性能的操作,因此炕泳,官方建議不要進行 DOM 節(jié)點跨層級的操作纵诞。在組件開發(fā)時,推薦通過 CSS 隱藏或顯示節(jié)點喊崖,不做真正地移除或添加 DOM 節(jié)點的操作挣磨,進而保證穩(wěn)定的 DOM 結(jié)構(gòu),提升性能荤懂。
2. Component Diff
Component Diff是專門針對更新前后的同一層級間的React組件比較的Diff 算法茁裙。React對于組件間的比較采取的策略如下:
- 如果是同一類型的組件,按照原策略繼續(xù)進行虛擬DOM 比較节仿。
- 如果不是晤锥,則將該組件判斷為dirty component,從而替換整個組件下的所有子節(jié)點, 即銷毀原組件廊宪,創(chuàng)建新組件矾瘾。
- 對于同一類型的組件,有可能其虛擬DOM沒有任何變化箭启,如果能夠確切的知道這點那可以節(jié)省大量的Diff運算的時間壕翩,因此,React允許用戶通過shouldComponentUpdate()判斷該組件是否需要進行diff 算法分析傅寡。
舉個例子來說放妈,當下圖中componentD改變?yōu)閏omponentG時,即使這兩個compoent結(jié)構(gòu)很相似荐操,但是react會判斷D和G并不是同類型組件芜抒,也就不會比較二者的結(jié)構(gòu)了,而是直接刪除了D托启,重新創(chuàng)建G及其子節(jié)點宅倒。
因此對于這種結(jié)構(gòu)的轉(zhuǎn)變的實際操作是:
D.destroy();
G = new G();
G.append(new E());
G.append(new F());
V.append(G);
3. Element Diff
Element Diff是專門針對同一層級的所有節(jié)點(包括元素節(jié)點和組件節(jié)點)的Diff算法。當節(jié)點處于同一層級時屯耸,React的Diff提供了三種節(jié)點操作:插入拐迁、移動和刪除。
- 插入:新的component類型不在老集合里肩民,即全新的節(jié)點唠亚,需要對新節(jié)點執(zhí)行插入操作
- 移動:在老集合里有新component類型,且element是可更新的類型持痰,generateComponentChildren已調(diào)用receiveComponent灶搜,這種情況下prevChild=nextChild,就可以復用以前的DOM節(jié)點,執(zhí)行移動操作割卖。
- 刪除:當老的component類型前酿,在新集合中也有,但對應的element不同則不能直接復用和更新鹏溯,需要執(zhí)行刪除操作罢维;當老component不在新集合里,也需要執(zhí)行刪除操作丙挽。
如下圖肺孵,老集合為節(jié)點A、B颜阐、C平窘、D,想生成新集合B凳怨、A瑰艘、D、C肤舞,通過新老集合差異化對比紫新,最簡單粗暴的方法為: 發(fā)現(xiàn)B != A,則新集合創(chuàng)建B李剖,老集合刪除A芒率;以此類推,在老集合刪除B篙顺、C敲董、D,在新集合添加A、D慰安、C。
可以發(fā)現(xiàn)這類操作煩瑣冗余聪铺,因為這些都是相同的節(jié)點化焕,只是由于位置順序發(fā)生變化,就需要進行繁雜低效的刪除铃剔、創(chuàng)建操作撒桨,其實只要對這些節(jié)點執(zhí)行移動操作即可。為此键兜,react提出了優(yōu)化機制 ---? Key機制
四凤类、 Key機制
React允許開發(fā)者對同一層級的同組子節(jié)點,添加唯一key進行區(qū)分普气。React會根據(jù)key來決定是刪除重新創(chuàng)建組件還是更新(移動)組件谜疤,原則是:
- key相同,組件有所變化,React會只更新組件對應變化的屬性夷磕。
- key不同履肃,組件會銷毀之前的組件,將整個組件重新渲染坐桩。
1. 移動規(guī)則
添加了key 之后尺棋,按如下步驟確認是否移動: 首先,對新集合中的節(jié)點進行循環(huán)遍歷 for (name in nextChildren)绵跷,通過唯一的 key 判斷新舊集合中是否存在相同的節(jié)點膘螟,if (prevChild === nextChild)。如果存在相同節(jié)點碾局,且child.mountIndex(當前節(jié)點在老集合中的位置)與 lastIndex(參考位置荆残,類似浮標)進行比較滿足 child._mountIndex < lastIndex,則進行移動操作擦俐,否則不執(zhí)行移動操作脊阴。
這是一種順序優(yōu)化手段,lastIndex = Math.max(prevChild.mountIndex, lastIndex) 將一直更新蚯瞧,表示訪問過的節(jié)點在老集合中最右的位置(即最大的位置)嘿期,如果新集合中當前訪問的節(jié)點比 lastIndex 大,說明當前訪問節(jié)點在老集合中就比上一個節(jié)點位置靠后埋合,則該節(jié)點不會影響其他節(jié)點的位置备徐,因此不用添加到差異隊列中,即不執(zhí)行移動操作甚颂,只有當訪問的節(jié)點比 lastIndex 小時蜜猾,才需要進行移動操作。
基于移動規(guī)則振诬,我們看幾個實例:
實例(1):同一層級的所有節(jié)點只發(fā)生了位置變化
按新集合中順序開始遍歷:
1. B在新集合中 lastIndex = 0, 在舊集合中 mountIndex = 1蹭睡,mountIndex > lastIndex 就認為 B 對于集合中其他元素位置無影響,不進行移動赶么。此時肩豁,lastIndex = max(prevChild.mountIndex, lastIndex) = 1,其中辫呻,prevChild.mountIndex表示B在老集合中的位置清钥。
2. A在舊集合中 mountIndex = 0, 此時, 滿足 mountIndex < lastIndex, 則對A進行移動操作放闺。此時祟昭,lastIndex = max(prevChild.mountIndex, lastIndex) = 1。
3. D和B操作相同怖侦,同(1)篡悟,不進行移動谜叹,此時lastIndex = 3。
4. C和A操作相同恰力,同(2)叉谜,進行移動,此時lastIndex = 3踩萎。
上述結(jié)論中的移動操作即對節(jié)點進行更新渲染停局,而不進行移動則表示無需更新渲染∠愀可見有key值后董栽,相比于之前的繁瑣冗余做法,極大的提升React 的性能企孩。
實例(2): 同一層級的節(jié)點發(fā)生了節(jié)點增刪和節(jié)點位置變化
按新锭碳、老集合中順序開始遍歷:
1. 同上面那種情形,B不進行移動勿璃,lastIndex=1擒抛。
2. 新集合中取得E,發(fā)現(xiàn)舊中不存在E,在 lastIndex處創(chuàng)建E补疑,lastIndex++歧沪。
3. 在舊集合中取到C,C不移動莲组,lastIndex=2诊胞。
4. 在舊集合中取到A,A移動到新集合中的位置锹杈,lastIndex=2撵孤。
5. 完成新集合中所有節(jié)點diff后,對老集合進行循環(huán)遍歷竭望,尋找新集合中不存在但老集合中的節(jié)點(此例中為D)邪码,刪除D節(jié)點。
2. key值的缺陷
如圖所示咬清,若新集合的節(jié)點更新為 D霞扬、A、 B枫振、C,與舊集合相比只有 D 節(jié)點移動萤彩,而 A粪滤、B、C 仍然保持原有的順序雀扶,理論上 diff 應該只需對 D 執(zhí)行移動操作杖小,然而由于 D 在舊集合中的位置是最大的肆汹,導致其他節(jié)點的 mountIndex < lastIndex,造成 D 沒有執(zhí)行移動操作予权,而是 A昂勉、B、C 全部移動到 D 節(jié)點后面的現(xiàn)象扫腺。
因此岗照,在開發(fā)過程中,盡量減少類似將最后一個節(jié)點移動到列表首部的操作笆环。因為當節(jié)點數(shù)量過大或更新操作過于頻繁時攒至,這在一定程度上會影響 React 的渲染性能。
3. key值設置
如果沒有添加唯一的key值時躁劣,會遇到這個錯:
這是React在遇到列表時卻又找不到key時提示的警告迫吐。雖然無視這條警告大部分界面也會正確工作,但這通常意味著潛在的性能問題账忘,因為React覺得自己可能無法高效的去更新這個列表志膀。
同時,key值必須是穩(wěn)定的(不能使用Math.random去創(chuàng)建key), 可預測并且是唯一的鳖擒,且React官方建議不要用遍歷的index作為這種場景下的節(jié)點的key屬性值溉浙。因為使用index作key的情況時,如果當前遍歷的所有節(jié)點類型都相同败去、內(nèi)部文本不同放航,當我們對原始的數(shù)據(jù)list進行了某些元素的順序改變操作,則會導致新舊集合中進行diff比較時圆裕,相同index所對應的新舊的節(jié)點的文本不一致了广鳍,促使一些節(jié)點需要更新渲染文本。而如果用了其他穩(wěn)定的唯一標識符作為key吓妆,則只會發(fā)生位置順序變化赊时,無需更新渲染文本,提升了性能行拢。
此外祖秒,使用index作為key很可能會存在一些出人意料的顯示錯誤的問題。例如舟奠;存在三個input輸入框竭缝,以index作為其key進行渲染時。
若想實現(xiàn)點擊第二個刪除按鈕沼瘫,刪除第二列抬纸。則會發(fā)現(xiàn),第二列未成功刪除耿戚,第三列被刪除掉了湿故。
為什么呢阿趁?
這是因為你認為你刪除了2,但React會認為你做了兩件事:「把2變成了3」以及「把3刪除了」坛猪。
看看這兩個數(shù)組:[123]和[13]脖阵,人類會說,這不就是少了個2嗎墅茉?但是計算機會遍歷數(shù)組:首先對比1和1命黔,發(fā)現(xiàn)1沒變;然后對比2和3躁锁,發(fā)現(xiàn)2變成了3; 最后對比undefined和3纷铣,發(fā)現(xiàn)「3被刪除了」。所以計算機的結(jié)論是:「2變成了3」以及「3被刪除了」战转。
因此搜立,React渲染邏輯為: 1沒變,復用之前的1和三角形槐秧;「2變成了3」啄踊,正方形左邊的2改為3。里面的正方形就地復用(正方形沒有被刪除)刁标;「3被刪除了」颠通,之前的「圓形」當然應該被刪掉,里面的子元素也要刪除膀懈。
因此顿锰,為了避免此類型錯誤,也為了提升性能启搂,不要使用index作為key值硼控。
# 參考內(nèi)容: