[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启发式算法的前提条件主要包括两点:bash

  1. 不一样的两个元素会产生不一样的树
  2. 可使用key属性来代表不一样的渲染中哪些元素是相同的

元素类型的比较

  函数React.createElement的第一个参数就是type,表示的就是元素的类型。React比较两棵元素树的过程是同步的,当React比较到元素树中同一位置的元素节点时,若是先后元素的类型不一样时,不论该元素是组件类型仍是DOM类型的,那么以这个节点(React元素)为子树的全部节点都会被销毁并从新构建。举个例子:   dom

//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后序的版本中不断细化启发式算法,甚至采用别的启发式算法。可是若是咱们有时候可以了解到内部算法的实现细节的话,对于优化应用性能能够起到很是好的效果,对于共同窗习的你们,以此共勉。

相关文章
相关标签/搜索