# Vue3文档

Vue3的基础内容都是基于Vue2来的,但Vue3响应式原理变了,使用Proxy重写了底层代码,并且使用组合式API开发思想,代码写起来更像原生,使逻辑更灵活的组合与复用。Vue3还基于TS编写,让Vue2一直难以兼容TS的问题也得到改变,在学习Vue3时也要先熟悉TS语法。组合式API和TS让Vue3与Vue2的开发习惯有着很大的差异,但只要上手写起来,相信很快就能熟练的,毕竟它还是基于Vue的。

# 组合式API

组合式API思想就是把原本分散在data、created、methods等相关的逻辑,通过setup集中到一起,在其中把每个功能代码块划分在一个逻辑区域,不用再写个变量再去data中找。并且还能像函数一样抽离公共setup片段,让代码更好的封装和重复使用。

在 Vue3 + Vite 搭建的单文件组件(SFC)项目中,直接在 <script setup> 中书写原生js代码,然后在Vue中使用就可以了,不需要再像过渡版那样再通过return把数据返回了。

<script setup>
  import { ref } from 'vue'

  // 通过ref定义一个响应式变量
  const isShow = ref(false)
  // 定义一个函数
  const toggleShow = () => {
    isShow.value = false
  }
</script>

<template>
  <div @click="toggleShow">点击</div>
  <div v-if="isShow">显示内容</div>
</template>

# 响应式基础

Vue3声明响应式数据需要使用 refreactive 来定义数据类型。ref用来声明基础数据类型,reactive用来声明引用类型(对象、数组和 Map、Set等)。

# reactive

使用reactive声明引用类型响应式数据时只有在声明的数据上才具有响应性,解构、赋值给其他值、或传入函数都会丢失,这也是reactive的局限,所以在使用reactive声明时要考虑是否涉及这几种情况。

<script setup>
  import { reactive } from 'vue'
  const state = reactive({ count: 0 })
  const arr = reactive(['a', 'b', 'c'])

  let n = state.count // n失去响应性连接
  n++ // 不影响原始的 state

  // 解构 count 失去了响应性连接
  let { count } = state
  count++ // 不会影响原始的 state

  // 重新赋值整个对象会失去响应性
  state = {
    count: 5
  }
  state.count++ // 不会生效

  arr.splice(1, 0) // arr失去响应性
  arr = [] // arr失去响应性

  // 该函数接收一个普通数字,并且
  callSomeFunction(state.count) // 将无法跟踪 state.count 的变化
</script>

<template>
  <!-- 在html中直接使用 -->
  <span>{{reactive.count}}</span>
</template>

# ref

ref定义基础数据类型,获取和设置需要通过 .value,但在html上会自动解包不需要.value。ref赋值、传参或者解构不会像reactive失去响应性,而且定义引用类型数据操作时也不会失去响应性,所以基于这一点,我们主要使用ref来声明响应式数据。

ref为什么要 .value,js不能直接检查普通变量的修改,所以把它设计成一个getter/setter对象,利于Vue进行检查每个ref是否修改。

<script setup>
  import { ref } from 'vue'
  // 基础类型
  const count = ref(0)
  const bool = ref(true)
  const str = ref('xxx')
  const arr = ref([1, 2, 3])
  const obj = ref({
    name: 'xxx',
    age: 18
  })

  // ref获取和设置需要通过.value
  count.value // 0
  count.value++ // 1
  obj.value.age // 18
</script>

<template>
  <!-- 在html中直接使用,自动解包 -->
  <span>{{count}}</span>
</template>

# ref解包

把 ref 赋值给 reactive 对象会自动解包,会相互影响,改变哪个都是响应式的。赋值给数组或者Map这种原生集合类型时,ref不会自动解包。

<script setup>
  import { ref } from 'vue'
  const object = {
    foo: ref(1)
  }
  const { foo } = object
  console.log(foo) // 不需要.value

  const count = ref(0)
  const state = reactive({
    count
  })
  // ref在响应式对象中也会自动解包
  state.count // 0
  state.count = 1
  console.log(count) // 1 count会受影响


  // ref作为数组和集合类型的元素不会自动解包
  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)
</script>

<template>
  <!-- 使用表达式不会自动解包,解构或者直接使用都可以 -->
  <span>{{object.foo + 1}}</span>
  <!-- 解构出来可以 -->
  <span>{{foo + 1}}</span>
  <!-- 直接使用也可以 -->
  <span>{{object.foo}}</span>
</template>

# 响应式API

一些针对响应式数据进行处理的工具函数。

# toRefs()、toRef()

要想解构reactive对象,可以使用 toRefs()toRef() 两个函数转为ref,使数据继续保持响应式。

<script setup>
  import { reactive, toRefs, toRef } from 'vue'
  const state = reactive({
    count: 0,
    name: 'xxx'
  })

  // 将 state 转成一个全是 ref 的对象,然后解构
  let { count } = toRefs(state)
  count.value++
  console.log(state.count) // 1

  // 将某个属性转为ref
  const name = toRef(state, 'name')
  console.log(name.value)
</script>

# shallowRef()、shallowReactive()

当给ref赋值为对象时,ref会对深层次的对象进行响应,可以使用 shallowRef() 只对最外层属性进行响应,从而节省性能。

同理shallowReactive()shallowReadonly()也是让最外层属性是响应性的,深层次的属性改变不进行影响。

# readonly()

接受一个对象 (不论是响应式还是普通的) 或是一个 ref,返回一个原值的只读代理。只读代理是深层的,对任何嵌套属性的访问都将是只读的。

<script setup>
  import { readonly } from 'vue'
  const state = readonly({
    count: 0,
    name: 'xxx'
  })
</script>

# 获取this

<script setup>中无法像Vue2那样直接使用 this 来获取实例,需要通过getCurrentInstance方法来获取。

<script setup>
import { getCurrentInstance } from 'vue'

const instance = getCurrentInstance()
console.log(instance)

// instance?.appContext.config.globalProperties 全局实例
console.log(instance?.appContext.config.globalProperties)

// instance?.proxy 当前组件实例
console.log(instance?.proxy)

// 使用
instance?.proxy?.$message.success('操作成功')
</script>

# 生命周期

Vue3生命周期有所改变,没有了 created 钩子,因为setup就相当于created一样。destroyed变为了unmounted。

  • onBeforeMount: 在组件被挂载之前被调用。
  • onMounted: 在组件挂载完成后执行。
  • onBeforeUpdate: 在组件即将因为响应式状态变更而更新其 DOM 树之前调用。
  • onUpdated: 在组件因为响应式状态变更而更新其 DOM 树之后调用。
  • onBeforeUnmount: 在组件实例被卸载之前调用。
  • onUnmounted: 在卸载组件实例之前调用。
  • onErrorCaptured: 在捕获了后代组件传递的错误时调用。

KeepAlive缓存组件中的两个声明周期钩子

  • onActivated: 组件被插入到 DOM 中时调用。
  • onDeactivated: 当组件从 DOM 中被移除时调用。

开发环境独有的两个声明

  • onRenderTracked: 当组件渲染过程中追踪到响应式依赖时调用。
  • onRenderTriggered: 当响应式依赖的变更触发了组件渲染时调用。

SSR服务端渲染独有的两个声明

  • onServerPrefetch: 在组件实例在服务器上被渲染之前调用。

使用:

