五星上将麦克阿瑟说过v-for不能没有key,就像西方不能没有耶路撒冷

正如麦克阿瑟所言,v-for不能没有key,就像西方不能没有耶路撒冷。这是为什么呢,是什么让五星上将如此重视?接下来走进科学,一步步揭开这层面纱

⭐️ Key 和 diff 算法

在vue3 中,在使用v-for 进行列表渲染时,官方建议为元素或组件绑定一个key属性。这样做主要是为了更好的执行diff算法,并确定需要删除、新增、移动位置的元素。由于vue3会根据每个元素的key值判断此类操作,因此key的作用非常重要。合适的key可以提高Dom更新速度,同时减少不必要的Dom操作。 key的值可以是number 或 string类型,但必须保证其唯一性。

⭐️ 认识 VNode和VDom

  1. 大家对key的解析可能会存在以下问题
  • 新旧nodes是什么?VNode又是什么?
  • 如果没有key,怎样才能尝试修改和复用节点呢?
  • 如果有key,如何按照key重新排列节点呢?
  1. Vuejs3 官网对key属性作用的具体解释如下:
  • Key 属性主要用在 Vuejs3 虚拟Dom算法中。在新旧节点对比时,用于辨识VNode
  • 如果不用key属性,Vuejs3会尝试使用一种算法,最大限度减少动态元素,并尽可能就地修改 或复用相同类型的元素
  • 如果用了key属性,Vuejs3 将根据key属性的值重新排列元素的顺序,并移除或销毁那些不存在key属性的元素
  1. 为了更好地理解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容器 插入了 divpspan 标签 并添加事件

🦄 虚拟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 方法更新列表。

patchKeyedChildrenpatchUnkeyedChildren方法,其实就是 Vuejs3中的差异算法(也称为diff算法)的内容。

相关推荐
Felven几秒前
C. Prefix Min and Suffix Max
算法
加农炮手Jinx几秒前
LeetCode 26. Remove Duplicates from Sorted Array 题解
算法·leetcode·力扣
加农炮手Jinx1 分钟前
LeetCode 88. Merge Sorted Array 题解
算法·leetcode·力扣
格林威1 分钟前
线阵工业相机:如何计算线阵相机的行频(Line Rate)?公式+实例
开发语言·人工智能·数码相机·算法·计算机视觉·工业相机·线阵相机
yueyue5434 分钟前
透过现象看本质:以fast_lio架构的整套算法的局部避障改为TEB算法为例深度探讨——如何成为一个合格的算法架构师?
算法·架构
梨花爱跨境4 分钟前
红人视频×A10算法:亚马逊转化率与流量闭环实战
算法
一叶飘零_sweeeet4 分钟前
2026 年 Java 面试必问:Spring AI 核心原理,90% 人答不全
java·面试·spring ai
阿Y加油吧9 分钟前
二刷 LeetCode:75. 颜色分类 & 31. 下一个排列 复盘笔记
笔记·算法·leetcode
风筝在晴天搁浅10 分钟前
LeetCode 378.有序矩阵中第K小的元素
算法·矩阵
rADu REME19 分钟前
SpringBoot + vue 管理系统
vue.js·spring boot·后端