学习vue3总结(有vue2基础的)

314 阅读6分钟

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
})