<script setup>
  import { onMounted } from 'vue'
  
  onMounted(() => {
    console.log(`the component is now mounted.`)
  })
</script>

# 组件注册

# 全局注册

import { createApp } from 'vue'
import HelloWorld from './components/HelloWorld.vue'

const app = createApp({})

app.component('MyComponent', HelloWorld)

# 局部注册

以前需要引入组件后通过components注册,在<script setup>中引入组件后不在需要components注册就可以使用。组件名默认是以文件名命名,如果想自定义组件名可以使用插件 vite-plugin-vue-setup-extend,使用PascalCase全大驼峰格式进行命名,以区分组件和普通元素标签。

# 异步组件

使用 defineAsyncComponent异步加载组件,仅在页面需要它渲染时才会调用加载内部实际组件的函数。

<script setup>
import { defineAsyncComponent } from 'vue'

const AdminPage = defineAsyncComponent(() => import('./components/AdminPageComponent.vue') )
</script>

<template>
  <AdminPage />
</template>

# 计算属性

计算属性 computed 依赖响应式(ref, reactive)进行计算,返回一个计算属性 ref,他和一般的 ref 类似依然是响应式的,调用需要使用.value。计算属性传参方式有两种,分为函数方式和对象方式,默认是传入一个函数。

<script setup>
  import { reactive, computed } from 'vue'

  const author = reactive({
    name: 'xxx',
    books: ['a', 'b', 'c']
  })

  // 一个计算属性 ref
  const isDisabled = computed(() => {
    return !author.books.length
  })

  const bookLength = computed(() => author.books.length)

  isDisabled.value // false
</script>

<template>
  <button :disabled="isDisabled">{{ isDisabled }}</button>
</template>

计算属性的 getter 和 setter,当输入一个含有get和set的对象时,就可以通过 getter 和 setter 读取和设置对应的值。getter的值应该和计算属性返回的值一样,最好不要做其他操作。

<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(newVal) {
      // 注意:我们这里使用的是解构赋值语法
      [firstName.value, lastName.value] = newVal.split(' ')
    }
  })

  fullName.value // John Doe

  fullName.value = 'Xiao Ming'

  firstName.value = 'Xiao'
  lastName.value = 'Ming'
</script>

# 监听器

监听器 watch 对响应式数据(ref, reactive)进行侦听,监测数据是否改变。它可以监听 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组。

# 监听一个值

<script setup>
  import { ref, watch } from 'vue'

  const msg = ref('xxx')
  const x = ref(0)
  const y = ref(0)

  // 单个 ref
  watch(msg, (newVal, oldVal) => {
    console.log(newVal)
    console.log(oldVal)
  })

  // getter 函数
  // 通过函数返回一个新的值再对其进行监听
  watch(() => x.value + y.value, (newVal, oldVal) => {
      console.log(newVal)
      console.log(oldVal)
    }
  )
</script>

# 监听多个值

<script setup>
  import { ref, watch } from 'vue'

  const msg = ref('xxx')
  const text = ref('123')
  const x = ref(0)
  const y = ref(0)

  // 多个ref
  // 通过数组来监听,返回的值newVal, oldVal也分别是个数组
  watch([msg, text], (newVal, oldVal) => {
    console.log(newVal) // ['xxx11', '12311']
    console.log(oldVal) // ['xxx', '123']
  })

  // ref 和 getter 函数组合
  watch([msg, () => x.value + y.value], (newVal, oldVal) => {
      console.log(newVal) // ['xxx11', sum]
      console.log(oldVal) // ['xxx11', 0]
    }
  )

  // 调用异步方法
  watch(msg, async () => {
    const response = await fetch('xxx')
    console.log(response)
  }, { immediate: true })
</script>

# 监听reactive响应式对象

<script setup>
  import { reactive, watch } from 'vue'

  const obj = reactive({ count: 0, size: 'small' })

  // 监听整个对象
  // 隐式地创建一个深层侦听器,监听所有
  watch(obj, (newVal, oldVal) => {
    // 在嵌套的属性变更时触发
    // 注意:`newVal` 此处和 `oldVal` 是相等的
    // 因为它们是同一个对象!
  })

  // 监听对象中的某个值
  // 需要通过getter 函数返回,不能直接监听
  watch(
    () => obj.count,
    (newVal, oldVal) => {
      console.log(newVal) // 1
      console.log(oldVal) // 0
    }
  )

  // 监听对象中的多个值
  watch(
    [() => obj.count, () => obj.size],
    (newVal, oldVal) => {
      console.log(newVal) // []
      console.log(oldVal) // []
    }
  )
</script>

# 深层侦听器

默认监听一个响应式对象,会隐式地创建一个深层侦听器——该回调函数在所有嵌套的变更时都会被触发。如果通过对getter函数返回的对象进行监听,则不会及时触发,需要使用 deep 选项进行强制转成深层侦听器。

谨慎使用

深度侦听需要遍历被侦听对象中的所有嵌套的属性,当用于大型数据结构时,开销很大。因此请只在必要时才使用它,并且要留意性能。

<script setup>
  import { reactive, watch } from 'vue'

  const person = reactive({
    name: 'xxx',
    student: {
      grade: 1,
      class: 2
    }
  })

  // 通过getter 函数返回的对象
  watch(
    () => obj.student,
    (newVal, oldVal) => {
      // 注意:`newVal` 此处和 `oldVal` 是相等的
      // *除非* state.someObject 被整个替换了
    },
    { deep: true }
  )
</script>

# 即时回调的侦听器​

watch 仅当数据源变化时,才会执行回调。如果希望在创建侦听器时,立即执行一遍回调。举例来说,我们想请求一些初始数据,然后在相关状态更改时重新请求数据。通过传入 immediate 选项来强制侦听器的回调立即执行。

watch(source, (newValue, oldValue) => {
  // 立即执行,且当 `source` 改变时再次执行
}, { immediate: true })

# watchEffect

相比较 watch 手动去监听某些属性,watchEffect可以允许我们自动跟踪回调的响应式依赖,只要在watchEffect回调函数中用到某个数据,它就会被触发,并且不需要使用immediate就可以立即就执行。

<script setup>
  import { ref, watchEffect } from 'vue'

  const todoId = ref(1)

  // 会立即就执行todoId为1的请求,当todoId改变时也会自动监听发送请求
  watchEffect(async () => {
    const response = await fetch( `http://xxxx.xxx/?todoId=${todoId.value}` )
    console.log(response)
  })

  todoId.value = 2 // 继续请求
</script>

对于这种只有一个依赖项的例子来说,watchEffect() 的好处相对较小。但是对于有多个依赖项的侦听器来说,使用 watchEffect() 可以消除手动维护依赖列表的负担。此外,如果你需要侦听一个嵌套数据结构中的几个属性,watchEffect() 可能会比深度侦听器更有效,因为它将只跟踪回调中被使用到的属性,而不是递归地跟踪所有的属性。

TIP

watchEffect 仅会在其同步执行期间,才追踪依赖。在使用异步回调时,只有在第一个 await 正常工作前访问到的属性才会被追踪。

# 监听触发时机

监听器默认都是在组件更新之前被调用,如果想在侦听器回调中访问更新之后的 DOM,需要在第三个参数中添加 flush: 'post' 选项。

watch(source, callback, {
  flush: 'post'
})

