Code前端首页关于Code前端联系我们

Vue3的v-for到底怎么用?和Vue2比有哪些不一样的避坑点?

terry 2小时前 阅读数 42 #Vue
文章标签 for用法for避坑

平时刷Vue3社区问答的时候,总能看到不少人踩v-for的坑,比如key属性到底能不能省、组合式API(特别是script setup语法糖)里怎么配合v-for绑定事件或者拿到循环索引、为什么循环对象数组突然报错不像Vue2那样默认好使?今天咱们就把这些高频问题拆碎了聊,顺便把v-for从基础到进阶用明白。

先搞懂v-for的基础语法——Vue3里变没变?

先说个好消息:v-for的核心遍历逻辑和大部分基础语法,和Vue2是几乎完全一致的,不会让你刚学完的东西白学,不管是遍历数组、遍历普通对象、遍历数字还是遍历字符串,写法都差不多,换汤不换药,汤的“鲜度”(也就是底层渲染效率)倒是提了不少。 遍历数组肯定是最常用的场景对吧?咱们先拿这个举个小例子感受一下,数组循环一般能拿到两个值:第一个是当前遍历到的元素项item,第二个是**循环索引index,顺序永远是item在前index在后,这个可记牢,不管Vue2还是Vue3都没改,比如我们有个商品列表数组productList:

<template>
  <ul>
    <!-- 基础遍历:只拿元素项 -->
    <li v-for="product in productList" :key="product.id">
      {{ product.name }} - {{ product.price }}
    </li>
    <!-- 带索引的遍历 -->
    <li v-for="(product, index) in productList" :key="product.id">
      第{{ index + 1 }}件:{{ product.name }}
    </li>
    <!-- 遍历普通对象:键值对 + 键名 + 索引(可选) -->
    <div v-for="(value, key, i) in productList[0]" :key="key">
      第{{ i + 1 }}个属性:{{ key }} = {{ value }}
    </div>
    <!-- 遍历数字:从1开始数到指定数 -->
    <span v-for="num in 5" :key="num">{{ num }} </span>
    <!-- 遍历字符串:拆成单个字符 -->
    <span v-for="char in 'Hello Vue3'" :key="char">{{ char }}</span>
  </ul>
</template>
<script setup>
import { ref } from 'vue'
const productList = ref([
  { id: 1, name: '无线蓝牙耳机', price: 199 },
  { id: 2, name: '机械键盘青轴', price: 399 },
  { id: 3, name: '便携充电宝20000mAh', price: 99 }
])
</script>

