众所周知,vue是数据驱动的,数据的改动先反馈到虚拟dom,虚拟dom再通过diff算法来渲染真实dom节点。并且,在数据驱动的框架直接修改dom会引发下面的问题:
-
如果改变了真实dom位置之后,再触发渲染,diff又会怎样执行呢?
-
改变了真实dom位置之后,把数据也按照真实dom做修改,再执行diff又会怎样呢?
为了解决这些疑问,我们安装sortablejs,然后写一个demo
首先是第一个问题
改变了真实dom位置之后,再触发渲染,diff又会怎样执行呢?
<template>
<section>
<div id="sort">
<div>{{sortArr[0]}}</div>
<div>{{sortArr[1]}}</div>
<div>{{sortArr[2]}}</div>
</div>
<button @click="id++">refresh: {{id}}</button>
</section>
</template>
import Sortable from 'sortablejs'
export default {
name: 'App',
data() {
return {
sortArr: [1, 2, 3],
id: 0,
}
},
mounted() {
var el = document.getElementById('sort')
var sortable = Sortable.create(el)
},
}
diff不会改变dom!!!
diff完全没有作用,这说明,虚拟dom和真实dom之间的是“引用”关系,真实dom虽说改变了位置,但是虚拟dom和真实dom之间的“引用”关系并没有发生改变,diff算法一看,好家伙,虚拟dom和上次完全一样,所以即使再通过虚拟dom重新渲染,真实dom也不会发生变化。
为了研究第二个问题,我们把代码增加逻辑
var sortable = Sortable.create(el, {
onEnd: ({ newIndex, oldIndex }) => { // 用箭头函数使this指向vue实例
const currRow = this.sortArr.splice(oldIndex, 1)[0]
this.sortArr.splice(newIndex, 0, currRow)
},
})
增加的逻辑就是拖拽改变真实dom后,再将数据同步,使数据和真实dom保持一致
可以发现,我进行的操作是将123=>132,但是操作之后,dom突然又变123,和vue里面存的数据不一致!!!即使触发重新渲染,也不会按照vue的数据渲染出正确的dom了,怎么会这样???
接下来我们就来还原下到底发生了什么
刚开始的映射关系是这样的
拖拽之后,重新渲染前的映射关系是这样的
关键的一步来了,我们将虚拟dom的数据和真实dom保持一致,所以虚拟dom的2和3换了一下位置,并且在这个时候触发了diff。
diff先patch虚拟dom1,但是1没有发生变化,略过
然后是patch虚拟dom2,注意此时的2指向真实dom的第三个元素
由于虚拟dom2只是text发生了变化,所以将2改成3,diff同步地将dom3改成了3,所以此时展示的是1,3,3
同理,下一个是patch虚拟dom3,虚拟dom3指向真实dom的第二个元素,虚拟dom3将3改成2,所以此时展示的是1,2,3,变回去了!!!
这就是数据驱动的框架操作dom之后的后果,虚拟dom和真实dom可能会不一致,所以不要轻易直接修改dom!!!
此时还有一个问题,若是给它们加了key之后会怎样?
<div id="sort">
<div :key="sortArr[0]">{{sortArr[0]}}</div>
<div :key="sortArr[1]">{{sortArr[1]}}</div>
<div :key="sortArr[2]">{{sortArr[2]}}</div>
</div>
直接告诉你答案吧,这样写是不行的
此时虚拟dom2还是指向真实dom3,并且key不相同,会先umount然后再重新创建div,所以开销更大,但还是无法能够修复错乱的映射关系。
那有没有一种可能,正常开发来说,都会采用下面这种写法
<div id="sort">
<div v-for="item in sortArr" :key="item">{{item}}</div>
</div>
就结果来说,这样是可行的,并且这样子写,diff的过程中,就直接把错乱的映射关系给修复了!连dom的手都没碰到!!!
原理如下:
v-for指令会形成一个Symbol(Fragment)的包装,会把它们一起处理
const patchKeyedChildren = (c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => {
let i = 0;
const l2 = c2.length;
let e1 = c1.length - 1; // prev ending index
let e2 = l2 - 1; // next ending index
// 1. sync from start
// (a b) c
// (a b) d e
while (i <= e1 && i <= e2) {
const n1 = c1[i];
const n2 = (c2[i] = optimized
? cloneIfMounted(c2[i])
: normalizeVNode(c2[i]));
if (isSameVNodeType(n1, n2)) { // 只有key相同的才会patch,所以不会像上面那样key不同就unmount
patch(n1, n2, container, null, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
}
else {
break;
}
i++;
}
……// 一大堆逻辑就是为了能让相同的key patch
key相同的才会patch这一点非常关键,直接把错乱的dom映射给修复了
发现这个问题的背景是写拖拽table的时候,为了方便直接用了sortablejs,并且为了样式统一,还是用的el-table。遇到相同问题的小伙伴,可以给el-table加一个row-key,并在tableData中定义一个key就行