watchEffect(callback, {
  flush: 'post'
})

// watchEffect的另一种写法
import { watchPostEffect } from 'vue'
watchPostEffect(() => {
  /* 在 Vue 更新后执行 */
})

# 停止监听

同步创建的监听器会自动停止,如果是异步创建的需要手动给停止掉,以防内存泄漏。

<script setup>
  import { watch, watchEffect } from 'vue'

  const msg = ref('xxx')

  setTimeout(() => {
    const unwatch = watch(msg, () => {})
    const unwatchEffect = watchEffect(() => {})
  }, 100)

  // 手动停止
  unwatch()
  unwatchEffect()
</script>

# 列表渲染

相比较Vue2,template使用列表渲染,key直接绑定在template就行了。

<template v-for="todo in todos" :key="todo.name">
  <li>{{ todo.name }}</li>
</template>

# 获取DOM

通过ref获取,绑到元素上就代表元素,绑到子组件上获取的是子组件的实例。

<script setup>
  import { ref, onMounted } from 'vue'
  import Child from './Child.vue'

  // 声明一个 ref 来存放该元素的引用
  // 必须和模板里的 ref 同名
  const input = ref(null)

  onMounted(() => {
    input.value.focus()

    child.value // <Child /> 组件的实例
  })
</script>

<template>
  <input ref="input" />
  <Child ref="child" />
</template>

获取 v-for 列表上的ref

ref 数组并不保证与源数组相同的顺序

<script setup>
  import { ref, onMounted } from 'vue'

  const list = ref([
    /* ... */
  ])

  const itemRefs = ref([])

  onMounted(() => {
    console.log(itemRefs.value)
  })
</script>

<template>
  <li v-for="item in list" ref="itemRefs">
    {{ item }}
  </li>
</template>

获取动态DOM元素上的ref,通过一个函数返回el元素然后赋值给一个变量,比如多个表单获取对应的ref进行验证。

<script setup>
  import { ref, onMounted } from 'vue'

  const formList = ref([/*...*/])

  const refList = ref([])

  // 通过函数获取ref列表,但是可能会存在多获取一遍ref的问题
  const getRefs = (el) => {
    refList.value.push(el)
  }
</script>

<template>
  <!-- <input :ref="(el) => { /* 将 el 赋值给一个数据属性或 ref 变量 */ }"> -->

  <!-- 要通过一个函数把el返回出来 -->
  <a-form v-for="item in formList" :ref="el => getRefs(el)">
    <a-form-item></a-form-item>
  </a-form>
</template>

解决动态获取ref会多获取一遍问题:

<script setup>
  import { ref, onMounted } from 'vue'

  const formList = ref([/*...*/])

  const refList = ref([])

  const getRefs = (el, index) => {
    refList.value[index] = el
  }
</script>

<template>

  <!-- 要通过一个函数把el返回出来 -->
  <a-form v-for="(item, index) in formList" :ref="el => getRefs(el, index)">
    <a-form-item></a-form-item>
  </a-form>
</template>

新用法(3.5+):使用useTemplateRef 代替了 ref

<script setup>
import { useTemplateRef, onMounted } from 'vue'

// 第一个参数必须与模板中的 ref 值匹配
const input = useTemplateRef('my-input')

onMounted(() => {
  input.value.focus()
})
</script>

<template>
  <input ref="my-input" />
</template>

# nextTick使用

import { nextTick } from 'vue'

async function increment() {
  count.value++
  await nextTick()
  // 现在 DOM 已经更新了
}

# Props传参

父组件向子组件传参,子组件通过 defineProps() 宏接收参数。接收参数可以直接在模板上使用,如果要在<script setup>中使用需要通过一个变量来接收。3.5+可以直接使用解构来接收。

<script setup>
  // 赋值给一个变量进行使用
  const props = defineProps(['msg', 'count'])
  console.log(props.msg)
  
  // 对象形式
  const props = defineProps({
    propA: String,
    propB: Number,
    propC: [Number, String],
    propD: {
      type: String,
      required: true
    },
    propE: {
      type: Number,
      default: 100
    },
    propF: {
      type: Arrary,
      default: () => []
    },
    propG: {
      type: Object,
      default: () => {}
    },
    // 自定义类型校验函数
    propH: {
      validator(value) {
        // The value must match one of these strings
        return ['success', 'warning', 'danger'].includes(value)
      }
    }
  })
</script>
<template>
  <h4>{{ msg }}</h4>
  <h4>{{ count }}</h4>
</template>

# Prop名字格式

一般,defineProps接收参数名使用camelCase小驼峰写法,在组件上传参时使用kebab-case形式进行传参。布尔类型传值时,不传默认为false,直接写参数名默认为true,不用再写true和false。

<!-- 父组件 -->
<template>
  <MyComponent greeting-message="hello" :dept-id="1" disabled />
</template>

<!-- 子组件 -->
<script setup>
  const props = defineProps({
    greetingMessage: String,
    deptId: Number,
    disabled: Boolean
  })

  console.log(props.disabled) // true
</script>

# 动态传参

在vue2.6.0后新增了属性绑定和事件名的动态参数写法,用 [] 包起来,其值为字符串类型,或者null。

<script setup>
  import { ref, computed } from 'vue'

  const attributeName = ref('msg')
  const eventName = ref('focus') // focus、click、change等

  // 复杂的属性名可以通过计算属性获取
  const someAttr = computed(() => {
    return '...'
  })
</script>
<template>
  <div>
    <!-- 动态属性 -->
    <a :[attributeName]="url"> ... </a>
    <a :[someAttr]="url"> ... </a>
    
    <!-- 动态绑定事件 -->
    <a @[eventName]="doSomething">
  </div>
</template>

# 多个props

有时候传参时需要传入一个对象中的多个多个值,一个一个的写太麻烦,可以使用 v-bind 直接传入整个对象。然后子组件就可以接收所有参数。

<script setup>
  import { reactive, onMounted } from 'vue'
  const obj = reactive({
    id: 1,
    name: 'xxx'
  })
</script>
<template>
  <div>
    <SubComponent :id="obj.id" :name="obj.name"></SubComponent>

    <!-- 简写 -->
    <SubComponent v-bind="obj"></SubComponent>
  </div>
</template>

# 解构Props

虽然props解构出来的参数不具有响应性,但Vue3.5+被解构出来的props依然可以被追踪到,父组件的数据被更改,传入的值依然会改变,所以可以直接使用解构来获取props了。并且通过解构还可以设置props的默认值。

<script setup>
  const { msg, count } = defineProps(['msg', 'count'])
  console.log(msg)

  // 设置默认值
  const { title = 'hello world' } = defineProps(['title'])
</script>

<template>
  <h4>{{ msg }}</h4>
  <h4>{{ count }}</h4>
</template>

# Props的使用和监听

Vue不允许直接修改传入的props参数,只能重新赋值进行使用。如果想监听某个prop的变化,可以使用getter函数或者computed计算属性。

<script setup>
  const props = defineProps(['msg', 'count'])
  // 需要通过getter 函数返回,不能直接监听
  watch(() => props.count, (newVal, oldVal) => {
    console.log(newVal, oldVal)
  })

  // 解构用法
  const { msg, count } = defineProps(['msg', 'count'])
  watch(() => count, (newVal, oldVal) => {
    console.log(newVal, oldVal)
  })

  // 计算属性更改
  const msgVal = computed(() => props.msg)
  const countVal = computed(() => count)
  console.log(msgVal.value)
  console.log(countVal.value)
