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}}}}
:::info
建议遍历可迭代对象时使用(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: `<MyComponentv-for="(item, index) in items":index="index":key="item.id"/>`}
但是,这不会自动将任何数据传递给组件,因为组件有自己独立的作用域。这会使组件与v-for的工作方式紧密耦合。明确其数据的来源可以使组件在其他情况下重用。
如果想将迭代后的数据传递到组件中,我们还需要传递props。
const myComponent = {props: ['item']}const app = {template: `<MyComponentv-for="(item, index) in items":item="item":index="index":key="item.id"/>`}
数组变化侦测
在Vue3中,Vue能够侦听响应式数组的变更方法,并在它们被调用时触发相关的更新。这些变更方法包括:
push()pop()shift()unshift()splice()sort()reverse()
但是在Vue2在,Vue通过重写数组相关的方法来监听数组的变更,通过在数组的原型上新增了一层原型来达到拦截:
这是因为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;}});}
以上代码,我们定义了data,data里有数组,然后我们分别去操作这些属性看看变化。
当我们直接把数组赋值为一个新数组的时候,数组是可以被拦截的:
vm.a = 1;vm.list = [2, 3, 4, 5, 6];console.log(vm.list);

当我们调用push方法的时候,数据确实发生了变化,但是set机制却没有触发:
vm.list.push(6);console.log(vm.list);

可以看到图片中,出现两次“数据获取”,这是因为我们在第一行中vm.list的时候执行了get机制,然后才能调用push()方法。
Object.defineProperty()没办法监听下列方法对数组的变更:
push()pop()shift()unshift()splice()sort()reverse()
但是当数组重新赋值的是却能被拦截的到,所以Vue2对上面这些方法进行了包裹封装(类似重写),大致的响应式原理如下:
// 定义一个 data 对象// Object.defineProperty 可以重新定义属性 给属性安插 getter setter 方法let data = {name: 'xiechen',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.prototypelet proto = Object.create(oldArrayPrototype) // 继承Array.from(['push', 'shift', 'unshift', 'pop']).forEach(method => {// 函数劫持,把函数重写proto[method] = function () {// 执行数组原本的方法oldArrayPrototype[method].call(this, ...arguments)// 更新视图updateView()}})// 给数组新增一个原型,target.__proto__ = protoObject.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 = newValupdateView()}}})}function updateView() {console.log('更新视图');}
以上就是Vue2对对象和数组进行的响应式拦截大概思路。
那么我们替换原数组的数据是否会重新渲染整个DOM列表(性能原因)?
不一定,Vue在对dom操作的时候进行了大量的新旧节点信息的对比算法,Vue会把dom重新渲染的程度最小化,做到已有的dom节点最大化的复用。
v-for 和 v-if 联合使用
Vue不推荐在同一个元素上使用v-if和v-for指令。
<!-- 不推荐 --><ul><liv-for="(item, index) of todoList"v-if="!item.completed":key="item.id">{{ item.content }}</li></ul>
以上代码,我们在li元素上使用了v-for指令进行循环todoList数组进行渲染,使用v-if指令判断completed为false时才会显示,然后你就会发现一个错误!
错误的意思说:item属性在渲染期间确实被访问了,但是item并没有定义。
:::info
这是因为Vue3在进行v-for解析的时候,v-if的优先级是高于v-for 的,这就会导致v-if获取不到item。
:::
那么如何解决这个问题呢?
1、我们可以利用template进行包裹,让v-for循环template标签
<ul><!-- 这样 v-for 的优先级就会比 v-if 高 --><template v-for="(item, index) of todoList" ><li v-if="!item.completed" :key="item.id">{{ item.content }}</li></template></ul>
2、使用computed过滤数组
const app = {template: `<ul><li v-for="(item, index) of notCompletedTodoList" :key="item.id">{{ item.content }}</li></ul>`,data(){return{todoList: [// ...]}},computed:{notCompletedTodoList(){return this.todoList.filter(el=> !el.completed)}}}
但是像这样的情况例外,你可以在一个元素上同时使用v-for和v-if指令:
<ul><li v-if="todoList.length > 0" v-for="(item, index) of todoList" :key="item.id">{{ item.content }}</li></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判断是否渲染在性能上所带来的消耗是不一样的。
