@[TOC](自学前端开发 - VUE 框架 (一) 基础、模板语法、响应式基础、Class 与 Style 的绑定、渲染)
前端有一些比较流行的框架,例如 react.js、angular.js、jQuery 等,Vue.js 也是其中之一。Vue是一款用于构建用户界面的 JavaScript 框架。它基于标准 HTML、CSS 和 JavaScript 构建,并提供了一套声明式的、组件化的编程模型,帮助程序员高效地开发用户界面。

vue 和 jQuery 除了完成特效方面稍微重叠,其他方面的定位是不同的:jQuery 主要定位是获取元素,Vue 的定位是方便操作及控制数据。

Vue.js 官网
Vue 官方文档

vue 学习之前

在学习 vue 之前,有一些准备工作:

  • vue 的环境
    主要是 vue 的使用方式
  • vue 的使用风格
    vue 本身有很多风格可以使用,例如传统 html 文件和单文件组件、选项式API和组合式API等

vue 的使用环境

vue 支持 npm 引入和文件(CDN)引入。

npm 引入

使用 npm 工具执行命令

npm init vue@latest

这一指令将会安装并执行 create-vue,它是 Vue 官方的项目脚手架工具。之后将会看到一些诸如 TypeScript 和测试支持之类的可选功能提示。如果不确定是否要开启某个功能,你可以直接按下回车键选择 No。在项目被创建后,通过以下步骤安装依赖并启动开发服务器:

cd <your-project-name>
npm install
npm run dev

现在应该已经运行起来了你的第一个 Vue 项目!请注意,生成的项目中的示例组件使用的是组合式 API 和

文件引入

可以使用 CDN 引入

<script src="https://unpkg/vue@3/dist/vue.global.js"></script>

这里使用了 unpkg,也可以使用任何提供 npm 包服务的 CDN,例如 jsdelivr 或 cdnjs。当然,也可以下载此文件并自行提供服务。

通过 CDN 使用 Vue 时,不涉及“构建步骤”。这使得设置更加简单,并且可以用于增强静态的 HTML 或与后端框架集成。但是将无法使用单文件组件 (SFC) 语法。

使用文件引入,也有几种使用版本:

  1. 全局构建版本:该版本的所有顶层 API 都以属性的形式暴露在了全局的 Vue 对象上。
<script src="https://unpkg/vue@3/dist/vue.global.js"></script>

<div id="app">{{ message }}</div>

<script>
  const { createApp } = Vue
  
  createApp({
    data() {
      return {
        message: 'Hello Vue!'
      }
    }
  }).mount('#app')
</script>
<!-- 脚本部分也可以写成 -->
<!-- 	<script>
		Vue.createApp({
			data() {
				return {
					message: 'Hello Vue!'
				}
			}
		}).mount('#app')
	</script> -->
  1. 使用ES模块版本:注意使用 <script type="module">,且导入的 CDN 指向的是 ES 模块构建版本
<div id="app">{{ message }}</div>

<script type="module">
  import { createApp } from 'https://unpkg/vue@3/dist/vue.esm-browser.js'
  
  createApp({
    data() {
      return {
        message: 'Hello Vue!'
      }
    }
  }).mount('#app')
</script>
  1. 使用导入映射表(Import Maps)版本
<script type="importmap">
  {
    "imports": {
      "vue": "https://unpkg/vue@3/dist/vue.esm-browser.js"
    }
  }
</script>

<div id="app">{{ message }}</div>

<script type="module">
  import { createApp } from 'vue'

  createApp({
    data() {
      return {
        message: 'Hello Vue!'
      }
    }
  }).mount('#app')
</script>

vue 的使用风格

vue 的文件格式

vue 可以在 html 中直接使用,或创建为单文件组件使用。在 html 中使用类似于普通的 js 或 jQuery 之类的使用方式。在大多数启用了构建工具的 Vue 项目中,可以使用一种类似 HTML 格式的文件来书写 Vue 组件,它被称为单文件组件。 顾名思义,Vue 的单文件组件会将一个组件的逻辑 (JavaScript),模板 (HTML) 和样式 (CSS) 封装在同一个文件里。官方推荐使用这种方法来编写模块化的组件。

