淺談React diff算法與key

寫在前面

提到React時我們腦中可能第一想法就是虛擬DOM和diff算法,也正是因為這兩個東西的存在,才會讓我們不必擔心由于頻繁操作DOM帶來的性能上的瓶頸,React本身在這方面已經(jīng)做得足夠隔離了,我們其實不用過于關注也能寫出不錯性能的代碼,要想深入扒開diff算法的具體實現(xiàn)不是易事,這里我們盡量形象的淺嘗輒止分析一下.

diff策略

傳統(tǒng)diff算法:當我們在對比兩個對象樹的差異時,傳統(tǒng)的diff算法是利用循環(huán)遞歸的方式對所有節(jié)點依次對比,算法的復雜度為O(n * n * n),其中n為節(jié)點總數(shù),怎么樣看起來很恐怖吧,試想一個大項目中DOM樹的節(jié)點樹的立方...所以顯然如果react不對傳統(tǒng)的diff算法進行優(yōu)化的話那么在大項目中的性能將會存在極大瓶頸,這里額外說一下,diff算法最早不是臉書團隊提出來的.
react-diff:
這里react團隊對傳統(tǒng)diff算法優(yōu)化主要基于三個策略,而這些策略最后都是對比vdom(網(wǎng)上很多帖子,包括書里介紹這部分的時候可能會比較隱晦難理解,我這里通俗總結了下):
1.DOM結構發(fā)生改變-----直接卸載并重新creat
2.DOM結構一樣-----不會卸載,但是會update
3.所有同一層級的子節(jié)點.他們都可以通過key來區(qū)分-----同時遵循1.2兩點

先看第一種情況,假設目前前后兩次DOM樹如下圖,那么diff算法具體怎么實現(xiàn)的呢?我們通過console.log更為直觀的看一下結果(console.log的狀態(tài)分別放在對應組件的生命周期里,其中以B組件為例,其他組件也雷同,代碼如下,如果有疑惑的同學可以翻看我以前生命周期的文章或者觀看別人的文章)

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é)點下面,這也就印證了我們所說的第一個策略,也是通用的策略,react-diff如果檢測DOM樹不一樣的情況就會直接銷毀當前節(jié)點(包括當前節(jié)點的所有子節(jié)點).另外值得一提的根據(jù)打印的順序我們也可以知道react渲染原則也是和JS執(zhí)行的順序原則是一樣的:
從左至右,從上到下
接下來看第二種策略,這次我們不改變DOM結構,只改變A節(jié)點的顏色,打印結果如下圖:


結果很明顯,只是單純的update,沒有銷毀及重新創(chuàng)建任何DOM節(jié)點...
所以根據(jù)以上的結果我們可以知道,要想最大化的實現(xiàn)diff算法的性能,我們在項目中盡量不改變DOM結構.比如插入DOM,刪除DOM,最好用visibility:hidden這樣的屬性.但是理想總歸是美好的,實際項目中有很多情況需要做刪除節(jié)點等操作,那么應該怎么辦呢?其實react團隊早就想好了相關的解決方案,比如key.

不可小覷的key

我們最開始寫react的時候應該都見過這樣一個warn:


這是由于我們在循環(huán)渲染列表時候(map)時候忘記標記key值報的警告,既然是警告,就說明即使沒有key的情況下也不會影響程序執(zhí)行的正確性.其實這個key的存在與否只會影響diff算法的復雜度,換言之,你不加key的情況下,diff算法就會以暴力的方式去根據(jù)一二的策略更新,但是你加了key,diff算法會引入一些另外的操作,這里不做贅述,有興趣的可以自己查閱一下.
加了key的好處:
其實key值不一定要在map的時候才去加,即使不map得時候也可以加,而且正確的加上key值還會帶來一定程度上的性能優(yōu)化,我們回歸一下,對初始DOM樹種的B,C調(diào)換位置,以下是不加key值得情況:


以下是加key的情況,代碼和打印結果如下:

return(
                    <div>
                        <A>
                            <B key="B"/>
                            <C key="C"/>
                        </A>
                        <D />
                    </div>
                )

return(
                    <div>
                        <A>
                            <C key="C"/>
                            <B key="B"/>
                        </A>
                        <D />
                    </div>

                )

結果顯而易見...

我們該不該把map的index作為key
我們在給循環(huán)渲染時總是會把index值或者隨機一個值作為key,比如以下代碼:

arr.map((val,index)=>{
                   return(
                            <span key={index}>val</span>
                   )
})

那么這樣做好不好呢?我們可以思考下,key做為DOM節(jié)點標識,如果是前后兩次arr分別為[1,2,3,4]和[5,6,7,8]和前后兩次arr分別為[1,2,3,4]和[4,3,2,1]的情況,很明顯前者可以認為是DOM改變了,后者可以認為是DOM節(jié)點的位移操作,那么對于第一種情況來說index作為key和沒有key值無區(qū)別,但是第二種情況用index作為key值效果沒有比用數(shù)據(jù)本身作為key值好,這里大家可以按照以上方式打印去看一下.所以結論是如果你的數(shù)據(jù)能確保唯一性,就用數(shù)據(jù)本身作為key值吧....
key值必須唯一且不重復么
答案是未必,前提條件是是否同一父節(jié)點,也就是是否同一data-reactId的節(jié)點,也就是說如下代碼是沒問題的:

                    <div>
                        <A>
                            <C key="C"/>
                            <B key="B"/>
                        </A>
                        <D key="C"/>
                    </div>

寫到這里我們應該粗略的大致了解diff算法的策略和key值一些作用了.如果想了解更多的話建議多查閱資料或者潛心直接扒源碼去看一下.

寫在最后

diff算法并不是萬能的,diff算法其實也有不夠完全盡如人意的重渲染方式.所以我們在寫react代碼想要最大化性能的時候還是要注意DOM樹的結構以及靈活運用一些不起眼的屬性

如果有不對的地方可以直接留言或者mail:tzn_goodjob@163.com

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容