网上有很多博客讲到,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. vscode 配置表

    { "git.ignoreMissingGitWarning": true, "editor.multiCursorModifier": "ctrlC ...

  2. springboot项目启动后tomcat服务器自动关闭 解决方法

    需要在pom.xml中添加 <dependency> <groupId>org.springframework.boot</groupId> <artifac ...

  3. 如何删除Image元素下面的空白行及为什么行内元素有底线

    翻译练习 原博客地址:Removing White Space Below Image Elements, or Why Inline Elements Have Descenders HTML中Im ...

  4. Aliyun Oss 上传文件

    目录 Aliyun OSS OSS 简介 OSS 基本概念 OSS 功能概述 OSS 使用 创建存储空间Bucket 创建子目录 Java编码 测试 Aliyun OSS OSS 简介 阿里云对象存储 ...

  5. 手把手教你SpringBoot2整合Redis

    此文仅为初学java的同学学习,大佬请勿喷,文末我会附上完整代码包供大家参考 redis的搭建教程此处略过,大家自行百度,本文的教程开始: 一.先在pom.xml中添加相关依赖 <!--redi ...

  6. redis基础:redis下载安装与配置,redis数据类型使用,redis常用指令,jedis使用,RDB和AOF持久化

    知识点梳理 课堂讲义 课程计划 1. REDIS 入 门 (了解) (操作)   2. 数据类型 (重点) (操作) (理解) 3. 常用指令   (操作)   4. Jedis (重点) (操作) ...

  7. FreeBSD 12.2 阿里云镜像使用说明

    目前直接从阿里云 12.1 升级 12.2 会导致错误.镜像非本人制作.FreeBSD 12.2 阿里云镜像使用说明镜像下载地址: http://t.cn/A6taB5jO修改内容:对 /usr/sr ...

  8. springboot整合mybatis。mapper.xml资源文件放置到resources文件夹下的配置&别名使用配置

  9. P2023 [AHOI2009]维护序列 题解(线段树)

    题目链接 P2023 [AHOI2009]维护序列 解题思路 线段树板子.不难,但是...有坑.坑有多深?一页\(WA\). 由于乘法可能乘\(k=0\),我这种做法可能会使结果产生负数.于是就有了这 ...

  10. 前端学习 node 快速入门 系列 —— 服务端渲染

    其他章节请看: 前端学习 node 快速入门 系列 服务端渲染 在简易版 Apache一文中,我们用 node 做了一个简单的服务器,能提供静态资源访问的能力. 对于真正的网站,页面中的数据应该来自服 ...