Vue演练场基础知识(三)列表渲染+计算属性

为学习Vue基础知识,我动手操作通关了Vue演练场,该演练场教程的目标是快速体验使用 Vue 是什么感受,设置偏好时我选的是选项式 + 单文件组件。以下是我结合深入指南写的总结笔记,希望对Vue初学者有所帮助。

七. 列表渲染

v-for是一个用于实现遍历的指令,可以加在template上,它的用处非常多,下面将一一列举:

遍历数组

v-for指令遍历数组(类似js中的forEach循环):

<script>
export default {
    data() {
        return {list: [{id: 1, msg: '春'},{id: 2, msg: '夏'}]}
    }
}
</script>

<template>
    <ul>
        <li v-for="item in list">
            {{item.msg}}
        </li>
    </ul>
</template>

<li v-for="item in list">可以换成:

  1. <li v-for="item of list">
  2. <li v-for="(item, idx) in list">
  3. <li v-for="({id, msg}, idx) in list">
  4. <li v-for="item in list" :key="item.id">

双重循环

v-for实现双重循环遍历:

<div v-for="item1 in list1">
    <div v-for="item2 in list2">
        {{item1}}-{{item2}}
    </div>
</div>

遍历对象

v-for遍历对象(类似Object.values(obj)):

<script>
export default {
    data() {
        return {obj: {a: 1, b: 2}};
    }
}
</script>

<template>
    <div v-for="value in obj">
        {{value}} // 打印出 1,2
    </div>
</template>

<div v-for="value in obj">可以换成:

  • <div v-for="(value, key) in obj">
  • <div v-for="(value, key, index) in obj">

遍历整数范围

也可以用v-for遍历一个整数范围(从1到n):

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

v-for 与 v-if

不要在同一个节点上使用v-ifv-for,因为v-ifv-for的优先级更高,这可能导致v-for的作用域内定义的迭代项变量还未定义就被使用:

<li v-for="item in arr" v-if="item.isShow">
    xxx
</li>

会报错Property "todo" was accessed during render but is not defined on instance. at <Repl>。可以改为像下面这样分开使用:

<template v-for="item in arr">
    <li v-if="item.isShow">
        xxx
    </li>
</template>

通过 key 管理状态

始终推荐给循环的每一个标签绑定一个唯一固定的key值,这样当用户在页面中增加或删除标签时,Vue就能追踪绑定的元素,识别哪个旧元素可以被复用。

<li v-for="item in arr" :key="item.id">xxx</li>

如果不设置key的话,Vue 默认按照“就地更新”的策略来更新通过 v-for 渲染的元素列表。
以下对就地更新概念的理解参考这个博客

就地更新的优势

什么是就地更新呢?
首先认识一下 Vue 的【更新】,假如 Vue 要复用一个旧元素,更新为新元素:

<!-- 旧元素 -->
<div>我最喜欢:<span>苹果</span><div>
<!-- 新元素 -->
<div>我最喜欢:<span>菠萝</span><div>

那它不会直接把旧元素删掉,重新建立新元素,而是会比较二者的差别,在旧元素的基础上打补丁,比如本例中它会把“苹果”改成“菠萝”。
而【就地】更新的意思是,当需要在某个位置渲染出一个新元素时,Vue会把现在处于那个位置的元素作为渲染新元素的基础。比如在下面的例子中,isLikeApple从true切换false时,Vue只需要改变“苹果”两个字就能完成更新,不需要重新渲染整个元素。

<div v-if="isLikeApple">
    <div>我最喜欢的水果是<span>苹果</span><div>
</div>
<div v-else>
    <div>我最喜欢的水果是<span>菠萝</span><div>
</div>

就地更新的问题

大多数情况下,就地更新都是非常高效的策略,以最小代价完成更新,减少了DOM操作,减少了浏览器性能消耗。但在下面两种情况下,就地更新的策略却会导致浪费性能甚至出现bug。

  1. 列表渲染输出的结果依赖【临时的DOM状态】(例如表单输入值)的情况
  2. 列表渲染输出的结果依赖【子组件】的情况

一. 列表渲染依赖【临时的DOM状态】
请看以下示例:

<template>
  <div>
    <ul>
      <li v-for="(item, index) of list">
        <span> 第{{ item }}个输入框 - </span>
        <input type="text" />
        <button @click="deleteItem(index)">DELETE</button>
      </li>
    </ul>
  </div>
</template>
<script>
  export default {
    data() {
      return {
        list: ['1', '2', '3'],
      }
    },
    methods: {
      deleteItem(index) {
        this.list.splice(index, 1);
      }
    }
  };
</script>