在初学阶段,只学习 html 使用方式。

vue 的 api 代码风格

Vue 的组件可以按两种不同的风格书写:选项式API组合式API

使用选项式 API,可以用包含多个选项的对象来描述组件的逻辑,例如 data、methods 和 mounted。选项所定义的属性都会暴露在函数内部的 this 上,它会指向当前的组件实例。

通过组合式 API,可以使用导入的 API 函数来描述组件逻辑。在单文件组件中,组合式 API 通常会与 <script setup> 搭配使用。这个 setup attribute 是一个标识,告诉 Vue 需要在编译时进行一些处理,让我们可以更简洁地使用组合式 API。比如,<script setup> 中的导入和顶层变量/函数都能够在模板中直接使用。

两种 API 风格都能够覆盖大部分的应用场景。它们只是同一个底层系统所提供的两套不同的接口。实际上,选项式 API 是在组合式 API 的基础上实现的!关于 Vue 的基础概念和知识在它们之间都是通用的。

在简单学习阶段,将使用选项式API风格。

vue 开发辅助工具

官方推荐安装浏览器开发者插件,使我们可以浏览一个 Vue 应用的组件树,查看各个组件的状态,追踪状态管理的事件,还可以进行组件性能分析。

下载并让浏览器加载插件:vuejs-devtools

vue 基础

vue.js 的 M-V-VM 架构

和 MVC、MTV 架构类似,vue 采用了 MVVM(Model-View-ViewModel) 架构思想,它是一种基于前端开发的架构模式。

  • Model : 指的是 vue 对象里的 data 属性里的数据,将会渲染至页面中去。
  • View : 指的是 vue 对象要渲染的HTML页面,也称为“视图模板”。
  • ViewModel : 指的是编写代码时的创建的应用实例对象,它是 vue.js 的核心,负责监控、连接 view 和 model,保证视图和数据的一致性。

创建 vue 应用

应用实例

每个 Vue 应用都是通过 createApp 函数创建一个新的 应用实例

import { createApp } from 'vue'

const app = createApp({
  /* 根组件选项 */
})

根组件

我们传入 createApp 的对象实际上是一个组件,每个应用都需要一个**“根组件”,其他组件将作为其子组件**。根组件可以使用根组件模板或自定义的单文件组件。使用根组件模板时,Vue 将自动使用容器的 innerHTML 作为模板。

import { createApp } from 'vue'

const app = createApp({
	/* 根组件选项 */
	data(){
  		return {
  			count: 0
  		}
	}
})

data 组件可以使用数据填充模板。

挂载应用

应用实例必须在调用了 .mount() 方法后才会渲染出来。该方法接收一个“容器”参数,可以是一个实际的 DOM 元素或是一个 CSS 选择器字符串:

<div id="app"></div>
app.mount('#app')

应用根组件的内容将会被渲染在容器元素里面。容器元素自己将不会被视为应用的一部分。.mount() 方法应该始终在整个应用配置和资源注册完成后被调用。同时请注意,不同于其他资源注册方法,它的返回值是根组件实例而非应用实例。

应用配置

应用实例会暴露一个 .config 对象允许我们配置一些应用级的选项,例如定义一个应用级的错误处理器,它将捕获所有由子组件上抛而未被处理的错误:

app.config.errorHandler = (err) => {
  /* 处理错误 */
}

多个应用实例

应用实例并不只限于一个。createApp API 允许你在同一个页面中创建多个共存的 Vue 应用,而且每个应用都拥有自己的用于配置和全局资源的作用域。

const app1 = createApp({
  /* ... */
})
app1.mount('#container-1')

const app2 = createApp({
  /* ... */
})
app2.mount('#container-2')

简单的 vue 应用

<div id="app">
	<p>{{ message }}</p>
</div>
<script type="module">
	import { createApp } from 'vue'

	let app = createApp({
		data(){
	  		return {
	  			message: 'Hello Vue!'
	  		}
		}
	});
	
	app.mount('#app');
