React key值
React中key属性的作用及原理解析
Warning: Each child in an array or iterator should have a unique "key" prop. Check the render method of `Test`. See https://fb.me/react-warning-keys for more information.
相信在react的使用过程中,大家或多或少都会遇到过这样的警告,这个警告是提醒开发者,需要对渲染的组件添加key属性,那么,这个key属性的作用到底是什么呢?
我们先来看一看下面的段实例代码和它的运行效果
import React, {Component} from 'react'; class Test extends Component { constructor(props) { super(props); this.state = { testArray: [{text: '组件1', id: 'a'}, {text: '组件2', id: 'b'}, {text: '组件3', id: 'c'}, {text: '组件4', id: 'd'}] } } //修改state打乱顺序 sort(){ this.setState({ testArray: [{text: '组件1', id: 'a'}, {text: '组件3', id: 'c'}, {text: '组件2', id: 'b'}, {text: '组件4', id: 'd'}] }) } render() { return <div> <div>不指定key属性</div> <ul> { this.state.testArray.map((item) => { return <li ><span>{item.text}</span><input/></li> }) } </ul> <div>指定key属性</div> <ul> { this.state.testArray.map((item) => { return <li key={item.id}><span>{item.text}</span><input/></li> }) } </ul> <button onClick={::this.sort}>打乱排序</button> </div> } } export default Test
打乱顺序前,在input中填入内容
打乱顺序后
我们可以观察一下,打乱顺序后,有无指定key属性运行结果的异同。相同的是,每一个项的input中的value都得到了保留,不同的是,如果我们不指定key属性,列表中组件的标题和input在打乱顺序之后,好像已经对不上号了,那么,是什么原因造成的呢?
我们来简单的了解一下react的diff算法策略,我们都知道,react为了提升渲染性能,在内部维持了一个虚拟dom,当渲染结构有所变化的时候,会在虚拟dom中先用diff算法先进行一次对比,将所有的差异化解决之后,再一次性根据虚拟dom的变化,渲染到真实的dom结构中。
而key属性的使用,则涉及到diff算法中同级节点的对比策略,当我们指定key值时,key值会作为当前组件的id,diff算法会根据这个id来进行匹配。如果遍历新的dom结构时,发现组件的id在旧的dom结构中存在,那么react会认为当前组件只是位置发生了变化,因此不会将旧的组件销毁重新创建,只会改变当前组件的位置,然后再检查组件的属性有没有发生变化,然后选择保留或修改当前组件的属性,因此我们可以发现如果我们指定了唯一的key值,如果只是打乱了数据源,数据源渲染出来的每一个子组件都是整体数据发生变化,而如果不显式指定key值,结果好像有点出乎我们的意料。
那么,如果没有显式指定key值,会发生什么事情呢?其实,如果没有显式指定,react会把当前组件数据源的index作为默认的key值,那么,这时候会发生什么事呢?我们以第二项作为例子,由于我们没有显式指定key值,key值会被默认指定为index,也就是1。当我们打乱了数据的顺序,数据源的第二项由{text: '组件2', id: 'b'}变成了{text: '组件3', id: 'c'},这时候执行diff算法时,发现key值为1的组件在旧的dom结构中存在,并且组件的位置还是原来的位置,所以,直接保留了原组件,但是组件的标题属性已经改变了,接着,修改组件的属性,渲染,于是,我们就看到了,输入框没改变,但是标题变了,很显然,这个结果,有时候并不是我们的本意。而如果我们显式指定了唯一的key值,依旧以第二项作为例子,执行diff算法时,发现第二项的组件变化了并且新的组件在旧的dom结构中存在,于是将第三项整体移动到第二项,然后检查属性有没有发生变化,渲染,最终出现的结果,就是整体的顺序改变了。
因此,在实际开发使用中,我们需要注意什么呢?
首先,我们要确保key值的唯一,事实上如果key值不唯一的话,react只会渲染第一个,剩下的react会认为是同一项,直接忽略。其次,尽量避免使用index值作为组件的key值,虽然显式使用index作为key值可以消除warning,但是,我们举例出现的情况依旧会出现。
React 的key详解
我们在学React的list章节的时候,一定会提到key这个属性,key是给React用的,主要是为了区分不同的组件,是组件的唯一标识,
当我们没用设置key属性的时候,React会给出警告,并且会把数组的index作为组件的key值,所以说key对于组件是必不可少的,那有同学可能会问了,如果key这么重要的话,为什么我只在用数组的map方法返回的组件中用过key,其它组件我却没有用过,
其实对于其它组件来说,它的固定的位置就是key,可以互相区分和定位
接下来说说用index作为key的坏处:.不稳定,数组经过排序,删除,插入元素,里面组件的的key可能就变了,这会带来一些难以预料的错误
看下面的例子,输入框里的时间是我们可以选的,初始为空
当我们执行添加操作,你会发现,初值不为空,并且数值和插入之前已存在的组件输入框相同
为了解释这种现象,我们先来讲,在什么情况下组件会重新创建,什么情况下只是更改属性但不重新创建
请看下面的例子:
var flag = 0; class Test extends React.Component { constructor(props){ super(props); this.state = { name: 'yewenjun' }; this.handleClick = this.handleClick.bind(this); } componentDidMount() { console.log('DidMount'); } componentWillUnmount() { console.log('unmount'); } handleClick () { flag ++; this.setState(state => ({ name: state.name === 'yewenjun' ? 'yewenhui' :'yewenjun' })) } render() { const name = this.state.name; return (<Name onClick={this.handleClick} name={name} key={flag} />) } } class Name extends React.Component { constructor(props) { console.log(props); super(props); } componentDidMount() { console.log('child DidMount'); } componentWillUnmount() { console.log('child unmount'); } render() { return (<div onClick={this.props.onClick}>{this.props.name}</div>) } } ReactDOM.render( <Test />, document.getElementById('root') );
有两个组件,Test,和Name,组件结构很简单,就不啰嗦了,直接讲现象,
全局变量:flag作为key,每次点击Name,均会改变key,看控制台打印的信息
我们会发现,Name这个组件,销毁之后重新创建了
加下来我们把key改为 定值,把key++注释掉即可
这时候我们会发现,控制台没有打印出刚才的信息,所以通过比较我们可以发现,一个组件, 重新创建与否,取决于它的key值变还是不变
好接下来,在解释之前的现象:为什么我往数组里添加了空值,但渲染出来的组件为什么却是有值的,并且还有原先的一样,
这是因为添加在数组的最开头,React在创建好新的虚拟dom之后会和老的虚拟dom之间进行diff运算,哎,发现,key为0的新老虚拟dom都有,就不会重新创建了,只会更新,然后还发现,input节点是一样的,所以就复用先前的input了,所以才会有值,并且和先前的一样,另外还有一个key为1的虚拟dom,和原先一比,发现没有,就会创建这个key为1的组件了,值还是原先已有的值
【前端面试】React中的key是什么,它有什么作用?
标识唯一性
比如在React的源码中,reconcileSingleElement协调方法,key用来比较两个元素,看能不能实现复用,如果能复用就直接使用该元素,不能则创建一个新的元素。
if key === null and child.key === null,then this only applies to
数组进行对比
原先是一个链表结构,不方便取值,这时我们可以通过数组来完成。
更新结点时需要key值判断是否相同元素。
此外在fiber中,通过map图,用get方法拿到key值,就能获取对应的结点。
web前端高级React - React从入门到进阶之列表元素及元素中key的作用
一、列表元素的渲染
在日常开发中,难免会遇到页面需要展示列表的需求,不管是只有一列还是多列数据,都是一堆结构相同内容不同的数据。在传统的html中有table元素可以显示多列表格,有ul>li可以显示多行单列的列表数据。
在真实项目中,我们前端页面上所展示的数据都是来key源于我们的后端,后端会为我们提供一些API,然后这些API返回的结果通常就是存有多条业务数据的数组,然后我们再利用JavaScript代码通过循环将数组中的数据处理后与html元素结合,渲染到页面上最终呈现给用户
那么在React中,把数组转换为元素列表的过程与JavaScript是一样的,我们也可以通过JavaScript代码与JSX标签相结合来渲染数据列表
在React中我们可以通过使用{}在JSX内构建一个元素集合
我们可以利用JavaScript中数组的map方法来遍历数组,并将数组中的每个元素与JSX的li标签结合,最后渲染到页面上
假如后端给我们返回来一个人员名单数组,要求我们把数组中的人名全部以列表的形式展示在页面上,下面我们就来实现一下这个需求
//我们定义一个userList来模拟后台返回的数据 const userList = ["Alvin","Yinnes","Shenhuinan","Louyaqian","Yaolu","Yuyueping"]; const listItems = userList.map(user => { return <li>{user}</li> }); ReactDOM.render( <ul>{ listItems }<ul/>, document.getElementById('root') );
二、列表组件的封装
上述代码运行完后,就会在页面上展示出所有人的名字了
但是在真实项目开发中,我们不可能直接把代码随意写成这样,一般我们都会把不同的业务逻辑封装成组件,这样既便于维护也方便复用
接下来我们就把上面的代码封装成一个独立的组件UserList
function UserList(props){ const users = props.users; const listItems = users.map(user => { return <li>{user}</li> }); return <ul>{listItems}</ul>; } //我们定义一个userList来模拟后台返回的数据 const userList = ["Alvin","Yinnes","Shenhuinan","Louyaqian","Yaolu","Yuyueping"]; ReactDOM.render( <UserList users={userList} />, document.getElementById('root') );
这样我们就简单的实现了一个列表组件了
但是当我们运行这段代码时,会发现控制台中会弹出一堆的警告信息:a key should be provided for list items,意思就是:我们应该为每个列表项提供一个key,也就是说当我们创建一个元素时,必须要包括一个特殊的key属性,关于key的更多信息,我们将会在详细介绍。
我们先把当前这个例子中的每个li加上这个特殊的key属性解决warning问题
function UserList(props){ const users = props.users; const listItems = users.map((user,index) => { return <li key={user+index}>{user}</li>//我们把用户名加上当前用户在数组中的索引作为key给li }); return <ul>{listItems}</ul>; } //我们定义一个userList来模拟后台返回的数据 const userList = ["Alvin","Yinnes","Shenhuinan","Louyaqian","Yaolu","Yuyueping"]; ReactDOM.render( <UserList users={userList} />, document.getElementById('root') );
这样就不会再报warning了
接下来我们再来看一下这个key是干嘛用的,为什么要加上一个key属性呢?
三、元素的key属性
那么key是什么,为什么要使用key呢
当页面元素发生变化时,key可以帮助React识别哪些元素改变了,比如添加或者删除。
另外,当React在进行新旧DOM元素对比时,也会利用元素的key属性来进行匹配对比,这样会大大提高渲染效率
因此我们应该给列表中的每个元素赋予一个确定的标识
一个元素的key最好是这个元素在列表中独一无二的字符,也就是说key的值应该是唯一的。一般情况下,我们使用数据的id来作为元素的key值
当元素没有确定的id的时候,万不得已的情况下可以使用元素的索引来作为key(墙裂不推荐),因为这样做可能会导致性能变差,甚至还会引起组件的状态问题
下面来看两段设置key的代码示例
- 利用数据的id作为属性key值
const userList = [{id:1, name:"Alvin"},{id:2, name:"Yinnes"},{id:3, name:"Shenhuinan"},{id:4, name:"Louyaqian"},{id:5, name:"Yaolu"},{id:6, name:"Yuyueping"}]; const listItems = userList.map(user => { return <li key={user.id}>{user.name}</li> });
- 利用索引作为key属性
const userList = ["Alvin","Yinnes","Shenhuinan","Louyaqian","Yaolu","Yuyueping"]; const listItems = userList.map((user,index) => { return <li key={index}>{user}</li> });
四、设置key属性的一般原则
元素的 key 只有放在就近的数组上下文中才有意义。比如说,如果你提取出一个 ListItem 组件,你应该把 key 保留在数组中的这个 < ListItem /> 元素上,而不是放在 ListItem 组件中的 < li> 元素上。
- 例子:不正确的使用 key 的方式
function ListItem(props) { const value = props.value; return ( // 错误!你不需要在这里指定 key: <li key={value.toString()}> {value} </li> ); } function NumberList(props) { const numbers = props.numbers; const listItems = numbers.map((number) => // 错误!元素的 key 应该在这里指定: <ListItem value={number} /> ); return ( <ul> {listItems} </ul> ); } const numbers = [1, 2, 3, 4, 5]; ReactDOM.render( <NumberList numbers={numbers} />, document.getElementById('root') );
- 例子:正确的使用 key 的方式
function ListItem(props) { // 正确!这里不需要指定 key: return <li>{props.value}</li>; } function NumberList(props) { const numbers = props.numbers; const listItems = numbers.map((number) => // 正确!key 应该在数组的上下文中被指定 <ListItem key={number.toString()} value={number} /> ); return ( <ul> {listItems} </ul> ); } const numbers = [1, 2, 3, 4, 5]; ReactDOM.render( <NumberList numbers={numbers} />, document.getElementById('root') );
总结:一般情况,一个好的经验法则是:需要循环输出的元素需要设置key属性,也就是说应该给循环中的元素添加key属性,比如在上面的例子中,我们应该给map方法中的元素设置key属性
五、key 只是在兄弟节点之间必须唯一
数组元素中使用的 key 在其兄弟节点之间应该是独一无二的。然而,它们不需要是全局唯一的。当我们生成两个不同的数组时,我们可以使用相同的 key 值
function Blog(props) { const sidebar = ( <ul> {props.posts.map((post) => <li key={post.id}> {post.title} </li> )} </ul> ); const content = props.posts.map((post) => <div key={post.id}> <h3>{post.title}</h3> <p>{post.content}</p> </div> ); return ( <div> {sidebar} <hr /> {content} </div> ); } const posts = [ {id: 1, title: 'Hello World', content: 'Welcome to learning React!'}, {id: 2, title: 'Installation', content: 'You can install React from npm.'} ]; ReactDOM.render( <Blog posts={posts} />, document.getElementById('root') );
key 会传递信息给 React ,但不会传递给我们的组件。如果我们想要在组件中使用 key 属性的值,那么需要用其他属性名(非key)显式传递这个值。比如在下面的代码中,我们可以通过props获取到Post组件的id和title属性,但是无法获取到key属性。
const content = posts.map((post) => <Post key={post.id}//无法通过props获取 id={post.id}//可以通过props获取 title={post.title} />//可以通过props获取 );
六、在JSX中使用map()
上面的例子中我们都是把map和JSX元素单独分开写的,实际上还可以在{}中使用map函数,因为JSX允许在大括号中嵌入任何表达式,如下代码:
function NumberList(props) { const numbers = props.numbers; return ( <ul> {numbers.map((number) => <ListItem key={number.toString()} value={number} /> )} </ul> ); }
web前端高级React - React从入门到进阶之JSX虚拟DOM渲染为真实DOM的原理和步骤
文章目录
一、分析与思考
前面我们已经学习了JSX的一些基本语法,通过前面的学习我们知道:在js文件中通过调用ReactDOM.render()方法,并传入一堆JSX代码(可以理解为虚拟DOM)。
当程序运行时这些JSX代码就会被转换为真实的DOM元素然后呈现在页面上;
那么这些JSX虚拟DOM是如何被转换为真实DOM的呢,原理又是怎样的呢?
当我们用react脚手架创建好一个项目后,会生成一个package.json文件,在这个文件的最下面有这样一段代码
"babel" :{ presets:["react-app"] }
了解babel的人应该都知道,babel主要是用来将ES6或更高版本的js代码转换为ES5的代码,从而提高兼容性。上面这段代码的配置同样也是如此,它可以将JSX虚拟DOM转换成React可识别的React DOM对象,如下图:
二、JSX虚拟DOM渲染为真实DOM的原理和步骤
接下来我们就来分析一下JSX虚拟DOM渲染为真实DOM的原理和步骤
- 基于babel-preset-react-app把JSX语法变为React.createElement的模式
- 只要遇到元素标签(或组件)都要调用createElement
- createElement的前两个参数是固定的:标签名(组件名)、属性,第三个及以后的参数是子元素
- 如果传递了属性,第二个参数是一个对象(包含了各属性的信息),没有传递属性则第二个参数为null
- 基于React.createElement方法执行创建出虚拟DOM对象(JSX对象)
- 首先创建一个对象
- type属性存储的是标签名或组件
- props属性:如果没有传递任何属性,也没有任何子元素,则为空对象;把传递的createElement的属性,都赋值给props;如果有子元素则新增一个children属性,可能是一个值也可能是一个数组
- 基于ReactDOM.render把创建的虚拟DOM对象渲染到页面指定的容器中
- ReactDOM.render([jsxObj],[container],[callback]),render接收三个参数:jsx对象,页面指定的容器和回调函数(可不传)
- callback渲染触发的回调函数,着这里可以获取到真实DOM
三、基于渲染原理重写createElement和render
React.createElement = function(type, props, ...children){ let jsxObj = { tyupe, props:{}, } //传递了属性,把传递的属性都放在jsxObj的props中 if(props !== null){ //基于es6实现 jsxObj.props = {...props}; //或者有es5的语法用for循环 } //如果传递了子元素,还需要给jsxObj的props设置children属性 if(children.length > 0){ jsxObj.props.children = children; //如果传递的子元素只有一项,则直接把第一项的值赋值给jsxObj.props.children即可 if(children.length === 1){ jsxObj.props.children = children[0]; } } return jsxObj; } ReactDOM.render = function render(jsxObj, container, callback){ let {type, props} = jsxObj; //创建DOM元素 if(typeof type === "string"){ //创建真实DOM元素对象 let element = document.createElement(type); //给创建的DOM设置属性 for(let key in props){ if(!props.hasOwnProperty(key)) break; //样式类和行内样式特殊处理 if(key === "className"){ element.setAttribute('class', props[key]); continue; } if(key === "style"){ let styObj = props[key]; for(let attr in styObj){ if(!styObj.hasOwnProperty(attr)) break; element.style[attr] = styObj[attr]; } continue; } //关于子元素处理 if(key === "children"){ let children = props[key]; if(!Array.isArray(children)){ children = [children]; } //循环子元素 children.forEach(item=>{ //如果是文本,则直接创建文本节点赋值给element,如果是新的虚拟DOM对象,则需要重复调用render方法,把新创建的DOM对象增加给element(递归) if(typeof item === "string"){ element.appendChild(document.createTextNode(item)); return; } render(item, element); }); } element.setAttribute(key, props[key]); } container.appendChild(element); callback && callback(); } }
web前端高级React - React从入门到进阶之元素渲染
文章目录
一、元素的渲染
- 元素是构成react应用的最小单元,它用于描述屏幕上输出的内容,也就是我们常说的虚拟DOM
- 与浏览器的DOM元素不同,React元素是创建开销极小的普通对象,然后通过ReactDOM更新真实DOM来与React元素保持一致
- 我们平时在浏览器中看到的内容都是由真实DOM元素构成的;React最终会把虚拟DOM元素转换为真实DOM并渲染
- 在第一章中我们讲项目的目录结构时提到public/index.html,在这个文件中有个id为“root”的div元素,我们将其称为“根”DOM节点,因为该节点内所有的内容都将有React DOM管理
- 我们在使用React构建应用时通常只会定义一个单一的根节点。但是如果我们想在一个已有的项目中引入React的话,那么我们可能需要在不同的部分单独定义React的根节点,因为在React的每个组件(后面章节会讲解组件)中只能有一个根节点存在
- 要想将React元素渲染到根DOM节点中,我们只需把它们传入ReactDOM的render方法即可,如下代码:
const element = <h1>Hello, world!</h1> ReactDOM.render( element, document.getElementById("root") );
执行上面的代码,页面上就会出现“Hello, world”
二、更新已渲染的元素
React 元素都是不可变的。当元素被创建之后,我们是无法改变其内容或属性的。
目前更新界面的唯一办法是创建一个新的元素,然后将它传入 ReactDOM.render() 方法替换原来的元素来重新渲染:
来看一下这个计时器的例子:
function tick() { const element = ( <div> <h1>Hello, world!</h1> <h2>现在是 {new Date().toLocaleTimeString()}.</h2> </div> ); ReactDOM.render( element, document.getElementById('root') ); } setInterval(tick, 1000);
- 上面代码中我们定义了一个tick方法,在方法中定义了React元素并调用ReactDOM的render方法将元素渲染到根DOM节点root中,并在定时器中每隔1秒调用一次函数从而重新渲染页面。
- 我们还可以将React元素和JavaScript代码进行分离,把要展示的React元素封装起来,然后通过属性传值(后面章节中讲解)的方式把值动态的传给React元素,从而达到react元素与js代码分离的效果,看如下示例:
function Clock(props) { return ( <div> <h1>Hello, world!</h1> <h2>现在是 {props.date.toLocaleTimeString()}.</h2> </div> ); } function tick() { ReactDOM.render( <Clock date={new Date()} />, document.getElementById('root') ); } setInterval(tick, 1000);
- 另外:除了函数外我们还可以创建一个 React.Component 的 ES6 类,该类封装了要展示的元素,需要注意的是在 render() 方法中,需要使用 this.props 替换 props,后面的章节中会详细讲解
class Clock extends React.Component { render() { return ( <div> <h1>Hello, world!</h1> <h2>现在是 {this.props.date.toLocaleTimeString()}.</h2> </div> ); } } function tick() { ReactDOM.render( <Clock date={new Date()} />, document.getElementById('root') ); } setInterval(tick, 1000);
三、元素的局部更新
React在更新已渲染的元素的时候,ReactDOM会将元素和它的子元素与它们之前的状态进行对比,并只会更新与之前状态不一样的元素,也就是说发生了变化的元素
我们可以通过浏览器的检查元素工具查看上一个例子来确认这一点。
尽管每一秒我们都会新建一个描述整个 UI 树的元素,React DOM 只会更新实际改变了的内容,也就是例子中的文本节点