-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Description
更新 --------------------------
受楼下答案的一些特殊情况影响,导致很多人都认为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
]
- 改变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
}
}
Activity
[-]key的作用是为了在diff算法执行时更快的找到对应的节点,提高diff速度。[/-][+]第一题:key的作用是为了在diff算法执行时更快的找到对应的节点,提高diff速度。[/+]yeild commentedon Feb 20, 2019
就我的使用来说(Vue)key的作用是为了在数据变化时强制更新组件,以避免“原地复用”带来的副作用。另外,某些情况下不带key可能性能更好,见:issuecomment
官方文档对于key的描述: 列表渲染-key | API-key
Zephylaci commentedon Feb 20, 2019
@yeild
路过,越俎代庖一下,不一定对...
“建议尽可能在使用 v-for 时提供 key,除非遍历输出的 DOM 内容非常简单,或者是刻意依赖默认行为以获取性能上的提升。”
你是说这个么..它的本意是你应该单独指定一个Key而不是使用index来作为key...就像你说的这里还有一个复用问题..
但,如果说不带key性能会更好,为啥还要建议带key呢?
这个性能提升的前提有一句“刻意依赖默认行为”..也就是说不是说你不带key就会有性能提升...
而实际上,只是单纯的说查找这一过程...你去试试 在arr中使用indexOf 和使用obj[key] 的性能区别..就能感受到性能差异了..
sunopar commentedon Feb 20, 2019
主要是为了提升diff【同级比较】的效率。自己想一下自己要实现前后列表的diff,如果对列表的每一项增加一个key,即唯一索引,那就可以很清楚的知道两个列表谁少了谁没变。而如果不加key的话,就只能一个个对比了。
LanjianNUll commentedon Feb 20, 2019
就react而言,key是对于列表组件而言,并且无key或者key不唯一会报错提示
yeild commentedon 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的情况:
使用id作为key的情况:
list构造:
因为不带key时节点能够复用,省去了销毁/创建组件的开销,同时只需要修改DOM文本内容而不是移除/添加节点,这就是文档中所说的“刻意依赖默认行为以获取性能上的提升”。
既然如此,为什么还要建议带key呢?因为这种模式只适用于渲染简单的无状态组件。对于大多数场景来说,列表组件都有自己的状态。
举个例子:一个新闻列表,可点击列表项来将其标记为"已访问",可通过tab切换“娱乐新闻”或是“社会新闻”。
不带key属性的情况下,在“娱乐新闻”下选中第二项然后切换到“社会新闻”,"社会新闻"里的第二项也会是被选中的状态,因为这里复用了组件,保留了之前的状态。要解决这个问题,可以为列表项带上新闻id作为唯一key,那么每次渲染列表时都会完全替换所有组件,使其拥有正确状态。
这只是个简单的例子,实际应用会更复杂。带上唯一key虽然会增加开销,但是对于用户来说基本感受不到差距,而且能保证组件状态正确,这应该就是为什么推荐使用唯一id作为key的原因。至于具体怎么使用,就要根据实际情况来选择了。
以上个人见解,如有误望指正。
azl397985856 commentedon Feb 21, 2019
我的理解是,vue和react虽然都采用了diff算法。 但是react本身的设计和vue的设计是截然不同的, vue采用了更加细粒度的更新组件的方式,即给每一个属性绑定监听, 而react是采用自顶而下的更新策略,每次小的改动都会生成一个全新的vdom。从而进行diff,如果不写key,可能就会发生本来应该更新却没有更新的bug。
这个bug其实和diff算法有关,react团队完全可以写一个没有这个“bug”版本的代码, 但是这是一种权衡,一种性能和方便使用的权衡。 写不写key能够提高性能的根本在于一方面diff算法会优先判断key是否相同,如果相同则不进行后面的运算。 如果key相同,就更好了,根本不需要重新创建节点
总结, 更确切的说应该是diff算法在你的
复杂
的列表稳定
的时候能够明显提高性能,因为节点可以重用。但是对于列表
频繁更新
的场景, 节点不能重用,但是diff 可以省略一部分逻辑
,因此性能也会更好。但是两者的性能优化不在同一个纬度,一个是 创建和更新节点(我称之为
渲染器
)的优化,一个是DOM diff 算法(我称之为
核心引擎
)的优化Zephylaci commentedon Feb 21, 2019
@yeild
你这里说的是创建的开销
而这道题讨论的是diff时的速度,我的理解是在说查找某一节点进行修改时的耗时..
我并不知道vue如果不指定key时对这个查找是否会有别的优化,但从这个解答来说是没有的.
所以其实我们说的性能不是一个东西
不过,于我来说通过你的回复也理清了一些创建过程的概念,总之是有收获的
yeild commentedon Feb 21, 2019
@Tarhyru 不是的,我是指如果不带key,则a.key 和 b.key 都是undefined,就直接进入两个节点相同的逻辑,到这里diff已经结束了,根本不会运算到后边'利用对象取值而不是遍历数组'找相同节点的那一步。 从这个角度来说,并没有体现“有key比无key diff算法效率更高这一点”。
也不是说楼主的回答是错误的,但仅在于在前面的逻辑中都没有找到相同的节点,才会优先通过keyMap查找,次而通过遍历查找,从这里优化到diff速度。
另外,这道题并非讨论diff速度,而是说key的作用。所以我给出的结论是:key的作用就是更新组件时判断两个节点是否相同。相同就复用,不相同就删除旧的创建新的。
@azl397985856 你的补充很好,我提到的“不带key性能更好”,其实是因为两个key都是undefined,自然就相同然后复用组件了,原文有歧义,已做修改。
Zephylaci commentedon Feb 21, 2019
@yeild
了解你的意思,这么说的确楼主的说法有歧义,而我在思考上也先入为主了...
但是,总觉得有齿轮没有咬合在一起..
“key的作用就是更新组件时判断两个节点是否相同。相同就复用,不相同就删除旧的创建新的。”
这是在说创建的情况吧?新增的时候如果没有key,就会复用默认的而不重新创建
那么修改呢?需要先通过key来找到需要修改的节点,而如果没有key则会遍历所有节点..这个过程应该也算是diff的过程吧?
那么严格来说:key对于修改节点时执行diff时会对性能有所提升,而对于新增这种情况时整体性能是下降的
这么描述是不是准确一些?
2019-03-01
补充一篇文章,感兴趣的去翻翻,也许认识会全一些..
vue diff算法解析
yeild commentedon Feb 21, 2019
@Tarhyru
关于遍历节点,实际上不论有没有key,都会遍历节点去找是否有sameVnode,而不是能够像对象取值一样拿到哪个节点需要修改的。你可以看下这部分代码,首先是while循环判断节点是否相同,只有没找到相同节点时,才会进最后的else,也就是楼主给出的代码,在这里key属性才能优化到查找速度
Zephylaci commentedon Feb 21, 2019
@yeild
学习到了,看代码的确,key只在查找复用节点的时候起到了查找作用...
那么,的确楼主的说法不完全错....但放在这道题里是不合适的
至少对于diff过程来说这个key是起不到提速效果的
oychao commentedon Feb 24, 2019
Diff算法只在有重新排序(包括中间插入和删除节点)的情况下才可能有优化的作用,因为这样会有已有节点的移动,删除,以及新节点的插入等操作,这种情况下最好是复用以前已经生成的节点。
楼上不加Key的时候速度还更快的原因是一种简单的特例,不加key的列表对比一定无法使两个表中的元素意义对应,这种情况下的更新无法保证重新排序后结果的正确性。
另外React的diff算法比Vue的diff算法更复杂一些。
Zephylaci commentedon Mar 1, 2019
@yeild
刚发现楼主回复了..
然后按它说的去查了下...发现我们两个前面的讨论也有一些误导性内容....
虽然过程可能的确是有没有key都是遍历...但是key似乎的确在diff中能起到提速作用...
具体看看这个?vue diff算法解析
59 remaining items
JamesChen111 commentedon Dec 28, 2020
不加key就默认采用“就地复用”策略,加key也是为了复用。大佬,给我解释一下这都是什么跟什么。
huskylengcb commentedon Feb 2, 2021
warning,not error
kankedelangzi commentedon May 13, 2021
报错是eslint的问题, 与为什么用key无关
boomstackcn commentedon Jul 6, 2021
看完了所有的答案,好像都没有一个实际案例来说明没有key的时候带来的问题以及有key的时候是如何解决的,下面给大家跑一个案例:
在这里有一个列表, 在创建之后被别有用心之人第一项的属性上加了一个
setAttribute("active", true)
,然后点击按钮更新了第一条数据,你会发现列表第一条的属性”active“依然存在,但如果加上了key,这个属性就消失了。那么就说明key在这里的作用就是创建了一个全新的节点,替换掉了原来的节点。还有一个例子是给任意一个节点绑定一个动态的key,然后设置一个按钮,每次点击就更新这个key值,你会发现,这个节点会不断的进行更新(这也是一个强制更新组件的一个方法)
shifengdiy commentedon Aug 22, 2021
react Diffing算法详解
render函数执行会产生react元素树,下次render会产生另外一个元素树,react需要对比两个元素树差别,来更新同步真实DOM,使用最简单的广度优先遍历,时间复杂度达到O(n^3)
react使用O(n)启发式算法,提出以下两个假设:
元素树的更新有以下几种情况:
注意情况
key元素的作用是用来指定当前元素无需删除,只需要原地复用
weidehai commentedon Nov 29, 2021
这句话不对吧,原来保留的节点是可以复用的
conglaidaobei commentedon May 16, 2022
有key: 真定位,强复用
没key: 无定位,弱复用
learn-shifeng commentedon May 16, 2022