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 · 44 comments

Comments

@fingerpan
Copy link

@fingerpan 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

This comment has been minimized.

Copy link

@yeild yeild commented Feb 20, 2019

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

@Tarhyru

This comment has been minimized.

Copy link

@Tarhyru Tarhyru commented Feb 20, 2019

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

@sunOpar

This comment has been minimized.

Copy link

@sunOpar sunOpar commented Feb 20, 2019

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

@LanjianNUll

This comment has been minimized.

Copy link

@LanjianNUll LanjianNUll commented Feb 20, 2019

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

@yeild

This comment has been minimized.

Copy link

@yeild 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

This comment has been minimized.

Copy link

@azl397985856 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

This comment has been minimized.

Copy link

@Tarhyru Tarhyru commented Feb 21, 2019

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

@yeild

This comment has been minimized.

Copy link

@yeild 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

This comment has been minimized.

Copy link

@Tarhyru Tarhyru commented Feb 21, 2019

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


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

@yeild

This comment has been minimized.

Copy link

@yeild yeild commented Feb 21, 2019

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

@Tarhyru

This comment has been minimized.

Copy link

@Tarhyru Tarhyru commented Feb 21, 2019

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

@oychao

This comment has been minimized.

Copy link

@oychao oychao commented Feb 24, 2019

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

@Tarhyru

This comment has been minimized.

Copy link

@Tarhyru Tarhyru commented Mar 1, 2019

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

@deepkolos

This comment has been minimized.

Copy link

@deepkolos deepkolos commented Mar 15, 2019

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

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

@fangjiale

This comment has been minimized.

Copy link

@fangjiale 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

This comment has been minimized.

Copy link

@songmengda songmengda commented May 10, 2019

长知识了

@luohong123

This comment has been minimized.

Copy link

@luohong123 luohong123 commented May 22, 2019

一、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

This comment has been minimized.

Copy link

@Younguser Younguser commented May 27, 2019

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

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

@dorese

This comment has been minimized.

Copy link

@dorese dorese commented Jul 9, 2019

谢谢大佬分享

@AlexZhong22c

This comment has been minimized.

Copy link

@AlexZhong22c AlexZhong22c commented Jul 10, 2019

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

@libin1991

This comment has been minimized.

Copy link

@libin1991 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

This comment has been minimized.

Copy link

@miaomiaogege miaomiaogege commented Jul 10, 2019

考虑问题 上来就是一些细小的 所谓的速度 性能 真的搞笑 前端好像 天天虚拟dom 什么 diff 都非得上纲上线 我说他们呢都很慢 将来肯定有更快的 所以我建议大家现在 都等着就好了 争论个没有意思的 跑题了都 真的是

@ZhuyichaoCode

This comment has been minimized.

Copy link

@ZhuyichaoCode ZhuyichaoCode commented Jul 10, 2019

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

不仅仅是react,vue也是一样, 同样会报错

@zhanglei0823

This comment has been minimized.

Copy link

@zhanglei0823 zhanglei0823 commented Jul 11, 2019

不知道可以这样理解? 在vue中key的作用,在没有绑定key的时候,对于列表的渲染都是采用就地复用的策略,效率高,可能会有造成渲染错乱的现象。在使用了key之后,重新列表渲染时,含key项匹配到就采用就地复用策略,新增项创建插入 这样渲染时的更精确,效率较高

@crp1212

This comment has been minimized.

Copy link

@crp1212 crp1212 commented Jul 11, 2019

key在复杂的场景肯定是提升效率的啊
假设key变化为
let keyA = [1, 2, 3, 4, 5, 12, 13, 14]
let keyB = [1, 2, 6, 7, 8, 3, 4, 5]
这样在diff的时候有key就只需要进行5次详细的比对
没有key就需要进行8次

@niujiangyao

This comment has been minimized.

Copy link

@niujiangyao niujiangyao commented Jul 12, 2019

@azl397985856
您好这块 如果不写key,可能就会发生本来应该更新却没有更新的bug。 不写key或者key为index的情况下,如果是普通的无状态的列表渲染也会更新,不会出现你说的本来应该更新却没有更新的bug,只是会在类似这种情况下出现点问题
image

你说的不写key本来应该更新却没有更新的bug是指这样吗 还是什么
而且我看react官网说不写key会默认index 但是我自己试不写key react会提醒加上key
image

@anthinkingcoder

This comment has been minimized.

