1.起因
公司内部系统用户反馈页面无法操作,必须关闭页面重新打开才能解决。按道理业务代码只是做data设置和方法操作,不应该导致整个vue都失效。
2.定位问题
打开控制台,发现输出了 undefined (reading key)
- 分析代码发现业务data数据里没有什么key的定义
- 推断可能是vue的vnode中的key设置问题
- 检查代码发有一个地方在重新绑定code的时候,设置了相同的值
- 初步推荐可能是绑定key的时设置了一样的值
3.验证问题 写个demo
1. 初始化重复key
- 这里加入了一个"测试数据响应式"按钮查看当前响应式是否工作正常。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.10/vue.min.js"></script>
</head>
<body>
<div id="app">
<h1>{{cnt}}<button @click="testReactive">测试数据响应式</button></h1>
<ul>
<li v-for="(item,index) in list" :key="item.id">{{item.id}}</li>
</ul>
<br>
</div>
</body>
<script>
new Vue({
el: '#app',
data: function () {
return {
cnt: 0,
list: [
{ id: 1 },
{ id: 1 },
{ id: 1 }
],
}
},
methods: {
testReactive() {
this.cnt++
console.log("xxxx")
}
},
})
</script>
</html>
好家伙,页面加载无任何报错信息,稳稳的
2. 初始化重复key,再统一设置其他重复key
<body>
<div id="app">
<h1>{{cnt}}<button @click="testReactive">测试数据响应式</button></h1>
<ul>
<li v-for="(item,index) in list" :key="item.id">{{item.id}}</li>
</ul>
<button @click="change">改变list的所有id为200</button>
<br>
</div>
</body>
<script>
new Vue({
el: '#app',
data: function () {
return {
cnt: 0,
list: [
{ id: 1 },
{ id: 1 },
{ id: 1 }
],
}
},
methods: {
change() {
this.list.forEach(item => {
item.id = 200
});
},
testReactive() {
this.cnt++
console.log("xxxx")
}
},
})
</script>
统一把所有key由1设置为200,看似很稳定吖
3.初始化不重复key,再统一设置其他重复key
<body>
<div id="app">
<h1>{{cnt}}<button @click="testReactive">测试数据响应式</button></h1>
<ul>
<li v-for="(item,index) in list" :key="item.id">{{item.id}}</li>
</ul>
<button @click="change">改变list的所有id为200</button>
<br>
</div>
</body>
<script>
new Vue({
el: '#app',
data: function () {
return {
cnt: 0,
list: [
{ id: 1 },
{ id: 2 },
{ id: 3 }
],
}
},
methods: {
change() {
this.list.forEach(item => {
item.id = 200
});
},
testReactive() {
this.cnt++
console.log("xxxx")
}
},
})
</script>
好家伙,也是我预期的效果。那问题来了,为什么会挂?
4.初始化不重复key,再统一设置其他重复key(这个新key是在原来初始化里存在的)
我们的把新key 统一设置为2
<script>
change() {
this.list.forEach(item => {
item.id = 2
});
},
</script>
结果问题重现了,nnd藏的够隐蔽的
我们可以看到gif我们可以看到 后台输出了
目前得出的规律是原始是正常的唯一key,重新操作后,只要新key中包含原来的key,并且重复出现,就会导致vue失去响应式
5.特殊情况,只有两条数据时响应式正常
<script>
new Vue({
el: '#app',
data: function () {
return {
cnt: 0,
list: [
{ id: 1 },
{ id: 2 },
],
}
},
})
</script>
晕~ 两条又没问题
4.分析问题
我们把vue.min.js 改成 vue.js
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.10/vue.js"></script>
</head>
<body>
<div id="app">
<h1>{{cnt}}<button @click="testReactive">测试数据响应式</button></h1>
<ul>
<li v-for="(item,index) in list" :key="item.id">{{item.id}}</li>
</ul>
<button @click="change">改变list的所有id为2</button>
<br>
</div>
</body>
<script>
new Vue({
el: '#app',
data: function () {
return {
cnt: 0,
list: [
{ id: 1 },
{ id: 2 },
{ id: 3 }
],
}
},
methods: {
change() {
this.list.forEach(item => {
item.id = 2
});
},
testReactive() {
this.cnt++
console.log("xxxx")
}
},
})
</script>
</html>
1.分析提示
可以看到vue已经做了提示
- 显示2次警告
[Vue warn]: Duplicate keys detected: '2'. This may cause an update error.说有重复的key定义,将可能导致更新错误 - 代码执行错误
TypeError: Cannot read properties of undefined (reading 'key')具体代码异常在at sameVnode (vue.js:5806:9)
2.分析源码
这里发现a对象居然是undefined的 所以直接挂掉了,跟踪代码我们可以看到在updateChildren调用的sameVnode
关于具体diff实现参考这里...
//判断节点是否相同
function sameVnode (a, b) {
return (
a.key === b.key && ( //这里发现a对象居然是undefined的 所以直接挂掉了
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
) || (
isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)
)
)
)
}
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
var oldStartIdx = 0;
var newStartIdx = 0;
var oldEndIdx = oldCh.length - 1;
var oldStartVnode = oldCh[0];
var oldEndVnode = oldCh[oldEndIdx];
var newEndIdx = newCh.length - 1;
var newStartVnode = newCh[0];
var newEndVnode = newCh[newEndIdx];
var oldKeyToIdx, idxInOld, vnodeToMove, refElm;
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
var canMove = !removeOnly;
{
checkDuplicateKeys(newCh);
}
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx];
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
if (isUndef(oldKeyToIdx)) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); }
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
} else {
vnodeToMove = oldCh[idxInOld];
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
oldCh[idxInOld] = undefined;
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
}
}
newStartVnode = newCh[++newStartIdx];
}
}
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
} else if (newStartIdx > newEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}
1.第一次diff
我们可以看到第一次diff的时候元素2,被移动到第一个位置
通过执行了canMove 实现移动。
这时候 oldCh 数组已经芭比Q了,变成了 [vnode,undefined,vnode]
2.第二次diff
这样当第二次diff的时候,由于vnode直接是undefined,所以undefined.key当然就挂掉了。
3.vue遇到异常时统一处理
当遇到运行时异常,vue会做统一拦截并把需要后续执行的逻辑终止。并且调用this.cleanupDeps();
把所有响应式的dep释放,这也是为什么报错后,响应式失效的原因
/**
* Evaluate the getter, and re-collect dependencies.
*/
Watcher.prototype.get = function get () {
pushTarget(this);
var value;
var vm = this.vm;
try {
value = this.getter.call(vm, vm);
} catch (e) {
if (this.user) {
handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
}
return value
};
3.diff流程图回顾
第一次diff
第二次diff
5. 解决方案
方法1
直接在代码中把:key 去掉,但是会降低diff的效率,因为所有节点都是undefined
方法2
后台确保返回的code是唯一,比如利用后台的数据库id值(不能确保100%不稳定)
方法3
统一使用自己生成的code,不要依赖业务代码的code,比如自己使用 Math.random生成唯一key
6.结论
key是vue中为了提高diff算法所使用的vnode的唯一标识,我们应该要理解其中的原理,并且合理使用。 如果因为一点性能提升,导致业务页面无法交互使用,可能会得不偿失。