1、创建一个 Vue 应用
1.1 应用实例
每个 Vue 应用都是通过 createApp 函数创建一个新的 应用实例:
javascript
import { createApp } from 'vue'
const app = createApp({
/* 根组件选项 */
})
1.2 根组件
我们传入 createApp
的对象实际上是一个组件,每个应用都需要一个"根组件",其他组件将作为其子组件。
如果你使用的是单文件组件,我们可以直接从另一个文件中导入根组件。
javascript
import { createApp } from 'vue'
// 从一个单文件组件中导入根组件
import App from './App.vue'
const app = createApp(App)
虽然本指南中的许多示例只需要一个组件,但大多数真实的应用都是由一棵嵌套的、可重用的组件树组成的。例如,一个待办事项 (Todos) 应用的组件树可能是这样的:
javascript
App (root component)
├─ TodoList
│ └─ TodoItem
│ ├─ TodoDeleteButton
│ └─ TodoEditButton
└─ TodoFooter
├─ TodoClearButton
└─ TodoStatistics
我们会在指南的后续章节中讨论如何定义和组合多个组件。在那之前,我们得先关注一个组件内到底发生了什么。
1.3 挂载应用
应用实例必须在调用了 .mount()
方法后才会渲染出来。该方法接收一个"容器"参数,可以是一个实际的 DOM 元素或是一个 CSS 选择器字符串:
html
<div id="app"></div>
javascript
app.mount('#app')
应用根组件的内容将会被渲染在容器元素里面。容器元素自己将不会
被视为应用的一部分。
.mount()
方法应该始终在整个应用配置和资源注册完成后被调用 。同时请注意,不同于其他资源注册方法,它的返回值是根组件实例而非应用实例。
DOM 中的根组件模板
根组件的模板通常是组件本身的一部分,但也可以直接通过在挂载容器内编写模板来单独提供:
html
<div id="app">
<button @click="count++">{{ count }}</button>
</div>
javascript
import { createApp } from 'vue'
const app = createApp({
data() {
return {
count: 0
}
}
})
app.mount('#app')
当根组件没有设置 template
选项时,Vue 将自动使用容器的 innerHTML
作为模板。
DOM 内模板通常用于无构建步骤添加链接描述的 Vue 应用程序。它们也可以与服务器端框架一起使用,其中根模板可能是由服务器动态生成的。
1.4 应用配置
应用实例会暴露一个 .config
对象允许我们配置一些应用级的选项,例如定义一个应用级的错误处理器,用来捕获所有子组件上的错误:
javascript
app.config.errorHandler = (err) => {
/* 处理错误 */
}
应用实例还提供了一些方法来注册应用范围内可用的资源,例如注册一个组件:
javascript
app.component('TodoDeleteButton', TodoDeleteButton)
这使得 TodoDeleteButton
在应用的任何地方都是可用的 。我们会在指南的后续章节中讨论关于组件和其他资源的注册 。你也可以在 API 参考中浏览应用实例 API 的完整列表。
确保在挂载应用实例之前完成所有应用配置!
1.5 多个应用实例
应用实例并不只限于一个
。createApp API 允许你在同一个页面中创建多个共存的 Vue 应用,而且每个应用都拥有自己的用于配置和全局资源的作用域。
javascript
const app1 = createApp({
/* ... */
})
app1.mount('#container-1')
const app2 = createApp({
/* ... */
})
app2.mount('#container-2')
如果你正在使用 Vue 来增强服务端渲染 HTML,并且只想要 Vue 去控制一个大型页面中特殊的一小部分,应避免将一个单独的 Vue 应用实例挂载到整个页面上,而是应该创建多个小的应用实例,将它们分别挂载到所需的元素上去。
2、模板语法
Vue 使用一种基于 HTML 的模板语法,使我们能够声明式地将其组件实例的数据绑定到呈现的 DOM 上。所有的 Vue 模板都是语法层面合法的 HTML,可以被符合规范的浏览器和 HTML 解析器解析。
在底层机制中,Vue 会将模板编译成高度优化的 JavaScript 代码。结合响应式系统,当应用状态变更时,Vue 能够智能地推导出需要重新渲染的组件的最少数量,并应用最少的 DOM 操作。
如果你对虚拟 DOM 的概念比较熟悉,并且偏好直接使用 JavaScript,你也可以结合可选的 JSX 支持直接手写渲染函数而不采用模板。但请注意,这将不会享受到和模板同等级别的编译时优化。
2.1 文本插值
最基本的数据绑定形式是文本插值,它使用的是"Mustache"语法 (即双大括号):
javascript
<span>Message: {{ msg }}</span>
双大括号标签会被替换为相应组件实例中 msg
属性的值。同时每次 msg
属性更改时它也会同步更新。
2.2 原始 HTML
双大括号会将数据解释为纯文本,而不是 HTML。若想插入 HTML,你需要使用 v-html 指令:
javascript
<p>Using text interpolation: {{ rawHtml }}</p>
<p>Using v-html directive: <span v-html="rawHtml"></span></p>
bash
Using text interpolation: <span style="color: red">This should be red.</span>
Using v-html directive: This should be red.
这里我们遇到了一个新的概念。这里看到的 v-html
attribute 被称为一个指令 。指令由 v-
作为前缀,表明它们是一些由 Vue 提供的特殊 attribute,你可能已经猜到了,它们将为渲染的 DOM 应用特殊的响应式行为。这里我们做的事情简单来说就是:在当前组件实例上,将此元素的 innerHTML 与 rawHtml 属性保持同步。
span
的内容将会被替换为 rawHtml
属性的值,插值为纯 HTML------数据绑定将会被忽略。注意,你不能使用 v-html
来拼接组合模板,因为 Vue 不是一个基于字符串的模板引擎 。在使用 Vue 时,应当使用组件作为 UI 重用和组合的基本单元。
在网站上动态渲染任意 HTML 是非常危险的,因为这非常容易造成 XSS 漏洞。请仅在内容安全可信时再使用 v-html,并且永远不要使用用户提供的 HTML 内容。
2.3 Attribute 绑定
双大括号不能在 HTML attributes 中使用。想要响应式地绑定一个 attribute,应该使用 v-bind 指令:
javascript
<div v-bind:id="dynamicId"></div>
v-bind
指令指示 Vue 将元素的 id
attribute 与组件的 dynamicId
属性保持一致。如果绑定的值是null
或者 undefined
,那么该 attribute 将会从渲染的元素上移除。
简写
因为 v-bind
非常常用,我们提供了特定的简写语法:
javascript
<div :id="dynamicId"></div>
开头为 :
的 attribute 可能和一般的 HTML attribute 看起来不太一样 ,但它的确是合法的 attribute 名称字符,并且所有支持 Vue 的浏览器都能正确解析它。此外,他们不会出现在最终渲染的 DOM 中 。简写语法是可选的,但相信在你了解了它更多的用处后,你应该会更喜欢它。
布尔型 Attribute
布尔型 attribute 依据 true / false 值来决定 attribute 是否应该存在于该元素上 。disabled 就是最常见的例子之一。
v-bind
在这种场景下的行为略有不同
javascript
<button :disabled="isButtonDisabled">Button</button>
当 isButtonDisabled
为真值或一个空字符串 (即 <button disabled="">
) 时,元素会包含这个 disabled
attribute。而当其为其他假值时 attribute 将被忽略。
动态绑定多个值
如果你有像这样的一个包含多个 attribute 的 JavaScript 对象:
javascript
const objectOfAttrs = {
id: 'container',
class: 'wrapper'
}
通过不带参数的 v-bind
,你可以将它们绑定到单个元素上:
javascript
<div v-bind="objectOfAttrs"></div>
2.4 使用 JavaScript 表达式
至此,我们仅在模板中绑定了一些简单的属性名。但是 Vue 实际上在所有的数据绑定中都支持完整的 JavaScript 表达式:
javascript
{{ number + 1 }}
{{ ok ? 'YES' : 'NO' }}
{{ message.split('').reverse().join('') }}
<div :id="`list-${id}`"></div>
这些表达式都会被作为 JavaScript ,以当前组件实例为作用域解析执行。
在 Vue 模板内,JavaScript 表达式可以被使用在如下场景上:
- 在文本插值中 (双大括号)
- 在任何 Vue 指令 (以 v- 开头的特殊 attribute) attribute 的值中
仅支持表达式
每个绑定仅支持单一表达式,也就是一段能够被求值的 JavaScript 代码 。一个简单的判断方法是是否可以合法地写在 return
后面。
因此,下面的例子都是无效的:
javascript
<!-- 这是一个语句,而非表达式 -->
{{ var a = 1 }}
<!-- 条件控制也不支持,请使用三元表达式 -->
{{ if (ok) { return message } }}
调用函数
可以在绑定的表达式中使用一个组件暴露的方法:
javascript
<time :title="toTitleDate(date)" :datetime="date">
{{ formatDate(date) }}
</time>
绑定在表达式中的方法在组件每次更新时都会被重新调用 ,因此不应该产生任何副作用,比如改变数据或触发异步操作。
受限的全局访问
模板中的表达式将被沙盒化,仅能够访问到有限的全局对象列表 。该列表中会暴露常用的内置全局对象,比如 Math
和 Date
。
没有显式包含在列表中的全局对象将不能在模板内表达式中访问,例如用户附加在 window
上的属性。然而,你也可以自行在 app.config.globalProperties 上显式地添加它们,供所有的 Vue 表达式使用。
2.5 指令 Directives
指令是带有 v-
前缀的特殊 attribute。Vue 提供了许多内置指令,包括上面我们所介绍的 v-bind
和 v-html
。
指令 attribute 的期望值为一个 JavaScript 表达式 (除了少数几个例外,即之后要讨论到的 v-for
、v-on
和 v-slot
)。一个指令的任务是在其表达式的值变化时响应式地更新 DOM。以 v-if 为例:
javascript
<p v-if="seen">Now you see me</p>
这里,v-if
指令会基于表达式 seen 的值的真假来移除/插入该 <p>
元素。
参数 Arguments
某些指令会需要一个"参数
",在指令名后通过一个冒号隔开做标识 。例如用 v-bind
指令来响应式地更新一个 HTML attribute:
javascript
<a v-bind:href="url"> ... </a>
<!-- 简写 -->
<a :href="url"> ... </a>
这里 href
就是一个参数,它告诉 v-bind
指令将表达式 url 的值绑定到元素的 href
attribute 上。在简写中,参数前的一切 (例如 v-bind:
) 都会被缩略为一个 :
字符。
另一个例子是 v-on
指令,它将监听 DOM 事件:
javascript
<a v-on:click="doSomething"> ... </a>
<!-- 简写 -->
<a @click="doSomething"> ... </a>
这里的参数是要监听的事件名称:click
。v-on
有一个相应的缩写,即 @
字符。我们之后也会讨论关于事件处理的更多细节。
动态参数
同样在指令参数上也可以使用一个 JavaScript 表达式,需要包含在一对方括号内:
javascript
<!--
注意,参数表达式有一些约束,
参见下面"动态参数值的限制"与"动态参数语法的限制"章节的解释
-->
<a v-bind:[attributeName]="url"> ... </a>
<!-- 简写 -->
<a :[attributeName]="url"> ... </a>
这里的 attributeName
会作为一个 JavaScript 表达式被动态执行,计算得到的值会被用作最终的参数 。举例来说,如果你的组件实例有一个数据属性 attributeName
,其值为 "href"
,那么这个绑定就等价于 v-bind:href
。
相似地,你还可以将一个函数绑定到动态的事件名称上:
javascript
<a v-on:[eventName]="doSomething"> ... </a>
<!-- 简写 -->
<a @[eventName]="doSomething">
在此示例中,当 eventName
的值是 "focus
" 时,v-on:[eventName]
就等价于 v-on:focus
。
动态参数值的限制
动态参数中表达式的值应当是一个字符串,或者是 null 。特殊值 nul
l 意为显式移除该绑定。其他非字符串的值会触发警告。
动态参数语法的限制
动态参数表达式因为某些字符的缘故有一些语法限制,比如空格和引号,在 HTML attribute 名称中都是不合法的。例如下面的示例:
javascript
<!-- 这会触发一个编译器警告 -->
<a :['foo' + bar]="value"> ... </a>
如果你需要传入一个复杂的动态参数,我们推荐使用计算属性替换复杂的表达式,也是 Vue 最基础的概念之一,我们很快就会讲到。
当使用 DOM 内嵌模板 (直接写在 HTML 文件里的模板) 时,我们需要避免在名称中使用大写字母,因为浏览器会强制将其转换为小写:
javascript
<a :[someAttr]="value"> ... </a>
上面的例子将会在 DOM 内嵌模板中被转换为 :[someattr]
。如果你的组件拥有 "someAttr
" 属性而非 "someattr
",这段代码将不会工作。单文件组件内的模板不受此限制。
修饰符 Modifiers
修饰符是以点开头的特殊后缀,表明指令需要以一些特殊的方式被绑定 。例如 .prevent
修饰符会告知 v-on
指令对触发的事件调用 event.preventDefault()
:
javascript
<form @submit.prevent="onSubmit">...</form>
之后在讲到 v-on
和 v-model
的功能时,你将会看到其他修饰符的例子。
最后,在这里你可以直观地看到完整的指令语法:
3、响应式基础
3.1 声明响应式状态
ref()
在组合式 API 中,推荐使用 ref() 函数来声明响应式状态:
javascript
import { ref } from 'vue'
const count = ref(0)
ref()
接收参数,并将其包裹在一个带有 .value
属性的 ref 对象中返回:
javascript
const count = ref(0)
console.log(count) // { value: 0 }
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
要在组件模板中访问 ref ,请从组件的 setup()
函数中声明并返回它们:
javascript
import { ref } from 'vue'
export default {
// `setup` 是一个特殊的钩子,专门用于组合式 API。
setup() {
const count = ref(0)
// 将 ref 暴露给模板
return {
count
}
}
}
html
<div>{{ count }}</div>
注意,在模板中使用 ref 时,我们不需要附加 .value
。为了方便起见,当在模板中使用时,ref 会自动解包 (有一些注意事项)。
你也可以直接在事件监听器中改变一个 ref:
javascript
<button @click="count++">
{{ count }}
</button>
对于更复杂的逻辑,我们可以在同一作用域内声明更改 ref 的函数,并将它们作为方法与状态一起公开:
javascript
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
function increment() {
// 在 JavaScript 中需要 .value
count.value++
}
// 不要忘记同时暴露 increment 函数
return {
count,
increment
}
}
}
然后,暴露的方法可以被用作事件监听器:
javascript
<button @click="increment">
{{ count }}
</button>
<script setup>
在 setup()
函数中手动暴露大量的状态和方法非常繁琐 。幸运的是,我们可以通过使用单文件组件 (SFC) 来避免这种情况 。我们可以使用 <script setup>
来大幅度地简化代码:
javascript
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
</script>
<template>
<button @click="increment">
{{ count }}
</button>
</template>
<script setup>
中的顶层的导入、声明的变量和函数可在同一组件的模板中直接使用。你可以理解为模板是在同一作用域内声明的一个 JavaScript 函数------它自然可以访问与它一起声明的所有内容。
为什么要使用 ref?
你可能会好奇:为什么我们需要使用带有 .value
的 ref ,而不是普通的变量?为了解释这一点,我们需要简单地讨论一下 Vue 的响应式系统是如何工作的。
当你在模板中使用了一个 ref ,然后改变了这个 ref 的值时,Vue 会自动检测到这个变化,并且相应地更新 DOM 。这是通过一个基于依赖追踪的响应式系统实现的。当一个组件首次渲染时,Vue 会追踪在渲染过程中使用的每一个 ref 。然后,当一个 ref 被修改时,它会触发追踪它的组件的一次重新渲染。
在标准的 JavaScript 中,检测普通变量的访问或修改是行不通的。然而,我们可以通过 getter 和 setter 方法来拦截对象属性的 get 和 set 操作。
该 .value
属性给予了 Vue 一个机会来检测 ref 何时被访问或修改。在其内部,Vue 在它的 getter 中执行追踪,在它的 setter 中执行触发。从概念上讲,你可以将 ref 看作是一个像这样的对象:
javascript
// 伪代码,不是真正的实现
const myRef = {
_value: 0,
get value() {
track()
return this._value
},
set value(newValue) {
this._value = newValue
trigger()
}
}
另一个 ref 的好处是,与普通变量不同,你可以将 ref 传递给函数,同时保留对最新值和响应式连接的访问。当将复杂的逻辑重构为可重用的代码时,这将非常有用。
该响应性系统在深入响应式原理章节中有更详细的讨论。
深层响应性
Ref 可以持有任何类型的值 ,包括深层嵌套的对象、数组或者 JavaScript 内置的数据结构,比如 Map
。
Ref 会使它的值具有深层响应性。这意味着即使改变嵌套对象或数组时,变化也会被检测到:
javascript
import { ref } from 'vue'
const obj = ref({
nested: { count: 0 },
arr: ['foo', 'bar']
})
function mutateDeeply() {
// 以下都会按照期望工作
obj.value.nested.count++
obj.value.arr.push('baz')
}
非原始值将通过 reactive() 转换为响应式代理,该函数将在后面讨论。
也可以通过 shallow ref 来放弃深层响应性 。对于浅层 ref,只有 .value
的访问会被追踪。浅层 ref 可以用于避免对大型数据的响应性开销来优化性能、或者有外部库管理其内部状态的情况。
DOM 更新时机
当你修改了响应式状态时,DOM 会被自动更新。但是需要注意的是,DOM 更新不是同步的 。Vue 会在"next tick"更新周期中缓冲所有状态的修改,以确保不管你进行了多少次状态修改,每个组件都只会被更新一次。
要等待 DOM 更新完成后再执行额外的代码 ,可以使用 nextTick() 全局 API:
javascript
import { nextTick } from 'vue'
async function increment() {
count.value++
await nextTick()
// 现在 DOM 已经更新了
}
3.2 reactive()
还有另一种声明响应式状态的方式,即使用 reactive()
API。与将内部值包装在特殊对象中的 ref
不同,reactive()
将使对象本身具有响应性:
javascript
import { reactive } from 'vue'
const state = reactive({ count: 0 })
在模板中使用:
javascript
<button @click="state.count++">
{{ state.count }}
</button>
响应式对象是 JavaScript 代理 ,其行为就和普通对象一样。不同的是,Vue 能够拦截对响应式对象所有属性的访问和修改,以便进行依赖追踪和触发更新。
reactive()
将深层地转换对象 :当访问嵌套对象时,它们也会被 reactive()
包装。当 ref
的值是一个对象时,ref()
也会在内部调用它。与浅层 ref 类似,这里也有一个 shallowReactive() API 可以选择退出深层响应性。
Reactive Proxy vs. Original
值得注意的是,reactive()
返回的是一个原始对象的 Proxy,它和原始对象是不相等的:
javascript
const raw = {}
const proxy = reactive(raw)
// 代理对象和原始对象不是全等的
console.log(proxy === raw) // false
只有代理对象是响应式的,更改原始对象不会触发更新 。因此,使用 Vue 的响应式系统的最佳实践是 仅使用你声明对象的代理版本。
为保证访问代理的一致性,对同一个原始对象调用 reactive() 会总是返回同样的代理对象,而对一个已存在的代理对象调用 reactive() 会返回其本身:
javascript
// 在同一个对象上调用 reactive() 会返回相同的代理
console.log(reactive(raw) === proxy) // true
// 在一个代理上调用 reactive() 会返回它自己
console.log(reactive(proxy) === proxy) // true
这个规则对嵌套对象也适用。依靠深层响应性,响应式对象内的嵌套对象依然是代理:
javascript
const proxy = reactive({})
const raw = {}
proxy.nested = raw
console.log(proxy.nested === raw) // false
reactive() 的局限性
reactive() API 有一些局限性:
- 有限的值类型 :它只能用于对象类型 (对象、数组和如 Map、Set 这样的集合类型)。它不能持有如 string、number 或 boolean 这样的原始类型。
- 不能替换整个对象:由于 Vue 的响应式跟踪是通过属性访问实现的,因此我们必须始终保持对响应式对象的相同引用。这意味着我们不能轻易地"替换"响应式对象,因为这样的话与第一个引用的响应性连接将丢失:
javascript
let state = reactive({ count: 0 })
// 上面的 ({ count: 0 }) 引用将不再被追踪
// (响应性连接已丢失!)
state = reactive({ count: 1 })
- 对解构操作不友好:当我们将响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,我们将丢失响应性连接:
javascript
const state = reactive({ count: 0 })
// 当解构时,count 已经与 state.count 断开连接
let { count } = state
// 不会影响原始的 state
count++
// 该函数接收到的是一个普通的数字
// 并且无法追踪 state.count 的变化
// 我们必须传入整个对象以保持响应性
callSomeFunction(state.count)
由于这些限制,我们建议使用 ref()
作为声明响应式状态的主要 API。
3.3 额外的 ref 解包细节
作为 reactive 对象的属性
一个 ref 会在作为响应式对象的属性被访问或修改时自动解包。换句话说,它的行为就像一个普通的属性:
javascript
const count = ref(0)
const state = reactive({
count
})
console.log(state.count) // 0
state.count = 1
console.log(count.value) // 1
只有当嵌套在一个深层响应式对象内时,才会发生 ref 解包。当其作为浅层响应式对象的属性被访问时不会解包。
数组和集合的注意事项
与 reactive
对象不同的是,当 ref
作为响应式数组或原生集合类型(如 Map) 中的元素被访问时,它不会被解包:
javascript
const books = reactive([ref('Vue 3 Guide')])
// 这里需要 .value
console.log(books[0].value)
const map = reactive(new Map([['count', ref(0)]]))
// 这里需要 .value
console.log(map.get('count').value)
在模板中解包的注意事项
在模板渲染上下文中,只有顶级的 ref
属性才会被解包。
在下面的例子中,count
和 object
是顶级属性,但 object.id
不是:
javascript
const count = ref(0)
const object = { id: ref(0) }
因此,这个表达式按预期工作:
javascript
{{ count + 1 }}
...但这个不会:
javascript
{{ object.id + 1 }}
渲染的结果将是 [object Object]1
,因为在计算表达式时 object.id 没有被解包,仍然是一个 ref 对象。为了解决这个问题,我们可以将 id
解构为一个顶级属性:
javascript
const { id } = object
javascript
{{ id + 1 }}
现在渲染的结果将是 2。
另一个需要注意的点是,如果 ref 是文本插值的最终计算值 (即 {``{ }}
标签),那么它将被解包,因此以下内容将渲染为 1:
javascript
{{ object.id }}
该特性仅仅是文本插值的一个便利特性,等价于 {``{ object.id.value }}
。
4、计算属性
4.1 基础示例
模板中的表达式虽然方便,但也只能用来做简单的操作。如果在模板中写太多逻辑,会让模板变得臃肿,难以维护。比如说,我们有这样一个包含嵌套数组的对象:
javascript
const author = reactive({
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
})
我们想根据 author
是否已有一些书籍来展示不同的信息:
javascript
<p>Has published books:</p>
<span>{{ author.books.length > 0 ? 'Yes' : 'No' }}</span>
这里的模板看起来有些复杂。我们必须认真看好一会儿才能明白它的计算依赖于 author.books
。更重要的是,如果在模板中需要不止一次这样的计算,我们可不想将这样的代码在模板里重复好多遍。
因此我们推荐使用计算属性来描述依赖响应式状态的复杂逻辑。这是重构后的示例:
javascript
<script setup>
import { reactive, computed } from 'vue'
const author = reactive({
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
})
// 一个计算属性 ref
const publishedBooksMessage = computed(() => {
return author.books.length > 0 ? 'Yes' : 'No'
})
</script>
<template>
<p>Has published books:</p>
<span>{{ publishedBooksMessage }}</span>
</template>
我们在这里定义了一个计算属性 publishedBooksMessage
。computed()
方法期望接收一个 getter
函数,返回值为一个计算属性 ref 。和其他一般的 ref 类似,你可以通过 publishedBooksMessage.value 访问计算结果。计算属性 ref 也会在模板中自动解包,因此在模板表达式中引用时无需添加 .value
。
Vue 的计算属性会自动追踪响应式依赖。它会检测到 publishedBooksMessage
依赖于 author.books
,所以当 author.books
改变时,任何依赖于 publishedBooksMessage
的绑定都会同时更新。
4.2 计算属性缓存 vs 方法
你可能注意到我们在表达式中像这样调用一个函数也会获得和计算属性相同的结果:
javascript
<p>{{ calculateBooksMessage() }}</p>
javascript
// 组件中
function calculateBooksMessage() {
return author.books.length > 0 ? 'Yes' : 'No'
}
若我们将同样的函数定义为一个方法而不是计算属性,两种方式在结果上确实是完全相同的,然而,不同之处在于计算属性值会基于其响应式依赖被缓存 。一个计算属性仅会在其响应式依赖更新时才重新计算。这意味着只要 author.books
不改变,无论多少次访问 publishedBooksMessage
都会立即返回先前的计算结果,而不用重复执行 getter
函数。
这也解释了为什么下面的计算属性永远不会更新,因为 Date.now()
并不是一个响应式依赖
javascript
const now = computed(() => Date.now())
相比之下,方法调用总是
会在重渲染发生时再次执行函数。
为什么需要缓存呢?想象一下我们有一个非常耗性能的计算属性 list
,需要循环一个巨大的数组并做许多计算逻辑,并且可能也有其他计算属性依赖于 list
。没有缓存的话,我们会重复执行非常多次list
的 getter,然而这实际上没有必要!如果你确定不需要缓存,那么也可以使用方法调用。
4.3 可写计算属性
计算属性默认是只读的。当你尝试修改一个计算属性时,你会收到一个运行时警告。只在某些特殊场景中你可能才需要用到"可写"的属性,你可以通过同时提供 getter 和 setter 来创建:
javascript
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
const fullName = computed({
// getter
get() {
return firstName.value + ' ' + lastName.value
},
// setter
set(newValue) {
// 注意:我们这里使用的是解构赋值语法
[firstName.value, lastName.value] = newValue.split(' ')
}
})
</script>
现在当你再运行 fullName.value = 'John Doe'
时,setter 会被调用而 firstName
和 lastName
会随之更新。
4.4 最佳实践
Getter 不应有副作用
计算属性的 getter
应只做计算而没有任何其他的副作用,这一点非常重要,请务必牢记 。举例来说,不要在 getter 中做异步请求或者更改 DOM !一个计算属性的声明中描述的是如何根据其他值派生一个值。因此 getter 的职责应该仅为计算和返回该值。在之后的指引中我们会讨论如何使用侦听器根据其他响应式状态的变更来创建副作用。
避免直接修改计算属性值
从计算属性返回的值是派生状态。可以把它看作是一个"临时快照",每当源状态发生变化时,就会创建一个新的快照。更改快照是没有意义的,因此计算属性的返回值应该被视为只读的,并且永远不应该被更改------应该更新它所依赖的源状态以触发新的计算
。
5、Class 与 Style 绑定
数据绑定的一个常见需求场景是操纵元素的 CSS class 列表和内联样式 。因为 class
和 style
都是 attribute,我们可以和其他 attribute 一样使用 v-bind
将它们和动态的字符串绑定。但是,在处理比较复杂的绑定时,通过拼接生成字符串是麻烦且易出错的。因此,Vue 专门为 class
和 style
的 v-bind
用法提供了特殊的功能增强 。除了字符串外,表达式的值也可以是对象或数组。
5.1 绑定 HTML class
绑定对象
我们可以给 :class
(v-bind:class
的缩写) 传递一个对象来动态切换 class:
javascript
<div :class="{ active: isActive }"></div>
上面的语法表示 active
是否存在取决于数据属性 isActive
的真假值。
你可以在对象中写多个字段来操作多个 class。此外,:class 指令也可以和一般的 class attribute 共存。举例来说,下面这样的状态:
javascript
const isActive = ref(true)
const hasError = ref(false)
配合以下模板:
javascript
<div
class="static"
:class="{ active: isActive, 'text-danger': hasError }"
></div>
渲染的结果会是:
javascript
<div class="static active"></div>
当 isActive
或者 hasError
改变时,class
列表会随之更新。举例来说,如果 hasError
变为 true
,class
列表也会变成 "static active text-danger"
。
绑定的对象并不一定需要写成内联字面量的形式,也可以直接绑定一个对象:
javascript
const classObject = reactive({
active: true,
'text-danger': false
})
javascript
<div :class="classObject"></div>
这也会渲染出相同的结果。我们也可以绑定一个返回对象的计算属性。这是一个常见且很有用的技巧:
javascript
const isActive = ref(true)
const error = ref(null)
const classObject = computed(() => ({
active: isActive.value && !error.value,
'text-danger': error.value && error.value.type === 'fatal'
}))
javascript
<div :class="classObject"></div>
绑定数组
我们可以给 :class
绑定一个数组来渲染多个 CSS class:
javascript
const activeClass = ref('active')
const errorClass = ref('text-danger')
javascript
<div :class="[activeClass, errorClass]"></div>
渲染的结果是:
javascript
<div class="active text-danger"></div>
如果你也想在数组中有条件地渲染某个 class,你可以使用三元表达式:
javascript
<div :class="[isActive ? activeClass : '', errorClass]"></div>
errorClass
会一直存在,但 activeClass
只会在 isActive
为真时才存在。
然而,这可能在有多个依赖条件的 class
时会有些冗长。因此也可以在数组中嵌套对象:
javascript
<div :class="[{ active: isActive }, errorClass]"></div>
在组件上使用
对于只有一个根元素的组件,当你使用了 class
attribute 时,这些 class 会被添加到根元素上并与该元素上已有的 class 合并。
举例来说,如果你声明了一个组件名叫 MyComponent
,模板如下:
html
<!-- 子组件模板 -->
<p class="foo bar">Hi!</p>
在使用时添加一些 class
:
html
<!-- 在使用组件时 -->
<MyComponent class="baz boo" />
渲染出的 HTML 为:
html
<p class="foo bar baz boo">Hi!</p>
class
的绑定也是同样的:
html
<MyComponent :class="{ active: isActive }" />
当 isActive
为真时,被渲染的 HTML 会是:
html
<p class="foo bar active">Hi!</p>
如果你的组件有多个根元素,你将需要指定哪个根元素来接收这个 class
。你可以通过组件的 $attrs
属性来实现指定:
html
<!-- MyComponent 模板使用 $attrs 时 -->
<p :class="$attrs.class">Hi!</p>
<span>This is a child component</span>
html
<MyComponent class="baz" />
这将被渲染为:
javascript
<p class="baz">Hi!</p>
<span>This is a child component</span>
你可以在透传 Attribute 一章中了解更多组件的 attribute 继承的细节。
5.2 绑定内联样式
绑定对象
:style
支持绑定 JavaScript 对象值,对应的是 HTML 元素的 style
属性:
javascript
const activeColor = ref('red')
const fontSize = ref(30)
html
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
尽管推荐使用 camelCase ,但 :style
也支持 kebab-cased 形式的 CSS 属性 key (对应其 CSS 中的实际名称),例如:
javascript
<div :style="{ 'font-size': fontSize + 'px' }"></div>
直接绑定一个样式对象通常是一个好主意,这样可以使模板更加简洁:
javascript
const styleObject = reactive({
color: 'red',
fontSize: '13px'
})
html
<div :style="styleObject"></div>
同样的,如果样式对象需要更复杂的逻辑,也可以使用返回样式对象的计算属性。
绑定数组
我们还可以给 :style
绑定一个包含多个样式对象的数组。这些对象会被合并后渲染到同一元素上:
html
<div :style="[baseStyles, overridingStyles]"></div>
自动前缀
当你在 :style
中使用了需要浏览器特殊前缀的 CSS 属性时,Vue 会自动为他们加上相应的前缀。Vue 是在运行时检查该属性是否支持在当前浏览器中使用。如果浏览器不支持某个属性,那么将尝试加上各个浏览器特殊前缀,以找到哪一个是被支持的。
样式多值
你可以对一个样式属性提供多个 (不同前缀的) 值,举例来说:
html
<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>
数组仅会渲染浏览器支持的最后一个值 。在这个示例中,在支持不需要特别前缀的浏览器中都会渲染为 display: flex
。
6、条件渲染
6.1 v-if
v-if
指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回真值时才被渲染。
html
<h1 v-if="awesome">Vue is awesome!</h1>
6.2 v-else
你也可以使用 v-else
为 v-if
添加一个"`else 区块"。
html
<button @click="awesome = !awesome">Toggle</button>
<h1 v-if="awesome">Vue is awesome!</h1>
<h1 v-else>Oh no 😢</h1>
一个 v-else
元素必须跟在一个 v-if
或者 v-else-if
元素后面,否则它将不会被识别。
6.3 v-else-if
顾名思义,v-else-if
提供的是相应于 v-if
的"else if 区块"。它可以连续多次重复使用:
html
<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
元素后面。
6.4 <template>
上的 v-if
因为 v-if
是一个指令,他必须依附于某个元素。但如果我们想要切换不止一个元素呢?在这种情况下我们可以在一个 <template>
元素上使用 v-if
,这只是一个不可见的包装器元素,最后渲染的结果并不会包含这个 <template>
元素
html
<template v-if="ok">
<h1>Title</h1>
<p>Paragraph 1</p>
<p>Paragraph 2</p>
</template>
v-else
和 v-else-if
也可以在 <template>
上使用。
6.5 v-show
另一个可以用来按条件显示一个元素的指令是 v-show
。其用法基本一样:
html
<h1 v-show="ok">Hello!</h1>
不同之处在于 v-show
会在 DOM 渲染中保留该元素;v-show
仅切换了该元素上名为 display
的 CSS 属性。
v-show
不支持在 <template>
元素上使用,也不能和 v-else
搭配使用。
6.6 v-if vs. v-show
v-if
是"真实的"按条件渲染,因为它确保了在切换时,条件区块内的事件监听器和子组件都会被销毁与重建。
v-if
也是惰性的:如果在初次渲染时条件值为 false,则不会做任何事。条件区块只有当条件首次变为 true 时才被渲染。
相比之下,v-show 简单许多,元素无论初始条件如何,始终会被渲染,只有 CSS display
属性会被切换。
总的来说,v-if
有更高的切换开销,而 v-show
有更高的初始渲染开销 。因此,如果需要频繁切换,则使用 v-show
较好;如果在运行时绑定条件很少改变,则 v-if
会更合适。
6.7 v-if 和 v-for
同时使用 v-if 和 v-for 是不推荐的,因为这样二者的优先级不明显。请查看风格指南获得更多信息。
当v-if
和 v-fo
r 同时存在于一个元素上的时候,v-if
会首先被执行 。请查看列表渲染指南获取更多细节。