</script>

# 暴露属性与方法

Vue2中可以通过$parent$children方法父子组件实例上的方法和属性,Vue3中关闭此操作,只能通过defineExpose编译宏进行手动暴露,然后在父组件中通过模板ref进行获取。

<!-- 子组件 -->
<script setup>
import { ref } from 'vue'
const count = ref<number>(12)
const getName = () => {
  console.log(111)
}

defineExpose({
  a: count, // 属性
  getName // 方法
})
</script>

父组件使用:

<script setup lang="ts">
import { ref } from 'vue'
import SubItem from './components/SubItem.vue'

const refVue = ref<InstanceType<typeof SubItem>>()

console.log(refVue.value) // { a: count, getName }
</script>

<template>
  <SubItem ref="refVue" msg="You did it!" />
</template>

# 自定义事件

自定义事件,需要通过 defineEmits() 宏来定义事件名后才能使用,在模板上可以直接使用$emit()来触发事件,在<script setup>中需要赋值给一个变量来调用。为了统一,都通过变量调用事件,不要直接在html上使用。

<!-- 子组件 -->
<script setup>
  // 数组写法
  defineEmits(['inFocus', 'submit'])

  // 对象写法,可以对函数进行校验
  const emit = defineEmits({
    submit(payload) {
      // 通过返回值为 `true` 还是为 `false` 来判断
      // 验证是否通过
    }
  })
</script>

事件传递通用写法:

<!-- 子组件 -->
<script setup>
  // 赋值给一个变量使用
  const emit = defineEmits(['CLOSE_DIALOG_EVENT'])

  function buttonClick() {
    emit('CLOSE_DIALOG_EVENT', { msg: 'xxx' })
  }
</script>
<template>
  <button @click="buttonClick">关闭</button>
</template>


<!-- 父组件 -->
<template>
  <div>
    <SubComponent @CLOSE_DIALOG_EVENT="closeDialog"></SubComponent>
  </div>
</template>

# 中央事件主线

Vue2 通过再创建一个Vue实例当做桥梁来进行组件之间的相互传参,Vue3也不再支持此方法,我们通过引入 mitt 插件来实现兄弟组件之间的事件传递。

安装:

npm install --save mitt

在main.ts中引入:

// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import './assets/main.css'

import mitt from 'mitt'

const emitter = mitt()
// TS注册
// 必须要扩展ComponentCustomProperties类型才能获得类型提示
declare module 'vue' {
  export interface ComponentCustomProperties {
    $Bus: typeof emitter
  }
}

const app = createApp(App)

app.use(createPinia())
app.use(router)
// 挂载到全局属性上
app.config.globalProperties.$Bus = emitter

app.mount('#app')

组件中使用:

<script setup lang="ts">
import { ref, getCurrentInstance } from 'vue'

// 获取vue实例
const instance = getCurrentInstance()

// 监听一个
instance?.proxy?.$Bus.emit('GET_EMITE_EVENT', { a: 1, b: 2})
instance?.proxy?.$Bus.on('GET_EMITE_EVENT', (params) => {
  console.log(params) // { a: 1, b: 2}
})

// 监听多个
instance?.proxy?.$Bus.emit('GET_ID_EVENT1', 1)
instance?.proxy?.$Bus.emit('GET_ID_EVENT2', 2)
instance?.proxy?.$Bus.on('*', (type, params) => {
  console.log(type) // GET_ID_EVENT1/GET_ID_EVENT2
  console.log(params) // 1/2
})

// 清除
instance?.proxy?.$Bus.emit('SEND_MSG_EVENT', 'xxx')
const sendMsg = (msg: string) => {
  console.log(msg)
}
instance?.proxy?.$Bus.on('SEND_MSG_EVENT', sendMsg)
instance?.proxy?.$Bus.off('SEND_MSG_EVENT', sendMsg)

// 清除全部
instance?.proxy?.$Bus.all.clear()
</script>

# 组件使用v-model

3.4+版本使用方式进行了更改,不再使用 modelValue 与 update:modelValue 进行双向绑定,直接使用defineModel宏。

Vue2组件实现 v-model 双向绑定通过 model 字段来定义传入的props和事件名,以此来实现组件上数据的双向绑定,Vue3中不需要使用 model 字段,直接使用 defineModel 宏接收就可以了,接收的值就是v-model的值,直接更改这个值就自动更新父组件的值,它是modelValueupdate:modelValue的简写。

<!-- // 子组件获取和提交 -->
<script setup>
// defineModel宏会自动接收v-model的值
const msgModel = defineModel()

// 更改v-model值
const updateValue = () => {
  msgModel.value = 'hello world'
}
</script>

<template>
  <input v-model="msgModel" />
  <button @click="updateValue">双向绑定</button>
</template>

<!-- // 父组件传入 -->
<script setup lang="ts">
import { ref } from 'vue'
import TheWelcome from '../components/TheWelcome.vue'

const msg = ref('xxxxx')
</script>

<template>
  <main>
    <p>{{ msg }}</p>
    <TheWelcome v-model="msg" />
  </main>
</template>

# 自定义参数名

自定义参数名,直接在v-model后面绑定对应的名称就可以了,并且可以同时自定义多个双向绑定。

<!-- // 子组件 -->
<script setup>
// 接收多个v-model
const title = defineModel('title')
const id = defineModel('id')

const bindValue = () => {
  title.value = 'hello world'
  id.value = 2
}
</script>

<template>
  <button @click="bindValue">双向绑定</button>
</template>

<!-- // 父组件 -->
<script setup lang="ts">
import { ref } from 'vue'
import TheWelcome from '../components/TheWelcome.vue'

const msg = ref('xxxxx')
const id = ref(1)
</script>

<template>
  <main>
    <p>{{ msg }}</p>
    <TheWelcome v-model:title="msg" v-model:id="id" />
  </main>
</template>

# 自定义修饰符

v-model后面可以跟一些修饰符,比如 .trim .lazy等,我们也可以自定义修饰符,比如定义一个capitalize修饰符,让传入的值第一个字符串变为大写。

<MyComponent v-model.capitalize="myText" />

组件的 v-model 上所添加的修饰符,可以通过defineModel接收的第二个参数进行获取。

<script setup>
const [myText, modifiers] = defineModel()

console.log(modifiers) // { capitalize: true }
</script>

<template>
  <input type="text" v-model="myText" />
</template>

所以根据第二个参数 modifiers 返回的对象,我们给defineModel传入一个对象,通过get和set两个选项就可以对这个修饰符判断进行逻辑处理。

<MyComponent v-model.capitalize="myText" />

<script setup>
const [myText, modifiers] = defineModel({
  set(value) {
    if (modifiers.capitalize) {
      return value.charAt(0).toUpperCase() + value.slice(1)
    }
    return value
  }
})
</script>

<template>
  <input type="text" v-model="myText" />
</template>

带参数的修饰符

<MyComponent v-model:title.capitalize="myText" v-model:id="userId"/>

# 透传 Attributes

透传属性就是在自定义组件上绑定的属性(除被用过的propsemits传值外的其他属性,没有被用的不算),如classstyleid 或绑定的事件都会被子组件内的元素所继承。

