Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

第 1 题:写 React / Vue 项目时为什么要在列表组件中写 key,其作用是什么? #1

Open
fingerpan opened this issue Jan 21, 2019 · 64 comments

Comments

@fingerpan
Copy link

fingerpan commented Jan 21, 2019

更新 --------------------------

受楼下答案的一些特殊情况影响,导致很多人都认为key不能"提高"diff速度。在此继续重新梳理一下答案。

在楼下的答案中,部分讨论都是基于没有key的情况diff速度会更快。确实,这种观点并没有错。没有绑定key的情况下,并且在遍历模板简单的情况下,会导致虚拟新旧节点对比更快,节点也会复用。而这种复用是就地复用,一种鸭子辩型的复用。以下为简单的例子:

<div id="app">
    <div v-for="i in dataList">{{ i }}</div>
</div>
var vm = new Vue({
  el: '#app',
  data: {
    dataList: [1, 2, 3, 4, 5]
  }
})

以上的例子,v-for的内容会生成以下的dom节点数组,我们给每一个节点标记一个身份id:

  [
    '<div>1</div>', // id: A
    '<div>2</div>', // id:  B
    '<div>3</div>', // id:  C
    '<div>4</div>', // id:  D
    '<div>5</div>'  // id:  E
  ]
  1. 改变dataList数据,进行数据位置替换,对比改变后的数据
 vm.dataList = [4, 1, 3, 5, 2] // 数据位置替换

 // 没有key的情况, 节点位置不变,但是节点innerText内容更新了
  [
    '<div>4</div>', // id: A
    '<div>1</div>', // id:  B
    '<div>3</div>', // id:  C
    '<div>5</div>', // id:  D
    '<div>2</div>'  // id:  E
  ]

  // 有key的情况,dom节点位置进行了交换,但是内容没有更新
  // <div v-for="i in dataList" :key='i'>{{ i }}</div>
  [
    '<div>4</div>', // id: D
    '<div>1</div>', // id:  A
    '<div>3</div>', // id:  C
    '<div>5</div>', // id:  E
    '<div>2</div>'  // id:  B
  ]

增删dataList列表项

  vm.dataList = [3, 4, 5, 6, 7] // 数据进行增删

  // 1. 没有key的情况, 节点位置不变,内容也更新了
  [
    '<div>3</div>', // id: A
    '<div>4</div>', // id:  B
    '<div>5</div>', // id:  C
    '<div>6</div>', // id:  D
    '<div>7</div>'  // id:  E
  ]

  // 2. 有key的情况, 节点删除了 A, B 节点,新增了 F, G 节点
  // <div v-for="i in dataList" :key='i'>{{ i }}</div>
  [
    '<div>3</div>', // id: C
    '<div>4</div>', // id:  D
    '<div>5</div>', // id:  E
    '<div>6</div>', // id:  F
    '<div>7</div>'  // id:  G
  ]

从以上来看,不带有key,并且使用简单的模板,基于这个前提下,可以更有效的复用节点,diff速度来看也是不带key更加快速的,因为带key在增删节点上有耗时。这就是vue文档所说的默认模式。但是这个并不是key作用,而是没有key的情况下可以对节点就地复用,提高性能。

这种模式会带来一些隐藏的副作用,比如可能不会产生过渡效果,或者在某些节点有绑定数据(表单)状态,会出现状态错位。VUE文档也说明了 这个默认的模式是高效的,但是只适用于不依赖子组件状态或临时 DOM 状态 (例如:表单输入值) 的列表渲染输出

楼下 @yeild 也提到,在不带key的情况下,对于简单列表页渲染来说diff节点更快是没有错误的。但是这并不是key的作用呀。

但是key的作用是什么?

我重新梳理了一下文字,可能这样子会更好理解一些。

key是给每一个vnode的唯一id,可以依靠key,更准确, 更的拿到oldVnode中对应的vnode节点。

1. 更准确

因为带key就不是就地复用了,在sameNode函数 a.key === b.key对比中可以避免就地复用的情况。所以会更加准确。

2. 更快

利用key的唯一性生成map对象来获取对应节点,比遍历方式更快。(这个观点,就是我最初的那个观点。从这个角度看,map会比遍历更快。)

原答案 -----------------------

vue和react都是采用diff算法来对比新旧虚拟节点,从而更新节点。在vue的diff函数中(建议先了解一下diff算法过程)。
在交叉对比中,当新节点跟旧节点头尾交叉对比没有结果时,会根据新节点的key去对比旧节点数组中的key,从而找到相应旧节点(这里对应的是一个key => index 的map映射)。如果没找到就认为是一个新增节点。而如果没有key,那么就会采用遍历查找的方式去找到对应的旧节点。一种一个map映射,另一种是遍历查找。相比而言。map映射的速度更快。
vue部分源码如下:

// vue项目  src/core/vdom/patch.js  -488行
// 以下是为了阅读性进行格式化后的代码

// oldCh 是一个旧虚拟节点数组
if (isUndef(oldKeyToIdx)) {
  oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
}
if(isDef(newStartVnode.key)) {
  // map 方式获取
  idxInOld = oldKeyToIdx[newStartVnode.key]
} else {
  // 遍历方式获取
  idxInOld = findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
}

创建map函数

function createKeyToOldIdx (children, beginIdx, endIdx) {
  let i, key
  const map = {}
  for (i = beginIdx; i <= endIdx; ++i) {
    key = children[i].key
    if (isDef(key)) map[key] = i
  }
  return map
}

遍历寻找

// sameVnode 是对比新旧节点是否相同的函数
 function findIdxInOld (node, oldCh, start, end) {
    for (let i = start; i < end; i++) {
      const c = oldCh[i]
      
      if (isDef(c) && sameVnode(node, c)) return i
    }
  }
@yygmind yygmind changed the title key的作用是为了在diff算法执行时更快的找到对应的节点,提高diff速度。 第一题:key的作用是为了在diff算法执行时更快的找到对应的节点,提高diff速度。 Feb 12, 2019
@yeild
Copy link

yeild commented Feb 20, 2019

就我的使用来说(Vue)key的作用是为了在数据变化时强制更新组件,以避免“原地复用”带来的副作用。另外,某些情况下不带key可能性能更好,见:issuecomment
官方文档对于key的描述: 列表渲染-key | API-key

@Tarhyru
Copy link

Tarhyru commented Feb 20, 2019

@yeild
路过,越俎代庖一下,不一定对...
“建议尽可能在使用 v-for 时提供 key,除非遍历输出的 DOM 内容非常简单,或者是刻意依赖默认行为以获取性能上的提升。”
你是说这个么..它的本意是你应该单独指定一个Key而不是使用index来作为key...就像你说的这里还有一个复用问题..
但,如果说不带key性能会更好,为啥还要建议带key呢?
这个性能提升的前提有一句“刻意依赖默认行为”..也就是说不是说你不带key就会有性能提升...
而实际上,只是单纯的说查找这一过程...你去试试 在arr中使用indexOf 和使用obj[key] 的性能区别..就能感受到性能差异了..

@sunopar
Copy link

sunopar commented Feb 20, 2019

主要是为了提升diff【同级比较】的效率。自己想一下自己要实现前后列表的diff,如果对列表的每一项增加一个key,即唯一索引,那就可以很清楚的知道两个列表谁少了谁没变。而如果不加key的话,就只能一个个对比了。

@LanjianNUll
Copy link

就react而言,key是对于列表组件而言,并且无key或者key不唯一会报错提示

@yeild
Copy link

yeild commented Feb 21, 2019

@Tarhyru key能提高diff效率其实是不准确的。

vue/patch.js,在不带key的情况下,判断sameVnode时因为a.key和b.key都是undefined,对于列表渲染来说已经可以判断为相同节点然后调用patchVnode了,实际根本不会进入到答主给的else代码,也就无从谈起“带key比不带key时diff算法更高效”了。

然后,官网推荐推荐的使用key,应该理解为“使用唯一id作为key”。因为index作为key,和不带key的效果是一样的。index作为key时,每个列表项的index在变更前后也是一样的,都是直接判断为sameVnode然后复用。

说到底,key的作用就是更新组件时判断两个节点是否相同。相同就复用,不相同就删除旧的创建新的。

正是因为带唯一key时每次更新都不能找到可复用的节点,不但要销毁和创建vnode,在DOM里添加移除节点对性能的影响更大。所以会才说“不带key可能性能更好”。看下面这个实验,渲染10w列表项,带唯一key与不带key的时间对比:

不使用key的情况:

<li v-for="item in list">{{ item.text }}</li>

image

使用id作为key的情况:

<li v-for="item in list" :key="item.id">{{ n.text }}</li>

image

list构造:

  const list1 = []
  const list2 = []
  for (let i = 0; i <= 100000; i++) {
    list1.push({
      id: i,
      text: i
    })
    list2.push({
      id: i * 2,
      name: 100000 - i
    })
  }

因为不带key时节点能够复用,省去了销毁/创建组件的开销,同时只需要修改DOM文本内容而不是移除/添加节点,这就是文档中所说的“刻意依赖默认行为以获取性能上的提升”。