Copy link

@anthinkingcoder anthinkingcoder commented Jul 12, 2019

map比遍历快不对吧 ,key只是samenode里的一个判定条件 即使有key这么一个判定条件 也是会需要遍历的。

@javaLuo

This comment has been minimized.

Copy link

@javaLuo javaLuo commented Jul 13, 2019

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

能说说具体都有什么副作用吗。现在用vcode这样的IDE,不加key都提示报错

@IAMSBLOL

This comment has been minimized.

Copy link

@IAMSBLOL IAMSBLOL commented Jul 18, 2019

提高diff算法性能,且减少对比出错风险~虽然不太可能出错。

@xiaoxixi6633 xiaoxixi6633 referenced this issue Jul 19, 2019
@xiaoxixi6633

This comment has been minimized.

Copy link

@xiaoxixi6633 xiaoxixi6633 commented Jul 19, 2019

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

@suminhohu

This comment has been minimized.

Copy link

@suminhohu suminhohu commented Jul 24, 2019

map比遍历快不对吧 ,key只是samenode里的一个判定条件 即使有key这么一个判定条件 也是会需要遍历的。

map 复杂度是 O(1), 遍历是 O(n) @anthinkingcoder

@EdenStrive

This comment has been minimized.

Copy link

@EdenStrive EdenStrive commented Jul 24, 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
    }
  }
@zhangzs000

This comment has been minimized.

Copy link

@zhangzs000 zhangzs000 commented Aug 13, 2019

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

@MissNanLan

This comment has been minimized.

Copy link

@MissNanLan MissNanLan commented Aug 16, 2019

Vue在渲染列表的时候采用就地复用的原则,如果改变了dom的顺序,那么vue不会dom元素的顺序做修改,而是就此复用当前的元素

假设我们要在BC之间插入一个元素,如图所示
image

就是把C变成F,D变成C,E变成D,最后插入E,是不是很麻烦
image
然后我们看看有key和没有key的区别
image

当然据官方文档中提到过的

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

@Geek-James

This comment has been minimized.

Copy link

@Geek-James Geek-James commented Aug 21, 2019

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

@iamhmx

This comment has been minimized.

Copy link

@iamhmx iamhmx commented Aug 29, 2019

在vue中,在遍历的数据进行新增或删除时会带来副作用,另外,带key和不带key会影响diff算法的具体操作,例如:A B C 要更新为 B A C,不带key的操作是:A更新为B,B更新为A,带key的操作则会直接移动A B节点,减少删除新增DOM的消耗。

@learn-shifeng

This comment has been minimized.

Copy link

@learn-shifeng learn-shifeng commented Sep 28, 2019

看了不少各位的表述,基本都是没看过diff算法源码的咬文嚼字,没有任何意义~

@weizhanzhan

This comment has been minimized.

Copy link

@weizhanzhan weizhanzhan commented Oct 11, 2019

看了好多也懵懵懂懂,哈哈,简单的记录下,作为vnode的唯一id,在更新的时候diff算法的过程中找到对应节点,然后判断两个节点是否相同,同则复用,否则删除;关于性能的问题看的也有点晕了,总之还是能通过key去匹配且能匹配的到的节点的情况下性能会好点,如果每次都会有新的节点或者整个渲染的节点和之前没有任何关系,那么不加key的性能会好点,省略去了逐个匹配和接下来创建和销毁vnode的过程,不知道能不能这么理解

@RianMan

This comment has been minimized.

Copy link

@RianMan RianMan commented Oct 16, 2019

这题好难,面试官表示我不问了。。。😂

@LuckyHH

This comment has been minimized.

Copy link

@LuckyHH LuckyHH commented Oct 29, 2019

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

你好,我想问一下,为什么不能用随机uuid来代替 key ? 使用随机 uuid 和你这里的 shortid 有什么区别吗?

@zqq1001

This comment has been minimized.

Copy link

@zqq1001 zqq1001 commented Nov 4, 2019

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

@wjh-0531

This comment has been minimized.

Copy link

@wjh-0531 wjh-0531 commented Nov 8, 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的原因。至于具体怎么使用,就要根据实际情况来选择了。

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

@wjh-0531

This comment has been minimized.

Copy link

@wjh-0531 wjh-0531 commented Nov 8, 2019

@yeild
你能将你举的新闻例子发一下么,我听得好有道理自己就是实现不出来

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
You can’t perform that action at this time.