React/Vue里的key到底有什么用?看完這篇你就知道了!(附demo代碼)
點(diǎn)擊上方藍(lán)色字體,選擇“標(biāo)星公眾號”
優(yōu)質(zhì)文章,第一時(shí)間送達(dá)
作者 | 大唐西域都護(hù)
來源 | urlify.cn/3eqY3a
76套java從入門到精通實(shí)戰(zhàn)課程分享
網(wǎng)上有很多博客講到,React、Vue里的key,與 Virtual DOM 及 DOM diff 有關(guān), 可以用來唯一標(biāo)識DOM節(jié)點(diǎn),提高diff效率,云云。
這大致是對的,但是,大多講得語焉不詳,像是在背答案。
具體怎么個(gè)提效法?為什么說用數(shù)組下標(biāo)當(dāng)作key是“反模式”?講了一堆,能不能來個(gè)眼見為實(shí),show me the code?
本文以React為例,嘗試稍微刨一刨,但又不刨到太底層,以足夠幫助理解為度。
1. VNode diff
首先介紹 Virtual DOM 結(jié)點(diǎn)(后續(xù)簡稱Virtual Node, VNode)是如何創(chuàng)建出來的。
現(xiàn)實(shí)中的React項(xiàng)目幾乎都會用到JSX,而JSX不能直接執(zhí)行,需要先經(jīng)babel編譯成js代碼,比如:
<div className="content">Hello world!</div>會被編譯成
React.createElement("div", {
className: "content"
}, "Hello world!");
(點(diǎn)擊這里查看在線編譯)
所以,只要調(diào)用 React.createElement 這個(gè)靜態(tài)方法,就可以創(chuàng)建出一個(gè)VNode。
無需深入VNode 的具體數(shù)據(jù)結(jié)構(gòu),只要看看這個(gè)工廠方法的參數(shù),就可以知道 DOM diff 到底 diff 了哪些內(nèi)容。
根據(jù)React官方文檔,該方法可以接收≥3個(gè)參數(shù):
第一個(gè)參數(shù)是type,指定結(jié)點(diǎn)類型,如果是HTML原生結(jié)點(diǎn),那么會是一個(gè)字符串,比如"div";如果是React組件,那么就會是一個(gè)class或function;
第二個(gè)參數(shù)是props,是一個(gè)對象或者null。比如前面的例子中,div標(biāo)簽上的"className"屬性就被加到這里來了;
第三(及第四,第五,……)個(gè)參數(shù)是childNode,該結(jié)點(diǎn)的子節(jié)點(diǎn)。前面的例子中,div的子節(jié)點(diǎn)是一個(gè)內(nèi)容為"Hello world!"的TextNode
是滴,DOM diff 具體diff 的東西,就是這幾個(gè)參數(shù)。為什么不會有別的?因?yàn)槟菢硬环蟁eact的設(shè)計(jì)理念:Data => UI 單向映射。
2. 動(dòng)態(tài)列表的diff困局
我們知道React在調(diào)用setState觸發(fā)render時(shí),會對新舊 Virtual DOM 做比較,力爭以最小的代價(jià)完成新DOM渲染任務(wù)。
結(jié)合上面提到的幾個(gè)參數(shù),具體比較過程大致是這樣的:
首先比較type。如果type不同,那沒什么好說的,直接銷毀重新create一個(gè);如果type相同,再往后看:
其次比較props,如果有變化,那就把變化的部分update;如果沒變化,那就再往后看:
最后比較子節(jié)點(diǎn),同樣地,有變化就update,沒變化就啥都不做
這在DOM結(jié)構(gòu)固定的一般情況下是很好用的,但當(dāng)我們希望從一個(gè)list映射出列表、而且這個(gè)list里的項(xiàng)隨時(shí)可能變化時(shí),就有點(diǎn)麻煩了。
比如說,原本list是這樣的:
[
{name: 'Smith', job: 'Engineer'},
{name: 'Alice', job: 'HR'},
{name: 'Jenny', job: 'Designer'}
]然后,Jenny被移到了最前面,那么Smith和Alice就相應(yīng)后移了,變成了
[
{name: 'Jenny', job: 'Designer'},
{name: 'Smith', job: 'Engineer'},
{name: 'Alice', job: 'HR'}
]對于React來說,如果它不知道這三個(gè)結(jié)點(diǎn)“本來”是誰,只是按照位置對應(yīng)關(guān)系逐個(gè)去檢查,會發(fā)現(xiàn)每個(gè)結(jié)點(diǎn)都變了:
Smith => Jenny
Alice => Smith
Jenny => Alice
于是React得出結(jié)論:列表中的所有結(jié)點(diǎn),全都需要update,重新渲染!
且慢!有沒有更好的方法?
3. 借助key破局
如果,React“知道”這三個(gè)結(jié)點(diǎn)“本來”是誰,那么事情就會簡單很多:
不需要更新任何DOM結(jié)點(diǎn),只需把Jenny對應(yīng)的結(jié)點(diǎn)摘下來,再插入到新的位置,完事。
但React怎么會知道誰是誰呢?
這需要我們開發(fā)者手動(dòng)告訴它,于是key出場了。
在做DOM diff 時(shí),如果同一個(gè)父組件下的兩個(gè)VNode擁有同樣的key,就會被視為同一個(gè)結(jié)點(diǎn),如果React據(jù)此判斷出,這個(gè)結(jié)點(diǎn)在列表中的排位發(fā)生了變化,就會像上面說的那樣,進(jìn)行“摘下-插入”處理。
為了證明這一點(diǎn),亮代碼!
首先上一個(gè)故意整出bug的版本:
class App extends React.Component {
state = {
list: [0, 1, 2]
}
add() {
const list = this.state.list;
this.setState({ list: [list.length, ...list] });
}
render() {
return (
<div className="App">
<button onClick={() => this.add()}>Input sth below, then click me</button>
<ul>
{ // 注意:這里故意用index作為key,引發(fā)bug
this.state.list.map((item, index) => (
<li key={index}>
<span>Item-{item}</span>
<input type="text" />
</li>
)
)
}
</ul>
</div>
);
}
}ReactDOM.render( <App />, document.getElementById('root'));
可以用 create-react-app起個(gè)項(xiàng)目,在本地試試這段代碼。演示效果如下,先在第二行文本框里輸入一些1:

然后,點(diǎn)擊上面的按鈕,會發(fā)現(xiàn)……

輸入了一串1的文本框沒有跟著Item-1走,而是留在了“原位”!
這就是用數(shù)組下標(biāo)作key引發(fā)的典型bug。原因就在于新列表里Item-0和原列表里的Item-1擁有同樣的key,被React視為同一個(gè)結(jié)點(diǎn),所以只是“就地”更新了子節(jié)點(diǎn)(文本),并沒有挪動(dòng)結(jié)點(diǎn)的位置。
而這個(gè)bug的巧妙之處就在于使用了<input>,它可以在VNode的type、props、children均無變化的前提下,被用戶行為改變其樣式(輸入的內(nèi)容),從而讓我們直觀地看到結(jié)點(diǎn)所處位置。感謝React官方提供了這個(gè)巧妙的case。
好,下面我們來修復(fù)這個(gè)bug。
修復(fù)方法很簡單:把 key={index} 改成 key={item} 就行了。
保存,刷新重試,我們就可以得到:

這下,對應(yīng)關(guān)系正確了,React正確地識別出了3個(gè)舊結(jié)點(diǎn),直接把新結(jié)點(diǎn)插入到列表開頭,而舊結(jié)點(diǎn)沒有變化。
看到這里,你應(yīng)該明白key到底有什么用,以及為什么index不宜做key了吧。
另外,如果沒有指定key,那么React會默認(rèn)使用index作為key,所以,只要是動(dòng)態(tài)列表,為了性能著想,請盡量用unique id作為key。
粉絲福利:Java從入門到入土學(xué)習(xí)路線圖
??????

??長按上方微信二維碼 2 秒
感謝點(diǎn)贊支持下哈 