既然如此,为什么还要建议带key呢?因为这种模式只适用于渲染简单的无状态组件。对于大多数场景来说,列表组件都有自己的状态。

举个例子:一个新闻列表,可点击列表项来将其标记为"已访问",可通过tab切换“娱乐新闻”或是“社会新闻”。

不带key属性的情况下,在“娱乐新闻”下选中第二项然后切换到“社会新闻”,"社会新闻"里的第二项也会是被选中的状态,因为这里复用了组件,保留了之前的状态。要解决这个问题,可以为列表项带上新闻id作为唯一key,那么每次渲染列表时都会完全替换所有组件,使其拥有正确状态。

这只是个简单的例子,实际应用会更复杂。带上唯一key虽然会增加开销,但是对于用户来说基本感受不到差距,而且能保证组件状态正确,这应该就是为什么推荐使用唯一id作为key的原因。至于具体怎么使用,就要根据实际情况来选择了。

以上个人见解,如有误望指正。

@azl397985856
Copy link

azl397985856 commented Feb 21, 2019

就我的使用来说(Vue)key的作用是为了在数据变化时强制更新组件,以避免“原地复用”带来的副作用,官方文档也说明了不带key性能更好,见:List Rendering - key

我的理解是,vue和react虽然都采用了diff算法。 但是react本身的设计和vue的设计是截然不同的, vue采用了更加细粒度的更新组件的方式,即给每一个属性绑定监听, 而react是采用自顶而下的更新策略,每次小的改动都会生成一个全新的vdom。从而进行diff,如果不写key,可能就会发生本来应该更新却没有更新的bug。
这个bug其实和diff算法有关,react团队完全可以写一个没有这个“bug”版本的代码, 但是这是一种权衡,一种性能和方便使用的权衡。 写不写key能够提高性能的根本在于一方面diff算法会优先判断key是否相同,如果相同则不进行后面的运算。 如果key相同,就更好了,根本不需要重新创建节点

总结, 更确切的说应该是diff算法在你的复杂的列表稳定的时候能够明显提高性能,因为节点可以重用。
但是对于列表频繁更新的场景, 节点不能重用,但是diff 可以省略一部分逻辑,因此性能也会更好。
但是两者的性能优化不在同一个纬度,一个是 创建和更新节点(我称之为渲染器)的优化,
一个是DOM diff 算法(我称之为核心引擎)的优化

@Tarhyru
Copy link

Tarhyru commented Feb 21, 2019

@yeild
你这里说的是创建的开销
而这道题讨论的是diff时的速度,我的理解是在说查找某一节点进行修改时的耗时..
我并不知道vue如果不指定key时对这个查找是否会有别的优化,但从这个解答来说是没有的.
所以其实我们说的性能不是一个东西
不过,于我来说通过你的回复也理清了一些创建过程的概念,总之是有收获的

@yeild
Copy link

yeild commented Feb 21, 2019

@Tarhyru 不是的,我是指如果不带key,则a.key 和 b.key 都是undefined,就直接进入两个节点相同的逻辑,到这里diff已经结束了,根本不会运算到后边'利用对象取值而不是遍历数组'找相同节点的那一步。 从这个角度来说,并没有体现“有key比无key diff算法效率更高这一点”。
也不是说楼主的回答是错误的,但仅在于在前面的逻辑中都没有找到相同的节点,才会优先通过keyMap查找,次而通过遍历查找,从这里优化到diff速度。
另外,这道题并非讨论diff速度,而是说key的作用。所以我给出的结论是:key的作用就是更新组件时判断两个节点是否相同。相同就复用,不相同就删除旧的创建新的。

@azl397985856 你的补充很好,我提到的“不带key性能更好”,其实是因为两个key都是undefined,自然就相同然后复用组件了,原文有歧义,已做修改。

@Tarhyru
Copy link

Tarhyru commented Feb 21, 2019

@yeild
了解你的意思,这么说的确楼主的说法有歧义,而我在思考上也先入为主了...
但是,总觉得有齿轮没有咬合在一起..
“key的作用就是更新组件时判断两个节点是否相同。相同就复用,不相同就删除旧的创建新的。”
这是在说创建的情况吧?新增的时候如果没有key,就会复用默认的而不重新创建
那么修改呢?需要先通过key来找到需要修改的节点,而如果没有key则会遍历所有节点..这个过程应该也算是diff的过程吧?
那么严格来说:key对于修改节点时执行diff时会对性能有所提升,而对于新增这种情况时整体性能是下降的
这么描述是不是准确一些?


2019-03-01
补充一篇文章,感兴趣的去翻翻,也许认识会全一些..
vue diff算法解析