透传过去的属性,对于 classstyle 会和子组件内的元素进行合并,v-on绑定的事件透传过去会被触发,内部元素也绑了事件则都会被触发。

如果子组件又直接套了个组件,没有其他元素,透传属性会直接继承到最里一层的组件。

<MyButton class="large" @click="onClick" />

<!-- 子组件 -->
<button class="large" @click="onClick"></button>

# 获取透传属性

在模板上可以直接通过 $attrs 获取,在 <script setup> 中需要通过 useAttrs() 方法进行获取。

<script setup>
import { useAttrs } from 'vue'
const attrs = useAttrs()
console.log(attrs)
</script>

<template>
  <div>
    <!-- DOM上直接通过$attrs获取 -->
    <span>attribute: {{ $attrs }}</span>
  </div>
</template>

只有一个根节点时透传属性会默认作用到组件的根节点身上,如果想要左右在组件里的某个元素上,我们可以使用v-bind绑在某个元素身上。

<template>
  <div class="box">
    <button v-bind="$attrs">Click Me</button>
  </div>
</template>

如果有多个根节点,要使用v-bind进行绑定某个根节点,否则透传会报错。

<template>
  <header v-bind="$attrs"></header>
  <main></main>
  <footer></footer>
</template>

# 禁用透传属性

<script setup>
defineOptions({
  inheritAttrs: false
})
</script>

# 插槽

插槽通过 <slot> 来分发内容,给一个占位符,在指定的位置添加特殊的元素,而不是在组件创建时就固定好内容,允许在不同的地方调用组件时添加定制化的内容。

<!-- FancyButton -->
<template>
  <div>
    <slot></slot>
  </div>
</template>


<!-- 父组件 -->
<script setup>
import { ref } from 'vue'
import FancyButton from './FancyButton.vue'

const msg = ref('xxx')

</script>

<template>
  <div>
    <FancyButton>
      <span>我是插槽内容:{{msg}}</span>
    </FancyButton>
  </div>
</template>

在父组件中添加就只能获取父组件的作用域,无法获取插槽所在子组件的作用域。

# 默认内容

slot 内添加默认的内容,父组件添加内容就不显示。

<template>
  <button type="submit">
    <slot>
      Submit <!-- 默认内容 -->
    </slot>
  </button>
</template>

# 具名插槽(多个插槽)

添加多个插槽时,要为每个插槽指定一个特殊属性 name

<template>
  <div class="container">
    <header>
      <slot name="header"></slot>
    </header>
    <main>
      <!-- 默认为 default -->
      <slot></slot> 
    </main>
    <footer>
      <slot name="footer"></slot>
    </footer>
  </div>
</template>

父组件添加内容,使用 v-slot 指定对应插槽名,并且只能添加在 <template> 上,default可以不用写,没有被具名插槽包裹的内容全部视为默认插槽内容。

<template>
  <BaseLayout>
    <template v-slot:header>
      <h1>我是头部插槽内容</h1>
    </template>

    <template v-slot:default>
      <p>我是默认插槽内容</p>
      <p>111111</p>
    </template>

    <template v-slot:footer>
      <p>我是尾部插槽内容</p>
    </template>
  </BaseLayout>
</template>

具名插槽 v-slot 可以简写成 #,因此 <template v-slot:header> 可以简写为 <template #header>

<template>  
  <BaseLayout>
    <template #header>
      <h1>我是头部插槽内容</h1>
    </template>

    <template #default>
      <p>我是默认插槽内容</p>
      <p>111111</p>
    </template>

    <template #footer>
      <p>我是尾部插槽内容</p>
    </template>
  </BaseLayout>
</template>

# 条件插槽

我们可以使用 $slots 来获取父组件传入的插槽进行逻辑判断。

<template>
  <div class="container">
    <header v-if="$slots.header">
      <slot name="header"></slot>
    </header>

    <main v-if="$slots.default">
      <slot></slot> 
    </main>

    <footer v-if="$slots.footer">
      <slot name="footer"></slot>
    </footer>
  </div>
</template>

# 动态插槽

<template>
  <BaseLayout>
    <template v-slot:[dynamicSlotName]>
      ...
    </template>

    <!-- 缩写为 -->
    <template #[dynamicSlotName]>
      ...
    </template>
  </BaseLayout>
</template>

# 作用域插槽

在父组件中获取子组件中的数据,可以在子组件 <slot> 上绑定子组件数据,传给父组件的 <template> 上,这样在父组件定义插槽内容就可以获取子组件的内容了。默认插槽可以通过v-slot指令接收,具名插槽直接使用插槽名进行接收。

<!-- MyComponent 子组件 -->
<script setup>
import { reactive } from 'vue'
const user = reactive({
  name: 'xxx',
  age: 18
})
</script>
<template>
  <span>
    <!-- 子组件就相当于props把值传入到父组件中 -->
    <slot v-bind:user="user"> {{ user.lastName }} </slot>
    <!-- 简写 -->
    <slot :user="user"> {{ user.lastName }} </slot>
  </span>
  <span>
    <slot name="footer" :user="user"></slot>
  </span>
</template>

<!-- 父组件使用 -->
<!-- 定义一个slotProps的props对象 -->
<template>
  <MyComponent>
    <!-- 默认插槽 -->
    <template v-slot="slotProps">
      {{ slotProps.user }}
    </template>
    <template #default="slotProps">
      {{ slotProps.user }}
    </template>
    <!-- 具名插槽 -->
    <template #footer="slotProps">
      {{ slotProps.user }}
    </template>

    <!-- 使用ES6解构 -->
    <template #default="{ user }">
      {{ user }}
    </template>
    <template #footer="{ user }">
      {{ user }}
    </template>
  </MyComponent>
</template>

# 插槽复用

在一个循环列表里定义一个具名插槽,可以实现循环来使用同一个插槽。

<ul>
  <li v-for="todo in todos" :key="todo.id" >
    <!-- 我们为每个 todo 准备了一个插槽, 将 `todo` 对象作为一个插槽的 prop 传入。 -->
    <slot name="todo" :todo="todo">
      <!-- 默认内容 -->
      {{ todo.text }}
    </slot>
  </li>
</ul>

使用:

<todo-list :todos="todos">
  <template #todo="{ todo }">
    <span v-if="todo.isComplete"></span>
    {{ todo.text }}
  </template>
</todo-list>

# 依赖注入

props嵌套太多,逐级传入的话太麻烦,使用provideinject 进行依赖注入,父组件使用 provide 提供依赖,任何层级子组件都可以使用 inject 获取注入数据。

父组件提供依赖,可以同时提供多个。

<script setup>
import { ref, reactive, provide } from 'vue'
const text = ref('text')
const msg = ref('xxx')
const count = ref(123)
const user = reactive({
  name: 'xxx',
  age: 18
})
const getData = () => {
  console.log(111)
}

// 注入变量
provide('message', msg)
provide('user', user)
// 注入只读属性
provide('count', readonly(count))
provide('text', text)

// 注入函数
provide('getData', getData)
</script>

子组件进行接收

<script setup>
import { inject } from 'vue'

// 接收属性
const message = inject('message')
const user = inject('user')
const count = inject('count')
// 默认值
const text = inject('text', '我是默认值')
// 接收函数
const getData = inject('getData')

// 默认执行函数,使用工厂函数来创建默认值,true表示默认值应该被当作一个工厂函数。
const value = inject('key', () => new ExpensiveClass(), true)

