[React技术内幕] key带来了什么

会议终结者
• 阅读 3317

  首先欢迎大家关注我的掘金账号和Github博客,也算是对我的一点鼓励,毕竟写东西没法获得变现,能坚持下去也是靠的是自己的热情和大家的鼓励。

  大家在使用React的过程中,当组件的子元素是一系列类型相同元素时,就必须添加一个属性key,否则React将给出一个warning:
  [React技术内幕] key带来了什么

  所以我们需要了解一下key值在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,所以我们不禁要问为什么?
  

一致性处理(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就采用了启发式的算法。
  

启发式算法

  了解一下什么是启发式算法:

启发式算法指人在解决问题时所采取的一种根据经验规则进行发现的方法。其特点是在解决问题时,利用过去的经验,选择已经行之有效的方法,而不是系统地、以确定的步骤去寻求答案。

  React启发式算法就是采用一系列前提假设,使得比较前后元素树的时间复杂度由O(n3)降低为O(n),React启发式算法的前提条件主要包括两点:

  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)的丢失有时候会造成不可预知的问题,需要尤为注意。
  
  [React技术内幕] key带来了什么
  
  那如果比较前后元素类型是相同的情况下,情况就有所区别,如果该元素类型是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'))

我们发现最后的显示效果是这样的:

[React技术内幕] key带来了什么

  到这里我们已经基本明白了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后序的版本中不断细化启发式算法,甚至采用别的启发式算法。但是如果我们有时候能够了解到内部算法的实现细节的话,对于优化应用性能可以起到非常好的效果,对于共同学习的大家,以此共勉。

点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
7个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
1年前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Wesley13 Wesley13
3年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这