写点什么

React 循环 DOM 时为什么需要添加 key

作者:beifeng1996
  • 2023-01-05
    浙江
  • 本文字数:2147 字

    阅读完需:约 7 分钟

一、React 渲染流程和更新流程

  • react 渲染流程:jsx -> 虚拟 dom -> 真实 dom

  • react 更新流程:props/state 改变 -> render 函数重新执行 -> 生成新的虚拟 dom 树 -> 新旧虚拟 dom 树进行 diff -> 计算出差异进行更新 ->更新到真实的 dom 树


所以在每次更新的时候,React 需要基于这两颗不同的树之间的差别来判断如何有效的更新 UI,如果一棵树参考另外一棵树进行完全比较更新,那么即使是最先进的算法,该算法的复杂程度为 O(n3),其中 n 是树中元素的数量,如果在 React 中使用了该算法,那么展示 1000 个元素所需要执行的计算量将在十亿的量级范围,这个开销太过昂贵了,React 的更新性能会变得非常低效;于是 React 对这个算法进行了优化,将其优化成了 O(n),这也就是传说中的 diff 算法,

二、diff 算法

diff 算法做了三处优化


  1. 同层节点之间相互比较,不会垮节点比较

  2. 不同类型的节点,产生不同的树结构

  3. 开发中,可以通过 key 来指定哪些节点在不同的渲染下保持稳定

2-1 对比不同类型的元素

  • 当节点为不同的元素,React 会拆卸原有的树,并且建立起新的树:当一个元素从<a>变成<img>,从<Article>变成<Comment>,或从<Button>变成<div>都会触发一个完整的重建流程

  • 当卸载一棵树时,对应的 DOM 节点也会被销毁,组件实例将执行 componentWillUnmount() 方法;

  • 当建立一棵新的树时,对应的 DOM 节点会被创建以及插入到 DOM 中,组件实例将执行 componentWillMount()方法,紧接着 componentDidMount() 方法

  • 比如下面的代码更改:React 会销毁 Comment 组件并且重新装载一个新的组件,而不会对 Counter 进行复用;


<div>    <Comment /></div>
<span> <Comment /></span>
复制代码

2-2 对比同一类型的元素

  • 当比对两个相同类型的 React 元素时,React 会保留 DOM 节点,仅比对及更新有改变的属性

  • 比如下面的代码更改:通过比对这两个元素,React 知道只需要修改 DOM 元素上的 className 属性


<div className="before" title="stu" /><div className="after" title="stu" />
复制代码


  • 比如下面的代码更改:当更新 style 属性时,React 仅更新有所更变的属性。通过比对这两个元素,React 知道只需要修改 DOM 元素上的 color 样式,无需修改 fontWeight。


<div style={{color="red",fontWeight:"bold"}} /><div style={{color="green",fontWeight:"bold"}} />
复制代码


  • 如果是同类型的组件元素:组件会保持不变,React 会更新该组件的 props,并且调用 componentWillReceiveProps() 和 componentWillUpdate() 方法,下一步调用 render() 方法,diff 算法将在之前的结果以及新的结果中进行递归;

2-3 对子节点递归

  • 在默认条件下,当递归 DOM 节点的子元素时,React 会同时遍历两个子元素的列表;当产生差异时,生成一个 mutation(改变)。

  • 如果在最后插入一条数据的情况:前面两个比较是完全相同的,所以不会产生 mutation,最后一个比较,产生一个 mutation,将其插入到新的 DOM 树中即可,但是如果是在前面插入一条数据,React 会对每一个子元素产生一个 mutation,而不是保持 <li>星际穿越</li><li>盗梦空间</li>的不变;这种低效的比较方式会带来一定的性能问题,所以就得使用 key 来优化


后面插一条数据<ul>     <li>星际穿越</li>     <li>盗梦空间</li> </ul> <ul>     <li>星际穿越</li>     <li>盗梦空间</li>     <li>大话西游</li> </ul>
前面插一条数据 <ul> <li>星际穿越</li> <li>盗梦空间</li> </ul> <ul> <li>大话西游</li> <li>星际穿越</li> <li>盗梦空间</li> </ul>
复制代码


参考 前端进阶面试题详细解答

三、key

要切记,在 diff 算法中,可以通过 key 来指定哪些节点在不同的渲染下保持稳定,并且要保证 key 是唯一的,不要使用随机数(随机数在下一次 render 时,会重新生成一个数字),也不能使用 index,这都对性能是没有优化的


import React, { Component } from "react";
export default class App extends Component { constructor(props) { super(props);
this.state = { movies: ["星际穿越", "盗梦空间"], }; }
render() { return ( <div> <h2>电影列表</h2> <ul> {this.state.movies.map((item, index) => { return <li key={item}>{item}</li>; })} </ul> <button onClick={(e) => this.insertMovie()}>添加电影</button> </div> ); }
insertMovie() { this.setState({ movies: ["大话西游", ...this.state.movies], }); }}
复制代码


代码解析:在默认条件下,当递归 DOM 节点的子元素时,React 会同时遍历两个子元素的列表;当产生差异时,生成一个 mutation。如果在 movies 后面添加数据,前面两个比较是完全相同的,所以不会产生 mutation;最后一个比较,产生一个 mutation,将其插入到新的 DOM 树中即可;如果在 movies 前面添加数据,React 会对每一个子元素产生一个 mutation,而不是保持 <li>星际穿越</li><li>盗梦空间</li>的不变,这种低效的比较方式会带来一定的性能问题,当子元素(这里的 li)拥有 key 时,React 使用 key 来匹配原有树上的子元素以及最新树上的子元素:在下面这种场景下,key 为"星际穿越"和"盗梦空间"的元素仅仅进行位移,不需要进行任何的修改; 将 key 为"大话西游"的元素插入到最前面的位置即可;


用户头像

beifeng1996

关注

还未添加个人签名 2022-09-01 加入

还未添加个人简介

评论

发布
暂无评论
React循环DOM时为什么需要添加key_React_beifeng1996_InfoQ写作社区