</script>
<template>
  <button @click="getData">{{ location }}</button>
</template>

应用层 Provide,提供一个全局依赖,任何组件都能获取到注入。

import { createApp } from 'vue'

const app = createApp({})

app.provide('message', 'hello!')

# 递归组件

Vue3中组件没有了name属性,组件引入后直接使用就行,那递归组件也是同样如此,文件名是什么,就直接使用递归组件的文件名作为组件进行循环递归。如果不想直接使用文件名,我们也可以自定义一个名字使用,就是再使用一个 script 标签导出名称,或者使用官方提供的插件 unplugin-vue-define-options

父组件:

<script setup>
import { ref, reactive } from 'vue'

import TreeVue from '@/components/Tree.vue'

// 树形数据
interface Tree {
  title: string,
  checked: boolean,
  children?: Tree[]
}
const treeData = reactive<Tree[]>([
  {
    title: '文件1-1',
    checked: false,
    children: [
      {
        title: '文件2-1',
        checked: false,
        children: []
      }
    ]
  },
  {
    title: '文件1-2',
    checked: false,
    children: []
  }
])
</script>
<template>
  <TreeVue :treeData="treeData"></TreeVue>
</template>

递归组件:

<!-- Tree 组件 -->
<script setup lang="ts">
interface Tree {
  title: string,
  checked: boolean,
  children?: Tree[]
}
const props = defineProps<{
  treeData: Tree[]
}>()
</script>

<!-- 组件重命名 -->
<script lang="ts">
export default {
  name: 'TreeItem'
}
</script>

<template>
  <div class="tree" v-for="(item, index) in treeData" :key="index">
    <input v-model="item.checked" type="checkbox"> <span>{{ item.title }}</span>
    <TreeItem v-if="item?.children?.length" :treeData="item?.children"></TreeItem>
  </div>
</template>

<style scoped>
.tree {
  width: 100%;
  margin-left: 20px;
}
</style>

# 缓存组件

使用 <KeepAlive> 缓存组件或者路由。

<script setup>
import { onActivated, onDeactivated } from 'vue'

// 生命周期
onActivated(() => {
  // 调用时机为首次挂载
  // 以及每次从缓存中被重新插入时
})

onDeactivated(() => {
  // 在从 DOM 上移除、进入缓存
  // 以及组件卸载时调用
})
</script>
<template>
  <!-- 非活跃的组件将会被缓存! -->
  <KeepAlive>
    <component :is="activeComponent" />
  </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>
</template>

在组合式API中组件会自动以文件名为该组件的 name,KeepAlive使用 include 去找组件名时可能和我们在路由中定义的名称不一样,导致无法缓存,所以要在需要缓存的组件中自定义组件命才能使用。

# 自定义组件名

自定义组件名可以向上面一样重新定义一个 script 或者使用插件 vite-plugin-vue-setup-extend,一般我们使用 vite-plugin-vue-setup-extend 插件来定义页面和组件,直接在 script 标签上定义 name 更容易进行区分。

# 安装
npm i vite-plugin-vue-setup-extend -D

配置vite.config.ts

import { defineConfig } from 'vite'
import VueSetupExtend from 'vite-plugin-vue-setup-extend'
export default defineConfig({
  plugins: [ VueSetupExtend() ]
})

使用

<script setup lang="ts" name="Home">

</script>

# 组合式函数

把相应的逻辑函数抽离成一个公共函数达到逻辑复用,然后引入使用,既可以在其他组件中使用,也可以引入其他组合式函数,这也就是Vue3使用基础js进行编程的好处,以最基础的js函数进行抽离,达到逻辑复用,回归本质。通过组合式函数,我们可以实现自定义hook。

按照惯例,组合式函数约定用驼峰命名法命名,并以“use”作为开头。返回值约定始终返回一个包含多个 ref 的普通对象,而不是 reactive 响应式对象,这样在获取是可以解构出响应的ref数据。

// mouse.js
// 组合式函数也可以引入使用声明周期
import { ref, onMounted, onUnmounted } from 'vue'

// 按照惯例,组合式函数名以“use”开头
export function useMouse() {
  // 被组合式函数封装和管理的状态
  const x = ref(0)
  const y = ref(0)

  // 组合式函数可以随时更改其状态。
  function update(event) {
    x.value = event.pageX
    y.value = event.pageY
  }

  // 一个组合式函数也可以挂靠在所属组件的生命周期上
  // 来启动和卸载副作用
  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  // 通过返回值暴露所管理的状态
  return { x, y }
}

在组件中引入使用:

<script setup>
import { useMouse } from './mouse.js'

const { x, y } = useMouse()
</script>

<template>Mouse position is at: {{ x }}, {{ y }}</template>

再比如封装一个请求函数,传入url请求地址返回成功和错误状态。并且还可以用 watchEffect()toValue() 搭配实现url更改后立即就调用。

// fetch.js
import { ref, watchEffect, toValue } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)

  const fetchData = () => {
    data.value = null
    error.value = null

    fetch(toValue(url))
      .then((res) => res.json())
      .then((json) => (data.value = json))
      .catch((err) => (error.value = err))
  }

  // 在watchEffect中执行
  watchEffect(() => {
    fetchData()
  })

  return { data, error }
}

在组件中重复使用:

<script setup>
import { useFetch } from './fetch.js'

const url = ref('/initial-url')
const { data, error } = useFetch(url)
// 重新触发 fetch
url.value = '/new-url'
</script>

传入参数处理:

封装组合式函数时,传入的参数可能是一个响应式数据,也可能是一个普通值,也可能是一个函数返回值,我们可以使用 toValue() 来处理传入的参数,将值、refs 或 getters 规范化为值。如果参数是 ref,它会返回 ref 的值;如果参数是函数,它会调用函数并返回其返回值。否则,它会原样返回参数。

import { toValue } from 'vue'

function useFeature(maybeRefOrGetter) {
  // 如果 maybeRefOrGetter 是一个 ref 或 getter,
  // 将返回它的规范化值。
  // 否则原样返回。
  const value = toValue(maybeRefOrGetter)
}

通过抽取组合式函数改善代码结构:

<script setup>
import { useFeatureA } from './featureA.js'
import { useFeatureB } from './featureB.js'
import { useFeatureC } from './featureC.js'

const { foo, bar } = useFeatureA()
const { baz } = useFeatureB(foo)
const { qux } = useFeatureC(baz)
</script>

# 自定义指令

Vue3在<script setup>中注册局部指令时,任何以 v 开头的驼峰式命名的变量都可以被用作一个自定义指令,注意是变量,一般定义为一个对象。而全局注册还是通过 directive 进行。

<script setup>
// 在模板中启用 v-focus
const vFocus = {
  mounted: (el) => el.focus()
}
</script>

<template>
  <input v-focus />
</template>

全局注册:

const app = createApp({})

// 使 v-focus 在所有组件中都可用
app.directive('focus', {
  /* ... */
})

全局注册多个:

在index.ts中引入指令文件,通过插件统一进行注册,最后在main.ts中使用。

自定义一个指令

// directive/modules/index.ts
/**
 * 自动获取焦点指令
 * @description 打开页面时输入表单自动获取焦点
 * @author changz
 * @param {xxx} - xxx
 * @example
 * <a-input v-focus></a-input>
 * */

