文章目录
介绍
什么是 Vue?
是一款用于构建用户界面的 JavaScript 框架
官方文档:文档链接
一、创建一个应用
1、应用实例
每个 Vue 应用都是通过 createApp
函数创建一个新的应用实例:
import { createApp } from 'vue'
const app = createApp({
/* 根组件选项 */
})
2、根组件
每个应用都需要一个“根组件”,其他组件将作为其子组件:
import { createApp } from 'vue'
// 从一个单文件组件中导入根组件
import App from './App.vue'
const app = createApp(App)
3、挂载应用
实例必须在调用了 .mount() 方法后才会渲染出来
<div id="app"></div>
app.mount('#app')
二、模版语法
Vue 使用一种基于 HTML 的模板语法,使我们能够声明式地将其组件实例的数据绑定到呈现的 DOM 上
1、文本插值
双大括号:{{ }}
<span>Message: {{ msg }}</span>
2、原始 HTML
若想插入 HTML,你需要使用 v-html
指令:
<p>用 v-html: <span v-html="rawHtml"></span></p>
3、Attribute 绑定
想要响应式地绑定一个 attribute,应该使用 v-bind
指令:
<div v-bind:id="dynamicId"></div>
如果绑定的值是 null 或者 undefined,那么该 attribute 将会从渲染的元素上移除
简写:
<div :id="dynamicId"></div>
布尔型 Attribute:
布尔型 attribute 依据 true / false 值来决定 attribute 是否应该存在于该元素上
<button :disabled="isButtonDisabled">Button</button>
当 isButtonDisabled 为真值或一个空字符串 (即 ) 时,元素会包含这个 disabled attribute。而当其为其他假值时 attribute 将被忽略
动态绑定多个值:
const objectOfAttrs = {
id: 'container',
class: 'wrapper'
}
<div v-bind="objectOfAttrs"></div>
4、使用 JavaScript 表达式
Vue 实际上在所有的数据绑定中都支持完整的 JavaScript 表达式:
可以使用在如下场景:
- 在文本插值中 (双大括号)
- 在任何 Vue 指令 (以 v- 开头的特殊 attribute) attribute 的值中
示例:
{{ number + 1 }}
{{ ok ? 'YES' : 'NO' }}
{{ message.split('').reverse().join('') }}
<div :id="`list-${id}`"></div>
5、指令 Directives
指令是带有 v- 前缀的特殊 attribute
指令 attribute 的期望值为一个 JavaScript 表达式
一个指令的任务是在其表达式的值变化时响应式地更新 DOM
示例:
<p v-if="seen">Now you see me</p>
三、响应式基础
1、声明响应式状态
1.1 ref()
(1)使用 ref() 函数来声明响应式状态:
import { ref } from 'vue'
const count = ref(0)
(2)ref() 接收参数,并将其包裹在一个带有 .value
属性的 ref 对象中返回:
const count = ref(0)
console.log(count) // { value: 0 }
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
(3)在组件模板中访问 ref:
注意,在模板中使用 ref 时,不需要附加 .value
import { ref } from 'vue'
export default {
// `setup` 是一个特殊的钩子,专门用于组合式 API。
setup() {
const count = ref(0)
// 将 ref 暴露给模板
return {
count
}
}
}
<div>{{ count }}</div>
(4)深层响应性
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')
}
也可以通过 shallowRef 来放弃深层响应性
1.2 reactive()
(1) reactive() 使对象本身具有响应性:
import { reactive } from 'vue'
const state = reactive({ count: 0 })
在模板中使用:
<button @click="state.count++">
{{ state.count }}
</button>
(2)reactive() 将深层地转换对象:当访问嵌套对象时,它们也会被 reactive() 包装
(3)shallowReactive() API 可以选择退出深层响应性
(4)为保证访问代理的一致性,对同一个原始对象调用 reactive() 会总是返回同样的代理对
象,而对一个已存在的代理对象调用 reactive() 会返回其本身
(5)reactive() 的局限性:
- 有限的值类型:它只能用于对象类型 (对象、数组和如 Map、Set 这样的集合类型)。它不能持有如 string、number 或 boolean 这样的原始类型
- 不能替换整个对象:由于 Vue 的响应式跟踪是通过属性访问实现的,因此我们必须始终保持对响应式对象的相同引用。这意味着我们不能轻易地“替换”响应式对象,因为这样的话与第一个引用的响应性连接将丢失
- 对解构操作不友好:当我们将响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,我们将丢失响应性连接:
由于这些限制,我们建议使用 ref() 作为声明响应式状态的主要 API
2、<script setup>
<script setup>
中的顶层的导入、声明的变量和函数可在同一组件的模板中直接使用
<template>
<button @click="increment">
{{ count }}
</button>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
</script>
3、DOM 更新时机
当你修改了响应式状态时,DOM 会被自动更新
但是需要注意的是,DOM 更新不是同步的。Vue 会在“next tick”更新周期中缓冲所有状态的修改,以确保不管你进行了多少次状态修改,每个组件都只会被更新一次。
要等待 DOM 更新完成后再执行额外的代码,可以使用 nextTick() 全局 API:
import { nextTick } from 'vue'
async function increment() {
count.value++
await nextTick()
// 现在 DOM 已经更新了
}
四、计算属性
计算属性 computed
:
(1)计算属性是一些基于响应式状态变化而变化的属性
(2)使用计算属性可以让我们在模板(template)中避免编写过多的逻辑,使代码更加简洁
(3)计算属性通过 computed() 函数来创建,返回值为一个计算属性 ref,通过 .value 访问,在模板中自动解包
(4)计算属性具有缓存性
1、基础示例
<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>
2、计算属性缓存 vs 方法
(1)计算属性值会基于其响应式依赖被缓存
一个计算属性仅会在其响应式依赖更新时才重新计算,意味着只要依赖值不改变,无论多少次访问都会立即返回先前的计算结果,而不用重复执行 getter 函数
(2)方法调用总是会在重渲染发生时再次执行函数。
3、可写计算属性
计算属性默认是只读的。当你尝试修改一个计算属性时,你会收到一个运行时警告。
只在某些特殊场景中你可能才需要用到“可写”的属性,你可以通过同时提供 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>
五、类与样式绑定
因为 class 和 style 都是 attribute,我们可以和其他 attribute 一样使用 v-bind 将它们和动态的字符串绑定。但是Vue 专门为 class 和 style 的 v-bind 用法提供了特殊的功能增强。除了字符串外,表达式的值也可以是对象或数组。
1、绑定 class
1.1 绑定对象
(1)给 :class
传递一个对象来动态切换 class:
<div :class="{ active: isActive }"></div>
(2)可以在对象中写多个字段来操作多个 class:
const isActive = ref(true)
const hasError = ref(false)
<div
class="static"
:class="{ active: isActive, 'text-danger': hasError }"
></div>
渲染结果:
<div class="static active"></div>
(3)绑定的对象并不一定需要写成内联字面量的形式,也可以直接绑定一个对象:
<div :class="classObject"></div>
const classObject = reactive({
active: true,
'text-danger': false
})
渲染为:
<div class="active"></div>
(4)也可以绑定一个返回对象的计算属性:
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>
1.2 绑定数组
可以给 :class 绑定一个数组来渲染多个 CSS class:
const activeClass = ref('active')
const errorClass = ref('text-danger')
<div :class="[activeClass, errorClass]"></div>
如果想在数组中有条件地渲染某个 class,可以使用三元表达式:
<div :class="[isActive ? activeClass : '', errorClass]"></div>
也可以在数组中嵌套对象:
<div :class="[{ active: isActive }, errorClass]"></div>
1.3 在组件上使用
对于只有一个根元素的组件,当你使用了 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>
2、绑定 style
2.1 绑定对象
(1):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 属性:
<div :style="{ 'font-size': fontSize + 'px' }"></div>
(2)直接绑定一个样式对象通常是一个好主意,这样可以使模板更加简洁:
const styleObject = reactive({
color: 'red',
fontSize: '13px'
})
<div :style="styleObject"></div>
2.2 绑定数组
还可以给 :style 绑定一个包含多个样式对象的数组
这些对象会被合并后渲染到同一元素上:
<div :style="[baseStyles, overridingStyles]"></div>
2.3 自动前缀
当你在 :style 中使用了需要浏览器特殊前缀的 CSS 属性时,Vue 会自动为他们加上相应的前缀
2.3 样式多值
你可以对一个样式属性提供多个 (不同前缀的) 值,举例来说:
<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>
六、条件渲染
1、 v-if
v-if 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回真值时才被渲染。
<h1 v-if="awesome">Vue is awesome!</h1>
v-else
你也可以使用 v-else 为 v-if 添加一个“else 区块”
<h1 v-if="awesome">Vue is awesome!</h1>
<h1 v-else>Oh no</h1>
v-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>
<template>
上的 v-if
想要切换不止一个元素的时候:
<template v-if="ok">
<h1>Title</h1>
<p>Paragraph 1</p>
<p>Paragraph 2</p>
</template>
2、v-show
另一个可以用来按条件显示一个元素的指令是 v-show。其用法基本一样:
<h1 v-show="ok">Hello!</h1>
不同之处在于 v-show 会在 DOM 渲染中保留该元素;
v-show 仅切换了该元素上名为 display 的 CSS 属性。
v-show 不支持在 元素上使用,也不能和 v-else 搭配使用
3、v-if 对比 v-show
(1)v-if 是“真实的”按条件渲染,因为它确保了在切换时,条件区块内的事件监听器和子组件都会被销毁与重建
(2)v-if 也是惰性的:如果在初次渲染时条件值为 false,则不会做任何事。条件区块只有当条件首次变为 true 时才被渲染;相比之下,v-show 简单许多,元素无论初始条件如何,始终会被渲染,只有 CSS display 属性会被切换
总的来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。
使用场景:如果需要频繁切换,则使用 v-show 较好;如果在运行时绑定条件很少改变,则 v-if 会更合适
七、列表渲染
1、v-for
可以使用 v-for 指令基于一个数组来渲染一个列表
(1)v-for 指令的值需要使用 item in items 形式的特殊语法
示例:
const items = ref([{ message: 'Foo' }, { message: 'Bar' }])
<li v-for="item in items">
{{ item.message }}
</li>
(2)v-for 也支持使用可选的第二个参数表示当前项的位置索引
const items = ref([{ message: 'Foo' }, { message: 'Bar' }])
<li v-for="(item, index) in items">
{{ index }} - {{ item.message }}
</li>
(3)可以在定义 v-for 的变量别名时使用解构,和解构函数参数类似:
<li v-for="{ message } in items">
{{ message }}
</li>
<!-- 有 index 索引时 -->
<li v-for="({ message }, index) in items">
{{ message }} {{ index }}
</li>
(4)也可以使用 of 作为分隔符来替代 in,这更接近 JavaScript 的迭代器语法:
<div v-for="item of items"></div>
2、v-for 与对象
(1)可以使用 v-for 来遍历一个对象的所有属性:
const myObject = reactive({
title: 'How to do lists in Vue',
author: 'Jane Doe',
publishedAt: '2016-04-10'
})
<ul>
<li v-for="value in myObject">
{{ value }}
</li>
</ul>
(2)可以通过提供第二个参数表示属性名 (例如 key):
<li v-for="(value, key) in myObject">
{{ key }}: {{ value }}
</li>
第三个参数表示位置索引:
<li v-for="(value, key, index) in myObject">
{{ index }}. {{ key }}: {{ value }}
</li>
3、在 v-for 里使用范围值
v-for 可以直接接受一个整数值
<span v-for="n in 10">{{ n }}</span>
4、<template>
上的 v-for
可以使用 v-for 来渲染一个包含多个元素的块
<ul>
<template v-for="item in items">
<li>{{ item.msg }}</li>
<li class="divider" role="presentation"></li>
</template>
</ul>
5、v-for 与 v-if
同时使用 v-if 和 v-for 是不推荐的
当它们同时存在于一个节点上时,v-if 比 v-for 的优先级更高。这意味着 v-if 的条件将无法访问到 v-for 作用域内定义的变量别名:
<!--
这会抛出一个错误,因为属性 todo 此时
没有在该实例上定义
-->
<li v-for="todo in todos" v-if="!todo.isComplete">
{{ todo.name }}
</li>
解决:
在外新包装一层 再在其上使用 v-for 可以解决这个问题
<template v-for="todo in todos">
<li v-if="!todo.isComplete">
{{ todo.name }}
</li>
</template>
6、通过 key
管理状态
为了给 Vue 一个提示,以便它可以跟踪每个节点的标识,从而重用和重新排序现有的元素,你需要为每个元素对应的块提供一个唯一的 key attribute:
<div v-for="item in items" :key="item.id">
<!-- 内容 -->
</div>
注意:
key 绑定的值期望是一个基础类型的值,例如字符串或 number 类型。
不要用对象作为 v-for 的 key
7、组件上使用 v-for
我们可以直接在组件上使用 v-for,和在一般的元素上使用没有区别
<MyComponent v-for="item in items" :key="item.id" />
将迭代后的数据传递到组件中,我们还需要传递 props:
<MyComponent
v-for="(item, index) in items"
:item="item"
:index="index"
:key="item.id"
/>
8、展示过滤或排序后的列表
有时,我们希望显示数组经过过滤或排序后的内容,而不实际变更或重置原始数据。在这种情况下,你可以创建返回已过滤或已排序数组的计算属性
const numbers = ref([1, 2, 3, 4, 5])
const evenNumbers = computed(() => {
return numbers.value.filter((n) => n % 2 === 0)
})
<li v-for="n in evenNumbers">{{ n }}</li>
八、事件处理
1、监听事件
使用 v-on
指令 (简写为 @
) 来监听 DOM 事件
用法:v-on:click=“handler” 或 @click=“handler”
事件处理器 (handler) 的值可以是:
(1)内联事件处理器:事件被触发时执行的内联 JavaScript 语句 (与 onclick 类似)。
(2)方法事件处理器:一个指向组件上定义的方法的属性名或是路径。
1.1 内联事件处理器
<button @click="count++">Add 1</button>
1.2 方法事件处理器
function greet(event) {
console.log(event)
}
<!-- `greet` 是上面定义过的方法名 -->
<button @click="greet">Greet</button>
2、在内联处理器中调用方法
可以在内联事件处理器中调用方法:
<button @click="say('hello')">Say hello</button>
3、在内联事件处理器中访问事件参数
有时我们需要在内联事件处理器中访问原生 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>
4、事件修饰符
Vue 为 v-on 提供了事件修饰符。修饰符是用 . 表示的指令后缀,包含以下这些:
.stop
.prevent
.self
.capture
.once
.passive
<!-- 单击事件将停止传递 -->
<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>
九、表单输入绑定 v-model
1、基本用法
1.1 文本 input
<input v-model="message" />
1.2 多行文本 textarea
<textarea v-model="message"></textarea>
1.2 复选框 input-checkbox
(1)单一的复选框,绑定布尔类型值:
<input type="checkbox" id="checkbox" v-model="checked" />
<label for="checkbox">{{ checked }}</label>
(2)也可以将多个复选框绑定到同一个数组或集合的值:
const checkedNames = ref([])
<div>Checked names: {{ checkedNames }}</div>
<input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
<label for="jack">Jack</label>
<input type="checkbox" id="john" value="John" v-model="checkedNames">
<label for="john">John</label>
<input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
<label for="mike">Mike</label>
(3)值绑定
true-value 和 false-value 是 Vue 特有的 attributes,仅支持和 v-model 配套使用
这里 toggle 属性的值会在选中时被设为 ‘yes’,取消选择时设为 ‘no’
<input
type="checkbox"
v-model="toggle"
true-value="yes"
false-value="no" />
1.2 单选按钮 input-radio
(1)基本用法
<div>Picked: {{ picked }}</div>
<input type="radio" id="one" value="One" v-model="picked" />
<label for="one">One</label>
<input type="radio" id="two" value="Two" v-model="picked" />
<label for="two">Two</label>
(2)值绑定:
<input type="radio" v-model="pick" :value="first" />
<input type="radio" v-model="pick" :value="second" />
pick 会在第一个按钮选中时被设为 first,在第二个按钮选中时被设为 second
1.2 选择器 select-option
(1)单个选择器:
<div>Selected: {{ selected }}</div>
<select v-model="selected">
<option disabled value="">Please select one</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
(2)多选 (值绑定到一个数组):
<div>Selected: {{ selected }}</div>
<select v-model="selected" multiple>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
(3)值绑定
<select v-model="selected">
<!-- 内联对象字面量 -->
<option :value="{ number: 123 }">123</option>
</select>
当某个选项被选中,selected 会被设为该对象字面量值 { number: 123 }
2、修饰符
(1).lazy
默认情况下,v-model 会在每次 input 事件后更新数据。可以添加 lazy 修饰符来改为在每次 change 事件后更新数据:
<!-- 在 "change" 事件后同步更新而不是 "input" -->
<input v-model.lazy="msg" />
(2).number
如果你想让用户输入自动转换为数字:
<input v-model.number="age" />
(3).trim
去除用户输入内容中两端的空格:
<input v-model.trim="msg" />
十、生命周期
1、生命周期图示
图示链接:官方文档链接
2、生命周期钩子
所有生命周期钩子:生命周期钩子
最常用的是 onMounted、onUpdated 和 onUnmounted:
(1)onMounted()
注册一个回调函数,在组件挂载完成后执行
(2)onUpdated()
注册一个回调函数,在组件因为响应式状态变更而更新其 DOM 树之后调用
(3)onUnmounted()
注册一个回调函数,在组件实例被卸载之后调用。
十一、侦听器 watch
使用 watch 函数在每次响应式状态发生变化时触发回调函数
参数如下:
(1)第一个参数是侦听器的源。这个来源可以是以下几种:
- 一个函数,返回一个值
- 一个 ref
- 一个响应式对象
- 或是由以上类型的值组成的数组
(2)第二个参数是在发生变化时要调用的回调函数
(3)第三个可选的参数是一个对象,支持以下这些选项:
- immediate:在侦听器创建时立即触发回调。第一次调用时旧值是 undefined。
- deep:如果源是对象,强制深度遍历,以便在深层级变更时触发回调。参考深层侦听器。
- flush:调整回调函数的刷新时机。参考回调的刷新时机及 watchEffect()。
- onTrack / onTrigger:调试侦听器的依赖。参考调试侦听器。
- once:回调函数只会运行一次。侦听器将在回调函数首次运行后自动停止。
1、基本示例
<script setup>
import { ref, watch } from 'vue'
const inputVal = ref('');
const answer = ref('');
watch(inputVal, () => {
answer.value = inputVal.value;
})
</script>
<template>
<p>
输入问题:
<input v-model="inputVal" />
</p>
<p>你的问题是:{{ answer }}</p>
</template>
侦听数据源类型:
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}`)
})
注意,不能直接侦听响应式对象的属性值,比如:
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}`)
}
)
2、深层侦听器
一个返回响应式对象的 getter 函数,只有在返回不同的对象时,才会触发回调:
watch(
() => state.someObject,
() => {
// 仅当 state.someObject 被替换时触发
}
)
可以给上面这个例子显式地加上 deep
选项,强制转成深层侦听器:
watch(
() => state.someObject,
(newValue, oldValue) => {
// 注意:`newValue` 此处和 `oldValue` 是相等的
// *除非* state.someObject 被整个替换了
},
{ deep: true }
)
此时 state.someObject 中的属性变化时,也会触发回调
3、即时回调的侦听器
watch 默认是懒执行的:仅当数据源变化时,才会执行回调
可以通过传入 immediate: true
选项来强制侦听器的回调立即执行:
watch(
source,
(newValue, oldValue) => {
// 立即执行,且当 `source` 改变时再次执行
},
{ immediate: true }
)
4、一次性侦听器
如果希望回调只在源变化时触发一次,请使用 once: true
选项
watch(
source,
(newValue, oldValue) => {
// 当 `source` 变化时,仅触发一次
},
{ once: true }
)
5、watchEffect()
官网介绍
立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行
const count = ref(1)
watchEffect(() => {
console.log('count:', count.value);
})
如果用 watch 实现,加 immediate:
const count = ref(1)
watch(
count,
() => {
console.log('count:', count.value);
},
{ immediate: true }
)
watch
vs. watchEffect
(1)watch 和 watchEffect 都能响应式地执行有副作用的回调。
(2)它们之间的主要区别是追踪响应式依赖的方式:
- watch 只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。另外,仅在数据源确实改变时才会触发回调。watch 会避免在发生副作用时追踪依赖,因此,我们能更加精确地控制回调函数的触发时机。
- watchEffect 则会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。这更方便,而且代码往往更简洁,但有时其响应性依赖关系会不那么明确。
6、回调的触发时机
(1)如果想在侦听器回调中能访问被 Vue 更新之后的所属组件的 DOM,你需要指明 flush: ‘post’ 选项:
watch(source, callback, {
flush: 'post'
})
watchEffect(callback, {
flush: 'post'
})
后置刷新的 watchEffect() 有个更方便的别名 watchPostEffect()
:
import { watchPostEffect } from 'vue'
watchPostEffect(() => {
/* 在 Vue 更新后执行 */
})
(2)同步侦听器
你还可以创建一个同步触发的侦听器,它会在 Vue 进行任何更新之前触发:
watch(source, callback, {
flush: 'sync'
})
watchEffect(callback, {
flush: 'sync'
})
同步触发的 watchEffect() 有个更方便的别名 watchSyncEffect()
:
import { watchSyncEffect } from 'vue'
watchSyncEffect(() => {
/* 在响应式数据变化时同步执行 */
})
7、停止侦听器
要手动停止一个侦听器,请调用 watch 或 watchEffect 返回的函数:
const unwatch = watchEffect(() => {})
// ...当该侦听器不再需要时
unwatch()
十二、模板引用
访问底层 DOM 元素,可以使用特殊的 ref
attribute:
<input ref="input">
1、访问模板引用
注意,你只可以在组件挂载后才能访问模板引用
<script setup>
import { ref, onMounted } from 'vue'
// 声明一个 ref 来存放该元素的引用
// 必须和模板里的 ref 同名
const input = ref(null)
onMounted(() => {
input.value.focus()
})
</script>
<template>
<input ref="input" />
</template>
如果不使用 <script setup>
,需确保从 setup() 返回 ref:
export default {
setup() {
const input = ref(null)
// ...
return {
input
}
}
}
2、v-for 中的模板引用
当在 v-for 中使用模板引用时,对应的 ref 中包含的值是一个数组,它将在元素被挂载后包含对应整个列表的所有元素:
<script setup>
import { ref, onMounted } from 'vue'
const list = ref([
/* ... */
])
const itemRefs = ref([])
onMounted(() => console.log(itemRefs.value))
</script>
<template>
<ul>
<li v-for="item in list" ref="itemRefs">
{{ item }}
</li>
</ul>
</template>
3、组件上的 ref
3.1 获得组件实例
模板引用也可以被用在一个子组件上。这种情况下引用中获得的值是组件实例:
<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>
3.2 访问子组件的属性
1、使用 <script setup>
使用了 <script setup>
的组件是默认私有的:一个父组件无法访问到一个使用了 <script setup>
的子组件中的任何东西,除非子组件在其中通过 defineExpose
宏显式暴露:
Child.vue
<script setup>
import { ref } from 'vue'
const a = 1
const b = ref(2)
// 像 defineExpose 这样的编译器宏不需要导入
defineExpose({
a,
b
})
</script>
2、选项式
一个子组件使用的是选项式 API 或没有使用 <script setup>
,被引用的组件实例和该子组件的 this 完全一致,这意味着父组件对子组件的每一个属性和方法都有完全的访问权
Child.vue
<script>
import { ref } from 'vue'
export default {
setup() {
const count = ref(1)
return {
count,
}
}
}
</script>
<template>
<div>
child
</div>
</template>
父组件:
<script setup>
import { ref, onMounted } from 'vue'
import Child from './Child.vue'
const child = ref(null)
onMounted(() => {
// child.value 是 <Child /> 组件的实例
console.log(child.value.count)
})
</script>
<template>
<Child ref="child" />
</template>
十三、组件
组件允许我们将 UI 划分为独立的、可重用的部分
1、定义一个组件
当使用构建步骤时,我们一般会将 Vue 组件定义在一个单独的 .vue 文件中,这被叫做单文件组件 (简称 SFC):
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">You clicked me {{ count }} times.</button>
</template>
当不使用构建步骤时,一个 Vue 组件以一个包含 Vue 特定选项的 JavaScript 对象来定义:
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
return { count }
},
template: `
<button @click="count++">
You clicked me {{ count }} times.
</button>`
// 也可以针对一个 DOM 内联模板:
// template: '#my-template-element'
}
2、使用组件
通过 <script setup>
,导入的组件都在模板中直接可用
<script setup>
import ButtonCounter from './ButtonCounter.vue'
</script>
<template>
<ButtonCounter />
</template>
若不使用 setup,则需要在 components 选项上注册它:
<template>
<ButtonCounter />
</template>
<script>
import ButtonCounter from './ButtonCounter.vue'
export default {
components: {
ButtonCounter
}
}
</script>
3、props
3.1 数组形式传递 props
(1)使用 defineProps
defineProps 是一个仅 <script setup>
中可用的编译宏命令,并不需要显式地导入
defineProps 会返回一个对象,其中包含了可以传递给组件的所有 props
子组件:
<script setup>
defineProps(['title'])
</script>
<template>
<h4>{{ title }}</h4>
</template>
父组件向子组件传递参数:
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const title = ref('这是传递给子组件的参数')
</script>
<template>
<Child ref="child" :title="title" />
</template>
(2)没有使用 <script setup>
props 必须以 props 选项的方式声明,props 对象会作为 setup() 函数的第一个参数被传入:
子组件:
<script >
export default {
props: ['title'],
setup(props) {
console.log(props.title)
}
}
</script>
<template>
<h4>{{ title }}</h4>
</template>
3.1 对象形式传递 props
使用 script setup
<script setup>
defineProps({
title: String,
likes: Number
})
</script>
<template>
<h4>{{ title }}</h4>
<h4>{{ likes }}</h4>
</template>
非 script setup:
<script>
export default {
props: {
title: String,
likes: Number
}
}
</script>
<template>
<h4>{{ title }}</h4>
<h4>{{ likes }}</h4>
</template>
3.3 递 prop 的细节
(1)动态 prop
使用 v-bind 或缩写 : 来进行动态绑定的 props
<BlogPost :title="post.title" />
(2)使用一个对象绑定多个prop
将一个对象的所有属性都当作 props 传入,可以使用没有参数的 v-bind
const post = {
id: 1,
title: 'My Journey with Vue'
}
<BlogPost v-bind="post" />
(3)单向数据流
- props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递
- 每次父组件更新后,所有的子组件中的 props 都会被更新到最新值,这意味着你不应该在子组件中去更改一个 prop
3.4 Prop 校验
Vue 组件可以更细致地声明对传入的 props 的校验要求
例如:
defineProps({
// 基础类型检查
// (给出 `null` 和 `undefined` 值则会跳过任何类型检查)
propA: Number,
// 多种可能的类型
propB: [String, Number],
// 必传,且为 String 类型
propC: {
type: String,
required: true
},
// Number 类型的默认值
propD: {
type: Number,
default: 100
},
// 对象类型的默认值
propE: {
type: Object,
// 对象或数组的默认值
// 必须从一个工厂函数返回。
// 该函数接收组件所接收到的原始 prop 作为参数。
default(rawProps) {
return { message: 'hello' }
}
},
// 自定义类型校验函数
propF: {
validator(value) {
// The value must match one of these strings
return ['success', 'warning', 'danger'].includes(value)
}
},
// 函数类型的默认值
propG: {
type: Function,
// 不像对象或数组的默认,这不是一个
// 工厂函数。这会是一个用来作为默认值的函数
default() {
return 'Default function'
}
}
})
4、监听事件
(1)父组件可以通过 v-on
或 @
来选择性地监听子组件上抛的事件,就像监听原生 DOM 事件那样:
<script setup>
import Child from './Child.vue'
function callback() {
console.log('--');
}
</script>
<template>
<Child @some-event="callback"/>
</template>
子组件可以通过调用内置的 $emit
方法,通过传入事件名称来抛出一个事件:
<template>
<button @click="$emit('someEvent')">click me</button>
</template>
(2)事件参数
子组件给 $emit 提供一个额外的参数:
<template>
<button @click="$emit('someEvent', 1)">click me</button>
</template>
父组件:
<script setup>
import Child from './Child.vue'
function callback(val) {
console.log('--', val); // -- 1
}
</script>
<template>
<Child @some-event="callback"/>
</template>
(3)声明触发的事件
组件可以显式地通过 defineEmits()
宏来声明它要触发的事件:
<script setup>
const emit = defineEmits(['enlarge-text'])
emit('enlarge-text')
</script>
如果没有在使用 <script setup>
,你可以通过 emits 选项定义组件会抛出的事件。你可以从 setup() 函数的第二个参数,即 setup 上下文对象上访问到 emit 函数:
export default {
emits: ['enlarge-text'],
setup(props, ctx) {
ctx.emit('enlarge-text')
}
}
5、插槽
5.1 插槽内容与出口
通过 Vue 的自定义 <slot>
元素来实现:
子组件
<template>
<div class="alert-box">
<strong>This is an Error for Demo Purposes</strong>
<br />
<slot></slot>
</div>
</template>
父组件
<script setup>
import AlartBox from './AlartBox.vue'
</script>
<template>
<AlartBox>这是插槽的内容</AlartBox>
</template>
5.2 渲染作用域
插槽内容可以访问到父组件的数据作用域
如:
<span>{{ message }}</span>
<FancyButton>{{ message }}</FancyButton>
5.3 默认内容
在外部没有提供任何内容的情况下,可以为插槽指定默认内容
<button type="submit">
<slot>
Submit <!-- 默认内容 -->
</slot>
</button>
但如果我们提供了插槽内容,那么被显式提供的内容会取代默认内容:
<SubmitButton>Save</SubmitButton>
5.4 具名插槽
元素可以有一个特殊的 attribute name,用来给各个插槽分配唯一的 ID,以确定每一处要渲染的内容
这类带 name 的插槽被称为具名插槽 (named slots)
没有提供 name 的 出口会隐式地命名为“default”
<!--BaseLayout组件-->
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
在父组件中使用时,需要使用一个含 v-slot 指令的 元素,并将目标插槽的名字传给该指令:
<BaseLayout>
<template v-slot:header>
<!-- header 插槽的内容放这里 -->
</template>
</BaseLayout>
v-slot有对应的简写#:
<BaseLayout>
<template #header>
<!-- header 插槽的内容放这里 -->
</template>
</BaseLayout>
完整代码:
<BaseLayout>
<template #header>
<h1>Here might be a page title</h1>
</template>
<template #default>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</template>
<template #footer>
<p>Here's some contact info</p>
</template>
</BaseLayout>
5.5 动态插槽名
动态指令参数在 v-slot 上也是有效的,即可以定义下面这样的动态插槽名
<base-layout>
<template v-slot:[dynamicSlotName]>
...
</template>
<!-- 缩写为 -->
<template #[dynamicSlotName]>
...
</template>
</base-layout>
5.6 作用域插槽
某些场景下插槽的内容可能想要同时使用父组件域内和子组件域内的数据
可以像对组件传递 props 那样,向一个插槽的出口上传递 attributes:
<!-- <MyComponent> 的模板 -->
<div>
<slot :text="greetingMessage" :count="1"></slot>
</div>
当需要接收插槽 props 时,默认插槽和具名作用域插槽有所不同:
(1)默认插槽
通过子组件标签上的 v-slot 指令,直接接收到了一个插槽 props 对象:
<MyComponent v-slot="slotProps">
{{ slotProps.text }} {{ slotProps.count }}
</MyComponent>
(2)具名作用域插槽
父组件:
<script setup>
import Child from './Child.vue'
</script>
<template>
<Child>
<template #header="headerProps">
{{ headerProps }}
</template>
<template #default="defaultProps">
{{ defaultProps }}
</template>
<template #footer="footerProps">
{{ footerProps }}
</template>
</Child>
</template>
子组件:
<template>
<h3>插槽</h3>
<div>
<slot name="header" message="123"></slot>
<slot message="456"></slot>
<slot name="footer" message="789"></slot>
</div>
</template>
5.7 高级列表组件示例
父组件:
<FancyList :api-url="url" :per-page="10">
<template #item="{ body, username, likes }">
<div class="item">
<p>{{ body }}</p>
<p>by {{ username }} | {{ likes }} likes</p>
</div>
</template>
</FancyList>
子组件
<script setup>
import { ref } from 'vue'
const items = ref([])
// mock remote data fetching
items.value = [
{ body: 'Scoped Slots Guide', username: 'Evan You', likes: 20 },
{ body: 'Vue Tutorial', username: 'Natalia Tepluhina', likes: 10 }
]
</script>
<template>
<ul>
<li v-for="item in items">
<slot name="item" v-bind="item"/>
</li>
</ul>
</template>
6、动态组件
有些场景会需要在两个组件间来回切换,比如 Tab 界面:
(1)通过 Vue 的 <component>
元素和特殊的 is attribute 实现
(2):is 的值可以是以下几种:
- 被注册的组件名
- 导入的组件对象
(3)当使用 来在多个组件间作切换时,被切换掉的组件会被卸载。我们可以通过 <KeepAlive>
组件强制被切换掉的组件仍然保持“存活”的状态。
示例:
<script setup>
import { ref } from 'vue'
import Home from './Home.vue'
import Posts from './Posts.vue'
const tabs = {
Home,
Posts
}
const cur = ref('Home')
</script>
<template>
<button @click="cur='Home'">Home</button>
<button @click="cur='Posts'">Posts</button>
<component :is="ta
7、组件 v-model
7.1 基本用法
v-model 可以在组件上使用以实现双向绑定
组件内部需要做两件事:
(1)将内部原生 元素的 value attribute 绑定到 modelValue
prop
(2)当原生的 input 事件触发时,触发一个携带了新值的 update:modelValue
自定义事件
子组件:
<!-- Child.vue -->
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
父组件:
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const searchText = ref('')
</script>
<template>
<Child v-model="searchText" />
<p>输入的内容是:{{ searchText }}</p>
</template>
从 Vue 3.4 开始,推荐的实现方式是使用 defineModel()
宏:
子组件:
<!-- Child.vue -->
<script setup>
const model = defineModel()
function update(val) {
model.value = val;
}
</script>
<template>
<input @input="update($event.target.value)"/>
</template>
defineModel() 返回的值是一个 ref。它可以像其他 ref 一样被访问以及修改,不过它能起到在父组件和当前变量之间的双向绑定的作用:
- 它的 .value 和父组件的 v-model 的值同步;
- 当它被子组件变更了,会触发父组件绑定的值一起更新。
这意味着你也可以用 v-model 把这个 ref 绑定到一个原生 input 元素上,在提供相同的 v-model 用法的同时轻松包装原生 input 元素:
<!-- Child.vue -->
<script setup>
const model = defineModel()
</script>
<template>
<input v-model="model"/>
</template>
父组件同上
7.2 v-model 的参数
组件上的 v-model 也可以接受一个参数:
如下例子的 titleParam
父组件:
<script setup>
import { ref } from 'vue'
import MyComponent from './MyComponent.vue'
const title = ref('v-model model 参数示例')
</script>
<template>
<h1>{{ title }}</h1>
<MyComponent v-model:titleParam="title" />
</template>
子组件:
<script setup>
const title = defineModel('titleParam')
</script>
<template>
<input type="text" v-model="title" />
</template>
7.3 多个 v-model 绑定
可以在单个组件实例上创建多个 v-model 双向绑定。
父组件:
<UserName
v-model:first-name="first"
v-model:last-name="last"
/>
子组件:
<script setup>
const firstName = defineModel('firstName')
const lastName = defineModel('lastName')
</script>
<template>
<input type="text" v-model="firstName" />
<input type="text" v-model="lastName" />
</template>
7.4 处理 v-model 修饰符
修饰符 capitalize,它会自动将 v-model 绑定输入的字符串值第一个字母转为大写:
<MyComponent v-model.capitalize="myText" />
8、依赖注入
Prop 逐级透传问题:
某个深层的子组件需要一个较远的祖先组件中的部分数据,则必须将其沿着组件链逐级传递下去,这会非常麻烦
provide 和 inject 可以帮助我们解决这一问题
8.1 Provide
(提供)
要为组件后代提供数据,需要使用到 provide() 函数:
- 第一个参数被称为注入名:可以是一个字符串或是一个 Symbol
- 第二个参数是提供的值:值可以是任意类型,包括响应式的状态,比如一个 ref
使用 <script setup>
:
<script setup>
import { provide } from 'vue'
provide('message', 'hello!')
</script
不使用 <script setup>
:
import { provide } from 'vue'
export default {
setup() {
provide('message', 'hello!')
}
}
应用层 Provide
除了在一个组件中提供依赖,我们还可以在整个应用层面提供依赖:
import { createApp } from 'vue'
const app = createApp({})
app.provide('message', 'hello!')
8.2 Inject
(注入)
要注入上层组件提供的数据,需使用 inject() 函数:
使用 <script setup>
<script setup>
import { inject } from 'vue'
const message = inject('message')
</script>
没有使用 <script setup>
import { inject } from 'vue'
export default {
setup() {
const message = inject('message')
return { message }
}
}
注入默认值
如果在注入一个值时不要求必须有提供者,那么我们应该声明一个默认值:
// 如果没有祖先组件提供 "message"
// `value` 会是 "这是默认值"
const value = inject('message', '这是默认值')
8.3 和响应式数据配合使用
当提供 / 注入响应式的数据时,建议尽可能将任何对响应式状态的变更都保持在供给方组件中
有的时候,我们可能需要在注入方组件中更改数据。在这种情况下,我们推荐在供给方组件内声明并提供一个更改数据的方法函数:
<!-- 在供给方组件内 -->
<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>
8.4 使用 Symbol 作注入名
如果你正在构建大型的应用,包含非常多的依赖提供,或者你正在编写提供给其他开发者使用的组件库,建议最好使用 Symbol 来作为注入名以避免潜在的冲突。
我们通常推荐在一个单独的文件中导出这些注入名 Symbol:
// keys.js
export const myInjectionKey = Symbol()
9、异步组件
在大型项目中,我们可能需要拆分应用为更小的块,并仅在需要时再从服务器加载相关组件
Vue 提供了 defineAsyncComponent 方法来实现此功能:
9.1 基本用法
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() => {
return new Promise((resolve, reject) => {
// ...从服务器获取组件
resolve(/* 获取到的组件 */)
})
})
// ... 像使用其他一般组件一样使用 `AsyncComp`
9.2 加载与错误状态
异步操作不可避免地会涉及到加载和错误状态,因此 defineAsyncComponent() 也支持在高级选项中处理这些状态:
const AsyncComp = defineAsyncComponent({
// 加载函数
loader: () => import('./Foo.vue'),
// 加载异步组件时使用的组件
loadingComponent: LoadingComponent,
// 展示加载组件前的延迟时间,默认为 200ms
delay: 200,
// 加载失败后展示的组件
errorComponent: ErrorComponent,
// 如果提供了一个 timeout 时间限制,并超时了
// 也会显示这里配置的报错组件,默认值是:Infinity
timeout: 3000
})