目录
响应式基础
声明响应式状态
ref()
在组合式 API 中,推荐使用 ref() 函数来声明响应式状态(这里就是接收一个值0):
import { ref } from 'vue'
const count = ref(0)
ref()
接收参数,并将其包裹在一个带有 .value
属性的 ref 对象中返回:
const count = ref(0)
console.log(count) // { value: 0 }
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
要在组件模板中访问 ref,请从组件的 setup()
函数中声明并返回它们:
import { ref } from 'vue'
export default {
// `setup` 是一个特殊的钩子,专门用于组合式 API。
setup() {//声明
const count = ref(0)
// 将 ref 暴露给模板
return {//返回
count
}
}
}
<div>{{ count }}</div>
注意,在模板中使用 ref 时,我们不需要附加.value,在模版使用的时候模版使用ref会有一些自动解包。也可以直接在事件监听器中改变一个ref
<button @click="count++">
{{ count }}
</button>
对于更复杂的逻辑,我们可以在同一作用域内声明更改 ref 的函数,并将它们作为方法与状态一起公开:
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
function increment() {
// 在 JavaScript 中需要 .value
count.value++
}
// 不要忘记同时暴露 increment 函数
return {
count,
increment
}
}
}
然后,暴露的方法可以被用作事件监听器:
<button @click="increment">
{{ count }}
</button>
这里是Codepen上的例子,没有使用任何构建工具。
在 setup()
函数中手动暴露大量的状态和方法非常繁琐。幸运的是,我们可以通过使用单文件组件SFC来避免这种情况。我们可以使用 <script setup>
来大幅度地简化代码(在单文件中直接加一个<script setup>就可以了):
<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 函数——它自然可以访问与它一起声明的所有内容。
在后面的我们基本上都会在组合式API中使用单文件组件+<script setup>,如果没有单文件组件,你仍可以在setup()选项中使用组合式API。
为什么要使用 ref而不是普通变量?
你可能会好奇:为什么我们需要使用带有 .value
的 ref,而不是普通的变量?为了解释这一点,我们需要简单地讨论一下 Vue 的响应式系统是如何工作的。
当你在模板中使用了一个 ref,然后改变了这个 ref 的值时,Vue 会自动检测到这个变化,并且相应地更新 DOM。这是通过一个基于依赖追踪的响应式系统实现的。当一个组件首次渲染时,Vue 会追踪在渲染过程中使用的每一个 ref。然后,当一个 ref 被修改时,它会触发追踪它的组件的一次重新渲染。
标准的 JavaScript 中,检测普通变量的访问或修改是行不通的。然而,我们可以通过 getter 和 setter 方法来拦截对象属性的 get 和 set 操作。
当.value
属性给予了 Vue 一个机会来检测 ref 何时被访问或修改。在其内部,Vue 在它的 getter 中执行追踪,在它的 setter 中执行触发。从概念上讲,你可以将 ref 看作是一个像这样的对象:
// 伪代码,不是真正的实现
const myRef = {
_value: 0,
get value() {
track()
return this._value
},
set value(newValue) {
this._value = newValue
trigger()
}
}
另一个 ref 的好处是,与普通变量不同,你可以将 ref 传递给函数,同时保留对最新值和响应式连接的访问。当将复杂的逻辑重构为可重用的代码时,这将非常有用。
深层响应性
Ref 可以持有任何类型的值,包括深层嵌套的对象、数组或者 JavaScript 内置的数据结构,比如 Map
。
Ref 会使它的值具有深层响应性。这意味着即使改变嵌套对象或数组时,变化也会被检测到:
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:
import { nextTick } from 'vue'
async function increment() {
count.value++
await nextTick()
// 现在 DOM 已经更新了
}
reactive()
还有另一种声明响应式状态的方式,即使用 reactive()
API。与将内部值包装在特殊对象中的 ref 不同,reactive()
将使对象本身具有响应性:
import { reactive } from 'vue'
const state = reactive({ count: 0 })
在模板中使用(state.count):
<button @click="state.count++">
{{ state.count }}
</button>
响应式对象是javascript代理,其行为就和普通对象一样。不同的是,Vue 能够拦截对响应式对象所有属性的访问和修改,以便进行依赖追踪和触发更新。
reactive()
将深层地转换对象:当访问嵌套对象时,它们也会被 reactive()
包装。当 ref 的值是一个对象时,ref()
也会在内部调用它。与浅层 ref 类似,这里也有一个 shallowReactive() API 可以选择退出深层响应性。
Reactive Proxy vs. Original
值得注意的是,reactive()
返回的是一个原始对象的代理Proxy,它和原始对象是不相等的:
const raw = {}
const proxy = reactive(raw)
// 代理对象和原始对象不是全等的
console.log(proxy === raw) // false
只有代理对象是响应式的,更改原始对象不会触发更新。因此,使用 Vue 的响应式系统的最佳实践是仅使用你声明对象的代理版本。
为保证访问代理的一致性,对同一个原始对象调用 reactive()
会总是返回同样的代理对象,而对一个已存在的代理对象调用 reactive()
会返回其本身:
// 在同一个对象上调用 reactive() 会返回相同的代理
console.log(reactive(raw) === proxy) // true
// 在一个代理上调用 reactive() 会返回它自己
console.log(reactive(proxy) === proxy) // true
这个规则对嵌套对象也适用。依靠深层响应性,响应式对象内的嵌套对象依然是代理:
const proxy = reactive({})
const raw = {}
proxy.nested = raw
console.log(proxy.nested === raw) // false
reactive()
的局限性
reactive()
API 有一些局限性:
-
有限的值类型:它只能用于对象类型 (对象、数组和如
Map
、Set
这样的集合类型)。它不能持有如string
、number
或boolean
这样的原始类型,不能对这些进行处理,需要用ref。 -
不能替换整个对象:由于 Vue 的响应式跟踪是通过属性访问实现的,因此我们必须始终保持对响应式对象的相同引用。这意味着我们不能轻易地“替换”响应式对象,因为这样的话与第一个引用的响应性连接将丢失:
let state = reactive({ count: 0 }) // 上面的 ({ count: 0 }) 引用将不再被追踪 // (响应性连接已丢失!) state = reactive({ count: 1 })
-
对解构操作不友好:当我们将响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,我们将丢失响应性连接:
const state = reactive({ count: 0 }) // 当解构时,count 已经与 state.count 断开连接 let { count } = state // 不会影响原始的 state,已经解构开了 count++ // 该函数接收到的是一个普通的数字 // 并且无法追踪 state.count 的变化 // 我们必须传入整个对象以保持响应性,只是在运行函数的时候将值传进来了 callSomeFunction(state.count)
由于这些限制,我们建议使用 ref()
作为声明响应式状态的主要 API。
额外的 ref 解包细节
作为 reactive 对象的属性
一个 ref 会在作为响应式对象的属性被访问或修改时自动解包。换句话说,它的行为就像一个普通的属性:
const count = ref(0)
const state = reactive({
count
})
console.log(state.count) // 0
state.count = 1
console.log(count.value) // 1
如果将一个新的 ref 赋值给一个关联了已有 ref 的属性,那么它会替换掉旧的 ref:
const otherCount = ref(2)
state.count = otherCount
console.log(state.count) // 2
// 原始 ref 现在已经和 state.count 失去联系
console.log(count.value) // 1
只有当嵌套在一个深层响应式对象内时,才会发生 ref 解包。当其作为浅层响应式对象的属性被访问时不会解包。
数组和集合的注意事项
与 reactive 对象不同的是,当 ref 作为响应式数组或原生集合类型 (如 Map
) 中的元素被访问时,它不会被解包:
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
不是:
const count = ref(0)
const object = { id: ref(1) }
因此,这个表达式按预期工作:
{{ count + 1 }}
但这个不会:
{{ object.id + 1 }}
渲染的结果将是 [object Object]1
,因为在计算表达式时 object.id
没有被解包,仍然是一个 ref 对象。为了解决这个问题,我们可以将 id
解构为一个顶级属性:
const { id } = object
{{ id + 1 }}
现在渲染的结果将是 2
。
另一个需要注意的点是,如果 ref 是文本插值的最终计算值 (即 {{ }}
标签),那么它将被解包,因此以下内容将渲染为 1
:
{{ object.id }}
该特性仅仅是文本插值的一个便利特性,等价于 {{ object.id.value }}
。
计算属性
基础示例
模板中的表达式虽然方便,但也只能用来做简单的操作。如果在模板中写太多逻辑,会让模板变得臃肿,难以维护。比如说,我们有这样一个包含嵌套数组的对象:
const author = reactive({
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
})
我们想根据 author
是否已有一些书籍来展示不同的信息:
<p>Has published books:</p>
<span>{{ author.books.length > 0 ? 'Yes' : 'No' }}</span>
这里的模板看起来有些复杂。我们必须认真看好一会儿才能明白它的计算依赖于 author.books
。更重要的是,如果在模板中需要不止一次这样的计算,我们可不想将这样的代码在模板里重复好多遍。
因此我们推荐使用计算属性来描述依赖响应式状态的复杂逻辑。这是重构后的示例:
<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
的绑定都会同时更新。
计算属性缓存 vs 方法
你可能注意到我们在表达式中像这样调用一个函数也会获得和计算属性相同的结果:
<p>{{ calculateBooksMessage() }}</p>
// 组件中
function calculateBooksMessage() {
return author.books.length > 0 ? 'Yes' : 'No'
}
若我们将同样的函数定义为一个方法而不是计算属性,两种方式在结果上确实是完全相同的,然而,不同之处在于计算属性值会基于其响应式依赖被缓存。一个计算属性仅会在其响应式依赖更新时才重新计算。这意味着只要 author.books
不改变,无论多少次访问 publishedBooksMessage
都会立即返回先前的计算结果,而不用重复执行 getter 函数。
这也解释了为什么下面的计算属性永远不会更新,因为 Date.now()
并不是一个响应式依赖:
const now = computed(() => Date.now())
相比之下,方法调用总是会在重渲染发生时再次执行函数。
为什么需要缓存呢?想象一下我们有一个非常耗性能的计算属性 list
,需要循环一个巨大的数组并做许多计算逻辑,并且可能也有其他计算属性依赖于 list
。没有缓存的话,我们会重复执行非常多次 list
的 getter,然而这实际上没有必要!如果你确定不需要缓存,那么也可以使用方法调用。
可写计算属性
计算属性默认是只读的。当你尝试修改一个计算属性时,你会收到一个运行时警告。只在某些特殊场景中你可能才需要用到“可写”的属性,你可以通过同时提供 getter 和 setter 来创建:
<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
会随之更新。
最佳实践
Getter 不应有副作用
计算属性的 getter 应只做计算而没有任何其他的副作用,这一点非常重要,请务必牢记。举例来说,不要改变其他状态、在 getter 中做异步请求或者更改 DOM!一个计算属性的声明中描述的是如何根据其他值派生一个值。因此 getter 的职责应该仅为计算和返回该值。在之后的指引中我们会讨论如何使用侦听器根据其他响应式状态的变更来创建副作用。
避免直接修改计算属性值
从计算属性返回的值是派生状态。可以把它看作是一个“临时快照”,每当源状态发生变化时,就会创建一个新的快照。更改快照是没有意义的,因此计算属性的返回值应该被视为只读的,并且永远不应该被更改——应该更新它所依赖的源状态以触发新的计算。
Class 与 Style 绑定
数据绑定的一个常见需求场景是操纵元素的 CSS class 列表和内联样式。因为 class
和 style
都是 attribute,我们可以和其他 attribute 一样使用 v-bind
将它们和动态的字符串绑定。但是,在处理比较复杂的绑定时,通过拼接生成字符串是麻烦且易出错的。因此,Vue 专门为 class
和 style
的 v-bind
用法提供了特殊的功能增强。除了字符串外,表达式的值也可以是对象或数组。
绑定 HTML class
绑定对象
我们可以给 :class
(v-bind:class
的缩写) 传递一个对象来动态切换 class:
<div :class="{ active: isActive }"></div>
上面的语法表示 active
是否存在取决于数据属性 isActive
的真假值。
你可以在对象中写多个字段来操作多个 class。此外,:class
指令也可以和一般的 class
attribute 共存。举例来说,下面这样的状态:
const isActive = ref(true)
const hasError = ref(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"
。
绑定的对象并不一定需要写成内联字面量的形式,也可以直接绑定一个对象:
const classObject = reactive({
active: true,
'text-danger': false
})
<div :class="classObject"></div>
这将渲染:
<div class="active"></div>
我们也可以绑定一个返回对象的计算属性。这是一个常见且很有用的技巧:
const isActive = ref(true)
const error = ref(null)
const classObject = computed(() => ({
active: isActive.value && !error.value,
'text-danger': error.value && error.value.type === 'fatal'
}))
<div :class="classObject"></div>
绑定数组
我们可以给 :class
绑定一个数组来渲染多个 CSS class:
const activeClass = ref('active')
const errorClass = ref('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="[{ [activeClass]: isActive }, errorClass]"></div>
在组件上使用
需要有Vue组件的知识基础。如果没有,可以暂时跳过,以后再阅读。
对于只有一个根元素的组件,当你使用了 class
attribute 时,这些 class 会被添加到根元素上并与该元素上已有的 class 合并。
举例来说,如果你声明了一个组件名叫 MyComponent
,模板如下:
<!-- 子组件模板 -->
<p class="foo bar">Hi!</p>
在使用时添加一些 class:
<!-- 在使用组件时 -->
<MyComponent class="baz boo" />
渲染出的 HTML 为:
<p class="foo bar baz boo">Hi!</p>
Class 的绑定也是同样的:
<MyComponent :class="{ active: isActive }" />
当 isActive
为真时,被渲染的 HTML 会是:
<p class="foo bar active">Hi!</p>
如果你的组件有多个根元素,你将需要指定哪个根元素来接收这个 class。你可以通过组件的 $attrs
属性来指定接收的元素:
<!-- MyComponent 模板使用 $attrs 时 -->
<p :class="$attrs.class">Hi!</p>
<span>This is a child component</span>
<MyComponent class="baz" />
这将被渲染为:
<p class="baz">Hi!</p>
<span>This is a child component</span>
你可以在透传Attribute一章中了解更多组件的 attribute 继承的细节。
绑定内联样式
绑定对象
:style
支持绑定 JavaScript 对象值,对应的是HTML元素style属性:
const activeColor = ref('red')
const fontSize = ref(30)
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
尽管推荐使用 camelCase,但 :style
也支持 kebab-cased 形式的 CSS 属性 key (对应其 CSS 中的实际名称),例如:
<div :style="{ 'font-size': fontSize + 'px' }"></div>
直接绑定一个样式对象通常是一个好主意,这样可以使模板更加简洁:
const styleObject = reactive({
color: 'red',
fontSize: '30px'
})
<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.js