import type { DirectiveBinding, VNode } from 'vue'

export default {
  mounted(el: HTMLInputElement, binding: DirectiveBinding, vnode: VNode) {
    if (el.tagName === 'input' || el.tagName === 'textarea') {
      el.focus()
    } else {
      const inputDom = el.querySelector('input') ?? el.querySelector('textarea')
      inputDom?.focus()
    }
  }
}

在index.ts引入指令,通过插件获取到Vue进行注册,然后在main中引入使用该插件。

// directive/index.ts
/**
 * @description 定义多个全局自定义指令
 * @author changz
 * @example 在main.js引入之后use挂载
 * */

import type { App, Directive } from 'vue'
// 导入自定义指令
import focus from './modules/focus'
import watermark from './modules/watermark'
import permission from './modules/permission'

export default {
  install(Vue: App) {
    Vue.directive('focus', focus as Directive)
    Vue.directive('watermark', watermark as Directive)
    Vue.directive('permission', permission as Directive)
  }
}
// main.ts
import { createApp } from 'vue'

import App from './App.vue'
import router from './router'
import store from './stores'
import directive from './directive'

const app = createApp(App)

app.use(store)
app.use(router)
app.use(directive)
app.mount('#app')

钩子函数:

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

钩子参数:

<script setup>
import { ref } from 'vue'
const arg = ref('msg')

// 在模板中启用 v-example
const vExample = {
  mounted(el, binding, vnode, prevVnode) {
    // el:指令绑定到的元素。这可以用于直接操作 DOM。
    console.log(el) // <div></div>

    // binding:一个对象,包含以下属性。
      // 传递给指令的值。
      console.log(binding.value) // 例如在 v-example:foo="2" 中,值是 2。
      // 之前的值,仅在 beforeUpdate 和 updated 中可用。
      console.log(binding.oldValue) // 无论值是否更改,它都可用。
      // 传递给指令的参数 (如果有的话)。
      console.log(binding.arg) // 例如在 v-example:foo 中,参数是 "foo"。
      // 一个包含修饰符的对象 (如果有的话)。
      console.log(binding.modifiers) // 例如在 v-example:foo 中,修饰符对象是 { foo: true }。
      // 使用该指令的组件实例。
      console.log(binding.instance)
      // 指令的定义对象。
      console.log(binding.dir)

    // vnode:代表绑定元素的底层 VNode。
    console.log(vnode)
    // prevNode:代表之前的渲染中指令所绑定元素的 VNode。仅在 beforeUpdate 和 updated 钩子中可用。
    console.log(prevNode)
  }
}
</script>

<template>
  <div v-example:foo="2"></div>

  <!-- 动态指令 -->
  <div v-example:[arg]="3"></div>

  <!-- 多个参数 -->
  <div v-demo="{ color: 'white', text: 'hello!' }"></div>
</template>

# 插件

Vue的插件为整个Vue实例添加全局功能,可以添加全局方法、属性、全局组件或自定义指令。编写一个对象,通过提供一个 install() 方法注册,然后使用 use 来使用和安装install提供的实例和参数。

定义一个插件:

// plugins/example.js
const myPlugin = {
  install(app, options) {
    // app vue实例
    console.log(app)
    // options use传入的参数
    console.log(options)

    // 配置此应用
    // 注入一个全局可用的方法
    app.config.globalProperties.$translate = (params) => {
      console.log(params)
    }
    // 注入一个全局属性
    app.config.globalProperties.attrNmae = 'xxx'

    // 注册一个全局组件
    app.component('MyComponent', {
      data() {
        return {
          count: 0
        }
      },
      template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
    })
    
    // 注册一个全局指令
    app.directive('focus', {
      /* ... */
    })

    // 提供一个全局依赖注入
    app.provide('example', options)
  }
}

安装插件:

import { createApp } from 'vue'

const app = createApp({})

app.use(myPlugin, {
  /* 可选的选项 */
})

# 过渡效果

Vue3过渡效果代替使用的组件分别是 <Transition><TransitionGroup> 其使用方法和Vue2相同。Transition内只能使用一个根元素,TransitionGroup用于多个元素。

<template>
  <button @click="show = !show">Toggle</button>
  <Transition>
    <p v-if="show">hello</p>
  </Transition>
</template>

<style>
/* 进入时开始和结束的css样式 */
.v-enter-from {}
.v-enter-to {}

/* 进入时开始到结束的过渡效果 */
.v-enter-active {
  transition: all 0.5s ease;
}

/* 离开时开始和结束的css样式 */
.v-leave-from {}
.v-leave-to {}

/* 离开时开始到结束的过渡效果 */
.v-leave-active {
  transition: opacity 0.5s ease;
}

/* 使用动画 */
.bounce-enter-active {
  animation: bounce-in 0.5s;
}
.bounce-leave-active {
  animation: bounce-in 0.5s reverse;
}
@keyframes bounce-in {
  0% {
    transform: scale(0);
  }
  50% {
    transform: scale(1.25);
  }
  100% {
    transform: scale(1);
  }
}
</style>

使用动画:

<template>
  <Transition name="bounce">
    <p v-if="show" style="text-align: center;">
      Hello here is some bouncy text!
    </p>
  </Transition>
</template>

<style>
/* 使用动画 */
.bounce-enter-active {
  animation: bounce-in 0.5s;
}
.bounce-leave-active {
  animation: bounce-in 0.5s reverse;
}
@keyframes bounce-in {
  0% {
    transform: scale(0);
  }
  50% {
    transform: scale(1.25);
  }
  100% {
    transform: scale(1);
  }
}
</style>

自定义类名,或者使用animate.css的样式:

  • enter-from-class
  • enter-to-class
  • enter-active-class
  • leave-from-class
  • leave-to-class
  • leave-active-class
<template>
  <!-- 假设你已经在页面中引入了 Animate.css -->
  <Transition
    name="custom-classes"
    enter-active-class="animate__animated animate__tada"
    leave-active-class="animate__animated animate__bounceOutRight"
  >
    <p v-if="show">hello</p>
  </Transition>
</template>

列表动画 TransitionGroup

<template>
  <TransitionGroup name="list" tag="ul">
    <li v-for="item in items" :key="item">
      {{ item }}
    </li>
  </TransitionGroup>
</template>

<style>
.list-enter-active,
.list-leave-active {
  transition: all 0.5s ease;
}
.list-enter-from,
.list-leave-to {
  opacity: 0;
  transform: translateX(30px);
}
</style>

# Teleport

使用 <Teleport> 将其内部的组件或DOM元素挂载到其他元素上去。其所处的环境还是在当前的组件里,不会影响组件间的任何逻辑关系,只是DOM元素是显示在被挂载的元素里。

比如一个modal弹窗,使用是组件中控制显示隐藏,但我们需要让他针对全窗口显示,只需要使用 <Teleport> 把它放到body上就行了。

<template>
  <button @click="open = true">Open Modal</button>

  <Teleport to="body">
    <div class="modal-mask" v-if="open">
      <div class="modal-container" v-if="open">
        <p>Hello from the modal!</p>
        <button @click="open = false">Close</button>
      </div>
    </div>
  </Teleport>
</template>

<style>
.modal-mask {
  position: fixed;
  z-index: 9998;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  transition: opacity 0.3s ease;
}

