目录:

  • 官方解释:介绍key

  • 官方建议:使用key

  • 初步思考:怎样使用key

  • 深度思考:为什么使用key&怎样正确使用key

  • 参考资料

React官方解释:

  • 一些场景下React中需要key属性来识别一个组件,key属性本身无法在组件的任何位置获取到,而key只需要在组件的兄弟中唯一,而无需全局唯一。

  • 在react引用的算法中包含Levenshtein distance ,编辑距离,指两个字符串之间,由一个转成另一个所需要最好编辑操作次数,可进行的操作包括将一个字符替换成另一个字符、插入字符、删除字符。

  • 通过key来匹配子元素,可以让react进行插入、删除、替换、移动都在O(n)内进行。

官方建议&第三方建议:

  • The key should always be supplied directly to the components in the array, not to the container HTML child of each component in the array:

// WRONG!
var ListItemWrapper = React.createClass({
  render: function() {
    return <li key={this.props.data.id}>{this.props.data.text}</li>;
  }
});
var MyComponent = React.createClass({
  render: function() {
    return (
      <ul>
        {this.props.results.map(function(result) {
          return <ListItemWrapper data={result}/>;
        })}
      </ul>
    );
  }
});
 
// Correct :)
var ListItemWrapper = React.createClass({
  render: function() {
    return <li>{this.props.data.text}</li>;
  }
});
var MyComponent = React.createClass({
  render: function() {
    return (
      <ul>
        {this.props.results.map(function(result) {
           return <ListItemWrapper key={result.id} data={result}/>;
        })}
      </ul>
    );
  }
});
  • You can also key children by passing a ReactFragment object.

//Not Good
var Swapper = React.createClass({
  propTypes: {
    // `leftChildren` and `rightChildren` can be a string, element, array, etc.
    leftChildren: React.PropTypes.node,
    rightChildren: React.PropTypes.node,

    swapped: React.PropTypes.bool
  }
  render: function() {
    var children;
    if (this.props.swapped) {
      children = [this.props.rightChildren, this.props.leftChildren];
    } else {
      children = [this.props.leftChildren, this.props.rightChildren];
    }
    return <div>{children}</div>;
  }
});
 
//Right
if (this.props.swapped) {
  children = React.addons.createFragment({
    right: this.props.rightChildren,
    left: this.props.leftChildren
  });
} else {
  children = React.addons.createFragment({
    left: this.props.leftChildren,
    right: this.props.rightChildren
  });
}

It is often easier and wiser to move the state higher in component hierarchy

浅层思考:

  • 正常情况下我们并不需要使用key,react可以给所有的Stateful Children生成唯一的react-id,并且过程相当智能,以用于之后的dom diff等功能。

    render() {
        return (
            <div>
                {this.state.data.map(function(result, index) {
                    return <DivItem data={result}/>;
                })}
                <div></div>
                <div></div>
            </div>
        );
    };
    render() {
        return (
            <div>
                <div>
                    {this.state.data.map(function(result, index) {
                        return <DivItem data={result}/>;
                    })}
                </div>
                <div></div>
                <div></div>
            </div>
        );
    };

react-map-without-key-without-parent
react-map-without-key-with-parent

这也是一个神奇效果,同级的div节点,通过循环产生的react-id和其他的并不是同级效果,不过仔细观察数据,也是很好理解的。观察react-id我们也可以发现,在没有外包直接父级别节点的情况下,通过循环产生的节点应该会有一个“虚拟父节点”,这时候的react-id和同层次的节点已经不一样,同时又不同于真正存在父节点的情况

  • 对官方的第一个建议,这个很好理解,key本身用在父组件中来我们看一下相关测试效果

// Wrong
class ListItem extends Component {
    ···
    render () {
        return (
            <li key={this.props.data.id}>{this.props.data.text}</li>
        );
    }
}
class HelloDemo extends Component {
    ···
    render() {
        return (
            <ul>
                {this.state.data.map(function(result) {
                    return <ListItem data={result}/>;
                })}
            </ul>
        );
    };
}
 
// Right
class ListItem extends Component {
    ···
    render () {
        return (
            <li>{this.props.data.text}</li>
        );
    }
}
class HelloDemo extends Component {
    ···

    render() {
        return (
            <ul>
                {this.state.data.map(function(result) {
                    return <ListItem key={result.id} data={result}/>;
                })}
            </ul>
        );
    };
}

react-without-key
react-with-index-as-key

即使没有key,或者错误使用key,react也能返回react-id,同时有一些警告,当然,错误使用key,结果和没有key是一样的,具体会有什么样的差别,下面会深究

react-warn

  • 对于官方第二个建议,我们可以使用ReactFragment,效果如下

