1、对虚拟DOM的理解
一、什么是虚拟DOM
所谓的虚拟DOM就是用一个js对象,来描述一个DOM节点
<div class="box">
<h3>我是一个标题</h3>
<ul>
<li>牛奶</li>
<li>咖啡</li>
<li>可乐</li>
</ul>
</div>
//虚拟dom
{
{
"sel": "div",
"data": { "class": { "box": true } }
"children": [
{
"sel": "h3",
"data": {},
"text": "我是一个标题"
},
{
"sel": "ul",
"data": {},
"children": [
{ "sel": "li", "data": {}, "text": "牛奶" }
{ "sel": "li", "data": {}, "text": "咖啡" }
{ "sel": "li", "data": {}, "text": "可乐" }
]
}
]
}
操作DOM是非常费时的,所以我们要通过其他的方法来实现,这时候虚拟DOM就出现了
二、为什么要用虚拟DOM
1.用js对象的计算属性换取操作DOM的性能消耗
2.更好的跨平台使用
3.我们对比变化前后的虚拟DOM节点,通过DOM-DIff算法计算出需要更新的地方,然后去更新需要更新的视图,这是虚拟DOM产生的原因和最大的用途
2、diff算法原理
1.涉及对象
【新的虚拟DOM和老的虚拟DOM】
2.步骤
1)对比是否是相同的元素,如果是不同的元素,直接删除,重新创建新的元素
2)如果是相同的元素
-
对比属性
-
对比children
-
新的有儿子,旧的没有儿子
-
新的没有儿子,旧的有儿子
-
新旧都是文本节点
-
新旧都有儿子,采用双指针对比 头头、尾尾、头尾、尾头
-
3.算法
深度递归+双指针
3、搭建snabbdom
官网
安装
npm init // 创建一个身份证
npm i -S snabbdom
思考💡:npm i -D xxx / npm i -S xxx / npm i -g xxx的区别?
安装缩写:
npm i 就是npm install 简写
npm i xxxx -D 就是 npm i xxxx --save-dev,是把依赖写入进devDependencies对象里面
npm i xxxx-S 就是 npm i xxxx--save,是把依赖写入进dependencies对象里面
npm i xxxx-g 就是 全局安装,在命令行的任何地方都可以操作,不会提示“命令不存在等错误”
npm i xxxx 就是本地安装,就是安装到当前命令行下的目录中,但不会记录在package.json中,npm install时不会自动安装此依赖
环境解释:
devDependencies是开发环境,上线后非必需,比如:webpack,gulp等压缩打包工具
dependencies是线上发布环境,上线后必须的,比如:ui库,字体库
4、搭建webpack运行snabbdom的demo
安装webpack的一系列命令:
npm i -D webpack@5 webpack-cli@3 webpack-dev-server@3
配置webpack.config.js:
参考官网提供的demo:webpack.docschina.org/
const path = require('path');
module.exports = {
// 入口文件
entry: './src/index.js',
//出口
output: {
//虚拟路径,不会物理生成
publicPath:'xuni'
filename: 'bundle.js',
},
devServer:{
port:'8080',
// 静态资源文件夹
contentBase:'www'
}
};
完成8080端口的访问:
// 修改启动命令
"scripts": {
"dev": "webpack-dev-server"
},
界面展示测试数据:
访问虚拟出口打包文件:
运行demo项目:
demo代码粘贴到index.js
demo代码做2处修改
// index.html
<body>
<div id="container"></div>
<script src="./xuni/bundle.js"></script>
</body>
访问demo数据:
5、h函数的用法
一、作用:
创建虚拟节点
//虚拟节点的全部属性:
children: undefined
data: {props: {}}
elm: undefined
key: undefined
sel: "a"
text: "尚硅谷"
二、3个参数:
//index.js
import {init,classModule,propsModule,styleModule,eventListenersModule,h,} from "snabbdom";
//创建虚拟节点
const myVnode1 = h('a', { props: { href: 'http://www.atguigu.com', target: '_blank' } }, '尚硅谷');
console.log(myVnode1);
三、2个参数:
data没有数据可以省略
const myVnode2 = h('div', '我是一个盒子');
四、虚拟节点上树:
//虚拟节点上树
const patch = init([classModule, propsModule, styleModule, eventListenersModule]);
const el = document.getElementById("container");
patch(el, myVnode3);
五、h函数嵌套:
const myVnode3 = h('ul', {}, [
h('li', {}, '牛奶'),
h('li', '咖啡',[
h('p','嘻嘻'),
h('p','哈哈')
]),
h('li', {}, '花生'),
])
6、手写patch【文本节点上树】
一、分析patch的过程
二、手写完成patch【文本节点】
1.新建patch.js文件:
patch 在发现新旧节点不一致的时候,需要新建节点,依赖createElement.js
//手写patch
import vnode from "./vnode";
import createElement from "./createElement";
export default function (oldVnode, newVnode) {
// 判断oldVnode是不是虚拟节点
if (oldVnode.sel == '' || oldVnode.sel == undefined) {
//传入的节点是DOM节点,需要包装成虚拟节点
oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode);
}
//判断新旧是否是同一个节点
if (oldVnode.sel === newVnode.sel && oldVnode.key === newVnode.key) {
console.log('是同一个节点');
} else {
// 不是同一个节点,暴力删除旧的,添加新的
createElement(newVnode, oldVnode.elm)
}
}
2.新建createElement.js:
createElement主要是将虚拟节点创建成DOM。插入到标杆之前,将DOM节点上树
//真正的创建节点,将vnode创建为DOM,插入到pivot之前
export default function (vnode, pivot) {
console.log('目的是把虚拟节点', vnode, '插入到标杆', pivot, '之前');
// 创建一个dom节点,domNode是一个孤儿节点
let domNode = document.createElement(vnode.sel);
// 判断vode是文本节点还是有子节点
if (vnode.text != '' && (vnode.children == undefined || vnode.children.length == 0)) {
// 文本节点
domNode.innerText = vnode.text;
// 孤儿节点上树插入到标杆之前
pivot.parentNode.insertBefore(domNode, pivot);
}
}
3.在index.js 调用自己的方法:
import h from "./mysabbdom/h";
import patch from "./mysabbdom/patch";
const vnode = h('h1', {}, '你好');
const container = document.getElementById('container');
patch(container, vnode);
4.界面的展示效果:
三、改造createElement函数
为了使createElement能处理递归的子节点,将上树操作放到patch.js中
1.createElement.js
//真正的创建节点,将vnode创建为DOM,插入到pivot之前
export default function (vnode) {
// 创建一个dom节点,domNode是一个孤儿节点
let domNode = document.createElement(vnode.sel);
// 判断vode是文本节点还是有子节点
if (vnode.text != '' && (vnode.children == undefined || vnode.children.length == 0)) {
// 文本节点
domNode.innerText = vnode.text;
// 补充elm属性
vnode.elm = domNode;
}
return vnode.elm;
}
2.patch.js
//手写patch
import vnode from "./vnode";
import createElement from "./createElement";
export default function (oldVnode, newVnode) {
// 判断oldVnode是不是虚拟节点
if (oldVnode.sel == '' || oldVnode.sel == undefined) {
//传入的节点是DOM节点,需要包装成虚拟节点
oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode);
}
//判断新旧是否是同一个节点
if (oldVnode.sel === newVnode.sel && oldVnode.key === newVnode.key) {
console.log('是同一个节点');
} else {
// 不是同一个节点,暴力删除旧的,添加新的
let newVnodeElm = createElement(newVnode);
// 孤儿节点上树
oldVnode.elm.parentNode.insertBefore(newVnodeElm,oldVnode.elm);
}
}
四、完成递归子节点
createElement.js:
createElement函数中完成递归子节点,节点是数组,获取到每一项子节点,将子节点转成DOM,追加到父节点中
//真正的创建节点,将vnode创建为DOM,插入到pivot之前
export default function createElement(vnode) {
// 创建一个dom节点,domNode是一个孤儿节点
let domNode = document.createElement(vnode.sel);
// 判断vode是文本节点还是有子节点
if (vnode.text != '' && (vnode.children == undefined || vnode.children.length == 0)) {
// 文本节点
domNode.innerText = vnode.text;
// 有子节点
} else if (Array.isArray(vnode.children) && vnode.children.length > 0) {
// 递归子节点
for (let i = 0; i < vnode.children.length; i++) {
let ch = vnode.children[i];
//将子节点变成DOM
let chDOM = createElement(ch);
// 将chDOM追加到父节点上
domNode.appendChild(chDOM);
}
}
// 补充elm属性
vnode.elm = domNode;
//返回elm,elm属性是一个纯DOM
return vnode.elm;
}
patch.js:
将返回的DOM节点上树,并且删除旧的节点
//手写patch
import vnode from "./vnode";
import createElement from "./createElement";
export default function (oldVnode, newVnode) {
// 判断oldVnode是不是虚拟节点
if (oldVnode.sel == '' || oldVnode.sel == undefined) {
//传入的节点是DOM节点,需要包装成虚拟节点
oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode);
}
//判断新旧是否是同一个节点
if (oldVnode.sel === newVnode.sel && oldVnode.key === newVnode.key) {
console.log('是同一个节点');
} else {
// 不是同一个节点,暴力删除旧的,添加新的
let newVnodeElm = createElement(newVnode);
// 孤儿节点上树
if (oldVnode.elm.parentNode && newVnodeElm) {
oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm);
}
// 删除老的节点
oldVnode.elm.parentNode.removeChild(oldVnode.elm);
}
}
界面效果:
按钮点击测试:
新旧节点不是同一节点,会暴力删除并新增
五、精细化比较
六、丰富pach.js
【精细化比较的部分,只完成了图中的绿色部分】
//手写patch
import vnode from "./vnode";
import createElement from "./createElement";
export default function (oldVnode, newVnode) {
// 判断oldVnode是不是虚拟节点
if (oldVnode.sel == '' || oldVnode.sel == undefined) {
//传入的节点是DOM节点,需要包装成虚拟节点
oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode);
}
//判断新旧是否是同一个节点
if (oldVnode.sel === newVnode.sel && oldVnode.key === newVnode.key) {
console.log('是同一个节点');
//【这部分后面会提成patchNode.js 方便递归调用】
// 判断oldVode和newVnode是不是同一个对象
if (newVnode === oldVnode) {
return;
};
//判断newVode有没有text
if (newVnode.text != undefined && (newVnode.children == undefined || newVnode.children.length == 0)) {
console.log('newVnode有text属性');
// 判断newVnode和oldVnode的text是否相同
if (newVnode.text != oldVnode.text) {
// 将newVnode的text给到oldNode.elm
oldVnode.elm.innerText = newVnode.text;
}
} else {
console.log('newVnode没有text属性');
// 判断oldVode有没有children
if (oldVnode.children != undefined && oldVnode.children.length > 0) {
// 最复杂的情况
//
//
//
} else {
// newVnode有children,oldVode没有
// ① 删除老节点
oldVnode.elm.innerHTML = ''
// ② 新节点的children添加到DOM
for (let i = 0; i < newVnode.children.length; i++) {
let dom = createElement(newVnode.children[i]);
oldVnode.elm.appendChild(dom);
}
}
}
} else {
// 不是同一个节点,暴力删除旧的,添加新的
let newVnodeElm = createElement(newVnode);
// 孤儿节点上树
if (oldVnode.elm.parentNode && newVnodeElm) {
oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm);
}
// 删除老的节点
oldVnode.elm.parentNode.removeChild(oldVnode.elm);
}
}
七、对代码和图示做说明:
newVnode有text,oldVnode可能是text也可能是children,做patch:
newVnode有children,oldVnode有text,做patch:
八、处理最复杂的情况
newVnode和oldVnode都有children:
出现一个四判定的算法 ① 新前与旧前 ② 新后与旧后 ③ 新后与旧前 ④ 新前与旧后
① 新前与旧前:
新前与旧前对比,如果命中,新前和旧前的指针会下移,如果没有命中,会走下一个判定,如果旧节点先循坏完毕,说明新节点中有要插入的节点,如果新节点先循坏完毕,说明旧节点中有删除的节点。
④ 新前与旧后:
当新前与旧后命中的时候,此时要移动节点,将新前节点移动到旧前节点的前面
③ 新后与旧前:
当新后与旧前命中的时候,此时需要移动节点,将新前节点移动到旧后的后面
总结:
九、diff更新子节点【四判定】
updateChildren.js
import patchVnode from "./patchVnode";
import createElement from './createElement'
function checkSameVnode(oldVnode, newVnode) {
return oldVnode.sel === newVnode.sel && oldVnode.key === newVnode.key;
}
export default function updateChildren(parentElm, oldCh, newCh) {
// 旧前
let oldStartIdx = 0;
// 新前
let newStartIdx = 0;
// 旧后
let oldEndIdx = oldCh.length - 1;
// 新后
let newEndIdx = newCh.length - 1;
// 旧前节点
let oldStartVnode = oldCh[0];
// 旧后节点
let oldEndVnode = oldCh[oldEndIdx];
// 新前节点
let newStartVnode = newCh[0];
// 新后节点
let newEndVnode = newCh[newEndIdx];
let keyMap = null;
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 略过已经加undefined标记的内容
if (oldStartVnode == null || oldCh[oldStartIdx] === undefined) {
oldStartVnode = oldCh[++oldStartIdx];
}
else if (oldEndVnode == null || oldCh[oldEndIdx] === undefined) {
oldEndVnode = oldCh[--oldEndIdx];
}
else if (newStartVnode == null || newCh[newStartIdx] === undefined) {
newStartVnode = newCh[++newStartIdx];
}
else if (newEndVnode == null || newCh[newEndIdx] === undefined) {
newEndVnode = newCh[--newEndIdx];
}
else if (checkSameVnode(oldStartVnode, newStartVnode)) {
// 新前与旧前
console.log('新前与旧前命中');
patchVnode(oldStartVnode, newStartVnode);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
}
else if (checkSameVnode(oldEndVnode, newEndVnode)) {
// 新后和旧后
console.log('新后和旧后命中');
patchVnode(oldEndVnode, newEndVnode);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndVnode];
}
else if (checkSameVnode(oldStartVnode, newEndVnode)) {
console.log('新后和旧前命中');
patchVnode(oldStartVnode, newEndVnode);
// 当新后与旧前命中的时候,此时要移动节点,移动新后指向的这个节点到老节点旧后的后面
parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
}
else if (checkSameVnode(oldEndVnode, newStartVnode)) {
// 新前和旧后
console.log('新前和旧后命中');
patchVnode(oldEndVnode, newStartVnode);
// 当新前和旧后命中的时候,此时要移动节点,移动新前指向的这个节点到老节点旧前的前面
parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
}
else {
// 四种都没有命中
// 制作keyMap一个映射对象,这样就不用每次都遍历老对象了
if (!keyMap) {
keyMap = {};
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
const key = oldCh[i].key;
if (key !== undefined) {
keyMap[key] = i;
}
}
}
// 寻找当前这项(newStartIdx)在keyMap中的映射的位置序号
const idxInOld = keyMap[newStartVnode.key];
if (idxInOld === undefined) {
// 如果idxInOld是undefined表示踏实全新的项,此时会将该项创建为DOM节点并插入到旧前之前
parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm);
}
else {
// 如果不是undefined,则不是全新的项,则需要移动
const elmToMove = oldCh[idxInOld];
patchVnode(elmToMove, newStartVnode);
// 把这项设置为undefined,表示已经处理完这项了
oldCh[idxInOld] = undefined;
// 移动
parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm);
}
// 指针下移,只移动新的头
newStartVnode = newCh[++newStartIdx];
}
}
// 循环结束后,处理未处理的项
if (newStartIdx <= newEndIdx) {
console.log('new还有剩余节点没有处理,要加项,把所有剩余的节点插入到oldStartIdx之前');
// 遍历新的newCh,添加到老的没有处理的之前
const before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
for (let i = newStartIdx; i <= newEndIdx; i++) {
// insertBefore方法可以自动识别null,如果是null就会自动排到队尾去
// newCh[i]现在还没有真正的DOM,所以要调用createElement函数变为DOM
parentElm.insertBefore(createElement(newCh[i]),before);
}
}
else if (oldStartIdx <= oldEndIdx) {
console.log('old还有剩余节点没有处理,要删除项');
// 批量删除oldStart和oldEnd指针之间的项
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
if (oldCh[i]) {
parentElm.removeChild(oldCh[i].elm);
}
}
}
}
patchVnode.js
import updateChildren from "./updateChildren";
export default function patchVnode(oldVnode, newVnode) {
// 判断oldVode和newVnode是不是同一个对象
if (newVnode === oldVnode) {
return;
};
//判断newVode有没有text
if (newVnode.text != undefined && (newVnode.children == undefined || newVnode.children.length == 0)) {
console.log('newVnode有text属性');
// 判断newVnode和oldVnode的text是否相同
if (newVnode.text != oldVnode.text) {
// 将newVnode的text给到oldNode.elm
oldVnode.elm.innerText = newVnode.text;
}
} else {
console.log('newVnode没有text属性');
// 判断oldVode有没有children
if (oldVnode.children != undefined && oldVnode.children.length > 0) {
// 最复杂的情况
// 老的有children,新的也有children
updateChildren(oldVnode.elm, oldVnode.children, newVnode.children)
} else {
// newVnode有children,oldVode没有
// ① 删除老节点
oldVnode.elm.innerHTML = ''
// ② 新节点的children添加到DOM
for (let i = 0; i < newVnode.children.length; i++) {
let dom = createElement(newVnode.children[i]);
oldVnode.elm.appendChild(dom);
}
}
}
}
vnode.js
vnode.js 增加了key
export default function vnode(sel, data, children, text, elm) {
//key值是标签的唯一标识,在data.key中,这里获取一下key
const key = data === undefined ? undefined : data.key
return { sel, data, children, text, elm, key };
}
index.js
import h from "./mysabbdom/h";
import patch from "./mysabbdom/patch";
// 命中新前与旧前 新增
const myVnode22 = h('ul', {}, [
h('li', { key: 'A' }, 'A'),
h('li', { key: 'B' }, 'B')
])
const myVnode33 = h('ul', {}, [
h('li', { key: 'A' }, 'A'),
h('li', { key: 'B' }, 'B'),
h('li', { key: 'B' }, 'C'),
h('li', { key: 'B' }, 'D'),
h('li', { key: 'B' }, 'E')
])
// 命中新前与旧前 删除
const myVnode222 = h('ul', {}, [
h('li', { key: 'A' }, 'A'),
h('li', { key: 'B' }, 'B'),
h('li', { key: 'C' }, 'C'),
h('li', { key: 'D' }, 'D'),
h('li', { key: 'E' }, 'E'),
])
const myVnode333 = h('ul', {}, [
h('li', { key: 'A' }, 'A'),
h('li', { key: 'B' }, 'B'),
])
// 命中新前和旧后
const myVnode2222 = h('ul', {}, [
h('li', { key: 'A' }, 'A'),
h('li', { key: 'B' }, 'B'),
h('li', { key: 'C' }, 'C'),
h('li', { key: 'D' }, 'D'),
h('li', { key: 'E' }, 'E')
])
const myVnode3333 = h('ul', {}, [
h('li', { key: 'E' }, 'E'),
h('li', { key: 'C' }, 'C'),
h('li', { key: 'M' }, 'M'),
])
// 命中新后与旧前
const myVnode22222 = h('ul', {}, [
h('li', { key: 'A' }, 'A'),
h('li', { key: 'B' }, 'B'),
h('li', { key: 'C' }, 'C'),
h('li', { key: 'D' }, 'D'),
h('li', { key: 'E' }, 'E')
])
const myVnode33333 = h('ul', {}, [
h('li', { key: 'E' }, 'E'),
h('li', { key: 'D' }, 'D'),
h('li', { key: 'C' }, 'C'),
h('li', { key: 'B' }, 'B'),
h('li', { key: 'A' }, 'A'),
])
const container = document.getElementById('container');
const btn = document.getElementById('btn');
patch(container, myVnode22222);
btn.onclick = function () {
patch(myVnode22222, myVnode33333)
}
十、diff算法的核心流程图
7、 手写h函数
一、vnode.js
h函数依赖vnode.js ,vnode主要是将虚拟dom的参数,转成对象返回
export default function (sel, data, children, text, elm) {
return {
sel, data, children, text, elm
}
}
二、h函数
h函数只能处理3个参数,其他情况暂时不考虑,判断第三个参数c的类型
形式1: h('sel', {}, '文字'); 直接将c作为text返回
形式2:h('sel', {}, []);对c进行遍历,将收集的children作为children返回
形式3:h('sel', {}, h()); 将c作为children直接返回
import vnode from './vnode'
// 编写一个低配版的h函数 只处理3个参数的情况
// 形式1: h('sel', {}, '文字');
// 形式2:h('sel', {}, []);
// 形式3:h('sel', {}, h());
export default function (sel, data, c) {
//判断参数的个数是否是3个
if (arguments.length != 3) {
throw new Error('对不起,参数个数只支持3个!');
}
//处理c的类型 形式1
if (typeof c === 'string' || typeof c === 'number') {
return vnode(sel, data, undefined, c, undefined)
//处理 形式2 数组
} else if (Array.isArray(c)) {
let children = [];
// 遍历数组的每一项,收集到children中
for (let i = 0; i < c.length; i++) {
// 检查c[i]必须是一个对象
if (!(typeof c[i] === 'object' && c[i].hasOwnProperty('sel'))) {
throw new Error('数组的一项不是h函数');
}
// 在调用c[i]的时候就执行了h函数,也就进行了c的类型判断
children.push(c[i]);
}
// 收集完,返回
return vnode(sel, data, children, undefined, undefined);
//处理形式3
} else if (typeof c === 'object' && c.hasOwnProperty('sel')) {
return vnode(sel, data, c, undefined, undefined)
} else {
throw new Error('有一项参数不规范!')
}
}
三、虚拟dom挂树
利用自己写的h函数,完成页面展示
import h from "./mysabbdom/h";
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule
} from "snabbdom";
const myVnode3 = h('ul', {}, [
h('li', {}, '牛奶'),
h('li', '咖啡', [
h('p', {}, '嘻嘻'),
h('p', {}, '哈哈')
]),
h('li', {}, '花生'),
])
//虚拟节点上树
const patch = init([classModule, propsModule, styleModule, eventListenersModule]);
const el = document.getElementById("container");
patch(el, myVnode3);
8、key值问题
vue 中 key 值的作用可以分为两种情况来考虑:
第一种情况是 v-if 中使用 key。由于 Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。因此当使用 v-if 来实现元素切换的时候,如果切换前后含有相同类型的元素,那么这个元素就会被复用。如果是相同的 input 元素,那么切换前后用户的输入不会被清除掉,这样是不符合需求的。因此可以通过使用 key 来唯一的标识一个元素,这个情况下,使用 key 的元素不会被复用。这个时候 key 的作用是用来标识一个独立的元素。
第二种情况是 v-for 中使用 key。用 v-for 更新已渲染过的元素列表时,它默认使用“就地复用”的策略。如果数据项的顺序发生了改变,Vue 不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处的每个元素。因此通过为每个列表项提供一个 key 值,来以便 Vue 跟踪元素的身份,从而高效的实现复用。这个时候 key 的作用是为了高效的更新渲染虚拟 DOM。
key 是为 Vue 中 vnode 的唯一标记,通过这个 key,diff 操作可以更准确、更快速
更准确:因为带 key 就不是就地复用了,在 sameNode 函数a.key === b.key对比中可以避免就地复用的情况。所以会更加准确。
更快速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快更快速
为什么不用index做key值?
使用index 作为 key和没写基本上没区别,因为不管数组的顺序怎么颠倒,index 都是 0, 1, 2...这样排列,导致 Vue 会复用错误的旧子节点,做很多额外的工作。