</script>

或者简写为

<div id="app">
	<p>{{ message }}</p>
</div>
<script>
	let app = Vue.createApp({
		data() {
			return {
				message: 'Hello Vue!'
			}
		}
	}).mount('#app')
</script>

data 组件跟随的是一个处理函数,通常在此使用 ajax 获取具体数据,并 return。vue 会将返回给 data 的数据渲染至页面模板。

vue 的模板语法

Vue 使用一种基于 HTML 的模板语法,使我们能够声明式地将其组件实例的数据绑定到呈现的 DOM 上。所有的 Vue 模板都是语法层面合法的 HTML,可以被符合规范的浏览器和 HTML 解析器解析。

在底层机制中,Vue 会将模板编译成高度优化的 JavaScript 代码。结合响应式系统,当应用状态变更时,Vue 能够智能地推导出需要重新渲染的组件的最少数量,并应用最少的 DOM 操作。

文本插值

最基本的数据绑定形式是文本插值,它使用的是“Mustache”语法 (即双大括号):

<span>Message: {{ msg }}</span>

双大括号标签会被替换为相应组件实例中 msg 属性的值。同时每次 msg 属性更改时它也会同步更新。

vue 指令

双大括号会将数据解释为纯文本,而不是 HTML。若想插入 HTML,你需要使用 v-html 指令:

<p>Using text interpolation: {{ rawHtml }}</p>
<p>Using v-html directive: <span v-html="rawHtml"></span></p>

这里的 v-html 指令的意思是:在当前组件实例上,将此元素的 innerHTML 与 rawHtml 属性保持同步。

指令其实就是标签的一个属性,它由 v- 作为前缀,表明是一个由 vue 提供的特殊属性。它们将为渲染的 DOM 应用特殊的响应式行为。指令属性的期望值大部分为一个 JS 表达式,某些指令会需要一个"参数",在指令后通过冒号 “:” 隔开做标识。

同样在指令参数上也可以使用一个 JavaScript 表达式,需要包含在一对方括号内,即动态参数

<a v-bind:[attributeName]="url"> ... </a>
<!-- 简写 -->
<a :[attributeName]="url"> ... </a>

这里的 attributeName 会作为一个 JavaScript 表达式被动态执行,计算得到的值会被用作最终的参数。举例来说,如果你的组件实例有一个数据属性 attributeName,其值为 "href",那么这个绑定就等价于 v-bind:href。相似地,你还可以将一个函数绑定到动态的事件名称上:

<a v-on:[eventName]="doSomething"> ... </a>
<!-- 简写 -->
<a @[eventName]="doSomething">

动态参数中表达式的值应当是一个字符串,或者是 null。特殊值 null 意为显式移除该绑定。其他非字符串的值会触发警告。动态参数表达式因为某些字符的缘故有一些语法限制,比如空格和引号,在 HTML 属性名称中都是不合法的。如果需要传入一个复杂的动态参数,推荐使用计算属性组件替换复杂的表达式。

另外,当使用 DOM 内嵌模板 (直接写在 HTML 文件里的模板) 时,我们需要避免在名称中使用大写字母,因为浏览器会强制将其转换为小写。

修饰符

指令还可以使用修饰符.” ,修饰符是以点开头的特殊后缀,表明指令需要以一些特殊的方式被绑定。例如 .number 表示此参数类型为数值,.toUpperCase() 表示调用JS方法(大写化)处理等。

指令语法

完整的指令语法如下:

属性绑定

双大括号不能在标签属性中使用,如果想要响应式的绑定一个属性,应该使用 v-bind 指令:

<div v-bind:id="dynamicId"></div>

v-bind 指令指示 Vue 将元素的 id 属性与组件的 dynamicId 属性保持一致。如果绑定的值是 null 或者 undefined,那么该属性将会从渲染的元素上移除。

也可以简写为

<div :id="dynamicId"></div>

对于 v-bind 指令绑定的属性,如果是布尔型属性,例如 disabled、hidden 等,可以这样使用:

<button :disabled="isButtonDisabled">Button</button>

isButtonDisabled 为真值或一个空字符串 (即 <button disabled="">) 时,元素会包含这个 disabled 属性。而当其为其他假值时属性将被忽略。

如果 v-bind 指令不带参数,则会绑定其包含的多个属性到元素对象上:

data() {
  return {
    objectOfAttrs: {
      id: 'container',
      class: 'wrapper'
    }
  }
}
<div v-bind="objectOfAttrs"></div>

JS 表达式

Vue 在所有的数据绑定中都支持完整的 JavaScript 表达式:

{{ number + 1 }}

{{ ok ? 'YES' : 'NO' }}

{{ message.split('').reverse().join('') }}

<div :id="`list-${id}`"></div>

在 vue 模板内,JS 表达式可以用于所有的文本插值和 Vue 指令属性的值中。需注意的是,每个绑定仅支持单一表达式,也就是一段能够被求值的 JavaScript 代码。一个简单的判断方法是是否可以合法地写在 return 后面。如果需要使用比较复杂的表达式,可以使用一个在组件中暴露的方法。

响应式基础

声明响应式状态

Vue 在创建根组件时,会根据 data 来声明组件的响应式状态,将此函数返回的对象用响应式系统进行包装。此对象的所有顶层属性都会被代理到组件实例上。这些实例上的属性仅在实例首次创建时被添加,因此需要确保它们都出现在 data 函数返回的对象上。若所需的值还未准备好,在必要时也可以使用 null、undefined 或者其他一些值占位。虽然也可以不在 data 上定义,直接向组件实例添加新属性,但这个属性将无法触发响应式更新。

另外,Vue 在组件实例上暴露的内置 API 使用 $ 作为前缀。它同时也为内部属性保留 _ 前缀。因此,应该避免在顶层 data 上使用任何以这些字符作前缀的属性。

响应式代理和原始值

之前在创建并挂载应用实例时,使用了两种方法,其效果相同:第一种方法是先创建应用实例并赋值给变量,再将其挂载;第二种是创建时就直接挂载,然后再赋值给变量。式代理。

let app = Vue.createApp({
	...
})
app.mount('#app')
let app = Vue.createApp({
	...
}).mount('#app')

两者看起来差不多,呈现效果也相同,但是对于变量 app 是完全不一样的。两种写法最大的区别是,第一种方法变量指向的是对象实例,而第二种方法变量指向的是实例的响应式代理。

对于同一实例中的数据,也有原始值响应式代理之分

let app = Vue.createApp({
	data() { return {
		someObject: {}
	}},
	mounted() {
		const newObject = {};
		this.someObject = newObject;
		console.log(newObject === this.someObject)		// false
	}
}).mount('#app')

从这个例子中可以看出,当挂载实例后再访问 this.someObject,其值为响应式代理,而 newObject 是原始值(不会变为响应式)。可以使用 this 来直接访问响应式代理对象。

在创建实例对象时,除了声明响应式状态外,还增加了一些属性和方法,例如可以使用 app.$data 来获取数据的代理对象,或使用 app.$data.someObject 来访问data内部的数据对象(同 app.someObject),使用 app.$el 能够获取实例对象的使用范围,使用 app.$el.parentElement 能够获取 实例对象所在的 DOM 元素等。

DOM 的更新时机

当更改响应式状态(data 状态)后,DOM 会自动更新。然而,DOM 的更新并不是同步的。相反,Vue 将缓冲它们直到更新周期的 “下个时机” 以确保无论你进行了多少次状态更改,每个组件都只需要更新一次。若要等待一个状态改变后的 DOM 更新完成,你可以使用 nextTick() 这个全局 API:

import { nextTick } from 'vue'

export default {
  methods: {
    increment() {
      this.count++
      nextTick(() => {
        // 访问更新后的 DOM
      })
    }
  }
}

有状态的方法

