Vue 的 v-for 列表循环

1,591 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 4 天,点击查看活动详情

文章首发于语雀,如有问题欢迎评论指正,感谢!


v-for可以用来循环数据,基本用语法是v-for="指令表达式",例如循环一个数组:

const app = {
  template: `
  <li v-for="(item, index) in items">
    {{ item.message }}
  </li>`,
  data(){
    return {
      items: [{ message: 'Foo' }, { message: 'Bar' }]
    }
  }
}

在使用v-for指令的时候,index属性表示当前项的下标,它是可选的。


你也可以使用of作为分隔符来替代in,这更接近JavaScript的迭代器语法:

const app = {
  template: `
  <li v-for="(item, index) of items">
    {{ item.message }}
  </li>
  <li v-for="(value, key, index) in person">
    {{ value }}
  </li>
  `,
  data(){
    return {
      items: [{ message: 'Foo' }, { message: 'Bar' }],
      person: {
        name: "张三",
        age: 28
      }
    }
  }
}

建议遍历可迭代对象时使用(item, index) of array,枚举对象的时候使用(value, key, index) of object

遍历数组

<ul>
  <li v-for="(item, index) of list" :key="item.id">{{ item.name }}</li>
</ul>

建议在使用v-for的时候搭配key属性,key属性必选是唯一的,方便Vue进行「就地更新策略」。

key值不建议使用数组的下标,这是因为当我们删除/新增项的时候不能保证key绝对的不变化;如果你的列表不会进行新增/删除数组的时候,可以使用index作为key

枚举对象

const app = {
  template: `
  <ul>
      <li v-for="(value, key, index) in privateInfo">
      	{{ key }}: {{ value }}
    		<template v-if="key === 'hobbies'">
      		<span v-for="(item, index) of value"> {{ item }}、 </span>
      	</template>
      </li>
  </ul>
  `,
  data(){
    return {
      privateInfo: {
        name: "Crystal",
        age: 18,
        hobbies: ["Travel", "Piano"]
      }
    }
  }
}

当使用v-for枚举对象的时候,遍历的顺序会基于对该对象调用Object.keys()的返回值来决定。

范围值

v-for可以直接接受一个整数值。在这种用例中,会将该模板基于 1...n 的取值范围重复多次。

<span v-for="n in 10">{{ n }}</span>

需要注意的是,n的初值是从 1 开始而非 0。

组件上使用 v-for

v-for可以直接应用在组件上使用,和在一般的元素上使用没有区别,同样需要提供key属性:

const app = {
  template: `
    <MyComponent
      v-for="(item, index) in items"
      :index="index"
      :key="item.id"
  	/>`
}

但是,这不会自动将任何数据传递给组件,因为组件有自己独立的作用域。 这会使组件与v-for的工作方式紧密耦合。明确其数据的来源可以使组件在其他情况下重用。

如果想将迭代后的数据传递到组件中,我们还需要传递props

const myComponent = {
  props: ['item']
}

const app = {
  template: `
    <MyComponent
      v-for="(item, index) in items"
      :item="item"
      :index="index"
      :key="item.id"
      />`
}

数组变化侦测

Vue3中,Vue能够侦听响应式数组的变更方法,并在它们被调用时触发相关的更新。这些变更方法包括:

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

但是在Vue2在,Vue通过重写数组相关的方法来监听数组的变更,通过在数组的原型上新增了一层原型来达到拦截:
Xnip2023-02-10_10-27-05.jpg
这是因为Object.defineProperty()只能实现对对象的属性进行拦截,无法对数组的属性进行拦截,例如下面的例子:

var vm = {
  data: {
    a: 1,
    b: 2,
    list: [1, 2, 3, 4, 5]
  }
};

for (const key in vm.data) {
  Object.defineProperty(vm, key, {
    get() {
      console.log("数据获取");
      return vm.data[key];
    },
    set(newVal) {
      console.log("数据设置");
      vm.data[key] = newVal;
    }
  });
}

以上代码,我们定义了datadata里有数组,然后我们分别去操作这些属性看看变化。

当我们直接把数组赋值为一个新数组的时候,数组是可以被拦截的:

vm.a = 1;
vm.list = [2, 3, 4, 5, 6];
console.log(vm.list);

image.png

当我们调用push方法的时候,数据确实发生了变化,但是set机制却没有触发:

vm.list.push(6); 
console.log(vm.list);

image.png
可以看到图片中,出现两次“数据获取”,这是因为我们在第一行中vm.list的时候执行了get机制,然后才能调用push()方法。


Object.defineProperty()没办法监听下列方法对数组的变更:

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

但是当数组重新赋值的是却能被拦截的到,所以Vue2对上面这些方法进行了包裹封装(类似重写),大致的响应式原理如下:

// 定义一个 data 对象
// Object.defineProperty 可以重新定义属性 给属性安插 getter setter 方法
let data = {
  name: '潘公子',
  age: [1, 2, 3]
}

// Array.prototype.push = function() {}
data.age.push(4)

// 执行观察者模式
observer(data)

// 专门用于劫持数据的
function observer(target) {
  if (typeof target !== 'object' || typeof target == null) {
    return target
  }

  if (Array.isArray(target)) {

    // 保存数组原本的原型
    let oldArrayPrototype = Array.prototype
    let proto = Object.create(oldArrayPrototype) // 继承

    Array.from(['push', 'shift', 'unshift', 'pop']).forEach(method => {
      // 函数劫持,把函数重写 
      proto[method] = function () { 
        // 执行数组原本的方法
        oldArrayPrototype[method].call(this, ...arguments)
        // 更新视图
        updateView()
      }
    })

    // 给数组新增一个原型,target.__proto__ = proto
    Object.setPrototypeOf(target, proto)
  }

  // 如果是对象直接执行响应式
  for (let key in target) {
    defineReactive(target, key, target[key])
  }
}

// 执行响应式
function defineReactive(target, key, value) {
  // 递归执行
  observer(value)
  // Object.defineProperty 只能劫持对象
  Object.defineProperty(target, key, {  
    get() {
      return value
    },
    set(newVal) {
      if (newVal !== value) {
        value = newVal
        updateView()
      }
    }
  })
}

function updateView() {
  console.log('更新视图');
}

以上就是Vue2对对象和数组进行的响应式拦截大概思路。


那么我们替换原数组的数据是否会重新渲染整个DOM列表(性能原因)?
不一定,Vue在对dom操作的时候进行了大量的新旧节点信息的对比算法,Vue会把dom重新渲染的程度最小化,做到已有的dom节点最大化的复用。