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

  • 相关阅读:
    每日日报2020.12.1
    每日日报2020.11.30
    981. Time Based Key-Value Store
    1146. Snapshot Array
    565. Array Nesting
    79. Word Search
    43. Multiply Strings
    Largest value of the expression
    1014. Best Sightseeing Pair
    562. Longest Line of Consecutive One in Matrix
  • 原文地址:https://www.cnblogs.com/panpanwelcome/p/16202247.html
Copyright © 2020-2023  润新知