likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 3

正如麦克阿瑟所言,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

<div onclick="onAlert()"><p>点我</p><span></span></div>
<script>
    function onAlert() {
      alert('hello world!')
    }
</script>

🦄 转化为虚拟Dom

const vnode = {
  tag: 'div',
  props: {
    onClick: () => alert('hello world!')
  },
  children: [
    {
      tag: 'p',
      children: '点我'
    },
    {
      tag: 'span',
    }
  ]
}

📌 渲染器render实现

/**
* @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传入函数执行看下效果

// 把虚拟dom渲染到id为app的元素下
renderer(vnode, document.getElementById('app'))

效果如下:

  • app容器 插入了 divpspan 标签 并添加事件

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

🦄 虚拟dom描述组件

以上讲了如何使用虚拟dom(vnode)描述真实dom,但还不够!如果我们封装了一个组件,又该如何使用虚拟dom进行描述呢?

🔨 方法组件

const MyComponent = function () {
  return {
    tag: 'div',
    props: {
      onClick: () => alert('MyComponent点击事件回调函数')
    },
    children: [
      {
        tag: 'span',
        children: 'MyComponent'
      },
      {
        tag: 'span',
      }
    ]
  }
}

🔨 对象组件

const MyComponent2 = {
  render() {
    return {
      tag: 'div',
      props: {
        onClick: () => alert('MyComponent2点击事件回调函数')
      },
      children: [
        {
          tag: 'span',
          children: 'MyComponent2'
        },
        {
          tag: 'span',
        }
      ]
    }
  }
}

🔨 修改渲染器支持组件渲染

/**
 * @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)
}

🔨 渲染组件

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'))

下图可看到,对应的组件及事件都已经挂载成功!

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

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

key 的作用和diff算法

先看一个例子,单击 button 按钮,会在列表中间插入一个 f 字符串,代买如下所示:

<!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算法)的内容。