@yeild
Copy link

yeild commented Feb 21, 2019

@Tarhyru
关于遍历节点,实际上不论有没有key,都会遍历节点去找是否有sameVnode,而不是能够像对象取值一样拿到哪个节点需要修改的。你可以看下这部分代码,首先是while循环判断节点是否相同,只有没找到相同节点时,才会进最后的else,也就是楼主给出的代码,在这里key属性才能优化到查找速度

@Tarhyru
Copy link

Tarhyru commented Feb 21, 2019

@yeild
学习到了,看代码的确,key只在查找复用节点的时候起到了查找作用...
那么,的确楼主的说法不完全错....但放在这道题里是不合适的
至少对于diff过程来说这个key是起不到提速效果的

@oychao
Copy link

oychao commented Feb 24, 2019

Diff算法只在有重新排序(包括中间插入和删除节点)的情况下才可能有优化的作用,因为这样会有已有节点的移动,删除,以及新节点的插入等操作,这种情况下最好是复用以前已经生成的节点。
楼上不加Key的时候速度还更快的原因是一种简单的特例,不加key的列表对比一定无法使两个表中的元素意义对应,这种情况下的更新无法保证重新排序后结果的正确性。
另外React的diff算法比Vue的diff算法更复杂一些。

@Tarhyru
Copy link

Tarhyru commented Mar 1, 2019

@yeild
刚发现楼主回复了..
然后按它说的去查了下...发现我们两个前面的讨论也有一些误导性内容....
虽然过程可能的确是有没有key都是遍历...但是key似乎的确在diff中能起到提速作用...
具体看看这个?vue diff算法解析

@deepkolos
Copy link

就自己的使用经验而言, 如果不带key, 那么vue会尽量复用dom节点, 不过这样如果列表做了进入动画, 离开动画, 就不会触发了, 因为是直接修改了变化了属性, key的应用一般都是动画相关, 那一块节点重新生成, 这样动画就可以生效了.

给路由配上key也是为了做页面转场动画或者开启一个新的页面(组件)生命周期

@fangjiale
Copy link

fangjiale commented Apr 9, 2019

"原地复用"不产生副作用的情况下,不用key效率最快

“原地复用”产生副作用,需要用key,且用key通过map查找比遍历查找效率更快

用key的主要作用是不产生副作用,跟不用key去比效率就没意义了,不是一个层面上的事,效率较快是和遍历查找相比而言

@yygmind yygmind changed the title 第一题:key的作用是为了在diff算法执行时更快的找到对应的节点,提高diff速度。 第 1 题:写 React / Vue 项目时为什么要在列表组件中写 key,其作用是什么? Apr 26, 2019
@songmengda
Copy link

长知识了

@luohong123
Copy link

一、Vue中的key

为了给 Vue 一个提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,你需要为每项提供一个唯一 key 属性。理想的 key 值是每项都有唯一 id

<div v-for="item in items" v-bind:key="item.id">
  <!-- 内容 -->
</div>

建议尽可能在使用 v-for 时提供 key attribute,除非遍历输出的 DOM 内容非常简单,或者是刻意依赖默认行为以获取性能上的提升。
因为它是 Vue 识别节点的一个通用机制,key 并不与 v-for 特别关联,key 还具有其他用途

key 的特殊属性主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试修复/再利用相同类型元素的算法。使用 key,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。

有相同父元素的子元素必须有独特的 key。重复的 key 会造成渲染错误。

参考文章

二、React 中的key

key帮助React识别哪些项目已更改,已添加或已删除。应该为数组内部的元素赋予键,以使元素具有稳定的标识:
key必须在唯一的

const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
  <li key={number.toString()}>
    {number}
  </li>
);

参考文章

@Younguser
Copy link

文中提到“v-for的内容会生成以下的dom节点数组,我们给每一个节点标记一个身份id”

楼主,请问如何为每一个节点标记一个身份id?

@dorese
Copy link

dorese commented Jul 9, 2019

谢谢大佬分享

@AlexZhong22c
Copy link

