阅读本文前请先了解组件更新原理 “组件更新-新旧节点相同”的部分
我们的用例如图,可以新添一项,并可选择checkbox,代码如下:
<div id="app">
<div>
<input type="text" v-model="name" />
<button @click="add">添加</button>
</div>
<ul>
<li v-for="(item, i) in list">
<input type="checkbox" /> {{item.name}}
</li>
</ul>
</div>
<script>
var vm = new Vue({
el: "#app",
data: {
name: "",
newId: 3,
list: [
{ id: 1, name: "李斯" },
{ id: 2, name: "吕不韦" },
{ id: 3, name: "嬴政" },
],
},
methods: {
add() {
//注意这里是unshift
this.list.unshift({ id: ++this.newId, name: this.name });
this.name = "";
},
},
});
</script>
页面长这样,先选中一项
再添加一项“妲己”
会发现之前被选中的“李斯”没有选中,而新添加的“妲己”被选中了。
在源码src/core/vdom/patch.js中,重要的方法看两个patchVnode,updateChildren,dom树的递归比较和更新就是这两个方法。
当比较ul的新老元素时,进入555行,isDef(oldCh) && isDef(ch),发现旧ul的children和新ul的children都存在,且不一样(每一次更新组件,都会执行render方法,重新创建vnode,所以肯定是不一样的vnode对象),执行updateChildren方法。
这个时候我们看oldCh和ch都是什么值
旧ul vnode的children是3个li vnode
新ul vnode的children是4个li vnode,新加了一个“妲己”
li下面有两个子元素, checkbox和一个文本节点item.name
旧li的子元素 checkbox值“oldCh[0].children[0].checked”为true,因为被勾选了,而新li的子元素的checkbox值还不存在,所以是undefined,还没有创建真实dom元素,还是虚拟dom状态。
现在进入updateChildren方法看看。
执行到430行,oldStartVnode和newStartVnode,
oldStartVnode代表旧ul的children的第一个虚拟节点li vnode;newStartVnode代表新ul的children的第一个虚拟节点li vnode。
这里有一个判断,sameNode方法, 正是因为v-for没有加key,就会认为新旧两个li的vnode是相同的,所以又会执行patchNode。这个时候又会进入patchNode方法。
然后又会执行到555行,判断li的children又不相同,又进入updateChildren,参数是li的children数组,分别是checkbox和item.name两个虚拟节点。
所以vnode的递归比较和更新就是这两个方法。更新在patchNode,children的比较顺序在updateChildren。
又执行到430行,分别判断第一个子元素oldStartVnode和newStartVnode,两个checkbox还是相同,又进入patchNode,这个时候执行到518行。
会把旧的checkbox真实dom对象赋值给新checkbox vnode的elm。所以在渲染到真实dom树的时候,第一个li标签,也就是“妲己”,里面的子元素checkbox就是选中的。
继续往下执行,没有改变dom值的操作,都是hook,回调。其实真正修改真实dom也是在这个方法中,像addVnodes,removeVnodes,setTextContent。
执行完之后回到上一个方法-updateChildren,还记得while循环吗?就是轮循去一个一个比较children,比较完li下面的checkbox,现在比较文本节点item.name。依然根据比较规则,会执行430行,又进入patchNode,这次执行到569行,因为是一个文本节点,在上面的判断isUndef(vnode.text),这是一个文本节点,是定义了的,所以会走else if分支。因为“李斯”和“妲己”不相同,所以会替换掉“李斯”,改为“妲己”。
所以就出现了现在新增的“妲己”是选中了的。
在比较第二个li vnode的children的时候,也是同样的道理,原来的第二个li的checkbox没有被选中,因为判断是同一个节点,旧checkbox vnode的elm会赋值给新checkbox vnode的elm。
现在如果加上key,以id为key,会发生什么?
因为在比较li的时候会用sameNode判断是否为相同的节点,其中一个比较是key,
会判断不是相同节点,就会执行不相同节点的操作,“创建新节点 -> 更新占位符节点 -> 删除旧节点的逻辑”,执行718行else分支。