[React技術內幕] key帶來了什麼

  首先歡迎你們關注個人掘金帳號和Github博客,也算是對個人一點鼓勵,畢竟寫東西無法得到變現,能堅持下去也是靠的是本身的熱情和你們的鼓勵。javascript

  你們在使用React的過程當中,當組件的子元素是一系列類型相同元素時,就必須添加一個屬性key,不然React將給出一個warning:
  java

  因此咱們須要瞭解一下key值在React中起到了什麼做用,在這以前咱們先出一個小題目:
  react

import React from 'react'
import ReactDOM from 'react-dom'

function App() {
    return (
        <ul>
            {
                [1,1,2,2].map((val)=><li key={val}>{val}</li>)
            }
        </ul>
    )
}

ReactDOM.render(<App/>,document.getElementById('root'))

  如今要提問了,上面的例子顯示的是: 1,1,2,2仍是1,2呢。事實上顯示的只有1和2,因此咱們不由要問爲何?
  git

一致性處理(Reconciliation)  

  咱們知道每當組件的propsstate發送改變時,React都會調用render去從新渲染UI,實質上render函數做用就是返回最新的元素樹。這裏咱們要明確一個點: 什麼是組件?什麼是元素?
  
  React元素是用來描述UI對象的,JSX的實質就是React.createElement的語法糖,做用就是生成React元素。而React組件是一個方法或者類(Class),其目的就是接受輸入並返回一個ReactElement,固然調用React組件通常採用的也是經過JSX的方法,其本質也是經過React.createElement方式去調用組件的。
  
  咱們以前說過,組件stateprops的改變會引發render函數的調用,而render函數會返回新的元素樹。咱們知道React使得咱們並不須要關心更改的內容,只須要將精力集中於數據的變化,React會負責先後UI更新。這時候React就面臨一個問題,若是對比當前的元素樹與以前的元素樹,從而找到最優的方法(或者說是步驟最少的方法)將一顆樹轉化成另外一棵樹,從而去更新真實的DOM元素。目前存在大量的方法能夠將一棵樹轉化成另外一棵樹,但它們的時間複雜度基本都是O(n3),這麼龐大的時間數量級咱們是不能接受的,試想若是咱們的組件返回的元素樹中含有100個元素,那麼一次一致性比較就要達到1000000的數量級,這顯然是低效的,不可接受的。這時React就採用了啓發式的算法。
  github

啓發式算法

  瞭解一下什麼是啓發式算法:算法

啓發式算法指人在解決問題時所採起的一種根據經驗規則進行發現的方法。其特色是在解決問題時,利用過去的經驗,選擇已經行之有效的方法,而不是系統地、以肯定的步驟去尋求答案。數組

  React啓發式算法就是採用一系列前提假設,使得比較先後元素樹的時間複雜度由O(n3)下降爲O(n),React啓發式算法的前提條件主要包括兩點:dom

  1. 不一樣的兩個元素會產生不一樣的樹
  2. 可使用key屬性來代表不一樣的渲染中哪些元素是相同的

元素類型的比較

  函數React.createElement的第一個參數就是type,表示的就是元素的類型。React比較兩棵元素樹的過程是同步的,當React比較到元素樹中同一位置的元素節點時,若是先後元素的類型不一樣時,不論該元素是組件類型仍是DOM類型的,那麼以這個節點(React元素)爲子樹的全部節點都會被銷燬並從新構建。舉個例子:
  函數

//old tree
<div>
  <Counter />
</div>

//new tree
<span>
  <Counter />
</span>

  上面表示先後兩個render函數返回的元素樹,因爲Counter元素的父元素由div變成了span,那麼那就致使Counter的卸載(unmount)和從新安裝(mount)。這看起來沒有什麼問題,可是在某些狀況下問題就會凸顯出來,好比狀態的丟失。下面咱們再看一個例子:
  性能

import React, {Component} from 'react'
import ReactDOM from 'react-dom'

class Counter extends Component {

    constructor(props){
        super(props);
    }

    state = {
        value: 0
    }

    componentWillMount(){
        console.log('componentWillMount');
    }

    componentDidMount(){
        this.timer = setInterval(()=>{
            this.setState({
                value: this.state.value + 1
            })
        },1000)
    }

    componentWillUnmount(){
        clearInterval(this.timer);
        console.log('componentWillUnmount');
    }

    render(){
        return(
            <div>{this.state.value}</div>
        )
    }
}

function Demo(props) {
    return props.flag ? (<div><Counter/></div>) : (<span><Counter/></span>);
}

class App extends Component{
    constructor(props){
        super(props);
    }

    state = {
        flag: false
    }

    render(){
        return(
            <div>
                <Demo flag = {this.state.flag}/>
                <button
                    onClick={()=>{
                        this.setState({
                            flag: !this.state.flag
                        })
                    }}
                >
                    Click
                </button>
            </div>
        )
    }
}

ReactDOM.render(<App/>, document.getElementById('root'))

  
  上面的例子中,咱們首先讓計數器Counter運行幾秒鐘,而後咱們點擊按鈕的話,咱們會發現計數器的值會歸零爲0,而且Counter分別調用componentWillUnmountcomponentWillMount並完成組件卸載與安裝的過程。須要注意的是,狀態(state)的丟失有時候會形成不可預知的問題,須要尤其注意。
  
  
  
  那若是比較先後元素類型是相同的狀況下,狀況就有所區別,若是該元素類型是DOM類型,好比:

<div className="before" title="stuff" />

<div className="after" title="stuff" />

