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 只会更新实际改变了的内容,也就是例子中的文本节点

 

posted @ 2022-04-28 11:45  PanPan003  阅读(207)  评论(0编辑  收藏  举报