在某些情况下,我们可能需要动态地创建一个方法函数,如果此方法是有状态的(会在运行时维护着一个内部状态),那么当多个组件实例都共享这同一个状态,则会互相影响。要保持每个组件实例的防抖函数都彼此独立,我们可以改为在 created 生命周期钩子中创建这个函数。

Class 与 Style 绑定

数据绑定的一个常见需求场景是操纵元素的 CSS class 列表和内联样式。因为 class 和 style 都是属性,我们可以和其他属性一样使用 v-bind 将它们和动态的字符串绑定。但是,在处理比较复杂的绑定时,通过拼接生成字符串是麻烦且易出错的。因此,Vue 专门为 class 和 style 的 v-bind 用法提供了特殊的功能增强。除了字符串外,表达式的值也可以是对象或数组。

绑定 class

可以给 :class (v-bind:class 的缩写) 传递一个对象来动态切换 class,例如 active 是否存在取决于数据属性 isActive 的真假值:

<div :class="{ active: isActive }"></div>

也可以在对象中写多个字段来操作多个 class。此外,:class 指令也可以和一般的 class 属性共存。举例来说,下面这样的状态和模板:

data() {
  return {
    isActive: true,
    hasError: false
  }
}
<div
  class="static"
  :class="{ active: isActive, 'text-danger': hasError }"
></div>

渲染的结果会是:

<div class="static active"></div>

isActive 或者 hasError 改变时,class 列表会随之更新。举例来说,如果 hasError 变为 true,class 列表也会变成 "static active text-danger"

绑定的对象并不一定需要写成内联字面量的形式,也可以直接绑定一个对象:

data() {
  return {
    classObject: {
      active: true,
      'text-danger': false
    }
  }
}
<div :class="classObject"></div>

或绑定一个返回对象的计算属性,渲染结果是一致的:

<div :class="classObject"></div>
data() {
  return {
    isActive: true,
    error: null
  }
},
computed: {
  classObject() {
    return {
      active: this.isActive && !this.error,
      'text-danger': this.error && this.error.type === 'fatal'
    }
  }
}

使用数组

我们可以给 :class 绑定一个数组来渲染多个 CSS class:

data() {
  return {
    activeClass: 'active',
    errorClass: 'text-danger'
  }
}
<div :class="[activeClass, errorClass]"></div>

渲染的结果是:

<div class="active text-danger"></div>

如果想在数组中有条件地渲染某个 class,你可以使用三元表达式:

<div :class="[isActive ? activeClass : '', errorClass]"></div>

errorClass 会一直存在,但 activeClass 只会在 isActive 为真时才存在。然而,这可能在有多个依赖条件的 class 时会有些冗长。因此也可以在数组中嵌套对象:

<div :class="[{ active: isActive }, errorClass]"></div>

绑定内联样式

:style 支持绑定 JavaScript 对象值,对应的是 HTML 元素的 style 属性:

data() {
  return {
    activeColor: 'red',
    fontSize: 30
  }
}
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
<!-- 也支持写为 -->
<div :style="{ 'color': activeColer, 'font-size': fontSize + 'px' }"></div>

直接绑定一个样式对象可以使模板更加简洁:

data() {
  return {
    styleObject: {
      color: 'red',
      fontSize: '13px'
    }
  }
}
<div :style="styleObject"></div>

同样的,如果样式对象需要更复杂的逻辑,也可以使用返回样式对象的计算属性。

使用数组

我们还可以给 :style 绑定一个包含多个样式对象的数组。这些对象会被合并后渲染到同一元素上:

<div :style="[baseStyles, overridingStyles]"></div>

当在 :style 中使用了需要浏览器特殊前缀的 CSS 属性时,Vue 会自动为他们加上相应的前缀。Vue 是在运行时检查该属性是否支持在当前浏览器中使用。如果浏览器不支持某个属性,那么将尝试加上各个浏览器特殊前缀,以找到哪一个是被支持的。

也可以对一个样式属性提供多个 (不同前缀的) 值,举例来说:

<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>

数组仅会渲染浏览器支持的最后一个值。在这个示例中,在支持不需要特别前缀的浏览器中都会渲染为 display: flex

渲染