那麼React包保持底層DOM元素不變,僅更新改變的DOM元素屬性,好比在上面的例子中,React僅會更新div標籤的className屬性。若是改變的是style屬性中的某一個屬性,也不會整個更改style,而僅僅是更新其中改變的項目。

  若是先後的比較元素是組件類型,那麼也會保持組件實例的不變,React會更新組件實例的屬性來匹配新的元素,並在元素實例上調用componentWillReceiveProps()componentWillUpdate()
  

key屬性

  在上面的先後元素樹比較過程當中,若是某個元素的子元素是動態數組類型的,那麼比較的過程可能就要有所區分,好比:
  

//注意:
//li元素是數組生成的,下面只是表示元素樹,並不表明實際代碼
//old tree
<ul>
  <li>first</li>
  <li>second</li>
</ul>

//new tree
<ul>
  <li>first</li>
  <li>second</li>
  <li>third</li>
</ul>

  當React同時迭代比較先後兩棵元素樹的子元素列表時,性能相對不會太差,由於前兩個項都是相同的,新的元素樹中有第三個項目,那麼React會比較<li>first</li>樹與<li>second</li>樹以後,插入<li>third</li>樹,可是下面這個例子就不一樣的:
  

//注意:
//li元素是數組生成的,下面只是表示元素樹,並不表明實際代碼
//old tree
<ul>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

//new tree
<ul>
  <li>Connecticut</li>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

  React在比較第一個li就發現了差別( <li>Duke</li><li>Connecticut</li>),若是React將第一個li中的內容進行更新,那麼你會發現第二個li(<li>Villanova</li><li>Duke</li>)也須要將li中內容進行更新,而且第三個<li>須要安裝新的元素,但事實真的是如此嗎?其實否則,咱們發現新的元素樹和舊的元素樹,只有第一項是不一樣的,後兩項其實並無發生改變,若是React懂得在舊的元素樹開始出插入<li>Connecticut</li>,那麼性能會極大的提升,關鍵問題是React如何進行這種判別,這時React就用到了key屬性
  
例如:

//注意:
//li元素是數組生成的,下面只是表示元素樹,並不表明實際代碼
//old tree
<ul>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

//new tree
<ul>
  <li key="2014">Connecticut</li>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

  經過key值React比較<li key="2015">Duke</li><li key="2014">Connecticut</li>時,會發現key值是不一樣,表示<li key="2014">Connecticut</li>是新插入的項,所以會在開始出插入<li key="2014">Connecticut</li>,隨後分別比較<li key="2015">Duke</li><li key="2016">Villanova</li>,發現li項沒有發生改變,僅僅只是被移動而已。這種狀況下,性能的提高是很是可觀的。所以,從上面看key值必需要穩定可預測的而且是惟一的。不穩定的key(相似於Math.random()函數的結果)可能會產生很是多的組件實例而且DOM節點也會非必要性的從新建立。這將會形成極大的性能損失和組件內state的丟失。
  
  回到剛開始的問題,若是存在兩個key值相同時,會發生什麼?好比:
  

<ul>
    {
        [1,1,2,2].map((val)=><li>{val}</li>)
    }
</ul>

  咱們會發現若是存在先後兩個相同的key,React會認爲這兩個元素實際上是一個元素,後一個具備相同key值的元素會被忽略。爲了驗證這個事實,咱們能夠看下一個例子:

import React, {Component} from 'react'
import ReactDOM from 'react-dom'

function Demo(props) {
    return (
        <div>{props.value}</div>
    )
}

class App extends Component {
    constructor(props) {
        super(props);
    }

    render() {
        return (
            <div>
                {
                    [1, 1, 2, 2].map((val, index) => {
                        return (
                            <Demo
                                key={val}
                                value={val + '-' + index}
                            />
                        )
                    })
                }
            </div>
        )
    }
}

ReactDOM.render(<App/>, document.getElementById('root'))

咱們發現最後的顯示效果是這樣的:

  到這裏咱們已經基本明白了key屬性在React中的做用,由於key是React內部使用的屬性,因此在組件內部是沒法獲取到key值的,若是你真的須要這個值,就須要換個名字再傳一次了。
  
  其實還有一個現象不知道你們觀察到了沒有,好比:
  

//case1
function App() {
    return (
        <ul>
            {
                [
                    <li key={1}>1</li>,
                    <li key={2}>2</li>
                ]
            }
        </ul>
    )
}
//case2
function App() {
    return (
        <ul>
            <li>1</li>
            <li>2</li>
        </ul>
    )
}

  咱們會發現,第一種場景是須要傳入key值的,第二種就不須要傳入key,爲何呢?其實咱們能夠看一下JSX編譯以後的代碼:
  

//case1
function App() {
    return React.createElement('ul',null,[
        React.createElement('li',{key: 1}, "1"),
        React.createElement('li',{key: 2}, "2")
    ])
}
//case2
function App() {
    return React.createElement('ul',
        null,
        React.createElement('li',{key: 1}, "1"),
        React.createElement('li',{key: 2}, "2")
    )
}

  咱們發現第一個場景中,子元素的傳入以數組的形式傳入第三個參數,可是在第二個場景中,子元素是以參數的形式依次傳入的。在第二種場景中,每一個元素出如今固定的參數位置上,React就是經過這個位置做爲自然的key值去判別的,因此你就不用傳入key值的,可是第一種場景下,以數組的類型將所有子元素傳入,React就不能經過參數位置的方法去判別,因此就必須你手動地方式去傳入key值。    React經過採用這種啓發式的算法,來優化一致性的操做。但這都是React的內部實現方式,可能在React後序的版本中不斷細化啓發式算法,甚至採用別的啓發式算法。可是若是咱們有時候可以瞭解到內部算法的實現細節的話,對於優化應用性能能夠起到很是好的效果,對於共同窗習的你們,以此共勉。

相關文章
相關標籤/搜索