目录:
官方解释:介绍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
浅层思考:
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> ); };
这也是一个神奇效果,同级的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> ); }; }
即使没有key,或者错误使用key,react也能返回react-id,同时有一些警告,当然,错误使用key,结果和没有key是一样的,具体会有什么样的差别,下面会深究
对于官方第二个建议,我们可以使用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> ); };
效果也是可以猜到的
我们甚至可以做这样的事情,注意,这里的data是一个数组,但是渲染出来的节点只有一个。从结果我们也可以看出来,当key相同时,节点不会重复渲染。因此得出的结论是,key必须保障唯一性,当然这也是官方的建议。key的唯一性只需要保证在同父级组件下,同页面无所谓。
render() { return ( <ul> {this.state.data.map(function(result) { return <ListItem key='1' data={result}/>; })} </ul> ); };
探究:
上面讲了半天,也是周四下午我和刘晶、罗黎讨论的主要内容,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> ); };
从结果我们已经非常容易看出来。当在一列数据的最前放添加数据时,他们对于节点的处理情况是不同的。
不添加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); };
我们知道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需要保证和不同批兄弟不一样