看上面这段代码,数组遍历里冒号后面的那个:key="product.id",我把它单独拎出来放在进阶避坑第一个重点讲,这个是Vue3里和Vue2变化最核心的规则之一,哦对,刚才说遍历对象的时候,顺序有没有调整?之前查Vue2的文档好像是Vue2.x是(值, 键, 索引)对吧?对,这个也没变,放心用就行,还有遍历数字,还是从1开始到N,不是0,要是习惯从零开始写代码的时候换成of也行?对,v-for="(item of productList"或者v-for="(item in productList)",在Vue里遍历可迭代对象(数组、对象包装后的普通数组?哦普通对象不能直接用of的包装,of遍历对象的话,用in和of好像都能用吗?等下我刚才写的都是in?对,不过ES6里遍历数组推荐of更规范,但Vue里in和of在遍历数组、数字、字符串上表现几乎完全相同,随便你用哪个都行,不过个人习惯用in,因为Vue2用惯了,但要是想区分ES6原生的话也没问题,随便选,不会报错。

避坑第一点:key属性Vue3里绝对不能随便用index或者省略!

这绝对是第一个必须讲烂了但大家还是天天踩的坑,Vue3里这个规则比Vue2严太多了!首先说省略的问题:Vue2.x你要是偶尔偷懒省略key,控制台只会给个黄色警告,不会报错,渲染也能凑合用,顶多偶尔列表闪一下或者数据不对;但Vue3里如果你用的是Vue 3.2.x以上的版本(现在基本没人用旧版了吧?),要是在同一层级的多个v-for或者v-for和v-if混用的元素的时候省略key,会不会?哦不管什么情况,Vue3的编译阶段都会给黄色警告吗?对,甚至在某些特殊场景(比如用了<transition-group过渡动画组件),没有key就直接报错!所以千万不能省! 接下来第二个坑:能不能用index当key?很多人写代码的时候嫌找唯一标识太麻烦,直接写:key="index",这个在Vue2里偶尔能用,但Vue3里绝对要谨慎!哦不是说完全不能用,而是只有在下面这3种极端情况可以用index当key不会出问题:1. 这个列表是完全静态的,不会有删除、插入、排序、过滤这种操作;2. 列表的顺序永远不会变;3. 列表里的元素没有自己的状态(比如没有输入框、没有选中状态、没有动画状态这些),其他时候,用index当key会出什么问题呢?举个最直观的小例子吧:假设你有个待办事项列表,每个待办项前面有个复选框,你用index当key,当你删除第二项的时候,第三项会跑到第二项的位置,key从2变成1,但Vue的虚拟DOM对比的时候,会复用key为1和2的DOM节点,也就是删除原来的第二个,把第三个的内容改成原来的第二个,把第四个改成第三个,但复选框的选中状态,Vue3的虚拟DOM更新机制会保留原来的DOM节点状态!原来的第二个节点是选中的,第三个是未选中的,现在第三个变成了原来的第二个的key,就会把原来第二个的选中状态保留到新的第二个(也就是原来的第三个),新的第三个就变成了未选中的,这个就很离谱了对吧? 那正确的key应该怎么选?**一定要选列表里每个元素的唯一的、不会变的、稳定的值,比如数据库里的id、uuid、或者后端给的唯一标识符,实在没有的话,也别用index,自己给每个元素加个临时的唯一值,比如用Date.now() + Math.random()生成一个临时的id,放在数组里的每个元素身上。

// 给没有唯一标识的数组元素加临时id
const tempList = [
  { name: '买菜', done: false },
  { name: '做饭', done: false },
  { name: '洗碗', done: false }
].map(item => ({ ...item, tempId: Date.now() + Math.random() * 100000000000 })

这个临时id只要在当前列表渲染周期里是唯一的就行,刷新页面没关系,下次渲染又会重新生成,但没关系,因为刷新页面整个DOM都会重新渲染,不会有复用的问题。

避坑第二点:v-for和v-if在Vue3里的优先级彻底变了!

这个是Vue3和Vue2比最大的语法规则的语法规则的语法规则的语法规则的变化之一,哦对,语法规则变化最大的一个点!先回忆一下Vue2.x的时候,v-for的优先级是高于v-if的,也就是说,当你把v-for和v-if写在同一个元素上的时候,Vue2.x会先把所有元素都遍历出来,然后再对每个遍历出来的元素判断要不要渲染不渲染,这样效率非常低,特别是当列表很大的时候,会浪费很多性能,生成很多没用的虚拟DOM节点,对吧? 那Vue3里把优先级彻底反过来了!现在是v-if的优先级高于v-for,什么意思?举个例子:

<!-- 错误写法!Vue3里会报错或者逻辑不对!
<template>
  <ul>
    <!-- 这里的product在v-if里根本找不到!因为先执行v-if,此时product还没遍历出来! -->
    <li v-for="product in productList" v-if="product.done" :key="product.id">
      {{ product.name }}
    </li>
  </ul>
</template>

刚才那段代码在Vue3里根本跑不通对吧?因为先判断v-if的时候,还没开始遍历productList,product还是未定义的,会直接报“product is not defined”的错误,那这个时候怎么办呢?有两种解决方案: 第一种是把v-if移到父元素上,也就是把v-for放在一个包裹元素(比如template标签,因为template标签不会生成真实的DOM节点,不会影响页面结构),然后把v-if写在template的子元素上?不对不对,等下刚才是想只渲染done为true的product,应该是把v-for放在template上,v-if放在template的子元素上?哦不对不对,刚才优先级的问题是v-if和v-for在同一个元素上对吧?那第一种解决方案是**把v-for写在一个template标签上,把v-if写在template的子元素上?或者反过来?等下举个正确的例子吧:

<!-- 正确写法一:用template标签包裹v-for,把v-if写在里面的li上,或者反过来?不,刚才说v-if优先级高,要是把v-if写在template上,那先判断template要不要渲染,那如果productList里有一个done为true的,但template上的v-if判断的是product,那还是不行,哦对,正确的写法一是**把v-for写在template标签上,把v-if写在template的子元素(比如li)上**,这样的话,v-for在template上先遍历,然后每个遍历出来的template包裹的子元素(li)再执行v-if,逻辑就对了,而且template不会生成真实的DOM,不会影响页面结构:
```html
<template>
  <ul>
    <template v-for="product in productList" :key="product.id">
      <li v-if="product.done">
        {{ product.name }}
      </li>
    </template>
  </ul>
</template>

哦等下刚才这个写法对吗?template标签上必须加key对吧?对,因为v-for在template上,所以key必须加在template上,不能加在里面的li上,这点也要注意! 第二种解决方案是提前在组合式API里用computed计算属性过滤好数组,然后直接遍历过滤后的数组,这种写法效率更高,代码也更清晰,更推荐大家用这种写法:

<template>
  <ul>
    <li v-for="product in doneProductList" :key="product.id">
      {{ product.name }}
    </li>
  </ul>
</template>
<script setup>
import { ref, computed } from 'vue'
const productList = ref([
  { id: 1, name: '买菜', done: true },
  { id: 2, name: '做饭', done: false },
  { id: 3, name: '洗碗', done: true }
])
// 提前用computed计算属性过滤好
const doneProductList = computed(() => {
  return productList.value.filter(product => product.done)
})
</script>

提前用computed的好处是:第一,computed是有缓存的,只有当productList里的内容(或者说doneProductList依赖的内容)发生变化的时候,才会重新计算过滤后的数组,否则直接用缓存的结果,效率比每次模板渲染都遍历一遍再过滤高太多了;第二,代码逻辑更清晰,模板只负责展示,数据处理都放在script里面,符合Vue3的“关注点分离”的思想对吧?

避坑第三点:script setup里怎么拿循环元素、怎么绑定带循环参数的事件?

这个也是大家刚从Vue2的选项式API转到Vue3的script setup语法糖的时候最常问的问题,其实很简单,和Vue2差不多,只是事件绑定的时候直接传循环元素或者循环索引就行,script setup里定义的函数直接接收参数就行,不用什么this了,因为script setup是Vue3的编译期语法糖,里面定义的变量和函数都是暴露给模板的,可以直接用,举个小例子:比如点击商品列表里的某个商品,弹出它的名字和价格:

<template>
  <ul>
    <li v-for="product in productList" :key="product.id" @click="handleProductClick(product, index)">
      {{ product.name }} - {{ product.price }}
    </li>
  </ul>
</template>
<script setup>
import { ref } from 'vue'
const productList = ref([
  { id: 1, name: '无线蓝牙耳机', price: 199 },
  { id: 2, name: '机械键盘青轴', price: 399 },
  { id: 3, name: '便携充电宝20000mAh', price: 99 }
])
// 直接定义函数,接收参数就行
const handleProductClick = (product, index) => {
  alert(`你点击了第${index + 1}件商品:${product.name},价格是${product.price}元`)
}
</script>

哦对,还有个常见的问题:比如在循环里用ref获取DOM元素,怎么获取每个循环元素对应的DOM呢?Vue2里可以用v-for和ref="xxx"结合,然后通过this.$refs.xxx拿到一个数组对吧?Vue3里也差不多,但要稍微注意一下,因为Vue3里的ref是响应式的,而且script setup里不能直接用this.$refs了,所以要定义一个空的ref数组,然后在循环里给DOM元素绑定:ref="(el) => { if(el) domList.push(el) }",这样就能拿到每个循环元素对应的DOM了,举个小例子:比如点击商品列表里的某个商品,让它对应的DOM元素背景变色:

<template>
  <ul>
    <li 
      v-for="(product, index) in productList" 
      :key="product.id" 
      :ref="(el) => { if(el) domList.push(el) }"
      @click="handleProductClick(index)"
    >
      {{ product.name }} - {{ product.price }}
    </li>
  </ul>
</template>
<script setup>
import { ref, onBeforeUpdate } from 'vue'
const productList = ref([
  { id: 1, name: '无线蓝牙耳机', price: 199 },
  { id: 2, name: '机械键盘青轴', price: 399 },
  { id: 3, name: '便携充电宝20000mAh', price: 99 }
])
// 定义一个空的ref数组,用来存循环DOM
const domList = ref([])
// 记得在每次DOM更新之前清空这个数组,不然会重复添加!
onBeforeUpdate(() => {
  domList.value = []
})
const handleProductClick = (index) => {
  // 先把所有DOM的背景色重置
  domList.value.forEach(dom => {
    dom.style.backgroundColor = ''
  })
  // 然后把点击的那个DOM的背景色改成黄色
  domList.value[index].style.backgroundColor = 'yellow'
}
</script>

哦对,刚才那段代码里加了个onBeforeUpdate生命周期钩子,这个很重要!因为每次productList发生变化的时候,DOM会重新渲染,循环DOM也会重新生成,所以如果不每次更新之前清空domList数组的话,数组里会有旧的DOM节点,就会出问题,比如点击某个商品的时候,找不到对应的DOM,或者找到的是旧的,这点一定要记牢!

进阶用法:v-for和结合动态渲染组件

这个是v-for的进阶用法之一,经常用在比如标签页切换、动态表单生成这些场景里,Vue3里这个用法和Vue2差不多,就是用动态绑定组件名,然后结合v-for遍历组件配置数组就行,举个小例子:比如有个标签页,每个标签页对应一个不同的组件:

<template>
  <div class="tab-container">
    <div class="tab-header">
      <span 
        v-for="(tab, index) in tabList" 
        :key="tab.id"
        :class="{ active: activeTab === index }"
        @click="activeTab = index"
      >
        {{ tab.name }}
      </span>
    </div>
    <div class="tab-content">
      <!-- 动态渲染组件,记得给组件传props也可以直接写在这里哦
      <!-- 哦对,给动态组件传props的话,直接用v-bind="tab.props"就行,批量传很方便 -->
      <component :is="tabList[activeTab].component" v-bind="tabList[activeTab].props" />
    </div>
  </div>
</template>
<script setup>
import { ref } from 'vue'
// 引入不同的标签页组件
import TabHome from './components/TabHome.vue'
import TabAbout from './components/TabAbout.vue'
import TabContact from './components/TabContact.vue'
// 标签页配置数组
const tabList = ref([
  { id: 1, name: '首页', component: TabHome, props: { title: '欢迎来到首页' } },
  { id: 2, name: '关于我们', component: TabAbout, props: { title: '了解我们' } },
  { id: 3, name: '联系我们', component: TabContact, props: { title: '有问题找我们' } }
])
// 当前激活的标签页索引
const activeTab = ref(0)
</script>
<style scoped>
.tab-header span {
  display: inline-block;
  padding: 10px 20px;
  cursor: pointer;
  margin-right: 10px;
  border: 1px solid #ccc;
  border-bottom: none;
  border-radius: 5px 5px 0 0;
}
.tab-header span.active {
  background-color: #fff;
  border-color: #000;
  border-bottom: 1px solid #fff;
  margin-bottom: -1px;
}
.tab-content {
  border: 1px solid #000;
  padding: 20px;
}
</style>

刚才那段代码里,我们用了个配置数组tabList,里面每个元素包含了标签页的id、名字、对应的组件、以及要传给组件的props,然后用v-for遍历配置数组生成标签页的头部,点击标签页头部的时候,activeTab会变成对应的索引,然后会根据activeTab的索引动态渲染对应的组件,同时用v-bind="tabList[activeTab].props"批量传props,这个写法是不是很方便?

总结一下Vue3 v-for的重点和避坑点

今天咱们聊了Vue3 v-for的基础语法、和Vue2比的最大变化、以及几个常见的避坑点、还有进阶用法,现在总结一下重点:

  1. 基础语法和Vue2几乎完全一致,遍历数组、对象、数字、字符串都可以用in或者of,推荐用in习惯用of都没问题;
  2. key属性绝对不能省,尽量不要用index当key,除非列表完全静态、顺序永远不变、元素没有自己的状态,正确的key应该选唯一的、稳定的值;
  3. v-for和v-if在同一个元素上的优先级彻底变了,现在是v-if优先级高于v-for,解决方法要么用template标签包裹v-for,要么提前用computed计算属性过滤好数组,更推荐后者;
  4. script setup里绑定带循环参数的事件直接传就行,获取循环DOM的话要定义一个空的ref数组,绑定:ref="(el) => { if(el) domList.push(el) }",然后在onBeforeUpdate生命周期钩子里面清空数组;
  5. v-for和结合可以动态渲染组件,配合配置数组批量传props很方便。

好了,今天关于Vue3 v-for的内容就聊到这里了,要是还有什么不懂的问题,欢迎在评论区留言讨论哦!

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门