一、API风格
选项式 API 和组合式 API。
选项式 API(也就是Vue2时的写法)
使用选项式 API,我们可以用包含多个选项的对象来描述组件的逻辑,例如 data、methods 和 mounted。选项所定义的属性都会暴露在函数内部的 this 上,它会指向当前的组件实例。
实际上,选项式 API 也是用组合式 API 实现的!
<script>
export default {
// data() 返回的属性将会成为响应式的状态
// 并且暴露在 `this` 上
data() {
return {
count: 0
}
},
// methods 是一些用来更改状态与触发更新的函数
// 它们可以在模板中作为事件监听器绑定
methods: {
increment() {
this.count++
}
},
// 生命周期钩子会在组件生命周期的各个不同阶段被调用
// 例如这个函数就会在组件挂载完成后被调用
mounted() {
console.log(`The initial count is ${this.count}.`)
}
}
</script>
<template>
<button @click="increment">Count is: {{ count }}</button>
</template>
组合式 API(Vue3的新写法)
- 更加简便
在单文件组件中,组合式 API 通常会与
<script setup>
搭配使用。这个setup attribute
是一个标识,告诉 Vue 需要在编译时进行转换,来减少使用组合式 API 时的样板代码。
- 直接在
script
标签上标记
<script setup>
import { ref, onMounted } from 'vue'
// 响应式状态------相当于data中的数据定义
const count = ref(0)
// 用来修改状态、触发更新的函数
function increment() {
count.value++
}
// 生命周期钩子
onMounted(() => {
console.log(`The initial count is ${count.value}.`)
})
</script>
<template>
<button @click="increment">Count is: {{ count }}</button>
</template>
- 在
setup()
函数中手动暴露状态和方法就很麻烦【忘记暴露就更难受┭┮﹏┭┮】。
import { reactive } from 'vue'
export default {
setup() {
const state = reactive({ count: 0 })
function increment() {
state.count++
}
// 不要忘记同时暴露 increment 函数
return {
state,
increment
}
}
}
二、模板语法
动态参数
<!--
注意,参数表达式有一些约束,
参见下面“动态参数值的限制”与“动态参数语法的限制”章节的解释
-->
<a v-bind:[attributeName]="url"> ... </a>
<!-- 简写 -->
<a :[attributeName]="url"> ... </a>
动态参数值的限制
动态参数期望结果为一个字符串,或者是 null。特殊值 null 意为显式移除该绑定。任何其他非字符串的值都将触发一个警告。
动态参数语法的限制
动态参数表达式因为某些字符的缘故有一些语法限制,比如空格和引号,在 HTML attribute 名称中都是不合法的。例如下面的示例:
<!-- 这会触发一个编译器警告 -->
<a :['foo' + bar]="value"> ... </a>
三、响应式基础
声明响应式状态
我们可以使用
reactive()
函数创建一个响应式对象或数组:
import { reactive } from 'vue'
export default {
// `setup` 是一个专门用于组合式 API 的特殊钩子
setup() {
const state = reactive({ count: 0 })
// 不要忘记暴露 state 到模板
return {
state
}
}
}
<script setup>
在
setup()
函数中手动暴露状态和方法可能非常繁琐。幸运的是,你可以通过使用构建工具来简化该操作。当使用单文件组件(SFC)时,我们可以使用<script setup>
来简化大量样板代码。
// 简化大量样板代码setup()
<script setup>
import { reactive } from 'vue'
const state = reactive({ count: 0 })
function increment() {
state.count++
}
</script>
<template>
<button @click="increment">
{{ state.count }}
</button>
</template>
声明方法
Vue
自动为methods
中的方法绑定了永远指向组件实例的this
。这确保了方法在作为事件监听器或回调函数时始终保持正确的this
。你不应该在定义methods
时使用箭头函数,因为这会阻止Vue
的自动绑定。
export default {
methods: {
increment: () => {
// 反例:无法访问此处的 `this`!
}
}
}
DOM 更新时机
当你更改响应式状态后,
DOM
也会自动更新。然而,你得注意 DOM 的更新并不是同步的。相反,Vue
将缓冲它们直到更新周期的 “下个时机” 以确保无论你进行了多少次声明更改,每个组件都只需要更新一次。若要等待一个状态改变后的
DOM
更新完成,你可以使用nextTick()
这个全局 API:
import { nextTick } from 'vue'
export default {
methods: {
increment() {
this.count++
nextTick(() => {
// 访问更新后的 DOM
})
}
}
}
响应式代理 vs. 原始对象
reactive()
返回的是一个原始对象的Proxy
,它和原始对象是不相等的:
const raw = {}
const proxy = reactive(raw)
// 代理和原始对象不是全等的
console.log(proxy === raw) // false
// 在同一个对象上调用 reactive() 会返回相同的代理
console.log(reactive(raw) === proxy) // true
// 在一个代理上调用 reactive() 会返回它自己
console.log(reactive(proxy) === proxy) // true
reactive() 的局限性
- reactive() API 有两条限制:
- 仅对对象类型有效(
对象、数组和 Map、Set 这样的集合类型
),而对string、number 和 boolean
这样的原始类型
无效。- 因为 Vue 的响应式系统是通过 property 访问进行追踪的,因此我们必须始终保持对该响应式对象的相同引用。响应式对象的引用变化将导致对初始引用的响应性连接丢失:
let state = reactive({ count: 0 })
// 上面的引用 ({ count: 0 }) 将不再被追踪(响应性连接已丢失!)
state = reactive({ count: 1 })
同时这也意味着当我们将响应式对象的 property 赋值或解构至本地变量时,或是将该 property 传入一个函数时,我们会失去响应性:
const state = reactive({ count: 0 })
// n 是一个局部变量,同 state.count
// 失去响应性连接
let n = state.count
// 不影响原始的 state
n++
// count 也和 state.count 失去了响应性连接
let { count } = state
// 不会影响原始的 state
count++
// 该函数接收一个普通数字,并且
// 将无法跟踪 state.count 的变化
callSomeFunction(state.count)
ref()
定义响应式变量
为了解决
reactive()
带来的限制,Vue
也提供了一个ref()
方法来允许我们创建可以使用任何值类型的响应式ref
:
ref()
从参数中获取到值,将其包装为一个带.value property
的ref
对象:
就是对reactive()
的完善,
注意:【在进行过滤时,对象一定要定义成响应式,并给初始属性,不然会导致form
表单的change
无法显示】
const count = ref(0)
console.log(count) // { value: 0 }
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
ref
被传递给函数或是从一般对象上被解构时,不会丢失响应性:
const obj = {
foo: ref(1),
bar: ref(2)
}
// 该函数接收一个 ref
// 需要通过 .value 取值
// 但它会保持响应性
callSomeFunction(obj.foo)
// 仍然是响应式的
const { foo, bar } = obj
ref
的解包
<script setup>
import { ref } from 'vue'
const count = ref(0)
// ref在响应式对象中解包
const state = reactive({
count
})
console.log(state.count) // 0
state.count = 1
console.log(count.value) // 1
/**数组和集合类型的 ref 解包
不像响应式对象,当 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)
function increment() {
count.value++
}
</script>
<template>
<button @click="increment">
{{ count }} <!-- ref模板中解包 ---无需 .value -->
</button>
</template>
响应性语法糖
不得不对
ref
使用.value
是一个受限于 JavaScript 语言限制的缺点。然而,通过编译时转换,我们可以在适当的位置自动添加 .value 来提升开发体验。Vue
提供了一种编译时转换,使得可以像这样书写之前的“计数器”示例:
<script setup>
let count = $ref(0)
function increment() {
// 无需 .value
count++
}
</script>
<template>
<button @click="increment">{{ count }}</button>
</template>
四、计算属性
- 定义
<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>
五、条件渲染
v-if
和 v-for
当
v-if
和v-for
同时存在于一个元素上的时候,v-if
会首先被执行。
同时使用v-if
和v-for
是不推荐的,因为这样二者的优先级不明显。
v-if vs v-show
-
v-if
是“真实的”按条件渲染,因为它确保了条件区块内的事件监听器和子组件都会在切换时被销毁与重建。 -
v-if
也是懒加载的:如果在初次渲染时条件值为 false,则不会做任何事。条件区块会直到条件首次变为 true 时才渲染。
相比之下,
v-show
简单许多,元素无论初始条件如何,始终会被渲染,仅作 CSS class 的切换。
总的来说,v-if
在首次渲染时的切换成本比v-show
更高。因此当你需要非常频繁切换时v-show
会更好,而运行时不常改变的时候v-if
会更合适。
六、侦听器
侦听来源类型
watch
的第一个参数可以是不同形式的“来源”:它可以是一个ref
(包括计算属性)、一个响应式对象、一个getter
函数、或多个来源组成的数组:
const x = ref(0)
const y = ref(0)
// 单个 ref
watch(x, (newX) => {
console.log(`x is ${newX}`)
})
// getter 函数
watch(
() => x.value + y.value,
(sum) => {
console.log(`sum of x + y is: ${sum}`)
}
)
// 多个来源组成的数组
watch([x, () => y.value], ([newX, newY]) => {
console.log(`x is ${newX} and y is ${newY}`)
})
注意,你不能侦听响应式对象的 property,例如:
const obj = reactive({ count: 0 })
// 这不起作用,因为你是向 watch() 传入了一个 number
watch(obj.count, (count) => {
console.log(`count is: ${count}`)
})
// 而是用 getter 函数:提供一个 getter 函数
watch(
() => obj.count,
(count) => {
console.log(`count is: ${count}`)
}
)
深层侦听器
直接给
watch()
传入一个响应式对象,会隐式地创建一个深层侦听器——该回调函数在所有嵌套的变更时都会被触发:
const obj = reactive({ count: 0 })
watch(obj, (newValue, oldValue) => {
// 在嵌套的 property 变更时触发
// 注意:`newValue` 此处和 `oldValue` 是相等的
// 因为它们是同一个对象!
console.log('count改变了')
})
obj.count++
watchEffect()
watch()
是懒执行的:仅在侦听源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。
const url = ref('https://...')
const data = ref(null)
//----------------------------------watch
async function fetchData() {
const response = await fetch(url.value)
data.value = await response.json()
}
// 立即获取
fetchData()
// ...再侦听 url 变化
watch(url, fetchData)
watchEffect(async () => {
const response = await fetch(url.value)
data.value = await response.json()
})
//-------------------------------watchEffect替代
watchEffect(async () => {
const response = await fetch(url.value)
data.value = await response.json()
})
watch vs watchEffect
watch
和watchEffect
都能响应式地执行有副作用的回调。它们之间的主要区别是追踪响应式依赖的方式:
watch
只追踪明确侦听的源,仅在响应源确实改变时才会触发回调。因此,我们能更加精确地控制回调函数的触发时机。watchEffect
,则会在副作用发生期间追踪依赖。代码往往更简洁,但其响应性依赖关系不那么明确。
七、模板 ref
Vue2
<input ref="input">
// 访问模板ref
this.$refs.input
Vue3
(必须同名定义)
<script setup>
import { ref, onMounted } from 'vue'
// 声明一个 ref 来存放该元素的引用
// 必须和模板 ref 同名
const input = ref(null)
onMounted(() => {
input.value.focus()
})
</script>
<template>
<input ref="input" />
</template>
组件上的 ref
ref
也可以被用在一个子组件上。此时ref
中引用的是组件实例
<script setup>
import { ref, onMounted } from 'vue'
import Child from './Child.vue'
const child = ref(null)
onMounted(() => {
// child.value 是 <Child /> 组件的实例
})
</script>
<template>
<Child ref="child" />
</template>
八、Prop声明
<script setup>
const props = defineProps(['foo'])
// 对象声明
defineProps({
title: String,
likes: Number
})
console.log(props.foo)
</script>
- 搭配
TS
<script setup lang="ts">
defineProps<{
title?: string
likes?: number
}>()
</script>
九、组件事件emit
defineEmits()
<script setup>
const emit = defineEmits(['inFocus', 'submit'])
// 对象语法(对触发事件的参数进行验证)
const emit = defineEmits({
// 没有校验
click: null,
// 校验 submit 事件
submit: ({ email, password }) => {
if (email && password) {
return true
} else {
console.warn('Invalid submit event payload!')
return false
}
}
})
</script>
- 搭配
TS
<script setup lang="ts">
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
</script>
十、搭配 TypeScript 使用 Vue
像 TypeScript 这样的类型系统可以在编译时通过静态分析检测出很多常见错误。这减少了生产环境中的运行时错误,也让我们在重构大型项目的时候更有信心。通过 IDE 中基于类型的自动补全,TypeScript 还改善了开发体验和效率。
Vue 本身就是用 TypeScript 编写的,并对 TypeScript 提供了头等的支持。所有的 Vue 官方库都提供了类型声明文件,开箱即用。
IDE 支持
Volar
是官方的VSCode
扩展,提供了Vue
单文件组件中的TypeScript
支持,还伴随着一些其他非常棒的特性。Volar
替代了Vetur
,那是我们之前为Vue 2
提供的官方VSCode
扩展。如果你已经安装了Vetur
,请确保在Vue 3
项目中将它禁用。TypeScript Vue Plugin
用于支持在TS
中import *.vue
文件。
十一、TypeScript 与组合式 API
- 为组件的
prop
标注类型 - 为组件的
emit
标注类型 - 为
ref()
标注类型 - 为
reactive()
标注类型 - 为
computed()
标注类型 - 为
事件处理器
标注类型 - 为
provide/inject
标注类型 - 为
模板 ref
标注类型 - 为
组件模板 ref
标注类型