寫在前面
提到React時(shí)我們腦中可能第一想法就是虛擬DOM和diff算法,也正是因?yàn)檫@兩個(gè)東西的存在,才會(huì)讓我們不必?fù)?dān)心由于頻繁操作DOM帶來的性能上的瓶頸,React本身在這方面已經(jīng)做得足夠隔離了,我們其實(shí)不用過于關(guān)注也能寫出不錯(cuò)性能的代碼,要想深入扒開diff算法的具體實(shí)現(xiàn)不是易事,這里我們盡量形象的淺嘗輒止分析一下.
diff策略
傳統(tǒng)diff算法:當(dāng)我們?cè)趯?duì)比兩個(gè)對(duì)象樹的差異時(shí),傳統(tǒng)的diff算法是利用循環(huán)遞歸的方式對(duì)所有節(jié)點(diǎn)依次對(duì)比,算法的復(fù)雜度為O(n * n * n),其中n為節(jié)點(diǎn)總數(shù),怎么樣看起來很恐怖吧,試想一個(gè)大項(xiàng)目中DOM樹的節(jié)點(diǎn)樹的立方...所以顯然如果react不對(duì)傳統(tǒng)的diff算法進(jìn)行優(yōu)化的話那么在大項(xiàng)目中的性能將會(huì)存在極大瓶頸,這里額外說一下,diff算法最早不是臉書團(tuán)隊(duì)提出來的.
react-diff:
這里react團(tuán)隊(duì)對(duì)傳統(tǒng)diff算法優(yōu)化主要基于三個(gè)策略,而這些策略最后都是對(duì)比vdom(網(wǎng)上很多帖子,包括書里介紹這部分的時(shí)候可能會(huì)比較隱晦難理解,我這里通俗總結(jié)了下):
1.DOM結(jié)構(gòu)發(fā)生改變-----直接卸載并重新creat
2.DOM結(jié)構(gòu)一樣-----不會(huì)卸載,但是會(huì)update
3.所有同一層級(jí)的子節(jié)點(diǎn).他們都可以通過key來區(qū)分-----同時(shí)遵循1.2兩點(diǎn)
先看第一種情況,假設(shè)目前前后兩次DOM樹如下圖,那么diff算法具體怎么實(shí)現(xiàn)的呢?我們通過console.log更為直觀的看一下結(jié)果(console.log的狀態(tài)分別放在對(duì)應(yīng)組件的生命周期里,其中以B組件為例,其他組件也雷同,代碼如下,如果有疑惑的同學(xué)可以翻看我以前生命周期的文章或者觀看別人的文章)
class B extends React.Component{
constructor(props){
super(props)
console.log('B is creat')
}
componentDidMount(){
console.log('B is did')
}
componentDidUpdate() {
console.log('B is updated.');
}
componentWillUnmount(){
console.log('B is unmount')
}
render(){
return(
<div>
B
{this.props.children}
</div>
)
}
}
我們看到react-diff算法是趨近于'暴力'的方式,并不是把A,B,C轉(zhuǎn)移到D節(jié)點(diǎn)下面,這也就印證了我們所說的第一個(gè)策略,也是通用的策略,react-diff如果檢測(cè)DOM樹不一樣的情況就會(huì)直接銷毀當(dāng)前節(jié)點(diǎn)(包括當(dāng)前節(jié)點(diǎn)的所有子節(jié)點(diǎn)).另外值得一提的根據(jù)打印的順序我們也可以知道react渲染原則也是和JS執(zhí)行的順序原則是一樣的:
從左至右,從上到下
接下來看第二種策略,這次我們不改變DOM結(jié)構(gòu),只改變A節(jié)點(diǎn)的顏色,打印結(jié)果如下圖:
結(jié)果很明顯,只是單純的update,沒有銷毀及重新創(chuàng)建任何DOM節(jié)點(diǎn)...
所以根據(jù)以上的結(jié)果我們可以知道,要想最大化的實(shí)現(xiàn)diff算法的性能,我們?cè)陧?xiàng)目中盡量不改變DOM結(jié)構(gòu).比如插入DOM,刪除DOM,最好用visibility:hidden這樣的屬性.但是理想總歸是美好的,實(shí)際項(xiàng)目中有很多情況需要做刪除節(jié)點(diǎn)等操作,那么應(yīng)該怎么辦呢?其實(shí)react團(tuán)隊(duì)早就想好了相關(guān)的解決方案,比如key.
不可小覷的key
我們最開始寫react的時(shí)候應(yīng)該都見過這樣一個(gè)warn:
這是由于我們?cè)谘h(huán)渲染列表時(shí)候(map)時(shí)候忘記標(biāo)記key值報(bào)的警告,既然是警告,就說明即使沒有key的情況下也不會(huì)影響程序執(zhí)行的正確性.其實(shí)這個(gè)key的存在與否只會(huì)影響diff算法的復(fù)雜度,換言之,你不加key的情況下,diff算法就會(huì)以暴力的方式去根據(jù)一二的策略更新,但是你加了key,diff算法會(huì)引入一些另外的操作,這里不做贅述,有興趣的可以自己查閱一下.
加了key的好處:
其實(shí)key值不一定要在map的時(shí)候才去加,即使不map得時(shí)候也可以加,而且正確的加上key值還會(huì)帶來一定程度上的性能優(yōu)化,我們回歸一下,對(duì)初始DOM樹種的B,C調(diào)換位置,以下是不加key值得情況:
以下是加key的情況,代碼和打印結(jié)果如下:
return(
<div>
<A>
<B key="B"/>
<C key="C"/>
</A>
<D />
</div>
)
return(
<div>
<A>
<C key="C"/>
<B key="B"/>
</A>
<D />
</div>
)
結(jié)果顯而易見...
我們?cè)摬辉摪裮ap的index作為key
我們?cè)诮o循環(huán)渲染時(shí)總是會(huì)把index值或者隨機(jī)一個(gè)值作為key,比如以下代碼:
arr.map((val,index)=>{
return(
<span key={index}>val</span>
)
})
那么這樣做好不好呢?我們可以思考下,key做為DOM節(jié)點(diǎn)標(biāo)識(shí),如果是前后兩次arr分別為[1,2,3,4]和[5,6,7,8]和前后兩次arr分別為[1,2,3,4]和[4,3,2,1]的情況,很明顯前者可以認(rèn)為是DOM改變了,后者可以認(rèn)為是DOM節(jié)點(diǎn)的位移操作,那么對(duì)于第一種情況來說index作為key和沒有key值無區(qū)別,但是第二種情況用index作為key值效果沒有比用數(shù)據(jù)本身作為key值好,這里大家可以按照以上方式打印去看一下.所以結(jié)論是如果你的數(shù)據(jù)能確保唯一性,就用數(shù)據(jù)本身作為key值吧....
key值必須唯一且不重復(fù)么
答案是未必,前提條件是是否同一父節(jié)點(diǎn),也就是是否同一data-reactId的節(jié)點(diǎn),也就是說如下代碼是沒問題的:
<div>
<A>
<C key="C"/>
<B key="B"/>
</A>
<D key="C"/>
</div>
寫到這里我們應(yīng)該粗略的大致了解diff算法的策略和key值一些作用了.如果想了解更多的話建議多查閱資料或者潛心直接扒源碼去看一下.
寫在最后
diff算法并不是萬能的,diff算法其實(shí)也有不夠完全盡如人意的重渲染方式.所以我們?cè)趯憆eact代碼想要最大化性能的時(shí)候還是要注意DOM樹的結(jié)構(gòu)以及靈活運(yùn)用一些不起眼的屬性
如果有不對(duì)的地方可以直接留言或者mail:tzn_goodjob@163.com