vue 将数据模型渲染到视图中,一般渲染分为条件渲染列表渲染

条件渲染

条件渲染是对条件进行判断决定是否进行渲染,一般使用 v-ifv-show 和配套的指令来完成

v-if

v-if 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回真值时才被渲染。

<h1 v-if="awesome">Vue is awesome!</h1>

也可以使用 v-else 为 v-if 添加一个“else 区块”。注意:一个 v-else 元素必须跟在一个 v-if 或者 v-else-if 元素后面,否则它将不会被识别。

<button @click="awesome = !awesome">Toggle</button>

<h1 v-if="awesome">Vue is awesome!</h1>
<h1 v-else>Oh no 😢</h1>

v-else-if 是相应于 v-if 的“else if 区块”。它可以连续多次重复使用:

<div v-if="type === 'A'">
  A
</div>
<div v-else-if="type === 'B'">
  B
</div>
<div v-else-if="type === 'C'">
  C
</div>
<div v-else>
  Not A/B/C
</div>

v-else 类似,一个使用 v-else-if 的元素必须紧跟在一个 v-if 或一个 v-else-if 元素后面。

<template> 上的 v-if

因为 v-if 是一个指令,他必须依附于某个元素。但如果我们想要切换不止一个元素呢?在这种情况下我们可以在一个 <template> 元素上使用 v-if,这只是一个不可见的包装器元素,最后渲染的结果并不会包含这个 <template> 元素。v-elsev-else-if 也可以在 <template> 上使用。

<template v-if="ok">
  <h1>Title</h1>
  <p>Paragraph 1</p>
  <p>Paragraph 2</p>
</template>

v-show

另一个可以用来按条件显示一个元素的指令是 v-show,其用法基本一样。

<h1 v-show="ok">Hello!</h1>

不同之处在于 v-show 会在 DOM 渲染中保留该元素;v-show 使用的是切换 css 的 display:none 来实现显示和隐藏。v-show 不支持在 <template> 元素上使用,也不能和 v-else 搭配使用。

v-if 和 v-show 的区别

v-if 是“真实的”按条件渲染,因为它确保了在切换时,条件区块内的事件监听器和子组件都会被销毁与重建。

v-if 也是惰性的:如果在初次渲染时条件值为 false,则不会做任何事。条件区块只有当条件首次变为 true 时才被渲染。

相比之下,v-show 简单许多,元素无论初始条件如何,始终会被渲染,只有 display 属性会被切换。

总的来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要频繁切换,则使用 v-show 较好;如果在运行时绑定条件很少改变,则 v-if 会更合适。

列表渲染

v-for

可以使用 v-for 指令基于一个数组来渲染一个列表。v-for 指令的值需要使用 item in items 形式的特殊语法,其中 items 是源数据的数组,而 item 是迭代项的别名:

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

v-for 块中可以完整地访问父作用域内的属性和变量。v-for 也支持使用可选的第二个参数表示当前项的位置索引。

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

v-for 也可以在定义变量别名时使用解构

<li v-for="{ message } in items">
  {{ message }}
</li>

<!-- 有 index 索引时 -->
<li v-for="({ message }, index) in items">
  {{ message }} {{ index }}
</li>

对于多层嵌套的 v-for,作用域的工作方式和函数的作用域很类似。每个 v-for 作用域都可以访问到父级作用域:

<li v-for="item in items">
  <span v-for="childItem in item.children">
    {{ item.message }} {{ childItem }}
  </span>
</li>

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

<div v-for="item of items"></div>

v-for 与对象

可以使用 v-for 来遍历一个对象的所有属性。遍历的顺序会基于对该对象调用 Object.keys() 的返回值来决定。

data() {
  return {
    myObject: {
      title: 'How to do lists in Vue',
      author: 'Jane Doe',
      publishedAt: '2016-04-10'
    }
  }
}
<ul>
  <li v-for="value in myObject">
    {{ value }}
  </li>
</ul>

可以通过提供第二个参数表示属性名 (例如 key),第三个参数表示位置索引:

<li v-for="(value, key, index) in myObject">
  {{ index }}. {{ key }}: {{ value }}
</li>

v-for 里使用范围值

v-for 可以直接接受一个整数值。在这种用例中,会将该模板基于 1…n 的取值范围重复多次。注意此处 n 的初值是从 1 开始而非 0。

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

<template> 上的 v-for

与模板上的 v-if 类似,可以在 <template> 标签上使用 v-for 来渲染一个包含多个元素的块。例如:

<ul>
  <template v-for="item in items">
    <li>{{ item.msg }}</li>
    <li class="divider" role="presentation"></li>
  </template>
</ul>

v-forv-if

同时使用 v-ifv-for 是不推荐的,因为这样二者的优先级不明显。当它们同时存在于一个节点上时,v-ifv-for 的优先级更高。这意味着 v-if 的条件将无法访问到 v-for 作用域内定义的变量别名:

<!--
 这会抛出一个错误,因为属性 todo 此时
 没有在该实例上定义(因为 v-if 优先级高于 v-for)
-->
<li v-for="todo in todos" v-if="!todo.isComplete">
  {{ todo.name }}
</li>

在外新包装一层 <template> 再在其上使用 v-for 可以解决这个问题 (这也更加明显易读):

<template v-for="todo in todos">
  <li v-if="!todo.isComplete">
    {{ todo.name }}
  </li>
</template>

通过 key 管理状态

Vue 默认按照就地更新的策略来更新通过 v-for 渲染的元素列表。当数据项的顺序改变时,Vue 不会随之移动 DOM 元素的顺序,而是就地更新每个元素,确保它们在原本指定的索引位置上渲染。

默认模式是高效的,但只适用于列表渲染输出的结果不依赖子组件状态或者临时 DOM 状态 (例如表单输入值) 的情况。

为了给 Vue 一个提示,以便它可以跟踪每个节点的标识,从而重用和重新排序现有的元素,需要为每个元素对应的块提供一个唯一的 key 属性(注意 key 是元素的一个属性,而不是对象的属性名):

<div v-for="item in items" :key="item.id">
  <!-- 内容 -->
</div>

当使用 <template v-for> 时,key 应该被放置在这个 <template> 容器上:

<template v-for="todo in todos" :key="todo.name">
  <li>{{ todo.name }}</li>
</template>

组件上使用 v-for

数组变化侦测

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

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

相对地,也有一些不可变 (immutable) 方法,例如 filter(),concat() 和 slice(),这些都不会更改原数组,而总是返回一个新数组。当遇到的是非变更方法时,我们需要将旧的数组替换为新的:

this.items = this.items.filter((item) => item.message.match(/Foo/))

这种方法并不会丢弃现有的DOM并重新渲染整个列表,vue 会最大化对 DOM 元素的重用。

展示过滤或排序后的结果

有时希望显示数组经过过滤或排序后的内容,而不实际变更或重置原始数据。在这种情况下,可以创建返回已过滤或已排序数组的计算属性。例如

data() {
  return {
    numbers: [1, 2, 3, 4, 5]
  }
},
computed: {
  evenNumbers() {
    return this.numbers.filter(n => n % 2 === 0)
  }
}
<li v-for="n in evenNumbers">{{ n }}</li>

在计算属性不可行的情况下 (例如在多层嵌套的 v-for 循环中),可以使用以下方法:

data() {
  return {
    sets: [[ 1, 2, 3, 4, 5 ], [6, 7, 8, 9, 10]]
  }
},
methods: {
  even(numbers) {
    return numbers.filter(number => number % 2 === 0)
  }
}
<ul v-for="numbers in sets">
  <li v-for="n in even(numbers)">{{ n }}</li>
</ul>

在计算属性中使用 reverse()sort() 的时候务必小心!这两个方法将变更原始数组,计算函数中不应该这么做。请在调用这些方法之前创建一个原数组的副本:

return [...numbers].reverse()

更多推荐

自学前端开发 - VUE 框架 (一): 基础、模板语法、响应式基础、Class 与 Style 的绑定、渲染