.modal-container {
  width: 300px;
  margin: auto;
  padding: 20px 30px;
  background-color: #fff;
  border-radius: 2px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
  transition: all 0.3s ease;
}
</style>

# Suspense

TIP

Suspense是一项实验性功能。它不一定会最终成为稳定功能,并且在稳定之前相关 API 也可能会发生变化。

<Suspense> 是一个内置组件,获取其中整个组件树中所有异步(异步请求或异步组件)的处理结果,并根据这个结果来渲染加载状态。比如一个页面布局有三个组件,每个组件中都请求加载数据,加载时都会有加载动画在不同的时候加载完成,这样就很不美观。使用Suspense就会解决这种问题,只显示一个加载状态,就像 Promise.all() 等待所有加载请求完。

<script setup>中需要使用await让组件自动成为一个异步依赖。

<script setup>
const res = await fetch(...)
const posts = await res.json()
</script>

<template>
  {{ posts }}
</template>

加载中状态:

<template>
  <Suspense>
    <!-- 具有深层异步依赖的组件 -->
    <Dashboard />

    <!-- 在 #fallback 插槽中显示 “正在加载中” -->
    <template #fallback>
      Loading...
    </template>
  </Suspense>
</template>

# 渲染函数 & JSX

Vue 提供了一个 h() 函数用于创建 vnodes:

import { h } from 'vue'

const vnode = h(
  'div', // type
  { id: 'foo', class: 'bar' }, // props
  [
    /* children */
  ]
)

# 组合式API结合TS

# 为ref / reactive标注类型

使用ref定义时,一般ts会自动推导类型。

<script setup lang="ts">
import { ref } from 'vue'
import type { Ref } from 'vue'
const title = ref<string>('首页')
const status: Ref<undefined | number> = ref('2020')

// 简写
const title = ref('首页')
const status = ref<undefined | number>(0)

// 不给初始值会自动转为包含 undefined 的联合类型
// 推导得到的类型:Ref<number | undefined>
const status = ref<number>()
</script>

在定义reactive对象时,一般ts会自动推导类型,但在使用时,比如赋值或者传参可能无法识别类型,所以最好在创建时为其进行类型标注。

<script setup lang="ts">
import { reactive } from 'vue'
interface StateData {
  name: string;
  count: number;
  type: undefined | '';
  date?: string;
  [key: string]: any
}
const state = reactive<StateData>({
  name: 'xxx',
  count: 0,
  type: undefined,
  data: '2023-01-01',
  status: 0
})

const tableData = reactive<StateData[]>([])
tableData.push(state)
</script>

# 为computed标注类型

与ref类似,一般简单类型computed都会自动推导出,我们可以为其定义类型。

const double = computed<number>(() => {
  // 若返回值不是 number 类型则会报错
})

# 为props标注类型

props使用ts类型进行标注时传入一个泛型对象,只需要标注其类型就可以,基于类型声明,也可以通过一个接口单独声明。

<script setup lang="ts">
  // 传入泛型进行声明
  const { title, bar } = defineProps<{
    title: string
    bar?: number
  }>()

  // 声明props接口
  interface Props {
    foo: string
    bar?: number
  }
  const { title, bar } = defineProps<Props>()
</script>

如果需要设置默认值,3.4及更低版本需要使用自定义宏 withDefaults 传入第二个参数来支持,3.4+可以通过响应式解构来添加默认值。

<script setup lang="ts">
  // 低版本定义默认值
  const props = withDefaults(defineProps<{
    msg: string
    arr: string[]
  }>(), {
    msg: 'hello',
    arr: () => []
  })

  // 接口写法
  interface Props {
    msg: string
    arr: string[]
  }
  const props = withDefaults(defineProps<Props>(), {
    msg: 'hello',
    arr: () => ['one', 'two']
  })


  // 3.4+定义默认值
  const { msg = 'hello', arr = ['one', 'two'] } = defineProps<{
    msg: string
    arr: string[]
  }>()
  const { msg = 'hello', arr = ['one', 'two'] } = defineProps<Props>()
</script>

# 为事件标注类型

通过一个函数给 defineEmits 传入一个对象来进行类型校验。

<!-- 子组件 -->
<script setup lang="ts">
import {  reactive, ref, getCurrentInstance } from 'vue'
interface MsgData {
  name: string
  id: number
}
const formData = reactive<MsgData>({
  name: '',
  id: 0
})
const emit = defineEmits<{
  'sendSome': [], // 什么都不传
  'sendMsg': [id: number], // 传递一个参数
  'sendMulMsg': [id: number, name: string], // 传递多个参数
  'sendObjMsg': [obj: MsgData], // 传递一个对象
  'sendOptMsg': [id?: number], // 可选参数
}>()

const sendEvent = () => {
  emit('sendMsg', 1)
  emit('sendMulMsg', 1, 'test')
  emit('sendObjMsg', formData)
  emit('sendOptMsg')
}

// 低版本写法
const emit = defineEmits<{
  (e: 'change', id: number): void
  (e: 'update', value: string): void
}>()
</script>

父组件接收:

<template>
  <div class="index">
    <!-- 子组件 -->
    <Child @sendMsg="getMsg" @sendMulMsg="getMulMsg" @sendObjMsg="getObjMsg" @sendOptMsg="getOptMsg"></Child>
  </div>
</template>
<script setup lang="ts">
  const getMsg = (id: number) => {
    console.log(id)
  }
  const getMulMsg = (id: number, name: string) => {
    console.log(id)
    console.log(name)
  }
  const getObjMsg = (obj: any) => {
    console.log(obj)
  }
  const getOptMsg = () => {
    console.log(1111)
  }
</script>

# 为模板引用标注类型

ref模板引用标明类型:

<script setup lang="ts">
import { ref, useTemplateRef, onMounted } from 'vue'

// const inputRef = ref<HTMLInputElement | null>(null)
// useTemplateRef代替ref
const inputRef = useTemplateRef<HTMLInputElement>(null)

onMounted(() => {
  inputRef.value?.focus()
})
</script>

<template>
  <input ref="inputRef" />
</template>

为组件模板引用标明类型:

<script setup lang="ts">
import { ref, useTemplateRef, InstanceType, onMounted } from 'vue'
import type { ComponentPublicInstance } from 'vue'

import HelloWorld from './components/HelloWorld.vue'

// const worldRef = ref<InstanceType<typeof HelloWorld> | null>(null)
const worldRef = useTemplateRef<InstanceType<typeof HelloWorld>>(null)

// 或者使用ComponentPublicInstance
// const worldRef = ref<ComponentPublicInstance | null>(null)
const worldRef = useTemplateRef<ComponentPublicInstance | null>(null)
</script>

<template>
  <component is="HelloWorld" ref="worldRef" />
</template>

# 为 provide / inject 标注类型

provide 和 inject 通常会在不同的组件中运行。要正确地为注入的值标记类型,Vue 提供了一个 InjectionKey 接口,它是一个继承自 Symbol 的泛型类型,可以用来在提供者和消费者之间同步注入值的类型:

import { provide, inject } from 'vue'
import type { InjectionKey } from 'vue'

const key = Symbol() as InjectionKey<string>

provide(key, 'foo') // 若提供的是非字符串值会导致错误

const foo = inject(key) // foo 的类型:string | undefined

建议将注入 key 的类型放在一个单独的文件中,这样它就可以被多个组件导入。