vue3作为未来的趋势,学习和使用是必然的(本文所有的示例都是采用setup语法糖)
一、响应式
响应式大致可以分为两种:一个是引用数据类型用的reactive(),一个是所有数据类型都可以用的ref(),(这里只是讲解初步使用,例如shallowRef(), shallowReactive()、、、等,后续会讲(可能
reactive()
在reactive中的数据,状态都是默认深层响应式的。这意味着即使在更改深层次的对象或数组,你的改动也能被检测到。
官网的例子如下:
import { reactive } from 'vue'
const obj = reactive({
nested: { count: 0 },
arr: ['foo', 'bar']
})
function mutateDeeply() {
// 以下都会按照期望工作
obj.nested.count++
obj.arr.push('baz')
}
// 但是,我们将响应式对象的属性赋值或解构至本地变量时,或是将该属性传入一个函数时,我们会失去响应性:
const state = reactive({ count: 0 })
// n 是一个局部变量,同 state.count
// 失去响应性连接
let n = state.count
// 不影响原始的 state
n++
// count 也和 state.count 失去了响应性连接
let { count } = state
// 不会影响原始的 state
count++
ref()
// 在ref中的数据,在使用时均使用.value进行使用,但是在模板中使用时候,vue会解包,所以可以直接使用:
<script setup>
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
function increment() {
count.value++
}
</script>
<template>
<button @click="increment">
{{ count }} <!-- 无需 .value -->
</button>
</template>
// 而且ref 被传递给函数或是从一般对象上被解构时,不会丢失响应性:
const obj = {
foo: ref(1),
bar: ref(2)
}
// 仍然是响应式的
const { foo, bar } = obj
// 当 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 和 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
二、计算属性
<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,这种方法只能取属性(get
const publishedBooksMessage = computed(() => {
return author.books.length > 0 ? 'Yes' : 'No'
})
</script>
<template>
<p>Has published books:</p>
<span>{{ publishedBooksMessage }}</span>
</template>
和vue2一样,computed也可以自定义get和set
<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 绑定
这里与vue2使用并无差别,所以没必要写
四、列表渲染
这里与vue2也是无太大差别,但是修复了一个东西,也就是v-if和v-for在同一层级下,vue2是v-for先,而vue3是v-if先进行,也就是说,v-if无法读取v-for里的东西(记得给v-for加上key,方便vue的diff
<!--
这会抛出一个错误,因为属性 todo 此时
没有在该实例上定义
-->
<li v-for="todo in todos" v-if="!todo.isComplete" :key="todo.name">
{{ todo.name }}
</li>
所以,如果有这种需求的话,在外新包装一层 <template> 再在其上使用 v-for 可以解决这个问题 (这也更加明显易读):
<template v-for="todo in todos" :key="todo.name">
<li v-if="!todo.isComplete">
{{ todo.name }}
</li>
</template>
五、事件处理
于vue2无大致区别
在内联事件处理器中访问事件参数
有时我们需要在内联事件处理器中访问原生 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)
}
六、表单输入绑定
与vue2无大致区别,只是v-model在组件的使用上有区别,后续组件那里会讲
七、生命周期钩子
vue2和vue3的区别
去除了created钩子,所有以前的钩子,例如onMounted()(以前名为mounted),和vue2一样,有onBeforeMount(),onMounted()两个类型(挂载前,挂载后),vue3新增了一些钩子,例如:onErrorCaptured(),onActivated()、、、等
onMounted()、 onBeforeMount()
注册一个回调函数,在组件挂载完成后执行。
<script setup>
import { ref, onMounted } from 'vue'
const el = ref()
onMounted(() => {
el.value // <div>
})
</script>
<template>
<div ref="el"></div>
</template>
onUpdated() onBeforeUpdated()
注册一个回调函数,在组件因为响应式状态变更而更新其 DOM 树之后调用。 这个钩子在服务器端渲染期间不会被调用。(不要在 updated 钩子中更改组件的状态,这可能会导致无限的更新循环!
<script setup>
import { ref, onUpdated } from 'vue'
const count = ref(0)
onUpdated(() => {
// 文本内容应该与当前的 `count.value` 一致
console.log(document.getElementById('count').textContent)
})
</script>
<template>
<button id="count" @click="count++">{{ count }}</button>
</template>
onUnmounted() onBeforeUnmounted()
注册一个回调函数,在组件实例被卸载之后调用。
<script setup>
import { onMounted, onUnmounted } from 'vue'
let intervalId
onMounted(() => {
intervalId = setInterval(() => {
// ...
})
})
onUnmounted(() => clearInterval(intervalId))
</script>
onErrorCaptured()
注册一个钩子,在捕获了后代组件传递的错误时调用。
- 类型
function onErrorCaptured(callback: ErrorCapturedHook): void
type ErrorCapturedHook = (
err: unknown,
instance: ComponentPublicInstance | null,
info: string
) => boolean | void
- 错误可以从以下几个来源中捕获
- 组件渲染
- 事件处理器
- 生命周期钩子
setup()函数- 侦听器
- 自定义指令钩子
- 过渡钩子
这个钩子带有三个实参:错误对象、触发该错误的组件实例,以及一个说明错误来源类型的信息字符串。
你可以在 errorCaptured() 中更改组件状态来为用户显示一个错误状态。注意不要让错误状态再次渲染导致本次错误的内容,否则组件会陷入无限循环。
这个钩子可以通过返回 false 来阻止错误继续向上传递。
- 错误传递规则
-
默认情况下,所有的错误都会被发送到应用级的
app.config.errorHandler(前提是这个函数已经定 义),这样这些错误都能在一个统一的地方报告给分析服务。 -
如果组件的继承链或组件链上存在多个
errorCaptured钩子,对于同一个错误,这些钩子会被按从底至 上的顺序一一调用。这个过程被称为“向上传递”,类似于原生 DOM 事件的冒泡机制。 -
如果
errorCaptured钩子本身抛出了一个错误,那么这个错误和原来捕获到的错误都将被发送 到app.config.errorHandler。 -
errorCaptured钩子可以通过返回false来阻止错误继续向上传递。即表示“这个错误已经被处理 了,应当被忽略”,它将阻止其他的errorCaptured钩子或app.config.errorHandler因这个错误 而被调用。
-
onRenderTracked() 仅在开发模式下可用
注册一个调试钩子,当组件渲染过程中追踪到响应式依赖时调用。
这个钩子仅在开发模式下可用,且在服务器端渲染期间不会被调用。
- 类型
function onRenderTracked(callback: DebuggerHook): void
type DebuggerHook = (e: DebuggerEvent) => void
type DebuggerEvent = {
effect: ReactiveEffect
target: object
type: TrackOpTypes /* 'get' | 'has' | 'iterate' */
key: any
}
onRenderTriggered() 仅在开发模式下可用
注册一个调试钩子,当响应式依赖的变更触发了组件渲染时调用。
这个钩子仅在开发模式下可用,且在服务器端渲染期间不会被调用。
- 类型
function onRenderTriggered(callback: DebuggerHook): void
type DebuggerHook = (e: DebuggerEvent) => void
type DebuggerEvent = {
effect: ReactiveEffect
target: object
type: TriggerOpTypes /* 'set' | 'add' | 'delete' | 'clear' */
key: any
newValue?: any
oldValue?: any
oldTarget?: Map<any, any> | Set<any>
}
onActivated()
注册一个回调函数,若组件实例是 <KeepAlive> 缓存树的一部分,当组件被插入到 DOM 中时调用。
这个钩子在服务器端渲染期间不会被调用。
- 类型
function onActivated(callback: () => void): void
onDeactivated()
注册一个回调函数,若组件实例是 <KeepAlive> 缓存树的一部分,当组件从 DOM 中被移除时调用。
这个钩子在服务器端渲染期间不会被调用。
- 类型
function onDeactivated(callback: () => void): void
onServerPrefetch() 仅在服务器上调用
注册一个异步函数,在组件实例在服务器上被渲染之前调用。
- 类型
function onServerPrefetch(callback: () => Promise<any>): void
-
详细信息
如果这个钩子返回了一个 Promise,服务端渲染会在渲染该组件前等待该 Promise 完成。
这个钩子仅会在服务端渲染中执行,可以用于执行一些仅存在于服务端的数据抓取过程。
-
示例
<script setup>
import { ref, onServerPrefetch, onMounted } from 'vue'
const data = ref(null)
onServerPrefetch(async () => {
// 组件作为初始请求的一部分被渲染
// 在服务器上预抓取数据,因为它比在客户端上更快。
data.value = await fetchOnServer(/* ... */)
})
onMounted(async () => {
if (!data.value) {
// 如果数据在挂载时为空值,这意味着该组件
// 是在客户端动态渲染的。将转而执行
// 另一个客户端侧的抓取请求
data.value = await fetchOnClient(/* ... */)
}
})
</script>
八、依赖注入
provide()
提供一个值,可以被后代组件注入。
- 类型
function provide<T>(key: InjectionKey<T> | string, value: T): void
- 详细信息
provide() 接受两个参数:第一个参数是要注入的 key,可以是一个字符串或者一个 symbol,第二个参数是要注入的值。
当使用 TypeScript 时,key 可以是一个被类型断言为 InjectionKey 的 symbol。InjectionKey 是一个 Vue 提供的工具类型,继承自 Symbol,可以用来同步 provide() 和 inject() 之间值的类型。
与注册生命周期钩子的 API 类似,provide() 必须在组件的 setup() 阶段同步调用。
- 示例
<script setup>
import { ref, provide } from 'vue'
import { fooSymbol } from './injectionSymbols'
// 提供静态值
provide('foo', 'bar')
// 提供响应式的值
const count = ref(0)
provide('count', count)
// 提供时将 Symbol 作为 key
provide(fooSymbol, count)
</script>
inject()
注入一个由祖先组件或整个应用 (通过 app.provide()) 提供的值。
- 类型
// 没有默认值
function inject<T>(key: InjectionKey<T> | string): T | undefined
// 带有默认值
function inject<T>(key: InjectionKey<T> | string, defaultValue: T): T
// 使用工厂函数
function inject<T>(
key: InjectionKey<T> | string,
defaultValue: () => T,
treatDefaultAsFactory: true
): T
- 详细信息
第一个参数是注入的 key。Vue 会遍历父组件链,通过匹配 key 来确定所提供的值。如果父组件链上多个组件对同一个 key 提供了值,那么离得更近的组件将会“覆盖”链上更远的组件所提供的值。如果没有能通过 key 匹配到值,inject() 将返回 undefined,除非提供了一个默认值。
第二个参数是可选的,即在没有匹配到 key 时使用的默认值。它也可以是一个工厂函数,用来返回某些创建起来比较复杂的值。如果默认值本身就是一个函数,那么你必须将 false 作为第三个参数传入,表明这个函数就是默认值,而不是一个工厂函数。
与注册生命周期钩子的 API 类似,inject() 必须在组件的 setup() 阶段同步调用。
当使用 TypeScript 时,key 可以是一个类型为 InjectionKey 的 symbol。InjectionKey 是一个 Vue 提供的工具类型,继承自 Symbol,可以用来同步 provide() 和 inject() 之间值的类型。
- 示例
假设有一个父组件已经提供了一些值,如前面 provide() 的例子中所示:
<script setup>
import { inject } from 'vue'
import { fooSymbol } from './injectionSymbols'
// 注入值的默认方式
const foo = inject('foo')
// 注入响应式的值
const count = inject('count')
// 通过 Symbol 类型的 key 注入
const foo2 = inject(fooSymbol)
// 注入一个值,若为空则使用提供的默认值
const bar = inject('foo', 'default value')
// 注入一个值,若为空则使用提供的工厂函数
const baz = inject('foo', () => new Map())
// 注入时为了表明提供的默认值是个函数,需要传入第三个参数
const fn = inject('function', () => {}, false)
</script>
依赖注入的示例
用我们比较常见的echarts来做例子,下面是注入
// 记得先yarn或者npm添加echarts, 在mian.ts里使用
import { createApp } from 'vue'
import * as echarts from 'echarts';
const app = createApp(App);
app.provide('$echarts', echarts)
下面是使用
<script setup lang="ts">
import { inject, onMounted, ref } from "vue"
const $echarts = inject('$echarts') as any
const echart = ref(null)
const options = {/**...**/}
onMounted(() => {
const myChart = $echarts.init(echart.value);
myChart.setOption(props.echartsOption);
})
</script>
<template>
<div ref="echart" style="width:400px;height:300px" />
</template>
九、侦听器
vue3里是分为两种,一个是watch(),另一个是watchEffect()
watch()
示例
<script setup>
import { ref, watch } from 'vue'
const question = ref('')
// 可以直接侦听一个 ref
watch(question, (newQuestion, oldQuestion) => {
console.log(newQuestion, oldQuestion)
})
</script>
<template>
<input v-model="question" />
</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 })
// 提供一个 getter 函数
watch(
() => obj.count,
(count) => {
console.log(`count is: ${count}`)
}
)
// 如果是写成这样就不行,因为 watch() 得到的参数是一个 number
watch(obj.count, (count) => {
console.log(`count is: ${count}`)
})
深层侦听器
直接给 watch() 传入一个响应式对象,那么这个watch默认是深层次的侦听,也就是所有嵌套的变更时都会被触发:
const obj = reactive({ count: 0 })
watch(obj, (newValue, oldValue) => {
// 在嵌套的属性变更时触发
// 注意:`newValue` 此处和 `oldValue` 是相等的
// 因为它们是同一个对象!
})
obj.count++
相比之下,一个返回响应式对象的 getter 函数,只有在返回不同的对象时,才会触发回调:
watch(
() => state.someObject,
() => {
// 仅当 state.someObject 被替换时触发
}
)
也可以给上面这个例子显式地加上 deep 选项,强制转成深层侦听器:
watch(
() => state.someObject,
(newValue, oldValue) => {
// 注意:`newValue` 此处和 `oldValue` 是相等的
// *除非* state.someObject 被整个替换了
},
{ deep: true }
)
watchEffect()
watch() 是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。举例来说,我们想请求一些初始数据,然后在相关状态更改时重新请求数据(这里让我感觉有点类似react里的useEffect,但是具体使用还是不相同的),watchEffect会自动帮你收集依赖,来更新:
const url = ref('https://...')
const data = ref(null)
watchEffect(async () => {
const response = await fetch(url.value)
data.value = await response.json()
})
watch() vs watchEffect()
watch 和 watchEffect 都能响应式地执行有副作用的回调。它们之间的主要区别是追踪响应式依赖的方式:
watch只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。另外,仅在数据源确实改变时才会触发回调。watch会避免在发生副作用时追踪依赖,因此,我们能更加精确地控制回调函数的触发时机。watchEffect,则会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。这更方便,而且代码往往更简洁,但有时其响应性依赖关系会不那么明确。
回调的触发时机
当你更改了响应式状态,它可能会同时触发 Vue 组件更新和侦听器回调。
默认情况下,用户创建的侦听器回调,都会在 Vue 组件更新之前被调用。这意味着你在侦听器回调中访问的 DOM 将是被 Vue 更新之前的状态。
如果想在侦听器回调中能访问被 Vue 更新之后的DOM,你需要指明 flush: 'post' 选项:
watch(source, callback, {
flush: 'post'
})
watchEffect(callback, {
flush: 'post'
})
后置刷新的 watchEffect() 有个更方便的别名 watchPostEffect():
import { watchPostEffect } from 'vue'
watchPostEffect(() => {
/* 在 Vue 更新后执行 */
})
停止侦听器
在 setup() 或 <script setup> 中用同步语句创建的侦听器,会自动绑定到宿主组件实例上,并且会在宿主组件卸载时自动停止。因此,在大多数情况下,你无需关心怎么停止一个侦听器。
但是,如果你是异步创建的话,就需要手动停止他
<script setup>
import { watchEffect, onBeforeUnmounted } from 'vue'
// 它会自动停止
watchEffect(() => {})
// 异步的话可以这样做!
let unwatch = ''
setTimeout(() => {
unwatch = watchEffect(() => {})
}, 100)
onBeforeUnmounted(() => {
unwatch()
})
</script>
如果是需要等待一些异步数据,你可以使用条件式的侦听逻辑:
const data = ref(null)
watchEffect(() => {
if (data.value) {
// 数据加载后执行某些操作...
}
})
十、模板引用(也就是ref
用法和vue2差别不大,只是父组件调用子组件的时候,需要做处理
<script setup>
import { ref, onMounted } from 'vue'
// 声明一个 ref 来存放该元素的引用
// 必须和模板里的 ref 同名
const input = ref(null)
onMounted(() => {
input.value.focus()
})
</script>
<template>
<input ref="input" />
</template>
十一、组件
创建组件的方式和vue2相差不远,只是如果使用setup语法糖,那么就不需要注册,直接使用
// 子组件
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">You clicked me {{ count }} times.</button>
</template>
// 父组件
<script setup>
import ButtonCounter from './ButtonCounter.vue'
</script>
<template>
<h1>Here is a child component!</h1>
<ButtonCounter />
</template>
props
使用的话,是用defineProps处理
<script setup>
const props = defineProps({
title: {
type:String, // 参数类型
default: "Vue3", //默认值
required: true, //是否必传
validator: value => {
return typeof(value) === 'string' // 除了验证是否符合type的类型,此处再判断该值结果是否符合验证
}
},
formData: Object
})
</script>
如果是使用ts的话
<script lang="ts" setup>
const props = defineProps<{
title: '必传且限定'|'其中一个'|'值', // 利用TS:限定父组件传 either 的值
child?: string|number,
formTitle?: string,
formData: any[]
}>()
</script>
如果用ts,又想设置默认值的话
<script lang='ts' setup>
interface Props {
title: '必传且限定'|'其中一个'|'值', // 利用TS:限定父组件传 either 的值
child?: string|number,
formTitle?: string,
formData: any[]
}
const props = withDefaults(defineProps<Props>(), {
title: '值',
formData: () => ['one', 'two'],
})
</script>
组件之间的事件
vue2里,是可以通过this.$ref.children.method来调用子组件的方法,this.$parent.method
来调用父组件的方法
但是,在vue3里,子组件直接调用父组件的方法是不行只能通过$emit的方法去触发,父组件直接调
用子组件的方法还是可以,但是需要在子组件里进行抛出。
下面是父组件直接调用子组件的方法:
// 子组件
<template>
<div />
</template>
<script setup lang="ts">
const show = () => {
console.log('children click');
}
defineExpose({ show }) // 一定要记得导出去,不然外界访问不了,因为你使用了<script setup>
</script>
// 父组件
<template>
<Echarts ref="example"></Echarts>
<button @click="handleClick" />
</template>
<script setup lang="ts">
import Echarts from "./Echarts.vue"
const handleClick = () => {
example.value.show
}
</script>
下面是子组件触发调用父组件的方法(于vue2大致相同):
//父组件
<template>
<Children @getData="getData" />
</template>
<script setup>
import Children from './components/Children.vue'
const getData = () => {
alert('我是爸爸的方法')
}
</script>
<template>
<div @click="clickChild">子组件<div>
</template>
<script setup>
import {defineEmits} from 'vue'
const emit = defineEmits(['getData'])
const clickChild = () => {
emit('getData')
}
</script>
插槽
与vue2无太大差异
具名插槽
// 子组件 BaseLayout.vue
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
// 使用
<BaseLayout>
<template v-slot:header>
<!-- header 插槽的内容放这里 -->
</template>
</BaseLayout>
// v-slot可以缩写为#
<BaseLayout>
<template #header>
<h1>Here might be a page title</h1>
</template>
</BaseLayout>
作用域插槽
<!-- <MyComponent> 的模板 -->
<div>
<slot name="header" :text="greetingMessage" :count="1"></slot>
</div>
<MyComponent>
<div #header="{{greetingMessage,count}}" > // es6的对象拓展
{{greetingMessage}} {{count}}
</div>
</MyComponent>
十二、异步组件
vue2里面是
() => import('@/views/index.vue'),
vue3的话有一个新的东西defineAsyncComponent
<script setup>
import { defineAsyncComponent } from 'vue'
const AdminPage = defineAsyncComponent(() =>
import('./components/AdminPageComponent.vue')
)
</script>
<template>
<AdminPage />
</template>
defineAsyncComponent()
定义一个异步组件,它在运行时是懒加载的。参数可以是一个异步加载函数,或是对加载行为进行更具体定制的一个选项对象。
- 类型
function defineAsyncComponent(
source: AsyncComponentLoader | AsyncComponentOptions
): Component
type AsyncComponentLoader = () => Promise<Component>
interface AsyncComponentOptions {
loader: AsyncComponentLoader
loadingComponent?: Component
errorComponent?: Component
delay?: number
timeout?: number
suspensible?: boolean
onError?: (
error: Error,
retry: () => void,
fail: () => void,
attempts: number
) => any
}
使用
const AsyncComp = defineAsyncComponent({
// 加载函数
loader: () => import('./Foo.vue'),
// 加载异步组件时使用的组件
loadingComponent: LoadingComponent,
// 展示加载组件前的延迟时间,默认为 200ms
delay: 200,
// 加载失败后展示的组件
errorComponent: ErrorComponent,
// 如果提供了一个 timeout 时间限制,并超时了
// 也会显示这里配置的报错组件,默认值是:Infinity
timeout: 3000
})
// 需要配合Suspense使用,但是Suspense还在测试阶段,其api到时候会有所改变!
<Suspense> <!-- 具有深层异步依赖的组件 -->
<AsyncComp /> <!-- 在 #fallback 插槽中显示 “正在加载中” -->
<template #fallback>
Loading...
</template>
</Suspense>
自定义指令
<script setup>
// 在模板中启用 v-focus
const vFocus = {
mounted: (el) => el.focus()
}
</script>
<template>
<input v-focus />
</template>
在 <script setup> 中,任何以 v 开头的驼峰式命名的变量都可以被用作一个自定义指令。在上面的例子中,vFocus 即可以在模板中以 v-focus 的形式使用。
在没有使用 <script setup> 的情况下,自定义指令需要通过 directives 选项注册:
export default {
setup() {
/*...*/
},
directives: {
// 在模板中启用 v-focus
focus: {
/* ... */
}
}
}
全局注册组件
const app = createApp({})
// 使 v-focus 在所有组件中都可用
app.directive('focus', {
/* ... */
})
指令钩子
const vDirective = {
// 在绑定元素的 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) {}
}
钩子参数
指令的钩子会传递以下几种参数:
-
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。 -
prevNode:之前的渲染中代表指令所绑定元素的 VNode。仅在beforeUpdate和updated钩子中可用。 -
举例来说,像下面这样使用指令:
<div v-example:foo.bar="baz">
binding 参数会是一个这样的对象:
{
arg: 'foo',
modifiers: { bar: true },
value: /* `baz` 的值 */,
oldValue: /* 上一次更新时 `baz` 的值 */
}
和内置指令类似,自定义指令的参数也可以是动态的。举例来说:
<div v-example:[arg]="value"></div>
简化形式
如果我们直接用一个函数来定义指令(不用上面的生命周期钩子回调),如下所示:
<div v-color="color"></div>
app.directive('color', (el, binding) => {
// 这会在 `mounted` 和 `updated` 时都调用
el.style.color = binding.value
})