highlight: a11y-dark
模板语法
1.属性绑定的值是 null
或 undefined
时,则该属性会从元素上被移除:
2.布尔型属性的值为 false
时,该属性会从元素上被移除:
3.指令是带有 v-
前缀的特殊属性,如:v-html
;
v-bind
和v-on
是特殊的内置指令,分别可以以简写方式书写,冒号后是指令参数:
...
...
...
逻辑渲染
1.如果要使用v-if
切换多个元素,就可以用 包裹:
Title
Paragraph 1
Paragraph 2
注意事项
v-show
不支持在 元素上使用
不推荐同时使用 v-if 和 v-for,因为二者的优先级不明显;
当 v-if
和 v-for
同时存在于一个元素上时,v-if
的优先级高,这意味着 v-if 会首先被执行,且无法访问到 v-for
作用域内的变量:
v-for
// 通过参数二获取索引
{{ parentMessage }} - {{ index }} - {{ item.message }}
// 直接解构对象元素
{{ message }} {{ index }}
// 可使用 `of` 替代 `in`,作用相同:
//遍历对象属性,参数二为属性名,参数三为索引:
{{ key }}: {{ value }}
输入绑定
修饰符
.lazy
默认情况下,v-model
在每次 input
事件后更新数据 (IME 拼字阶段除外),但可以通过添加 lazy
修饰符来改为在每次 change
事件后更新数据:
.number
将输入自动转换为数字:(如果输入值无法被 parseFloat()
处理,则返回原始值)
.trim
自动去除输入内容中两端的空格:
侦听器
深层侦听器
watch
浅层侦听时,嵌套属性变化并不会触发侦听器;
watch
深侦听时,会遍历被侦听对象的所有嵌套属性,当被侦听对象过于复杂时,性能开销会很大;
1.0> 直接给 watch()
传入一个响应式对象,会隐式地创建一个深层侦听器:
const obj = reactive({ count: 0 })
watch(obj, (newValue, oldValue) => {
// 在嵌套的属性变更时触发
// 注意:`newValue` 此处和 `oldValue` 是相等的
// 因为它们是同一个对象!
})
2.0> 如下浅层侦听,一个返回响应式对象的 getter
函数,只有在返回不同对象时,才会触发侦听回调:
watch(
() => state.someObject,
() => {
// 仅当 state.someObject 被替换时触发
}
)
watchEffect()
watch
只追踪明确侦听的数据源,并且仅在数据源改变时才会触发回调;
watchEffect
会在回调同步执行过程中自动追踪所有能访问到的响应式属性,并在创建后会立即执行一次;
watchEffect(async () => {
const response = await fetch(url.value)
data.value = await response.json()
})
注意:watchEffect
仅在同步执行期间才追踪依赖;在使用异步回调时,只有在第一个 await
正常工作前能够访问到的属性才会被追踪;
回调的触发时机
如果在响应式状态更新后,同时触发了DOM
更新和侦听器回调,则侧侦听器回调会在 Vue 组件 DOM 更新之前被调用,这意味着在侦听器回调中只能访问到 DOM 更新之前的状态。如果想在侦听器回调中能访问更新之后的DOM,就需要指明 flush: 'post'
选项:
watch(source, callback, {
flush: 'post'
})
// 或
watchEffect(callback, {
flush: 'post'
})
后置刷新的 watchEffect()
有个更方便的别名 watchPostEffect()
:
watchPostEffect(() => {
/* 在 Vue 组件 DOM 更新后执行 */
})
停止侦听器
在 setup()
或 中用同步语句创建的侦听器,会自动绑定到宿主组件实例上,并且在宿主组件卸载时自动停止。因此,大多数情况下无需关心如何停止侦听器。
一个关键点是,侦听器必须用同步语句创建:如果在异步回调中创建侦听器,那么它不会绑定到当前组件上,就必须手动停止,以防内存泄漏:
要手动停止一个侦听器,要调用 watch
或 watchEffect
返回的函数:
const unwatch = watchEffect(() => {})
// ...当该侦听器不再需要时
unwatch()
注意,需要异步创建侦听器的情况很少,请尽可能选择同步创建;如果需要等待一些异步数据,可以使用条件式的侦听逻辑:
// 需要异步请求得到的数据
const data = ref(null)
watchEffect(() => {
if (data.value) {
// 数据加载后执行某些操作...
}
组件基础
子组件
子组件通过 defineEmits
宏来声明需要抛出的事件,然后调用 $emit
方法出事件:
动态组件
参数 is
是被注册的组件名或导入的组件对象
组件传值
Props 声明
子组件使用 defineProps()
宏显式声明所接受的 props
// Props声明
defineProps({
greetingMessage: String
})
// 属性传值
静态 vs 动态 Prop
1.0> 静态传值:参数值是字符时,则无须绑定,直接赋值即可;
2.0> 动态传值:参数值是非字符时,则需要动态绑定(当参数类型为Boolean时,可以不传值,默认会转为true);
使用一个对象绑定多个 prop
props 对象的属性可以分别当作 props 传入:
const form = {
id: 1,
title: 'My Journey with Vue'
}
## 或
单向数据流
props 遵循单向绑定原则,数据状态向下流往子组件,而不会逆向传递;这避免了子组件修改父组件的情况,否则数据流将很容易变得混乱而难以理解。一般要在子组件中修改 prop 的需求无非以下两种场景:
1.0>prop 被用于传入初始值;这种情况下,最好是新定义一个局部数据属性,从 props 上获取初始值即可,如下:
const props = defineProps(['initialCounter'])
const counter = ref(props.initialCounter)
2.0> 需要对传入的 prop 值做进一步转换;这种情况中,最好是基于该 prop 值定义一个计算属性:
const props = defineProps(['size'])
const normalizedSize = computed(() => props.size.trim().toLowerCase())
Prop 校验
props声明时可以约束数据类型、是否必填;
defineProps({
// 1.0> 基础类型检查
// (给出 `null` 和 `undefined` 值则会跳过任何类型检查)
propA: Number,
// 2.0> 多种可能的类型
propB: [String, Number],
// 3.0> 必传,且为 String 类型
propC: {
type: String,
required: true
},
// 4.0> Number 类型的默认值
propD: {
type: Number,
default: 100
},
// 5.0> 对象类型的默认值
propE: {
type: Object,
// 对象或数组的默认值必须从一个工厂函数返回
default(rawProps) {
return { message: 'hello' }
}
},
// 6.0> 自定义类型校验函数
propF: {
validator(value) {
// The value must match one of these strings
return ['success', 'warning', 'danger'].includes(value)
}
},
// 7.0> 函数类型的默认值
propG: {
type: Function,
// 不像对象或数组的默认,这不是一个工厂函数,这会是一个用来作为默认值的函数
default() {
return 'Default function'
}
}
})
注意:
defineProps()
宏中的参数不可以访问 中定义的其他变量,因为编译时整个表达式都会被移到外部的函数中。
所有 prop 默认都是可选的,除非声明了 required: true
。
除 Boolean
外的未传递可选 prop 都会有一个默认值 undefined
。
事件传递
触发与监听事件
## 子组件中
## 父组件中
注:和原生 DOM 事件不一样,组件触发的事件没有冒泡机制;只能监听直接子组件触发的事件;平级或多层嵌套的组件间通信,应使用Vuex或Pinia;
声明事件
在 中可以使用
$emit
直接抛出事件,但在 中要先通过
defineEmits()
宏声明事件;
defineEmits()
宏不能在子函数中使用,必须放在 的顶级作用域下。
emits
选项还支持对象语法,它允许我们对触发事件的参数进行验证:
在 TypeScript 中,还可以使用纯类型标注来声明触发的事件:
注:如果原生事件的名字 (如 click
) 被定义在 emits
选项中,则监听器只会监听组件触发的 click
事件而不再响应原生 click
事件。
v-model
参数
默认情况下,v-model
在组件上使用 modelValue
作为 prop
,并以 update:modelValue
作为对应事件;也可通过给 v-model
指定一个参数来更改默认 prop
的名称:
如下,在子组件中应声明一个 title prop
,并通过触发 update:title
事件更新父组件值:
透传 Attributes
Attributes 继承
“透传 attribute”是指传给一个组件,却没有被该组件声明为 props
、emits
或 v-on 事件
的属性 ;最常见的就是 class、style 和 id。
当子组件只有一个根元素时,透传的 attribute 会自动被添加到子组件的根元素上。例如:在父组件中为子组件添加的class
或style
,会直接被透传到子组件的根元素上,并与子组件根元素上的class
和style
合并。
v-on
监听器继承
v-on
事件监听器同样也会被透传;如果子组件根元素自身也通过 v-on
绑定了事件监听器,则这个监听器和从父组件继承的监听器都会被触发。
深层组件继承
如果子组件的根元素又是一个组件,则透传属性会继续向下传递;但透传不包括声明过的 props
或针对 emits
声明事件的v-on
侦听;也就是说,声明过的 props
和侦听函数已经被当前组件消费了,因此不会再往下传递。
禁用 Attributes 继承
如果不想要组件自动地继承 attribute,可以在组件选项中设置 inheritAttrs: false``;
如果使用了 ,就需要一个额外的
块来书写这个选项声明:
最常见的需要禁用 attribute 继承的场景就是 attribute 需要应用在根节点以外的其他元素上;将inheritAttrs
设为false
,就可以完全控制透传进来的 attribute 被如何使用。
透传进来的 attribute 可以在模板的表达式中直接用 $attrs
访问:
Fallthrough attribute: {{ $attrs }}
也可以通过 v-bind 将透传的属性应用到任意元素上,如下:
注意:a>. 和 props 不同,透传 attributes 在 JavaScript 中保留了它们原始的大小写,所以像 foo-bar
这样 attribute 需要通过 $attrs['foo-bar']
来访问。
b>. 像 @click
这样的一个 v-on
事件监听器将在此对象下被暴露为一个函数 $attrs.onClick
。
多根节点的 Attributes 继承
多根节点的组件没有自动透传的行为,因为Vue并不知道要将 attribute 透传到哪个根元素上;因此需要通过 v-bind
显式绑定 $attrs
,以明确需要将 attribute 透传到哪个根元素,如下:
...
...
在 JavaScript 中访问透传 Attributes
在中可以使用
useAttrs()
API 来访问所有透传 attribute:
注意:attrs
对象是最新的透传 attribute,但它并不是响应式的 (考虑到性能因素);因此,不能通过侦听器去监听其变化;如果需要响应性,可以使用 prop。
插槽
渲染作用域
因为插槽内容在父组件模板中定义,因此可以访问父组件作用域,而无法访问子组件的数据;
总结:Vue 模板中的表达式只能访问其定义时所处的作用域,这和 JavaScript 的词法作用域规则是一致的。
默认内容
在外部没有提供任何内容的情况下,可以在
标签之间为插槽指定默认内容:
具名插槽
元素有一个特殊的name 属性
,用来给插槽分配唯一 ID,以确定每一处要渲染的内容:
这种带 name
的插槽被称为具名插槽,没有提供 name
的插槽会隐式命名为“default”;
要为具名插槽传入内容,需要使用含 v-slot
指令的 元素,并将目标插槽的名字传给该指令:
// `v-slot` 有对应的简写 `#`
Here might be a page title
当组件同时接收默认插槽和具名插槽时,所有位于顶级的非 节点都被隐式地视为默认插槽内容:
Here might be a page title
A paragraph for the main content.
And another one.
Here's some contact info
动态插槽名
动态指令参数在 v-slot
上也是有效的,即可以定义下面这样的动态插槽名:
...
...
作用域插槽
默认情况下,插槽内容无法访问到子组件的数据;如果要在插槽中访问子组件的数据,就需要子组件主动传递 prop 数据对象传给插槽:
然后,在父组件中通过在子组件标签上定义 v-slot
指令,来接收插槽 props 对象:
{{ slotProps.text }} {{ slotProps.count }}
也可以在 v-slot
中使用解构:
{{ text }} {{ count }}
具名作用域插槽
具名插槽也是类似的,插槽 props 可以作为 v-slot
指令的值被访问到v-slot:name="slotProps",如下
:
{{ headerProps }}
{{ defaultProps }}
{{ footerProps }}
向具名插槽中传入 props:
注意:插槽上的 name
是一个 Vue 特别保留的属性,不会作为 props 传递给插槽;headerProps的结果是{ message="hello"}
依赖注入
Prop 逐级透传问题
当组件嵌套太深时,通过 Prop 逐级透传数据会变得很繁琐;而provide
和 inject
可以将父组件作为依赖提供者,为所有后代组件注入由父组件提供的依赖。
Provide (提供)
为后代组件提供数据,需要用到 provide()
函数:
provide()
函数接收两个参数;参数一是注入名,参数二注入值,注入值可以是任意类型,包括响应式状态;后代组件使用注入名来查找期望的值;可以多次调用 provide()
,使用不同的注入名,注入不同的依赖值;当注入值为响应式状态时(如:ref),后代组件就可以由此和提供者建立响应式联系。
应用层 Provide
可以在整个应用层面提供依赖:
import { createApp } from 'vue'
const app = createApp({})
app.provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
在应用级别提供的数据在该应用内的所有组件中都可注入;
Inject (注入)
要注入上层组件提供的数据,需使用 inject()
函数:
如果提供的值是一个 ref,注入进来的会是该 ref 对象,而不会自动解包为其内部的值;
注入默认值
默认情况下,如果注入名不存在,则会抛出一个运行时警告;但可为 inject 声明一个默认值,和 props 类似:
const value = inject('message', '这是默认值')
默认值也可通过函数或初始化类来获得:
const value = inject('key', () => new ExpensiveClass())
和响应式数据配合使用
尽量将任何对响应式状态的变更都保持在供给方组件中,这样可以确保所提供状态的声明和变更操作都内聚在同一个组件内,使其更容易维护。
作为依赖供给方,如果想确保提供的数据不能被注入方组件更改,可以使用 readonly()
来包装提供的值:
使用 Symbol 作注入名
大型应用中,可能会包含非常多的依赖提供,或者你正在编写提供给其他开发者使用的组件库,建议最好使用 Symbol 来作为注入名以避免潜在的冲突。
推荐在一个单独的文件中导出这些注入名 Symbol:
// keys.js
export const myInjectionKey = Symbol()
// 在供给方组件中
import { provide } from 'vue'
import { myInjectionKey } from './keys.js'
provide(myInjectionKey, { /*
要提供的数据
*/ });
// 注入方组件
import { inject } from 'vue'
import { myInjectionKey } from './keys.js'
const injected = inject(myInjectionKey)
异步组件
基本用法
在大型项目中,可能需要拆分应用为更小的块,并仅在需要时从服务器加载组件;Vue 提供了 defineAsyncComponent
方法来实现此功能, 该方法接收一个返回 Promise 的加载函数,这个 Promise 的 resolve
方法从服务器获取组件,也可以调用 reject(reason)
表明加载失败,如下:
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() => {
return new Promise((resolve, reject) => {
// ...从服务器获取组件
resolve(/* 获取到的组件 */)
})
})
// ... 像使用其他一般组件一样使用 `AsyncComp`
因为 ES 模块动态导入也会返回一个 Promise,所以多数情况下推荐将 defineAsyncComponent 和 ES动态导入搭配使用;Vite 和 Webpack 都支持此语法 ,并且会将其作为打包时的代码分割点,如下:
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() =>
import('./components/MyComponent.vue')
)
最后得到的 AsyncComp
是一个外层包装过的组件,仅在页面需要渲染时才会调用加载函数;AsyncComp 会将收到的 props 和插槽传给内部组件,所以可以使用这个异步的包装组件无缝地替换原始组件,同时实现延迟加载。
与普通组件一样,异步组件可以使用 app.component()
全局注册:
app.component('MyComponent', defineAsyncComponent(() =>
import('./components/MyComponent.vue')
))
也可以直接在父组件中直接定义它们:
加载与错误状态
异步操作一般会涉及到加载中、加载错误等状态,因此 defineAsyncComponent()
也支持在高级选项中处理这些状态:
const AsyncComp = defineAsyncComponent({
// 加载函数
loader: () => import('./Foo.vue'),
// 加载异步组件时使用的组件
loadingComponent: LoadingComponent,
// 展示加载组件前的延迟时间,默认为 200ms
delay: 200,
// 加载失败后展示的组件
errorComponent: ErrorComponent,
// 如果提供了一个 timeout 时间限制,并超时了
// 也会显示这里配置的报错组件,默认值是:Infinity
timeout: 3000
})
加载组件会在内部组件加载前(时)先行显示,但在加载组件显示之前默认有 200ms 延迟,这是因为在网络状况较好时,加载完成得很快,加载组件和最终组件之间的替换太快可能产生闪烁,反而影响用户感受。报错组件会在加载器函数返回的 Promise 抛错时被渲染;最后,还可以指定一个超时时间,在请求耗时超过指定时间时也会渲染报错组件。
组合式函数
组合式函数
“组合式函数”是利用 Vue 组合式 API 来封装和复用有状态逻辑的函数;与普通函数抽取不同的是在组合式函数中可以保留响应性、访问模板页面,就相当于在 Vue 页面中编写代码一样。
通过抽取组合式函数改善代码结构
和无渲染组件的对比
相比无渲染组件,组合式函数不会产生额外的组件实例开销,因此推荐在纯逻辑复用时使用组合式函数,在需要同时复用逻辑和视图布局时使用无渲染组件。
自定义指令
基本介绍
自定义指令则是为了复用涉及普通元素 DOM 访问的逻辑。
在 中,任何以
v
开头的驼峰式命名的变量都可以被用作一个自定义指令,上述示例中vFocus
即可以在模板中以 v-focus
的形式使用。
自定义指令全局注册:
const app = createApp({})
// 使 v-focus 在所有组件中都可用
app.directive('focus', {
/* ... */
})
注意:只有当所需功能只能通过直接 DOM 操作来实现时,才应该使用自定义指令。其他情况下应该尽可能地使用 v-bind
这样的内置指令来声明式地使用模板,这样更高效,也对服务端渲染更友好。
注意:只有当所需功能只能通过直接 DOM 操作来实现时,才应该使用自定义指令。其他情况下应该尽可能地使用 v-bind
这样的内置指令来声明式地使用模板,这样更高效,也对服务端渲染更友好。
指令钩子
自定义指令有很多钩子函数(都是可选的),分别在不阶段被调用,常用的是mounted
,如下:
const myDirective = {
// 在绑定元素的 attribute 前
// 或事件监听器应用前调用
created(el, binding, vnode, prevVnode) {
// 下面会介绍各个参数的细节
},
// 在元素被插入到 DOM 前调用
beforeMount(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件更新前调用
beforeUpdate(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都更新后调用
updated(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载前调用
beforeUnmount(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载后调用
unmounted(el, binding, vnode, prevVnode) {}
}
内置组件
Teleport
用于将组件内部的一部分模板“传送”到其它 DOM 结构中去;
最典型的应用场景是,将代码片段传送到 dialog 模态框中去;
Hello from the modal!
官方文档:https://v3.cn.vuejs.org/guide/teleport.html#%E4%B8%8E-vue-components-%E4%B8%80%E8%B5%B7%E4%BD%BF%E7%94%A8
Transition
会在一个元素或组件进入和离开 DOM 时应用动画。
TransitionGroup
会在 v-for
列表中的元素或组件被插入、移动、移除时应用动画。
KeepAlive
是一个内置组件,用于在多个组件间动态切换时缓存被移除的组件实例。
Suspense
是一个内置组件,用于在组件树中协调对异步依赖的处理,它让可以在组件树上层等待下层的多个嵌套异步依赖项解析完成,并可以在等待时渲染一个加载状态。