为什么是for (i = beginIdx; i <= endIdx; ++i) {呢?除了传说中性能有提升外,和i++还有没有其他的区别?

@libin1991
Copy link

libin1991 commented Jul 10, 2019

3 (1)

vue和react虽然都采用了diff算法。 但是diff设计是截然不同的, vue采用依赖收集追踪,可以更加细粒度的更新组件,即给模板使用到的每一个属性绑定监听, 而react是采用自顶而下的更新策略,每次小的改动都会生成一个全新的vdom。

不管是什么diff算法,核心都是一样的,key的作用主要是为了高效的更新虚拟DOM列表,key 值是用来判断 VDOM 元素项的唯一依据 。 使用key不保证100%比不使用快,这就和Vdom不保证比操作原生DOM快是一样的,这只是一种权衡,其实对于用index作为key是不推荐的,除非你能够保证他们不会发生变化。这个key要体现唯一,通常推荐使用server给的SQL-ID。通常接口返回的又没有SQL-ID,怎么办呢,又不能用随机数,只能用index代替喽!

推荐使用shortid生成唯一key的数组,和数据数组一起使用,省去提交数据时再重组数组。

案例:

import React from 'react';
import shortid from 'shortid';

class Demo extends React.Component {
    constructor(props) {
    super(props);
    this.state = {
      data: ['a', 'b', 'c']
    }
    this.dataKeys = this.state.data.map(v => shortid.generate());
  }
  
    deleteOne = index => { // 删除操作
        const { data } = this.state;
        this.setState({ data: data.filter((v, i) => i !== index) });
        this.dataKyes.splice(index, 1);
    }
    
    render() {
      return (
          <ul>
               {
                   data.map((v, i) => 
                    <li 
                        onClick={i => this.deleteOne(i)}  
                        key={this.dataKeys[i]}
                    >
                        {v}
                    </li>
                    )
               } 
            </ul>
      )
  }
}

另外需要指明的是:

  • key不是用来提升react的性能的,不过用好key对性能是有帮组的。
  • 不能使用random来使用key
  • key相同,若组件属性有所变化,则react只更新组件对应的属性;没有变化则不更新。
    -key值不同,则react先销毁该组件(有状态组件的componentWillUnmount会执行),然后重新创建该组件(有状态组件的constructor和componentWillUnmount都会执行)

React小技巧汇总

@miaomiaogege
Copy link

加 key 是为了特异性识别 如果不加key 就相当于 你的元素里面没有加id 编号 做增删改查 只会默认按照排序索引进行增删改查 然后 就会导致业务逻辑 跟你的试图操作有出入 你会说 咦我想编辑第二个 为何第一个没了

@yygmind yygmind added the Vue label Dec 16, 2019
@yygmind yygmind added the React label Jan 2, 2020
@TrrantChen
Copy link

TrrantChen commented Jan 13, 2020

其实是要分场景的,以vue的diff算法为例,
在常见的这种例子中

before

<div>1</div>
<div>2</div>          
<div>3</div>

after

<div>2</div>
<div>3</div>          
<div>4</div>

不加key的情况下并不会对dom节点进行移动、增删操作,只会更新dom节点的信息,所以性能比较好。

而加key的情况下假设如下所示
before

<div>1</div><!--key 设置为1-->
<div>2</div><!--key 设置为2-->          
<div>3</div><!--key 设置为3-->

after

<div>2</div><!--key 设置为2-->
<div>3</div><!--key 设置为3-->         
<div>4</div><!--key 设置为4-->

加了key的情况下,diff算法为了复用dom节点,会多次进行dom的移动和插入操作。
这种情况下,性能是很比前一种情况不如的。

然后可以换一种场景

before

<h1>1</h1>
<div>2</div>          
<h2>3</h2>

after

<span>4</span>
<div>2</div>          
<label>6</label>

不加key的情况下,会创建after中的dom元素,再插到<h1>1</h1>前面,然后把before中的元素全都删光,也就是说<div></div>并不会被复用。

而如果加了key

before

<h1>1</h1><!--key 设置为1-->
<div>2</div><!--key 设置为2-->          
<h2>3</h2><!--key 设置为3-->

after

<span>4</span><!--key 设置为4-->
<div>2</div><!--key 设置为2-->          
<label>6</label><!--key 设置为6-->

那么相比不加key的情况,<div>2</div>会被进行移动,而不是先创建,再删除,所以这种情况下,加了key会比不加key要好,dom操作会相对少点。
至于其他两个元素,加不加key都一样,即使前后设置的key值一样也没用,都会先创建新的dom节点,然后再把老的dom节点删除。

@gaoxinxiao
Copy link

不是列表的key是哪里来的

@linshuzhen
Copy link

以下是我的理解,如果出现错误,请大神指正!
1、vue官方文档中说的“刻意依赖默认行为以获取性能上的提升”,是因为在不带key的情况下,节点能够复用,省去了销毁/创建组件的开销,同时只需要修改DOM文本内容而不是移除/添加节点。但是这种只能用在渲染比较简单的无状态组件,否则会产生副作用。
2、而对于大多数场景来说,列表组件都有自己的状态。所以为了在数据变化时强制更新组件,以避免‘原地复用’带来的副作用。
3、提升diff算法【同级比较】的效率,当有唯一key存在时,在第一步判断sameVnode函数中加快判断节点是否相同,不同则替换节点,而减少通过patchVnode函数去挨个判断,以及递归判断子节点。diff算法中不足点是当父节点不同时,就直接替换整个父节点,而如果此时子节点是相同的,也将被替换,无法复用

@xutao-haohaoxuexiba
Copy link

看了N个关于vue中的key的用法说明,没有一个通俗易懂,一看就明白的例子,在这总结一下,diff算法的作用为了标记节点(组件),当节点内容或者组件内容有所改变时,它能够快速地找到对应的节点进行复用,eg:排队时,每个人都有自己的名字,一开始是乱的,老师想让俩个人调换位置,直接把俩个人调换一下就行,而不是因为俩个人调换位置把所有人都移动一下

@18sby
Copy link

18sby commented Jun 22, 2020

简单来说就是在大部分情况下增强复用性,在 diff 两个新老 list 的时候,vue 会采用
1.两个开头指针不断向后遍历,如果一直相同,那么可以复用。
2.两个尾部指针向前移动,相同即可复用。
3.老的 list 的头指针和新的 list 的尾指针对比,相同即可复用。
4.老的 list 的尾指针和新的 list 的头指针对比,相同即可复用。
5.这里开始用到 key 了,如果上面的四种同级比较策略没有找到可以复用的 dom 元素,通过唯一的 key 再去老的 list 中找一下,看看是否能找到可以复用的 dom,(这里源码使用 hash 存储老的 list 中的 key => 老的 list 的索引的映射,在寻找的过程中是 O(1) 的时间复杂度),这里如果利用 key 也没有找到可以复用的 dom ,那么只能重新创建了。
总结一下:通过 key 去查找是否可以复用的 dom,复用总比重新创建要节省内存开销

@xsfxtsxxr
Copy link

xsfxtsxxr commented Jul 22, 2020

可以用这个自动生成唯一key值的库来解决 https://github.com/dylang/shortid

你好,我想问一下,为什么不能用随机uuid来代替 key ? 使用随机 uuid 和你这里的 shortid 有什么区别吗?
@LuckyHH
uuid跟shortid应该是相同功能的库,都是生成唯一的随机数,我们项目都是用的uuid,没用过shortid

@fingerpan
Copy link
Author

参考楼主的例子(请各位老师解答疑惑,抱拳)

[1, 2, 3, 4, 5]
         
[4, 1, 3, 5, 2]

如果不带key,比对时候应该是1删除,4新建、2删除,1创建、3相同,可以复用、4删除,5创建、5删除,2创建。
整个遍历时候发现只有3能复用,如果加了key,不是交换位置就好了吗,可是为什么说这样的例子不加key效率更高呢,?

如果不带key, 对比的时候应该是所有节点的位置都没有变化,只不过innerText变化了。带key的情况是innerText不变,节点位置交换

@fingerpan
Copy link
Author

参考楼主的例子(请各位老师解答疑惑,抱拳)

[1, 2, 3, 4, 5]
         
[4, 1, 3, 5, 2]

如果不带key,比对时候应该是1删除,4新建、2删除,1创建、3相同,可以复用、4删除,5创建、5删除,2创建。
整个遍历时候发现只有3能复用,如果加了key,不是交换位置就好了吗,可是为什么说这样的例子不加key效率更高呢,?

如果不带key, 对比的时候应该是所有节点的位置都没有变化,只不过innerText变化了。带key的情况是innerText不变,节点位置交换
@fingerpan
意思是 “带key的位置交换” 比 “不带key的创建+删除节点” 的效率更低的意思么?莫非不带key的时候,只更新了innerText,没有删除+创建DOM?

就简单dom列表而言,确实不带key的时候只更新了innerText。所以比带key的“创建+删除”节点效率更高。

@tw19920521
Copy link

key是dom节点唯一标识,在新旧dom节点渲染时,如果key变化了直接对节点销毁重建,
如果key没有变,在比较标签上其他属性,有变化直接更新节点,否则按兵不动

@JamesChen111
Copy link

更新 --------------------------

受楼下答案的一些特殊情况影响,导致很多人都认为key不能"提高"diff速度。在此继续重新梳理一下答案。

在楼下的答案中,部分讨论都是基于没有key的情况diff速度会更快。确实,这种观点并没有错。没有绑定key的情况下,并且在遍历模板简单的情况下,会导致虚拟新旧节点对比更快,节点也会复用。而这种复用是就地复用,一种鸭子辩型的复用。以下为简单的例子:

<div id="app">
    <div v-for="i in dataList">{{ i }}</div>
</div>
var vm = new Vue({
  el: '#app',
  data: {
    dataList: [1, 2, 3, 4, 5]
  }
})

以上的例子,v-for的内容会生成以下的dom节点数组,我们给每一个节点标记一个身份id:

  [
    '<div>1</div>', // id: A
    '<div>2</div>', // id:  B
    '<div>3</div>', // id:  C
    '<div>4</div>', // id:  D
    '<div>5</div>'  // id:  E
  ]
  1. 改变dataList数据,进行数据位置替换,对比改变后的数据
 vm.dataList = [4, 1, 3, 5, 2] // 数据位置替换

 // 没有key的情况, 节点位置不变,但是节点innerText内容更新了
  [
    '<div>4</div>', // id: A
    '<div>1</div>', // id:  B
    '<div>3</div>', // id:  C
    '<div>5</div>', // id:  D
    '<div>2</div>'  // id:  E
  ]

  // 有key的情况,dom节点位置进行了交换,但是内容没有更新
  // <div v-for="i in dataList" :key='i'>{{ i }}</div>
  [
    '<div>4</div>', // id: D
    '<div>1</div>', // id:  A
    '<div>3</div>', // id:  C
    '<div>5</div>', // id:  E
    '<div>2</div>'  // id:  B
  ]

增删dataList列表项

  vm.dataList = [3, 4, 5, 6, 7] // 数据进行增删

  // 1. 没有key的情况, 节点位置不变,内容也更新了
  [
    '<div>3</div>', // id: A
    '<div>4</div>', // id:  B
    '<div>5</div>', // id:  C
    '<div>6</div>', // id:  D
    '<div>7</div>'  // id:  E
  ]

  // 2. 有key的情况, 节点删除了 A, B 节点,新增了 F, G 节点
  // <div v-for="i in dataList" :key='i'>{{ i }}</div>
  [
    '<div>3</div>', // id: C
    '<div>4</div>', // id:  D
    '<div>5</div>', // id:  E
    '<div>6</div>', // id:  F
    '<div>7</div>'  // id:  G
  ]

从以上来看,不带有key,并且使用简单的模板,基于这个前提下,可以更有效的复用节点,diff速度来看也是不带key更加快速的,因为带key在增删节点上有耗时。这就是vue文档所说的默认模式。但是这个并不是key作用,而是没有key的情况下可以对节点就地复用,提高性能。

这种模式会带来一些隐藏的副作用,比如可能不会产生过渡效果,或者在某些节点有绑定数据(表单)状态,会出现状态错位。VUE文档也说明了 这个默认的模式是高效的,但是只适用于不依赖子组件状态或临时 DOM 状态 (例如:表单输入值) 的列表渲染输出

楼下 @yeild 也提到,在不带key的情况下,对于简单列表页渲染来说diff节点更快是没有错误的。但是这并不是key的作用呀。

但是key的作用是什么?

我重新梳理了一下文字,可能这样子会更好理解一些。

key是给每一个vnode的唯一id,可以依靠key,更准确, 更的拿到oldVnode中对应的vnode节点。

1. 更准确

因为带key就不是就地复用了,在sameNode函数 a.key === b.key对比中可以避免就地复用的情况。所以会更加准确。

2. 更快

利用key的唯一性生成map对象来获取对应节点,比遍历方式更快。(这个观点,就是我最初的那个观点。从这个角度看,map会比遍历更快。)

原答案 -----------------------

vue和react都是采用diff算法来对比新旧虚拟节点,从而更新节点。在vue的diff函数中(建议先了解一下diff算法过程)。
在交叉对比中,当新节点跟旧节点头尾交叉对比没有结果时,会根据新节点的key去对比旧节点数组中的key,从而找到相应旧节点(这里对应的是一个key => index 的map映射)。如果没找到就认为是一个新增节点。而如果没有key,那么就会采用遍历查找的方式去找到对应的旧节点。一种一个map映射,另一种是遍历查找。相比而言。map映射的速度更快。
vue部分源码如下:

// vue项目  src/core/vdom/patch.js  -488行
// 以下是为了阅读性进行格式化后的代码

// oldCh 是一个旧虚拟节点数组
if (isUndef(oldKeyToIdx)) {
  oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
}
if(isDef(newStartVnode.key)) {
  // map 方式获取
  idxInOld = oldKeyToIdx[newStartVnode.key]
} else {
  // 遍历方式获取
  idxInOld = findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
}

创建map函数

function createKeyToOldIdx (children, beginIdx, endIdx) {
  let i, key
  const map = {}
  for (i = beginIdx; i <= endIdx; ++i) {
    key = children[i].key
    if (isDef(key)) map[key] = i
  }
  return map
}

遍历寻找

// sameVnode 是对比新旧节点是否相同的函数
 function findIdxInOld (node, oldCh, start, end) {
    for (let i = start; i < end; i++) {
      const c = oldCh[i]
      
      if (isDef(c) && sameVnode(node, c)) return i
    }
  }

有key的情况下,会使用map映射查找节点,没有key时,遍历查找。即,使用map映射能查找到的节点,使用遍历也能查找到。
这样是不是就只是性能上的问题了?

@JamesChen111
Copy link

简单来说就是在大部分情况下增强复用性,在 diff 两个新老 list 的时候,vue 会采用
1.两个开头指针不断向后遍历,如果一直相同,那么可以复用。
2.两个尾部指针向前移动,相同即可复用。
3.老的 list 的头指针和新的 list 的尾指针对比,相同即可复用。
4.老的 list 的尾指针和新的 list 的头指针对比,相同即可复用。
5.这里开始用到 key 了,如果上面的四种同级比较策略没有找到可以复用的 dom 元素,通过唯一的 key 再去老的 list 中找一下,看看是否能找到可以复用的 dom,(这里源码使用 hash 存储老的 list 中的 key => 老的 list 的索引的映射,在寻找的过程中是 O(1) 的时间复杂度),这里如果利用 key 也没有找到可以复用的 dom ,那么只能重新创建了。
总结一下:通过 key 去查找是否可以复用的 dom,复用总比重新创建要节省内存开销

不加key就默认采用“就地复用”策略,加key也是为了复用。大佬,给我解释一下这都是什么跟什么。

@huskylengcb
Copy link

就react而言,key是对于列表组件而言,并且无key或者key不唯一会报错提示

warning,not error

@kankedelangzi
Copy link

就react而言,key是对于列表组件而言,并且无key或者key不唯一会报错提示

报错是eslint的问题, 与为什么用key无关

@boomstackcn
Copy link

boomstackcn commented Jul 6, 2021

看完了所有的答案,好像都没有一个实际案例来说明没有key的时候带来的问题以及有key的时候是如何解决的,下面给大家跑一个案例:

<template>
  <ul>
    <li v-for="item in list">{{item.name}}</li>
  </ul>
  <button @click="changeList">删除列表</button>
</template>

<script>
export default {
  data () {
    return {
      list: []
    }
  },
  mounted () {
    this.list = Array.from({length: 10}).map((_,index) => {
      return {
        name: index,
        id: index
      }
    })
    this.$nextTick(()=>{
      let listNodes = document.getElementsByTagName("li")
      listNodes[0].setAttribute("active", true)
    })
  },
  methods: {
    changeList () {
        this.list.splice(0,1, {
          name: 9,
          id: 9
        })
    }
  }
}
</script>

在这里有一个列表, 在创建之后被别有用心之人第一项的属性上加了一个setAttribute("active", true),然后点击按钮更新了第一条数据,你会发现列表第一条的属性”active“依然存在,但如果加上了key,这个属性就消失了。那么就说明key在这里的作用就是创建了一个全新的节点,替换掉了原来的节点。
还有一个例子是给任意一个节点绑定一个动态的key,然后设置一个按钮,每次点击就更新这个key值,你会发现,这个节点会不断的进行更新(这也是一个强制更新组件的一个方法)

@shifengdiy
Copy link

react Diffing算法详解

render函数执行会产生react元素树,下次render会产生另外一个元素树,react需要对比两个元素树差别,来更新同步真实DOM,使用最简单的广度优先遍历,时间复杂度达到O(n^3)
react使用O(n)启发式算法,提出以下两个假设:

  1. 不同的元素肯定会产生不同的树
  2. 可以使用key来指定哪一些元素不用更新

元素树的更新有以下几种情况:

  1. 无需更新
  2. 全新的节点,新增一个DOM阶段
  3. 需要修改属性的节点,更新样式或者其他属性
  4. 不需要的节点,需要删除

注意情况

  1. 根元素相同的元素不会删除节点,而是会原地复用
  2. key可以用来指定无需更新的元素

key元素的作用是用来指定当前元素无需删除,只需要原地复用

@weidehai
Copy link

正是因为带唯一key时每次更新都不能找到可复用的节点,不但要销毁和创建vnode

这句话不对吧,原来保留的节点是可以复用的

@conglaidaobei
Copy link

有key: 真定位,强复用
没key: 无定位,弱复用

@learn-shifeng
Copy link

learn-shifeng commented May 16, 2022 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests