网上有很多博客讲到,React、Vue里的key,与 Virtual DOM 及 DOM diff 有关, 可以用来唯一标识DOM节点,提高diff效率,云云。

这大致是对的,但是,大多讲得语焉不详,像是在背答案。

具体怎么个提效法?为什么说用数组下标当作key是“反模式”?讲了一堆,能不能来个眼见为实,show me the code?

本文以React为例,尝试稍微刨一刨,但又不刨到太底层,以足够帮助理解为度。

1. VNode diff

首先介绍 Virtual DOM 结点(后续简称Virtual Node, VNode)是如何创建出来的。

现实中的React项目几乎都会用到JSX,而JSX不能直接执行,需要先经babel编译成js代码,比如:

<div className="content">Hello world!</div>

会被编译成

React.createElement("div", {
className: "content"
}, "Hello world!");

点击这里查看在线编译

所以,只要调用 React.createElement 这个静态方法,就可以创建出一个VNode。

无需深入VNode 的具体数据结构,只要看看这个工厂方法的参数,就可以知道 DOM diff 到底 diff 了哪些内容。

根据React官方文档,该方法可以接收≥3个参数:

  • 第一个参数是type,指定结点类型,如果是HTML原生结点,那么会是一个字符串,比如"div";如果是React组件,那么就会是一个class或function;
  • 第二个参数是props,是一个对象或者null。比如前面的例子中,div标签上的"className"属性就被加到这里来了;
  • 第三(及第四,第五,……)个参数是childNode,该结点的子节点。前面的例子中,div的子节点是一个内容为"Hello world!"的TextNode

是滴,DOM diff 具体diff 的东西,就是这几个参数。为什么不会有别的?因为那样不符合React的设计理念:Data => UI 单向映射。

2. 动态列表的diff困局

我们知道React在调用setState触发render时,会对新旧 Virtual DOM 做比较,力争以最小的代价完成新DOM渲染任务。

结合上面提到的几个参数,具体比较过程大致是这样的:

  • 首先比较type。如果type不同,那没什么好说的,直接销毁重新create一个;如果type相同,再往后看:
  • 其次比较props,如果有变化,那就把变化的部分update;如果没变化,那就再往后看:
  • 最后比较子节点,同样地,有变化就update,没变化就啥都不做

这在DOM结构固定的一般情况下是很好用的,但当我们希望从一个list映射出列表、而且这个list里的项随时可能变化时,就有点麻烦了。

比如说,原本list是这样的:

[
{name: 'Smith', job: 'Engineer'},
{name: 'Alice', job: 'HR'},
{name: 'Jenny', job: 'Designer'}
]

然后,Jenny被移到了最前面,那么Smith和Alice就相应后移了,变成了

[
{name: 'Jenny', job: 'Designer'},
{name: 'Smith', job: 'Engineer'},
{name: 'Alice', job: 'HR'}
]

对于React来说,如果它不知道这三个结点“本来”是谁,只是按照位置对应关系逐个去检查,会发现每个结点都变了:

  • Smith => Jenny
  • Alice => Smith
  • Jenny => Alice

于是React得出结论:列表中的所有结点,全都需要update,重新渲染!

且慢!有没有更好的方法?

3. 借助key破局

如果,React“知道”这三个结点“本来”是谁,那么事情就会简单很多:

不需要更新任何DOM结点,只需把Jenny对应的结点摘下来,再插入到新的位置,完事。

但React怎么会知道谁是谁呢?

这需要我们开发者手动告诉它,于是key出场了。

在做DOM diff 时,如果同一个父组件下的两个VNode拥有同样的key,就会被视为同一个结点,如果React据此判断出,这个结点在列表中的排位发生了变化,就会像上面说的那样,进行“摘下-插入”处理。

为了证明这一点,亮代码!

首先上一个故意整出bug的版本:

class App extends React.Component {
state = {
list: [0, 1, 2]
} add() {
const list = this.state.list;
this.setState({ list: [list.length, ...list] });
} render() {
return (
<div className="App">
<button onClick={() => this.add()}>Input sth below, then click me</button>
<ul>
{
// 注意:这里故意用index作为key,引发bug
this.state.list.map((item, index) => (
<li key={index}>
<span>Item-{item}</span>
<input type="text" />
</li>
)
)
}
</ul>
</div>
);
}
}

ReactDOM.render(
<App />,
document.getElementById('root')
);

可以用 create-react-app起个项目,在本地试试这段代码。演示效果如下,先在第二行文本框里输入一些1:

然后,点击上面的按钮,会发现……

输入了一串1的文本框没有跟着Item-1走,而是留在了“原位”!

这就是用数组下标作key引发的典型bug。原因就在于新列表里Item-0和原列表里的Item-1拥有同样的key,被React视为同一个结点,所以只是“就地”更新了子节点(文本),并没有挪动结点的位置。

而这个bug的巧妙之处就在于使用了<input>,它可以在VNode的type、props、children均无变化的前提下,被用户行为改变其样式(输入的内容),从而让我们直观地看到结点所处位置。感谢React官方提供了这个巧妙的case

好,下面我们来修复这个bug。

修复方法很简单:把 key={index} 改成 key={item} 就行了。

保存,刷新重试,我们就可以得到:

这下,对应关系正确了,React正确地识别出了3个旧结点,直接把新结点插入到列表开头,而旧结点没有变化。

全文结束。看到这里,你应该明白key到底有什么用,以及为什么index不宜做key了吧!