Vue根据代码渲染出了三个空输入框后,假如用户在B的输入框输入文本,然后删除B,正常的结果应该是剩下一个A(空输入框)和一个C(空输入框):
设置key之后
实际操作后却发现C的输入框里居然出现了文字:
设置key之前
这是因为Vue不会监控表单输入值等临时DOM状态,当它在B的基础上更新成C时,无法识别出输入了文字的输入框与空输入框有区别,会认为B和C的输入框是一样的,不需要更新,于是保留了B的输入框和里面的文字,直接用在C上了。
总结:若在列表中不设置key,Vue将默认使用就地更新的策略,类似于把索引当作key值,见Vue.js v2.7.4 源码:

if (isTrue(children._isVList) &&
    isDef(c.tag) &&
    isUndef(c.key) &&
    isDef(nestedIndex)) {
    c.key = "__vlist".concat(nestedIndex, "_").concat(i, "__");
}

当列表中的项包含了临时DOM状态,且列表顺序发生变化时,就有可能出现临时DOM状态错位的异常。
所以推荐给每一个列表项都绑定上唯一且固定的key值,帮助Vue追踪绑定的元素,代替就地更新。比如上面的例子设置了key以后,用户再删除B输入框时,Vue会比较各列表项的key值,删除掉B和里面的输入框,并将原本在列表第三项的C直接复用到列表第二项。

<li v-for="(item, index) of list" :key="item">

二. 列表渲染依赖【子组件】
Vue不仅识别不了不同的临时DOM,也无法监控不了子组件内部的数据,因为子组件中可能有很复杂的逻辑。但如果两个子组件虽然使用了同一个组件,但传入的参数不同,Vue能判断出它们是不同的,不能直接复用。
请看以下示例:

<!-- index.vue -->
<template>
  <div>
    <ul>
      <li v-for="(item, index) of list">
        <MyComponent :value="item.value" />
        <button @click="deleteItem(index)">删除</button>
      </li>
    </ul>
  </div>
</template>
<script>
  import MyComponent from './MyComponent.vue';
  export default {
    components: {
      MyComponent
    },
    data() {
      return {
        list: [
          { id: 1, value: 'A' }, { id: 2, value: 'B' }, { id: 3, value: 'C' }
        ],
      }
    },
    methods: {
      deleteItem(index) {
        this.list.splice(index, 1);
      }
    }
  };
</script>
<!-- 子组件 MyComponent.vue -->
<script>
  export default {
    props: ['value'],
  };
</script>
<template>
  <span> {{ value }}创建于:{{ new Date().getHours() }}时{{ new Date().getMinutes() }}分{{ new Date().getSeconds() }}秒 </span>
</template>

用户删除列表第二项以后,正常的效果应该是C原封不动展示在列表第二项:
在这里插入图片描述

实际操作的结果却是C中的子组件被卸载重装了一次,导致C展示的创建时间变了:
在这里插入图片描述

本例中Vue渲染元素的处理逻辑如下:
由于列表没设置key,用户点击删除B后,Vue就把B就地更新为C了。
由于本例中B和C中的子组件收到了不同的value参数,导致Vue发现了B的子组件不能直接复用,但由于Vue不能跟踪到子组件内部数据,所以只能直接用新的参数重渲染整个子组件。因此用户删除B后,C的子组件展示出来的创建时间改变了。
如果B和C中的子组件MyComponent没有接收参数或者收到参数的值一样,Vue将把两个子组件当成完全一样的,直接复用,和上文中“列表渲染依赖【临时的DOM状态】”的例子一样。

修复这个问题的方法依然是给每个列表项设置key,使Vue在用户删除B后将原本在列表第三项的C直接复用到列表第二项。

<li v-for="(item, index) of list" :key="item">

组件上使用v-for

上面我们已经认识了怎么在原生DOM中使用v-for

<li v-for="item in arr" :key="item.id">
    {{item.id}}-{{item.msg}}
</li>

可以看到,v-for定义在原生DOM上时,其作用域包含了该DOM的标签上和标签内部,那如果v-for定义在组件上呢?

<!-- index.vue -->
// index.vue
<script>
import MyComponent from './MyComponent.vue'
export default {
    data() {
        return {arr: [id: 1, msg: '111'}]};
    },
    component: {MyComponent}
}
</script>
<template>
    <MyComponent v-for="item in arr" :key="item.id">// 在v-for作用域内
        {{item.id}}-{{item.msg}} // 在v-for作用域内
    </MyComponent>
</template>
<!-- 子组件 MyComponent.vue -->
<template>
    …
    {{item.msg}} // 不在v-for作用域内,报错,item未定义
    …
</template>

可以看到,v-for定义在子组件上时,其作用域也包含了该组件的标签上和标签内部,但不包含子组件的内部逻辑。这是因为如果组件依赖了v-for传入的参数,那该组件就只能与v-for搭配使用了,限制了该组件的使用场景。如果你希望在组件中使用v-for迭代项的值,应通过props传递:

<MyComponent
    v-for="(item, idx) in arr"
    // key是特殊的props,是v-for跟踪节点的标识,不要传入同名props
    :key="item.id" 
    // 以下是来自迭代项的值
    :item="item"
    :idx="idx"
    :id="item.id"
    :msg="item.msg"
>
    {{item.id}}-{{item.msg}}
</MyComponent>

八. 计算属性

模板中的表达式虽然方便,但也只能用来做简单的操作。如果在模板中写太多逻辑,会让模板变得臃肿,难以维护。
比如现在我们希望根据一个复杂对象里的一个层次较深的属性来生成文本:

<div>{{obj.list[0].student.name === 'xwq' ? 'yes' : 'no'}}</div>

这个表达式对于模板来说太长了,我们推荐使用计算属性来描述依赖响应式状态的复杂逻辑。

<script>
export default {
  data() {
    obj: {list: [{student: {name: 'xwq'}]}
  },
  computed: {
    // 一个计算属性的 getter
    result () {
      // `this` 指向当前组件实例
      return this.obj.list[0].student.name === 'xwq' ? 'yes' : 'no';
    }
  }
}
</script>
<template>
  <div>{{result}}</div>
</template>

响应式依赖

Vue会自动检测出计算属性依赖的所有响应式依赖,比如上面的例子中result依赖着this.obj.list[0].student.name(而不是this.obj)。
由于计算属性的取值完全取决于响应式依赖的值,所以若响应式依赖不变或计算属性没有响应式依赖的话,就算整个组件重渲染,计算属性也永远不会更新。比如在本文上面解释就地更新时,我们在模板中写出了一个这样的表达式:

<template>
  <span> 创建于:{{ new Date().getHours() }}时{{ new Date().getMinutes() }}分{{ new Date().getSeconds() }}秒 </span>
</template>

使用计算属性后变成:

<script>
export default {
  computed: {
    createTime () {
   	  const date = new Date();
      return `${date.getHours()}${date.getMinutes()}${date.getSeconds()}`;
    }
  }
}
</script>
<template>
  <span> 创建于:{{createTime}}秒 </span>
</template>

之后虽然该组件重渲染了,但由于new Date()并不是一个响应式依赖,所以createTime并没有更新。

计算属性缓存 vs 方法

组件中还可以定义方法,如果能用方法methods代替计算属性computed,那计算属性是不是就没必要了?

 computed: {
   result () {
     return this.obj.list[0].student.name === 'xwq' ? 'yes' : 'no';
   }
 }
 // 若用methods替代computed
 methods: {
   getResult () {
     return this.obj.list[0].student.name === 'xwq' ? 'yes' : 'no';
   }
 }

事实上,虽然计算的结果是一样的,但是计算属性比方法多了缓存的特性。只要响应式依赖不变,计算属性就不会变,不需要重新计算。相比之下,方法调用总是会在重渲染发生时再次执行函数。如果某个计算属性的计算过程非常耗性能,用方法来计算的话会极大占用计算资源,拖慢性能。

可写计算属性

计算属性的值完全取决于它所依赖的响应式依赖的值,若响应式依赖不变,计算属性就不应该变动,所以不能像修改普通属性那样直接修改计算属性。但我们可以同时提供 gettersetter ,这样读取计算属性时会调用getter 计算出的结果,对计算属性赋值时会调用setter,由setter的逻辑来更新响应式依赖,并以此触发计算属性重新计算。

export default {
  data() {
    return {
      firstName: 'John',
      lastName: 'Doe'
    }
  },
  computed: {
    fullName: {
      // getter
      get() {
        return this.firstName + ' ' + this.lastName
      },
      // setter
      set(newValue) {
        // 注意:我们这里使用的是解构赋值语法
        [this.firstName, this.lastName] = newValue.split(' ')
      }
    }
  }
}

获取上一个值(仅 3.4+ 支持)

可以通过访问计算属性的 getter 的第一个参数来获取计算属性返回的上一个值:

 // getter
 get(previous) {
   console.log('上一个fullName是:', previous);
   return this.firstName + ' ' + this.lastName
 },

最佳实践

  1. Getter 不应有副作用
    getter应该被设计为纯粹地计算一个值的逻辑,不要在 getter 中作计算以外的任何多余的事情,包括做异步请求、打印到控制台、更改 DOM、改变其他状态等。这些功能应靠侦听器实现。
  2. 避免直接修改计算属性值
    计算属性是它的所有响应式依赖的快照,直接更改计算属性是没有意义的,应更新响应式依赖,以此触发重新计算计算属性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值