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

  1. const app = {
  2. template: `
  3. <li v-for="(item, index) in items">
  4. {{ item.message }}
  5. </li>`,
  6. data(){
  7. return {
  8. items: [{ message: 'Foo' }, { message: 'Bar' }]
  9. }
  10. }
  11. }

在使用v-for指令的时候,index属性表示当前项的下标,它是可选的。
你也可以使用of作为分隔符来替代in,这更接近JavaScript的迭代器语法:

  1. const app = {
  2. template: `
  3. <li v-for="(item, index) of items">
  4. {{ item.message }}
  5. </li>
  6. <li v-for="(value, key, index) in person">
  7. {{ value }}
  8. </li>
  9. `,
  10. data(){
  11. return {
  12. items: [{ message: 'Foo' }, { message: 'Bar' }],
  13. person: {
  14. name: "张三",
  15. age: 28
  16. }
  17. }
  18. }
  19. }

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

遍历数组

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

建议在使用**v-for**的时候搭配**key**属性,**key**属性必选是唯一的,方便**Vue**进行「就地更新策略」。
key值不建议使用数组的下标,这是因为当我们删除/新增项的时候不能保证key绝对的不变化;如果你的列表不会进行新增/删除数组的时候,可以使用index作为key

枚举对象

  1. const app = {
  2. template: `
  3. <ul>
  4. <li v-for="(value, key, index) in privateInfo">
  5. {{ key }}: {{ value }}
  6. <template v-if="key === 'hobbies'">
  7. <span v-for="(item, index) of value"> {{ item }}、 </span>
  8. </template>
  9. </li>
  10. </ul>
  11. `,
  12. data(){
  13. return {
  14. privateInfo: {
  15. name: "Crystal",
  16. age: 18,
  17. hobbies: ["Travel", "Piano"]
  18. }
  19. }
  20. }
  21. }

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

范围值

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

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

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

组件上使用 v-for

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

  1. const app = {
  2. template: `
  3. <MyComponent
  4. v-for="(item, index) in items"
  5. :index="index"
  6. :key="item.id"
  7. />`
  8. }

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

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

  1. const myComponent = {
  2. props: ['item']
  3. }
  4. const app = {
  5. template: `
  6. <MyComponent
  7. v-for="(item, index) in items"
  8. :item="item"
  9. :index="index"
  10. :key="item.id"
  11. />`
  12. }

数组变化侦测

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

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

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

  1. var vm = {
  2. data: {
  3. a: 1,
  4. b: 2,
  5. list: [1, 2, 3, 4, 5]
  6. }
  7. };
  8. for (const key in vm.data) {
  9. Object.defineProperty(vm, key, {
  10. get() {
  11. console.log("数据获取");
  12. return vm.data[key];
  13. },
  14. set(newVal) {
  15. console.log("数据设置");
  16. vm.data[key] = newVal;
  17. }
  18. });
  19. }

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

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

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

image.png

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

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

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

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

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

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

  1. // 定义一个 data 对象
  2. // Object.defineProperty 可以重新定义属性 给属性安插 getter setter 方法
  3. let data = {
  4. name: 'xiechen',
  5. age: [1, 2, 3]
  6. }
  7. // Array.prototype.push = function() {}
  8. data.age.push(4)
  9. // 执行观察者模式
  10. observer(data)
  11. // 专门用于劫持数据的
  12. function observer(target) {
  13. if (typeof target !== 'object' || typeof target == null) {
  14. return target
  15. }
  16. if (Array.isArray(target)) {
  17. // 保存数组原本的原型
  18. let oldArrayPrototype = Array.prototype
  19. let proto = Object.create(oldArrayPrototype) // 继承
  20. Array.from(['push', 'shift', 'unshift', 'pop']).forEach(method => {
  21. // 函数劫持,把函数重写
  22. proto[method] = function () {
  23. // 执行数组原本的方法
  24. oldArrayPrototype[method].call(this, ...arguments)
  25. // 更新视图
  26. updateView()
  27. }
  28. })
  29. // 给数组新增一个原型,target.__proto__ = proto
  30. Object.setPrototypeOf(target, proto)
  31. }
  32. // 如果是对象直接执行响应式
  33. for (let key in target) {
  34. defineReactive(target, key, target[key])
  35. }
  36. }
  37. // 执行响应式
  38. function defineReactive(target, key, value) {
  39. // 递归执行
  40. observer(value)
  41. // Object.defineProperty 只能劫持对象
  42. Object.defineProperty(target, key, {
  43. get() {
  44. return value
  45. },
  46. set(newVal) {
  47. if (newVal !== value) {
  48. value = newVal
  49. updateView()
  50. }
  51. }
  52. })
  53. }
  54. function updateView() {
  55. console.log('更新视图');
  56. }

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

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

v-for 和 v-if 联合使用

Vue不推荐在同一个元素上使用v-ifv-for指令。

  1. <!-- 不推荐 -->
  2. <ul>
  3. <li
  4. v-for="(item, index) of todoList"
  5. v-if="!item.completed"
  6. :key="item.id">
  7. {{ item.content }}
  8. </li>
  9. </ul>

以上代码,我们在li元素上使用了v-for指令进行循环todoList数组进行渲染,使用v-if指令判断completedfalse时才会显示,然后你就会发现一个错误!
image.png
错误的意思说:item属性在渲染期间确实被访问了,但是item并没有定义。 :::info 这是因为Vue3在进行v-for解析的时候,v-if的优先级是高于v-for 的,这就会导致v-if获取不到item。 :::

那么如何解决这个问题呢?
1、我们可以利用template进行包裹,让v-for循环template标签

  1. <ul>
  2. <!-- 这样 v-for 的优先级就会比 v-if 高 -->
  3. <template v-for="(item, index) of todoList" >
  4. <li v-if="!item.completed" :key="item.id">
  5. {{ item.content }}
  6. </li>
  7. </template>
  8. </ul>

2、使用computed过滤数组

  1. const app = {
  2. template: `
  3. <ul>
  4. <li v-for="(item, index) of notCompletedTodoList" :key="item.id">
  5. {{ item.content }}
  6. </li>
  7. </ul>
  8. `,
  9. data(){
  10. return{
  11. todoList: [
  12. // ...
  13. ]
  14. }
  15. },
  16. computed:{
  17. notCompletedTodoList(){
  18. return this.todoList.filter(el=> !el.completed)
  19. }
  20. }
  21. }

但是像这样的情况例外,你可以在一个元素上同时使用v-forv-if指令:

  1. <ul>
  2. <li v-if="todoList.length > 0" v-for="(item, index) of todoList" :key="item.id">
  3. {{ item.content }}
  4. </li>
  5. </ul>

这是因为v-if并不依赖v-for解构的属性,所以你可以进行判断!

Vue2中,v-for的优先级是高于v-if的,到了Vue3版本的时候把它们的优先级进行了调整互换,从下面两方面来看这是必然的:
1、逻辑层:v-if是优先级要大于v-for的,v-if决定了是否要进行渲染,v-for决定了如何进行渲染。
2、性能层:如果先使用v-for判断如何进行渲染,再使用v-if判断是否渲染在性能上所带来的消耗是不一样的。