React/Vue里的key到底有什么用?看完这篇你就知道了!(附demo代码)的更多相关文章

  1. APP的缓存文件到底应该存在哪?看完这篇文章你应该就自己清楚了

    APP的缓存文件到底应该存在哪?看完这篇文章你应该就自己清楚了 彻底理解android中的内部存储与外部存储 存储在内部还是外部 所有的Android设备均有两个文件存储区域:"intern ...

  2. Vue中的key到底有什么用?

    key是为Vue中的vnode标记的唯一id,通过这个key,我们的diff操作可以更准确.更快速 diff算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的key与旧节点进行 ...

  3. vue3 到底哪里好?看这一篇就够了

    之前写的关于 vue3 的文章,好多人吐槽:这些API每次使用都要引入一遍,感觉有点麻烦. 今天我们就来看看 vue3 相比 vue2 的优点有些啥? 为啥有些人说:自从写了 ts vue3 再也回不 ...

  4. 【转】APP的缓存文件到底应该存在哪?看完这篇文章你应该就自己清楚了

    只要是需要进行联网获取数据的APP,那么不管是版本更新,还是图片缓存,都会在本地产生缓存文件.那么,这些缓存文件到底放在什地方合适呢?系统有没有给我们提供建议的缓存位置呢?不同的缓存位置有什么不同呢? ...

  5. 【python】迭代器与生成器到底是什么?看完你就知道

    迭代器跟生成器,与上篇文章讲的装饰器一样,都是属于我的一个老大难问题. 通常就是遇到的时候就去搜一下,结果在一大坨各种介绍博客中看了看,回头又忘记了. 你是不是也是这样呢? 俗话说:好记性不如烂笔头, ...

  6. vue :class 可以接收 字符串 数组 和 对象 对象里面的key值 根据true或false 显示不显示

    vue :class 可以接收 字符串 数组 和 对象 对象里面的key值 根据true或false 显示不显示 https://cn.vuejs.org/v2/guide/class-and-sty ...

  7. vue中:key 和react 中key={} 的作用,以及ref的特性?

    vue中:key 和react 中key={} 为了给 vue 或者react 一个提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,你需要为每项提供一个唯一 key 属性 一句话概括就是 ...

  8. 富文本编辑器TinyMCE的使用(React Vue)

    富文本编辑器TinyMCE的使用(React Vue) 一,需求与介绍 1.1,需求 编辑新闻等富有个性化的文本 1.2,介绍 TinyMCE是一款易用.且功能强大的所见即所得的富文本编辑器. Tin ...

  9. VUE温习:内存泄漏、Vue.$set、key作用与虚拟diff算法

    一.内存泄漏 1.指令绑定了事件,却没有解绑事件,容易产生内存泄漏.(曾经遇到过的案例) 2.v-if指令产生内存泄漏,比如v-if删除了父级元素,却没有删除父级元素里的dom片段 3.跳转到别的路由 ...

随机推荐

  1. Jmeter beanshell编程实例

    1.引言 BeanShell是一种小型的,免费的,可嵌入的符合Java语法规范的源代码解释器,具有对象脚本语言特性. 在Jmeter实践中,由于BeanShell组件较高的自由度,通常被用来处理较为复 ...

  2. python进阶(2)python最常用的模块

    math math.ceil(a): 用来返回≥a的最小整数 math.floor(a):用来返回≤a的最大整数 round(a [,b]) 如果没有参数b,只有a,round()作用是四舍五入 如果 ...

  3. OSS对象储存

    简介 阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量.安全.低成本.高可靠的云存储服务. 使用流程 名词解释  Endpoint(访问域名) Ac ...

  4. 后端程序员之路 3、fastcgi、fastcgi++

    CGI与FastCGI - wanghetao - 博客园http://www.cnblogs.com/wanghetao/p/3934350.html eddic/fastcgipp: A C++ ...

  5. 更改EFI分区位置

    我是win10 + arch 双系统,并且efi分区用的是win10自动创建的(大小100m),所以这些空间很快就不够用了(内核和initramfs都放在了ESP分区当中) 我原本是直接把win的ef ...

  6. jQuery实现游戏推荐

    1.需求:点击添加游戏按钮实现添加游戏,点击删除按钮,删除游戏. 2.实现思路:分别给添加按钮和删除按钮添加click事件. 3.遇到的问题:自己添加的游戏不能进行删除. 4.原因分析:文档加载完毕后 ...

  7. 关于 C++ 中的强制转换 - 基础篇

    引言 假设有基类 A,包含了虚函数 func1,以及有派生类 B,继承于类 A,派生类 B 中实现了函数 func1.此时可以用 A 类型的指针指向 B 类型的对象,并用 A 类型的指针调用 B 类型 ...

  8. js--this指向的相关问题

    前言 关于this的指向问题是前端面试中常考的知识点,也是我们开发学习中较难理解的问题.作为JavaScript的基础,需要我们彻底理解这一关键词.this作为JavaScript中非常复复杂的机制, ...

  9. 靶场练习-Sqli-labs通关记录(1-4关)

                              0x00 实验环境 本地:Win 10 靶场:sqli-labs(共65关,每日一关) 0x02 通关记录 简介:一天一关! (1)第一关: 简单的 ...

  10. Linux下基础命令

      (1)ls(查看列表) (2)ls  -l(查看列出文件详细信息) (3)ls  -al (查看全部列出文件详细信息) (4)ls  -dl(查看目录信息) (5)pwd(查看当前工作的目录)   ...