1、响应式核心
1.1 ref() : 基本类型响应。
可以持有任何类型的值,包括深层嵌套的对象、数组或者 JavaScript 内置的数据结构。Ref 会使它的值具有深层响应性。这意味着即使改变嵌套对象或数组时,变化也会被检测到:
import { ref } from 'vue'
const count = ref(0)
console.log(count) // { value: 0 }
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
// =============== 第2示例 =============
const obj = ref({
nested: { count: 0 },
arr: ['foo', 'bar']
})
function mutateDeeply() {
// 以下都会按照期望工作
obj.value.nested.count++
obj.value.arr.push('baz')
}
// 得到的类型:Ref<string | number>
const year = ref<string | number>('2020')
year.value = 2020 // 成功!
额外的 ref 解包细节:
只有当嵌套在一个深层响应式对象内时,才会发生 ref 解包。只有最顶级的ref包才会被解包。
当其作为浅层响应式对象的属性被访问时不会解包。
一个 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
1.2 reactive() : 对象类型响应式
与将内部值包装在特殊对象中的 ref 不同
import { reactive } from 'vue'
const state = reactive({ count: 0 })
局限性:
-
有限的值类型:它只能用于对象类型 (对象、数组和如
Map、Set这样的集合类型)。它不能持有如string、number或boolean这样的原始类型。 - 不能替换整个对象:由于 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)
3. computed() : 计算属性
计算属性值会基于其响应式依赖被缓存
我们在这里定义了一个计算属性
publishedBooksMessage。computed()方法期望接收一个 getter 函数,返回值为一个计算属性 ref。和其他一般的 ref 类似,你可以通过publishedBooksMessage.value访问计算结果。计算属性 ref 也会在模板中自动解包,因此在模板表达式中引用时无需添加.value。
Vue 的计算属性会自动追踪响应式依赖。它会检测到publishedBooksMessage依赖于author.books,所以当author.books改变时,任何依赖于publishedBooksMessage的绑定都会同时更新。
<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>
1.4 readonly() : 只读响应式
1.5 watch() / watchEffect() / watchPostEffect() : 侦听器
1、watch只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。另外,仅在数据源确实改变时才会触发回调。watch 会避免在发生副作用时追踪依赖,因此,我们能更加精确地控制回调函数的触发时机。
2、watchEffect,则会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。这更方便,而且代码往往更简洁,但有时其响应性依赖关系会不那么明确。
1.5.1、watch()
1、deep: true 深层监听;
2、immediate:true 立即执行监听;
3、once:true 源变化时触发一次;
watch(
() => state.someObject,
(newValue, oldValue) => {
// 注意:`newValue` 此处和 `oldValue` 是相等的
// *除非* state.someObject 被整个替换了
},
{ deep: true }
// 或
{ immediate: true }
// 或
{ once: true }
)
watch的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组:
在组合式 API 中,我们可以使用 watch 函数在每次响应式状态发生变化时触发回调函数:
<script setup>
import { ref, watch } from 'vue'
const question = ref('')
const answer = ref('Questions usually contain a question mark. ;-)')
const loading = ref(false)
// 可以直接侦听一个 ref
watch(question, async (newQuestion, oldQuestion) => {
if (newQuestion.includes('?')) {
loading.value = true
answer.value = 'Thinking...'
try {
const res = await fetch('https://yesno.wtf/api')
answer.value = (await res.json()).answer
} catch (error) {
answer.value = 'Error! Could not reach the API. ' + error
} finally {
loading.value = false
}
}
})
</script>
<template>
<p>
Ask a yes/no question:
<input v-model="question" :disabled="loading" />
</p>
<p>{{ answer }}</p>
</template>
侦听数据源的类型
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}`)
})
const obj = reactive({ count: 0 })
// 错误,因为 watch() 得到的参数是一个 number
watch(obj.count, (count) => {
console.log(`Count is: ${count}`)
})
// 正确,提供一个 getter 函数
watch(
() => obj.count,
(count) => {
console.log(`Count is: ${count}`)
}
)
1.5.2、watchEffect()
watchEffect 仅会在其同步执行期间,才追踪依赖。在使用异步回调时,只有在第一个 await 正常工作前访问到的属性才会被追踪。
这个例子中,回调会立即执行,不需要指定 immediate: true。在执行期间,它会自动追踪 todoId.value 作为依赖(和计算属性类似)。每当 todoId.value 变化时,回调会再次执行。有了 watchEffect(),我们不再需要明确传递 todoId 作为源值。
const todoId = ref(1)
const data = ref(null)
watch(
todoId,
async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
},
{ immediate: true }
)
等价于
watchEffect(async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
})
1.5.3、watchPostEffect()
侦听器回调中能访问被 Vue 更新之后的所属组件的 DOM
watch(source, callback, {
flush: 'post'
})
watchEffect(callback, {
flush: 'post'
})
等价于
import { watchPostEffect } from 'vue'
watchPostEffect(() => {
/* 在 Vue 更新后执行 */
})
1.5.4、watchsyncEffect()
谨慎使用
同步侦听器不会进行批处理,每当检测到响应式数据发生变化时就会触发。可以使用它来监视简单的布尔值,但应避免在可能多次同步修改的数据源 (如数组) 上使用。
同步触发的侦听器,它会在 Vue 进行任何更新之前触发:
watch(source, callback, {
flush: 'sync'
})
watchEffect(callback, {
flush: 'sync'
})
等价于
import { watchSyncEffect } from 'vue'
watchSyncEffect(() => {
/* 在响应式数据变化时同步执行 */
})
2、响应式工具
isRef 、unref 、 toRef 、 toRefs 、isReactive 、 isReadonly
3、进阶式响应式
shallowRef 、 shallowReactive 、 toRaw 、 markRaw 、 customRef
useTemplateRef: 访问模版引用
<script setup>
import { useTemplateRef, onMounted } from 'vue'
// 第一个参数必须与模板中的 ref 值匹配
const input = useTemplateRef('my-input')
onMounted(() => {
input.value.focus()
})
</script>
<template>
<input ref="my-input" />
</template>
组件上的ref
<script setup>
import { useTemplateRef, onMounted } from 'vue'
import Child from './Child.vue'
const childRef = useTemplateRef('child')
onMounted(() => {
// childRef.value 将持有 <Child /> 的实例
})
</script>
<template>
<Child ref="child" />
</template>
有一个例外的情况,使用了 <script setup> 的组件是默认私有的:一个父组件无法访问到一个使用了 <script setup> 的子组件中的任何东西,除非子组件在其中通过 defineExpose 宏显式暴露:
请注意,defineExpose 必须在任何 await 操作之前调用。否则,在 await 操作后暴露的属性和方法将无法访问。
<script setup>
import { ref } from 'vue'
const a = 1
const b = ref(2)
// 像 defineExpose 这样的编译器宏不需要导入
defineExpose({
a,
b
})
</script>
4、生命周期钩子(导入使用)
onBeforeMount 、 onMounted 、 onBeforeUpdate 、 onUpdated 、 onBeforeUnmount、onUnmounted、onActivated 、 onDeactivated 、 onErrorCaptured
5、依赖注入
5.1、provide() : 提供
provide() 函数接收两个参数。第一个参数被称为注入名,可以是一个字符串或是一个 Symbol。后代组件会用注入名来查找期望注入的值。一个组件可以多次调用 provide(),使用不同的注入名,注入不同的依赖值。
在整个应用层面提供依赖:
在应用级别提供的数据在该应用内的所有组件中都可以注入。这在你编写插件时会特别有用,因为插件一般都不会使用组件形式来提供值。
import { createApp } from 'vue'
const app = createApp({})
app.provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
要为组件后代提供数据,需要使用到 provide()函数:
<script setup>
import { provide } from 'vue'
provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
</script>
5.1、inject():注入
要注入上层组件提供的数据,需使用
inject()函数:
<!-- 在供给方组件内 -->
<script setup>
import { provide, ref } from 'vue'
const location = ref('North Pole')
function updateLocation() {
location.value = 'South Pole'
}
provide('location', {
location,
updateLocation
})
</script>
<!-- 在注入方组件 -->
<script setup>
import { inject } from 'vue'
const { location, updateLocation } = inject('location')
</script>
<template>
<button @click="updateLocation">{{ location }}</button>
</template>
子组件不可修改数据readonly
<script setup>
import { ref, provide, readonly } from 'vue'
const count = ref(0)
provide('read-only-count', readonly(count))
</script>
6、script setup 内置宏 (无需import)
6.1、defineProps:父 ---> 子组件传值
所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递
defineProps 是一个仅 <script setup> 中可用的编译宏命令,并不需要显式地导入。声明的 props 会自动暴露给模板。defineProps 会返回一个对象,其中包含了可以传递给组件的所有 props:
const props = defineProps(['title'])
console.log(props.title)
Prop校验
defineProps({
// 基础类型检查
// (给出 `null` 和 `undefined` 值则会跳过任何类型检查)
propA: Number,
// 多种可能的类型
propB: [String, Number],
// 必传,且为 String 类型
propC: {
type: String,
required: true
},
// 必传但可为 null 的字符串
propD: {
type: [String, null],
required: true
},
// Number 类型的默认值
propE: {
type: Number,
default: 100
},
// 对象类型的默认值
propF: {
type: Object,
// 对象或数组的默认值
// 必须从一个工厂函数返回。
// 该函数接收组件所接收到的原始 prop 作为参数。
default(rawProps) {
return { message: 'hello' }
}
},
// 自定义类型校验函数
// 在 3.4+ 中完整的 props 作为第二个参数传入
propG: {
validator(value, props) {
// value 必须匹配以下字符串其中之一
return ['success', 'warning', 'danger'].includes(value)
}
},
// 函数类型的默认值
propH: {
type: Function,
// 不像对象或数组的默认,这不是一个
// 工厂函数。这会是一个用来作为默认值的函数
default() {
return 'Default function'
}
}
})
6.2、defineEmits: 子 ---> 父;声明需要抛出的事件
<script setup>
const emit = defineEmits(['inFocus', 'submit'])
function buttonClick() {
emit('submit')
}
</script>
支持对象语法
<script setup lang="ts">
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
</script>
事件校验
<script setup>
const emit = defineEmits({
// 没有校验
click: null,
// 校验 submit 事件
submit: ({ email, password }) => {
if (email && password) {
return true
} else {
console.warn('Invalid submit event payload!')
return false
}
}
})
function submitForm(email, password) {
emit('submit', { email, password })
}
</script>
<!-- MyComponent 子组件-->
<button @click="$emit('someEvent')">Click Me</button>
<!-- 父组件-->
<MyComponent @some-event="callback" />
6.3、defineExpose:子组件通过 defineExpose 宏显式暴露。可参考以上3、useTemplateRef
6.4、defineOptions 、
6.5、defineSlots 、
6.6、defineModel(3.4+)
返回的值是一个 ref。它可以像其他 ref 一样被访问以及修改,不过它能起到在父组件和当前变量之间的双向绑定的作用:
1、它的 .value 和父组件的 v-model 的值同步;
2、当它被子组件变更了,会触发父组件绑定的值一起更新。
这意味着你也可以用 v-model 把这个 ref 绑定到一个原生 input 元素上,在提供相同的 v-model 用法的同时轻松包装原生 input 元素:
Child.vue:
<script setup>
const model = defineModel()
function update() {
model.value++
}
</script>
<template>
<div>Parent bound v-model is: {{ model }}</div>
<button @click="update">Increment</button>
</template>
父组件可以用 v-model 绑定一个值:
<Child v-model="countModel" />
7、其他常用
nextTick:要等待 DOM 更新完成后再执行额外的代码,可以使用 nextTick() 全局 API:
import { nextTick } from 'vue'
async function increment() {
count.value++
await nextTick()
// 现在 DOM 已经更新了
}
useId、useTemplateRef、onScopeDispose
8、class / style
8.1 class
变量ref:
const isActive = ref(true)
const hasError = ref(false)
// 配合以下模板:
<div
class="static"
:class="{ active: isActive, 'text-danger': hasError }"
></div>
//渲染的结果会是:
<div class="static active"></div>
变量reactive:
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>
// 渲染结果:
<div class="active text-danger"></div>
三元表达式:
<div :class="[isActive ? activeClass : '', errorClass]"></div>
8.2 style
基础用法:
const activeColor = ref('red')
const fontSize = ref(30)
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
// 或
<div :style="{ color: activeColor, 'font-size': fontSize + 'px' }"></div>
简洁用法:
const styleObject = reactive({
color: 'red',
fontSize: '30px'
})
<div :style="styleObject"></div>
9、在内联事件处理器中访问事件参数
有时我们需要在内联事件处理器中访问原生 DOM 事件。你可以向该处理器方法传入一个特殊的 $event 变量,或者使用内联箭头函数:
<!-- 使用特殊的 $event 变量 -->
<button @click="warn('Form cannot be submitted yet.', $event)">
Submit
</button>
<!-- 使用内联箭头函数 -->
<button @click="(event) => warn('Form cannot be submitted yet.', event)">
Submit
</button>
// 使用
function warn(message, event) {
// 这里可以访问原生事件
if (event) {
event.preventDefault()
}
alert(message)
}
事件修饰符:
使用修饰符时需要注意调用顺序,因为相关代码是以相同的顺序生成的。因此使用
@click.prevent.self会阻止元素及其子元素的所有点击事件的默认行为,而@click.self.prevent则只会阻止对元素本身的点击事件的默认行为。
<!-- 单击事件将停止传递 -->
<a @click.stop="doThis"></a>
<!-- 提交事件将不再重新加载页面 -->
<form @submit.prevent="onSubmit"></form>
<!-- 修饰语可以使用链式书写 -->
<a @click.stop.prevent="doThat"></a>
<!-- 也可以只有修饰符 -->
<form @submit.prevent></form>
<!-- 仅当 event.target 是元素本身时才会触发事件处理器 -->
<!-- 例如:事件处理器不来自子元素 -->
<div @click.self="doThat">...</div>
<!-- 添加事件监听器时,使用 `capture` 捕获模式 -->
<!-- 例如:指向内部元素的事件,在被内部元素处理前,先被外部处理 -->
<div @click.capture="doThis">...</div>
<!-- 点击事件最多被触发一次 -->
<a @click.once="doThis"></a>
<!-- 滚动事件的默认行为 (scrolling) 将立即发生而非等待 `onScroll` 完成 -->
<!-- 以防其中包含 `event.preventDefault()` -->
<div @scroll.passive="onScroll">...</div>
10、两种API风格
- 组合式API(Composition API):函数式,推荐新项目 + TS ;<script setup>语法糖,逻辑可以抽离复用
- 选项式API (Options API):data/methods/mounted,Vue2的遗留写法,Vue3也支持
快速示例 script setup
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
const count = ref(0)
const double = computed(() => count.value * 2)
function add() {
count.value++
}
onMounted(() => {
console.log('挂载完成')
})
</script>
<template>
<div>{{ count }} {{ double }}</div>
</template>
11、生命周期

12、注册组件
<script setup>
import ComponentA from './ComponentA.vue'
</script>
<template>
<ComponentA />
</template>
13、自定义指令
在 <script setup> 中,任何以 v 开头的驼峰式命名的变量都可以当作自定义指令使用
<script setup>
// 在模板中启用 v-highlight
const vHighlight = {
mounted: (el) => {
el.classList.add('is-highlight')
}
}
</script>
<template>
<p v-highlight>This sentence is important!</p>
</template>
指令的钩子
const myDirective = {
// 在绑定元素的 attribute 前
// 或事件监听器应用前调用
created(el, binding, vnode) {
// 下面会介绍各个参数的细节
},
// 在元素被插入到 DOM 前调用
beforeMount(el, binding, vnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted(el, binding, vnode) {},
// 绑定元素的父组件更新前调用
beforeUpdate(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都更新后调用
updated(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载前调用
beforeUnmount(el, binding, vnode) {},
// 绑定元素的父组件卸载后调用
unmounted(el, binding, vnode) {}
}
钩子参数
指令的钩子会传递以下几种参数:
el:指令绑定到的元素。这可以用于直接操作 DOM。-
binding:一个对象,包含以下属性。-
value:传递给指令的值。例如在v-my-directive="1 + 1"中,值是2。 -
oldValue:之前的值,仅在beforeUpdate和updated中可用。无论值是否更改,它都可用。 -
arg:传递给指令的参数 (如果有的话)。例如在v-my-directive:foo中,参数是"foo"。 -
modifiers:一个包含修饰符的对象 (如果有的话)。例如在v-my-directive.foo.bar中,修饰符对象是{ foo: true, bar: true }。 -
instance:使用该指令的组件实例。 -
dir:指令的定义对象。
-
vnode:代表绑定元素的底层 VNode。prevVnode:代表之前的渲染中指令所绑定元素的 VNode。仅在beforeUpdate和updated钩子中可用。
13、KeepAlive
<KeepAlive> 是一个内置组件,它的功能是在多个组件间动态切换时缓存被移除的组件实例。
包含/排除
<!-- 以英文逗号分隔的字符串 -->
<KeepAlive include="a,b">
<component :is="view" />
</KeepAlive>
<!-- 正则表达式 (需使用 `v-bind`) -->
<KeepAlive :include="/a|b/">
<component :is="view" />
</KeepAlive>
<!-- 数组 (需使用 `v-bind`) -->
<KeepAlive :include="['a', 'b']">
<component :is="view" />
</KeepAlive>
生命周期
onActivated 在组件挂载时也会调用,并且 onDeactivated 在组件卸载时也会调用。
<script setup>
import { onActivated, onDeactivated } from 'vue'
onActivated(() => {
// 调用时机为首次挂载
// 以及每次从缓存中被重新插入时
})
onDeactivated(() => {
// 在从 DOM 上移除、进入缓存
// 以及组件卸载时调用
})
</script>