// Wrong
render() {
    let line = [<span>道士下山</span>, <span>捉妖记</span>];
    return (
        <ul>
            {line}
        </ul>
    );
}
// Right
import Addons from "react/addons";
...
render() {
    let line = Addons.addons.createFragment({
        daoshi: <span>道士下山</span>,
        zhuoyao: <span>捉妖记</span>
    });
    return (
        <ul>
            {line}
        </ul>
    );
};

效果也是可以猜到的

react-array-without-key
react-createfragment

  • 我们甚至可以做这样的事情,注意,这里的data是一个数组,但是渲染出来的节点只有一个。从结果我们也可以看出来,当key相同时,节点不会重复渲染。因此得出的结论是,key必须保障唯一性,当然这也是官方的建议。key的唯一性只需要保证在同父级组件下,同页面无所谓。

    render() {
        return (
            <ul>
                {this.state.data.map(function(result) {
                    return <ListItem key='1' data={result}/>;
                })}
            </ul>
        );
    };

react-the-same-key

探究:

上面讲了半天,也是周四下午我和刘晶、罗黎讨论的主要内容,key无疑会影响到react-id的生成方式,但我们关心的是这到底有什么影响??我们以后写代码要注意什么??
首先,key的出现,是变化的节点,也就是Dynamic Children,我们考虑到这样的使用情况,往往是需要对节点结构进行增、删、改、查的功能,那么key会对节点产生什么样的影响?

增加节点和删除节点的功能类似,这里仅以增加节点的代码为例:
//公用方法,在最前面添加一个节点
    handleClick() {
        let data = this.state.data;
        data.unshift({id:10, text: '盗梦空间'});
        this.setState(data);
    };
//不添加key
    render() {
        return (
            <ul onClick={this.handleClick.bind(this)}>
                {this.state.data.map(function(result, index) {
                    return <ListItem data={result}/>;
                })}
            </ul>
        );
    };
//key为index
    render() {
        return (
            <ul onClick={this.handleClick.bind(this)}>
                {this.state.data.map(function(result, index) {
                    return <ListItem key={index} data={result}/>;
                })}
            </ul>
        );
    };
//key为特定唯一值
    render() {
        return (
            <ul onClick={this.handleClick.bind(this)}>
                {this.state.data.map(function(result, index) {
                    return <ListItem key={result.id} data={result}/>;
                })}
            </ul>
        );
    };

no-key
index-as-key
unique-key

从结果我们已经非常容易看出来。当在一列数据的最前放添加数据时,他们对于节点的处理情况是不同的。

  • 不添加key的情况下,react自动生成key,长度为n的数组前添加一个数据,在dom结构上将会引发n词内容替换和一次插入(但是被插入的节点其实是最后一个节点)

  • key为index的情况和第一种类似,因为数组的index本身发生了改变

  • key为一个unique值,react将会直接在最前方调用一次插入,显然这样的效率是最高的。

再举替换的例子
// 替换部分
    handleClick() {
        let data = this.state.data;
        [data[0], data[3]] = [data[3], data[0]];
        this.setState(data);
    };

change-order-without-key
change-order-with-key

我们知道react当中,即使是相同的组件,当key发生改变的时候,React也会直接跳过Dom diff,完全弃置之前组件的所有子元素,从头重新开始渲染。上面的例子其实引出一点小问题,如果交换元素的时候,仅仅替换内容是不是更快?也就是前者的替换方式可能要优于后者?

这里想出三个解释:

  • 很多情况下,在第一种方式中,React对元素进行diff操作后会确定最高效的操作是改变其中元素的属性值,而这样的操作是非常低效的,同时可能导致浏览器查询缓存,甚至导致网络新请求。而后面一种情况证明,移动DOM节点是最高效的做法。参考官网的例子图(下方有)也可以看出

  • 在之前的插入和删除操作中,如果元素是带有属性的,你改变了靠前的属性,其后所有的节点都会受到影响

  • 最重要的一点,在第一种情况下,react认为节点其实并没有改变,仅仅是帮助你改变了节点的内容,也就是说,如果你用第一种方式,并且重新给出四个数据,react内部会认为这四个节点和之前的节点是一样的节点,只是将内容替换掉了。但是需要注意Component doesn’t have initial state defined but the previous one,你对之前那四个节点做的事情,很可能会影响到新的四个节点。这些节点并不是Stateful Children,而是Dynamic Children,我们需要的就是react能够完全替换节点,重新渲染。

个人建议:

  • Dynamic Children需要使用key

  • key需要在父节点调用处定义

  • key不需要保证和非兄弟相异

  • key需要保证和同批兄弟不一样

  • key需要保证和不同批兄弟不一样

相关参考:

react-1
react-2


sobranie
150 声望3 粉丝