正如麦克阿瑟所言,v-for不能没有key,就像西方不能没有耶路撒冷。这是为什么呢,是什么让五星上将如此重视?接下来走进科学,一步步揭开这层面纱
⭐️ Key 和 diff 算法
在vue3 中,在使用v-for 进行列表渲染时,官方建议为元素或组件绑定一个key属性。这样做主要是为了更好的执行diff算法,并确定需要删除、新增、移动位置的元素。由于vue3会根据每个元素的key值判断此类操作,因此key的作用非常重要。合适的key可以提高Dom更新速度,同时减少不必要的Dom操作。 key的值可以是number 或 string类型,但必须保证其唯一性。
⭐️ 认识 VNode和VDom
- 大家对key的解析可能会存在以下问题
- 新旧nodes是什么?VNode又是什么?
- 如果没有key,怎样才能尝试修改和复用节点呢?
- 如果有key,如何按照key重新排列节点呢?
- Vuejs3 官网对key属性作用的具体解释如下:
- Key 属性主要用在 Vuejs3 虚拟Dom算法中。在新旧节点对比时,用于辨识VNode
- 如果不用key属性,Vuejs3会尝试使用一种算法,最大限度减少动态元素,并尽可能就地修改 或复用相同类型的元素
- 如果用了key属性,Vuejs3 将根据key属性的值重新排列元素的顺序,并移除或销毁那些不存在key属性的元素
- 为了更好地理解key 的作用,下面先介绍一下VNode的概念
- VNode的全称是 virtual node,也就是虚拟节点
- 事实上,无论是组件还是元素,它们最终在vuejs3中表示出来的都是一个个VNode
- VNode本质上是一个Javascript的对象,代码如下所示:
🦄 真实Dom
html
<div onclick="onAlert()"><p>点我</p><span></span></div>
<script>
function onAlert() {
alert('hello world!')
}
</script>
🦄 转化为虚拟Dom
js
const vnode = {
tag: 'div',
props: {
onClick: () => alert('hello world!')
},
children: [
{
tag: 'p',
children: '点我'
},
{
tag: 'span',
}
]
}
📌 渲染器render实现
js
/**
* @param {object} vnode 虚拟dom对象
* @param {HTMLElement} container 挂载虚拟dom的真实dom容器
*/
function renderer(vnode, container) {
const { tag, props, children } = vnode
const el = document.createElement(tag)
for (const key in props) {
if (/^on/.test(key)) {
// 转换为合法的监听事件名称
const eventNmae = key.substring(2).toLowerCase()
// 在当前创建的el元素上挂载监听事件
el.addEventListener(eventNmae, props[key])
}
}
if (typeof children === 'string') {
// 创建一个文本节点添加到el元素下
el.appendChild(document.createTextNode(children))
} else if (Array.isArray(children)) {
// 子节点为数组,递归调用renderer函数
children.forEach(vnode => renderer(vnode, el))
}
// 将元素挂载到容器上
container.appendChild(el)
}
现在将上面转换的虚拟dom传入函数执行看下效果
js
// 把虚拟dom渲染到id为app的元素下
renderer(vnode, document.getElementById('app'))
效果如下:
- 在
app容器
插入了div
、p
、span
标签 并添加事件
🦄 虚拟dom描述组件
以上讲了如何使用虚拟dom(vnode
)描述真实dom,但还不够!如果我们封装了一个组件
,又该如何使用虚拟dom进行描述呢?
🔨 方法组件
js
const MyComponent = function () {
return {
tag: 'div',
props: {
onClick: () => alert('MyComponent点击事件回调函数')
},
children: [
{
tag: 'span',
children: 'MyComponent'
},
{
tag: 'span',
}
]
}
}
🔨 对象组件
js
const MyComponent2 = {
render() {
return {
tag: 'div',
props: {
onClick: () => alert('MyComponent2点击事件回调函数')
},
children: [
{
tag: 'span',
children: 'MyComponent2'
},
{
tag: 'span',
}
]
}
}
}
🔨 修改渲染器支持组件渲染
js
/**
* @param {object} vnode 虚拟dom对象
* @param {HTMLElement} container 挂载虚拟dom的真实dom容器
*/
function renderer(vnode, container) {
const { tag } = vnode
if(typeof tag === 'string') {
mountElement(vnode, container)
} else if(typeof tag === 'function') {
mountComponent(tag(), container)
} else if(typeof tag === 'object') {
mountComponent(tag.render(), container)
}
}
function mountElement(vnode, container) {
const { tag, props, children } = vnode
const el = document.createElement(tag)
for(const key in props) {
if(/^on/.test(key)) {
// 转换为合法的监听事件名称
const eventNmae = key.substring(2).toLowerCase()
// 在当前创建的el元素上挂载监听事件
el.addEventListener(eventNmae, props[key])
}
}
if(typeof children === 'string') {
// 创建一个文本节点添加到el元素下
el.appendChild(document.createTextNode(children))
} else if(Array.isArray(children)) {
// 子节点为数组,递归调用renderer函数
children.forEach(vnode => renderer(vnode, el))
}
console.log(container)
// 将元素挂载到容器上
container.appendChild(el)
}
function mountComponent(vnode, container) {
// 递归调用renderer
renderer(vnode, container)
}
🔨 渲染组件
js
const vnode = {
tag: 'div',
children: [
{
tag: 'span',
props: {
onClick: () => alert('span点击事件回调函数')
},
children: '我是span标签'
},
// 组件
{
tag: MyComponent,
},
// 组件
{
tag: MyComponent2,
}
]
}
// 把虚拟dom渲染到id为app的元素下
renderer(vnode, document.getElementById('app'))
下图可看到,对应的组件及事件都已经挂载成功!
key 的作用和diff算法
先看一个例子,单击 button
按钮,会在列表中间插入一个 f
字符串,代买如下所示:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<template id="myapp">
<ul>
<li v-for="item in list" :key="item">{{item}}</li>
</ul>
<button @click="insertF">添加</button>
</template>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
const App = {
template: "#myapp",
data() {
return {
list: ["a", "b", "c", "d"]
}
},
methods: {
insertF() {
this.list.splice(2, 0, 'f')
}
},
}
Vue.createApp(App).mount('#app')
</script>
</body>
</html>
可以看到,在<li>
元素中使用v-for指令遍历list数组来展示a,b,c,d 字符,并为<li>
元素绑定里 key
属性,key对应的值是数组的每一项(key的值要保证唯一)。
接着,当单击 <button>
的时候,会回调 insetF
函数。然后在该函数中调用list 数组的splice方法,在索引为2处插入 "f",会触发视图更新。
下一篇
麦克阿瑟曾经说过,如果一篇文章太长,那么就拆成两篇
后续 来分分析一下 Vuejs3列表更新的原理。Vuejs3 会根据列表项有没有key而调用不同的方法来更新列表.
- 如果有key,则调用
patchKeyedChildren
方法更新列表. - 如果没有key,则调用
patchUnkeyedChildren
方法更新列表。
patchKeyedChildren
和 patchUnkeyedChildren
方法,其实就是 Vuejs3
中的差异算法(也称为diff算法
)的内容。