likes
comments
collection
share

Vue3全家桶之—— Vue3 Composition API

作者站长头像
站长
· 阅读数 6

Vue3全家桶之—— Vue3 Composition API

环境准备

项目创建

Vite创建

使用Vite创建项目

Vite 需要 Node.js 版本 14.18+,16+。然而,有些模板需要依赖更高的 Node 版本才能正常运行,当你的包管理器发出警告时,请注意升级你的 Node 版本。

npm create vite@latest

yarn create vite

pnpm create vite

然后按照提示操作即可!这种方式创建的Vue3项目只默认集成了Vite,像RouterPinia都需要自己手动集成,如果需要就在Select a variant项选择Customize with create-vue,此时会按照create-vue来创建,根据你的需求选择需要哪些依赖。

➜ npm create vite@latest
✔ Project name: … vite-project
✔ Select a framework: › Vue
✔ Select a variant: › Customize with create-vue ↗
	.....

Vue脚手架【推荐】

执行命令,这一指令将会安装并执行 create-vue,它是 Vue 官方的项目脚手架工具。

npm init vue@latest

Vue.js - The Progressive JavaScript Framework

✔ Project name: … <your-project-name> # 项目名称
✔ Add TypeScript? … No / Yes # 添加ts
✔ Add JSX Support? … No / Yes # 添加jsx支持
✔ Add Vue Router for Single Page Application development? … No / Yes # 添加 router
✔ Add Pinia for state management? … No / Yes # 添加 Pinia
✔ Add Vitest for Unit testing? … No / Yes # 添加 vitest
✔ Add an End-to-End Testing Solution? … No / Cypress / Playwright # 添加端到端测试
✔ Add ESLint for code quality? … No / Yes # 添加Eslit
✔ Add Prettier for code formatting? … No / Yes # 添加Prettier

Scaffolding project in /Users/fanliu/Desktop/vue-project...

Done. Now run:

  cd vue-project
  npm install
  npm run format
  npm run dev

根据提示,进入项目目录安装依赖,执行命令npm run dev启动项目

Vue3全家桶之—— Vue3 Composition API

添加vue环境变量

在使用组件时报错

Vue3全家桶之—— Vue3 Composition API

新建一个环境变量文件,或者在env.d.ts文件添加vue文件申明

declare module '*.vue' {
  import { ComponentOptions } from 'vue'
  const componentOptions: ComponentOptions
  export default componentOptions
}

VsCode插件安装

安装volar插件支持Vue3开发,第一第二个都需要安装。

Vue3全家桶之—— Vue3 Composition API 如果安装了vetur插件,它是支持Vue2语法的,需要先将插件禁用,在写Vue2的时候可以打开。

Vue3全家桶之—— Vue3 Composition API

组合式 API:setup()

setup定义方式有两种,

setup函数模式

第一种script标签内定一个setup函数,在函数中返回的对象会暴露给模板和组件实例。

<script>
export default {
	setup() {
		let str = '1'
		
		return {
			str
		}
	}
}
</script>
<template>
	<span> {{str}} </span>
</template>

setup语法糖【推荐】

script标签上添加setup属性,再通过lang属性设置语言为ts

推荐使用ts来开发Vue3,在开发提示上更友好,尽管类型定义很麻烦。

<script setup lang="ts">
let str = '1'
</script>
<template>
	<span> {{str}} </span>
</template>

ref全家桶

ref()

ref()接收一个内部值,返回一个响应式的、可更改ref 对象,此对象只有一个指向其内部值的属性 .valueref()包装的数据需要通过.value的形式赋值

Vue2中,只有被data函数包裹起来的数据才会是响应式的,同理ref()函数也可以将变量变成响应式。

如果只是普通的通过let const定义的变量,当数据修改后是无法在视图上更新。

<template>
  <span> 姓名 {{ name }} </span>

  <span> 年龄 {{ age }} </span>
  <button @click="change">修改</button>
</template>

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

let name = 'xg' // 死数据,修改后无法更新视图
let age = ref(18) // 响应式数据 通过.value方式修改

function change() {
  name = '小鬼'
  age.value = 20
}
</script>

为 ref() 标注类型

还可以通过泛型方式约束ref数据类型,默认可以通过[[../TypeScript#类型推断|类型推断|]]推测出,也可以使用[[../TypeScript#泛型(Generics)|泛型]]。

<script setup lang="ts">
import { ref } from 'vue'
type Person = {
  name: string
  age: number
}
const person = ref<Person>({
  name: 'xg',
  age: 18
})
</script>

也可以通过Ref定义类型

<script setup lang="ts">
import { ref } from 'vue'
import type { Ref } from 'vue'
type Person = {
  name: string
  age: number
}
const person: Ref<Person> = ref({
  name: 'xg',
  age: 18
})

function change() {
  person.value.name = '小鬼'
  person.value.age = 20
}
</script>

isRef()

判断数据是否为一个ref对象

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

let name = 'xg' // 死数据,修改后无法更新视图
let age = ref(18) // 响应式数据 通过.value方式修改

console.log(isRef(age)) // true
console.log(isRef(name)) // false
</script>

shallowRef()

shallowRef 创建的数据只会做浅层响应式处理。也就是说,只有当数据值本身发生改变时,视图会更新,但如果对象的属性发生变化,视图不会更新。

当创建的值是引用类型的时候,才会发生浅层响应式,如果是基础类型则不会。原理就是在调用的时候,只是返回value,不会通过toReactive做响应处理。

<script setup lang="ts">
import { shallowRef } from 'vue'
const person = shallowRef({
  name: 'xg',
  age: 18
})
function change() {
  person.value.name = '泰裤辣' // ❌ 视图不会改变
  person.value = {
    name: '小鬼',
    age: 20
  }
}
</script>

和Ref一起使用

如果和Ref一起使用,shallowRef会被影响。

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

const person1 = shallowRef({
  name: 'xg'
})
const person2 = ref({
  name: 'ikun'
})

function change() {
	person1.value = {  // shallowRef 会被 ref影响 视图数据也会更新
    name: 'shallowRef'
  }
  person2.value.name = 'ref'
}
</script>

和Ref区别

  • ref数据是深层次的,无论哪一层数据改变都可以通过.value形式修改,修改后的数据可以同步更新到视图上。
  • shallowRef是浅层次的,只能通过修改整个对象去改变数据,如果通过.value形式是无法更新视图。
  • shallowRef不能和Ref同时使用,不然会影响shallowRef造成视图更新。

tirggerRef()

强制更新DOM。使用shallowRef不会处罚页面更新,加上tirggerRef()就可以。

其实 ref 的原理就是由 shallowRef + tirggerRef 组成,也会有 ref 和 shallowRef 同时使用时会影响 shallowRef 造成视图更新,因为 ref 内部会调用一次 tirggerRef

<script setup lang="ts">
import { shallowRef, triggerRef } from 'vue'
const person = shallowRef({
  name: 'xg',
  age: 18
})
function change() {
  person.value.name = '泰裤辣' 
	triggerRef(person)  // 强制更新视图
}
</script>

customRef()

创建一个自定义的 ref,通过回调函数接收tracktrigger,要求返回的对象里实现getset。也就是说将数据相应的收集、触发过程,交给我们手动实现,并在过程中执行一些操作。


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

function MyRef<T>(value: T) {
  return customRef((track, trigger) => {
    return {
      get() {
        track()
        return value
      },
      set(newValue) {
        value = newValue
        trigger()
      }
    }
  })
}

const person = MyRef({
  name: 'ikun'
})

function change() {
  person.value.name = 'customRef'
}
</script>

ref获取DOM

和2一样,ref支持获取到对应的DOM对象,在使用的时候DOM上的ref名称要和定义时保持一致。

<template>
  <span ref="spanRef">这是一个span</span>
  <button @click="getDOM">获取</button>
</template>

<script setup lang="ts">
import { ref } from 'vue'
const spanRef = ref<HTMLSpanElement>() // 要和DOM保持一致

function getDOM() {
  console.log('spanRef', spanRef.value?.innerText)  // spanRef 这是一个span
}
</script>

reactive全家桶

reactive()

用于创建相应式数据,返回一个对象的响应式代理,也就是使用Proxy,一般用来绑定复杂数据类型,比如数组、对象。

如果用ref创建数组、对象等复杂数据,其实源码里也是调用的reactive

<script setup lang="ts">
import { reactive } from 'vue'

const person = reactive({
  name: 'ikun',
  age: 18
})
</script>

reactive源码约束了数据类型

Vue3全家桶之—— Vue3 Composition API

不允许绑定基础数据类型,不然会报错

<script setup lang="ts">
import { reactive } from 'vue'

const person = reactive('inkun') // ❌ 类型“string”的参数不能赋给类型“object”的参数。
</script>

使用reactive修改数据,无须通过.value形式

import {reactive} from 'vue'

const person = reactive({
  name: 'ikun',
  age: 18
})

function change() {
  person.name = '🐔你太美'
}

为 reactive() 标注类型

reactive可以直接使用类型约束

不推荐使用 reactive() 的泛型参数,因为处理了深层次 ref 解包的返回值与泛型参数的类型不同。

<script setup lang="ts">
import { reactive } from 'vue'

interface Person {
  name: string
  age: number
}

const person: Person = reactive({
  name: 'ikun',
  age: 18
})

function change() {
  person.name = '🐔你太美'
}
</script>

异步赋值数组

在异步情况下,给数组赋值如果是全覆盖会出现proxy丢失问题,造成数据不是相应式的。

<script setup lang="ts">
import { reactive } from 'vue'
let person = reactive<string[]>([])

setTimeout(() => {
  person = ['xg', 'ly', 'hcy', 'jys'] // 模拟后台返回数据
  console.log('person', person)
}, 2000)
</script>

打印出来的数据没有Proxy代理,而且页面也没有更新。是因为新的数组将reactive定义的数组完全给覆盖掉了,导致Proxy也丢失。

Vue3全家桶之—— Vue3 Composition API

解决办法

使用解构赋值 + push,在不破坏原数组的基础上添加新的数据

<script setup lang="ts">
import { reactive } from 'vue'
let person = reactive<string[]>([])

setTimeout(() => {
  const list = ['xg', 'ly', 'hcy', 'jys']  // 模拟后台返回数据
  person.push(...list)
  console.log('person', person)
}, 2000)
</script>

Vue3全家桶之—— Vue3 Composition API

和ref区别

reactiveref两都用于创建响应式数据,它们有以下几个主要区别:

  • 数据类型reactive函数接收复杂类型Array、Object、Map、Set,并将其转换为响应式对象,并且对象中所有属性都将变成响应式的。ref函数可以接收任意类型,并将其包装在一个特殊的响应式引用对象中,响应式引用对象只有一个.value属性,该属性包含实际的值。
  • 访问属性reactive创建的响应式对象可以直接访问其属性,就像访问普通对象一样,不需要额外的.value。而ref创建的响应式引用对象必须通过.value属性来访问修改。

shallowReactive()

shallowRef一样,也是将数据浅层响应式处理,如果是深层次数据只会改变值,不会更新视图。

<script setup lang="ts">
import { shallowReactive } from 'vue'
const person = shallowReactive({
  singer: {
    name: 'hcy'
  },
  dancer: {
    name: 'ikun'
  },
})

function edit() {
  person.dancer.name = '🐔你太美' // 数据变了 但是视图没有更新
  console.log('person', person)
}
</script>

这里的响应式处理只到第一层,也就是singer这一层,如果修改第一层数据还是会触发视图更新。

<script setup lang="ts">
import { shallowReactive } from 'vue'
const person = shallowReactive({
  singer: {
    name: 'hcy'
  },
  dancer: {
    name: 'ikun'
  },
  num: 1
})

function edit() {
  person.dancer.name = '🐔你太美' // 视图也会更新
  person.num = 2 // 修改第一层
  console.log('person', person)
}
</script>

和reactive一起使用

它同样会有shallowRef的问题,如果一起使用会影响数据变化

<template>
  <span>shallowReactive{{ person }}</span>
  <br />
  <span>reactive{{ person2 }}</span>
  <button @click="edit">修改</button>
</template>

<script setup lang="ts">
import { shallowReactive, reactive } from 'vue'
const person = shallowReactive({
  singer: {
    name: 'hcy'
  },
  dancer: {
    name: 'ikun'
  }
})

const person2 = reactive({
  name: 'xg',
  age: 18
})
function edit() {
  person2.name = 'reactive'

  person.dancer.name = '🐔你太美' // 受 reactive 影响 也会更新视图
  console.log('person', person)
}
</script>

isReactive()

检查一个对象是否是由 reactive() shallowReactive() 创建的,返回一个布尔值。

<script setup lang="ts">
import { shallowReactive, reactive, isReactive } from 'vue'
const person = shallowReactive({
  singer: {
    name: 'hcy'
  },
  dancer: {
    name: 'ikun'
  }
})

const person2 = reactive({
  name: 'xg',
  age: 18
})
function edit() {
  isReactive(person)
  isReactive(person2)
}
</script>

to全家桶

toRef()

从对象中获取某个数据并将它转换为响应式,如果数据是非响应式的转变后不会更新视图,是响应式数据在转变的时候会更新视图。

toRef有三种形式,第一种直接收一个参数,等同于ref

<script setup lang="ts">
import { toRef } from 'vue'
let name = 'ikun'

let newName = toRef(name)  // 直接转换数据 和ref一样效果
</script>

第二种方式接收一个函数,在函数内部返回需要转换的变量,返回的是一个只读ref,当访问 .value 时会调用此 getter 函数

用来和组件props结合使用,关于禁止对 props 做出更改的限制依然有效,尝试将新的值传递给 ref 等效于尝试直接更改 props,这是不允许的。

<script setup lang="ts">
import { toRef } from 'vue'
let name = 'ikun'

let fnName = toRef(() => {
  console.log('我被调用了')
  return name
})

function edit() {
  console.log('fnName', fnName)
  fnName.value = '1'  // ❌ 无法为“value”赋值,因为它是只读属性。
}
</script>

第三种方式接收两个参数,第一个参数为需要转换的对象数据。第二个参数为对象的key,此时会从对象里获取到key的数据后将其转换并返回。当返回的数据修改时,也会更新源对象里的数据。

主要用途用来将响应式数据从主体里结构出来,赋给某个独立的对象。

<script setup lang="ts">
import { toRef } from 'vue'
const person = {
  name: 'ikun',
  age: 18
}

const newAge = toRef(person, 'age')

function edit() {
  newAge.value = 3  // 此时数据变了,但是视图上数据没有更新。
  
  console.log('newAge', newAge)  // newAge Ref<3>
  console.log('person', person) // person {name: 'ikun', age: 3} person对象数据也会改变
}
</script>

上面结构的都是普通数据,所以在修改数据后页面是不会进行更新的,如果转换的数据是响应式就可以。

<script setup lang="ts">
import { toRef, reactive } from 'vue'
const person = reactive({
  name: 'ikun',
  age: 18
})

const newAge = toRef(person, 'age')

function edit() {
  newAge.value = 3  // 页面数据也会改变
  console.log('newAge', newAge)  // newAge Ref<3>
}
</script>

源码理解

为什么会这样? 因为toRef源码里在创建ref对象时并没有处理依赖收集和触法以来的过程,只做了值的改变,所以普通对象无法更新视图。

这样做的好处就是如果转换的是reactive,它内部已经有了收集、更新操作,如果在toRef里面再触发的话,会出现重复更新问题。


export function toRef<T extends object, K extends keyof T>(
  object: T,
  key: K,
  defaultValue?: T[K]
): ToRef<T[K]> {
  const val = object[key]
  return isRef(val)
    ? val
    : (new ObjectRefImpl(object, key, defaultValue) as any)
}

class ObjectRefImpl<T extends object, K extends keyof T> {
  public readonly __v_isRef = true
 
  constructor(
    private readonly _object: T,
    private readonly _key: K,
    private readonly _defaultValue?: T[K]
  ) {}
 
  get value() {
    const val = this._object[this._key]
    return val === undefined ? (this._defaultValue as T[K]) : val
  }
 
  set value(newVal) {
    this._object[this._key] = newVal
  }
}

toRefs()

作用和toRef一样,只不过可以处理多个,一般都用来解构/展开返回reactive里的数据。

<script setup lang="ts">
import { toRefs, reactive } from 'vue'
const person = reactive({
  name: 'ikun',
  age: 18
})

const {name, age} = toRefs(person)

function edit() {
  name.value = '🐔你太美' 
  age.value = 3
}
</script>

toRaw()

将一个相应式数据转换为一个原始对象,这个响应式数据可以是由 reactive()readonly()shallowReactive() 或者shallowReadonly()创建的代理对应的原始对象。

<script setup lang="ts">
import { toRaw, reactive } from 'vue'
const person = reactive({
  name: 'ikun',
  age: 18
})

const newPerson = toRaw(person)

function edit() {
  console.log(newPerson)  // {name: 'ikun', age: 18}
  console.log(person)  // Reactive<Object> 这里式proxy代理包装了一层
}
</script>

computed计算属性

computed计算属性,当依赖的属性值发生变化时,才会触发他的更改。如果依赖的值不发生变化的时候,使用的就是缓存中的属性值。

在使用上和Vue2没有区别,支持两种写法,一种传入一个对象自己实现getset。一种是直接传入对象,返回修改后的数据。

<script setup lang="ts">
import { ref, computed } from 'vue'
let firstName = ref('i')
let lastName = ref('kun')

let name1 = computed({
  get() {
    return firstName.value + '🐔' + lastName.value
  },

  set(newVla) {
    ;[firstName.value, lastName.value] = newVla.split('🐔')
  }
})

let name2 = computed(() => {
  return firstName.value + '🐔' + lastName.value
})
</script>

为 computed() 标注类型

computed() 会自动从其计算函数的返回值上推导出类型,同时也支持泛型指定类型。

let name2 = computed<string>(() => {
  return firstName.value + '🐔' + lastName.value
})

watch侦听器

监听一个或者多个响应式数据,并在数据变化的时候调用所给的回调函数。

监听单个数据

import { ref, watch } from 'vue'

let name = ref('ikun')
watch(name, (newVal, oldVal) => {
  console.log('newVal', newVal)
  console.log('oldVal', oldVal)
})

监听多个数据,此时的newVlaoldVal都变成数组。

import { ref, watch } from 'vue'

let name = ref('ikun')
let name2 = ref('ikun2')
watch([name, name2], (newVal, oldVal) => {
  console.log('newVal', newVal)
  console.log('oldVal', oldVal)
})

监听对象某个属性,使用get函数返回属性。

import { reactive, watch } from 'vue'
const person = reactive({
  name: 'ikun',
  age: 18
})
watch(
  () => person.value.name,
  (newVal, oldVal) => {
    console.log('newVal', newVal)
    console.log('oldVal', oldVal)
  }
)

当监听ref创建的对象时,需要开启deep选项,而reactive内部已经默认打开了,所以不需要开启。

import { ref, watch } from 'vue'

const person = ref({
  name: 'ikun',
  age: 18
})
watch(
  () => person.value.name,
  (newVal, oldVal) => {
    console.log('newVal', newVal)
    console.log('oldVal', oldVal)
  },
  {
    deep: true
  }
)

第三个可选参数是一个对象,支持以下这些选项:

  • immediate:在侦听器创建时立即触发回调。第一次调用时旧值是 undefined
  • deep:如果源是对象,强制深度遍历,以便在深层级变更时触发回调。参考深层侦听器。
  • flush:调整回调函数的刷新时机。
    • pre:组件更新前执行
    • sync:同步触发执行
    • post:组件更新后执行
  • onTrack / onTrigger:调试侦听器的依赖。

watchEffect 【新增】

立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新运行该函数。

watchEffect里用到谁只会监听谁

import { ref, watchEffect } from 'vue'

const name1 = ref('ikun')

watchEffect(() => {
  console.log('name1', name1)
})

第二个参数为可选对象参数

  • flush:调整回调函数的刷新时机。
    • pre:组件更新前执行
    • sync:同步触发执行
    • post:组件更新后执行

清除副作用

在触发监听前会调用一个函数处理逻辑,当使用的数据发生变化前会先执行该函数,这里可以处理一些防抖、请求等操作。

比如这个例子,当todoId发生变化后,会先执行onCleanup函数将loading设为true。然后再去请求接口,将后台返回的数据赋值给data

import { ref, watchEffect } from 'vue'

const todoId = ref(1)
const data = ref(null)
const loading = ref(false)

watchEffect(async (onCleanup) => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/todos/${todoId.value}`
  )
  onCleanup(() =>{
    loading.value = true
  })
  data.value = await response.json()
})

停止侦听器

watchEffect会返回一个函数,执行该函数会停止后续的监听操作。

watch 同样可以使用

import { ref, watchEffect } from 'vue'

const name1 = ref('ikun')

const stop = watchEffect(() => {
  console.log('name1', name1)
})

stop()

和watch区别

  • watch需要明确指定要观察的数据源,不会监听回调中的任何数据,适用于需要精确控制的情况,可以在回调函数中处理新旧数据的变化情况。
  • watchEffect会在同步执行过程中自动追踪依赖的数据,适用于需要自动化处理的情况,无需手动指定依赖项,但无法访问旧数据。

总的来说,如果需要在特定的数据发生变化时执行特定的操作,使用watch;如果只需要追踪数据的变化并在变化时执行一段代码,使用watchEffect

组件

组件定义和使用

全局组件

main.ts中注册后,全局都可以使用,不需要单独引入

// main.ts
import HomeTop from '@/components/home/HomeTop.vue'

app.component('HomeTop', HomeTop)

// home.vue

<template>
  <div id="main">
    <!-- 顶部 -->
    <HomeTop></HomeTop>
  </div>
</template>

局部组件

在组件内引入,引入后不需要注册,直接在template中使用

<template>
  <div id="main">
    <!-- 顶部 -->
    <HomeTop></HomeTop>
  </div>
</template>
<script setup lang="ts">
import HomeTop from '@/components/home/HomeTop.vue'
</script>

生命周期 【改动】

Vue3全家桶之—— Vue3 Composition API

Vue2不同的是,3移除了一些2中的勾子如 beforeMountbeforeUpdatedestroyed等,同时新增了一些勾子如onBeforeUpdateonBeforeUnmount

  • onBeforeMount():件被挂载之前被调用
  • onMounted():组件挂在完成后调用
  • onBeforeUpdate():组件DOM更新之前调用
  • onUpdated():组件DOM更新之后调用
  • onBeforeUnmount():组件实例卸载之前调用
  • onUnmounted():组件实例卸载之后调用
  • onErrorCaptured():子组件发生错误调用
  • onActivated():若组件实例是keep-alive缓存的一部分,当组件被插入到 DOM 中时调用。
  • onDeactivated():若组件实例是keep-alive缓存的一部分,当组件从 DOM 中移除时调用
  • onMounted():组件挂在完成后

组件通信

父传子

父组件传递 父组件通过属性绑定,如果是动态属性需要添加:

<template>
  <div id="main">
    <!-- 顶部 -->
    <HomeTop :title='title'  name='zs'></HomeTop>
  </div>
</template>
<script setup lang="ts">
import HomeTop from '@/components/home/HomeTop.vue'
const title = 'homeTop'
</script>

非TS方式

子组件接收参数 通过defineProps,它是一个编译宏命令,并不需要显式地导入。在模版中可以直接使用属性title,而在js中使用就需要加一个参数接收,然后通过props.title方式使用。

defineProps也可以像vue2那样定义参数类型、默认值

<!-- HomeTop.vue -->
<template>
  <h4>{{ title }}</h4>
  <span>{{ name }}</span>
</template>
<script setup>
const props = defineProps({
	title: {
		type: string,
		default: 'hello word'
	},
	list: {
		type: Array,
		default: () => {
			return [1,2,3]
		}
	},
	name: String,
})
console.log(props.title)
</script>

TS方式

子组件接收参数 如果使用了TS,也就是在script标签里指定lang=ts,可以在泛型里直接约束参数,当然也可以使用interface接口定义类型。

<!-- HomeTop.vue -->
<template>
  <h4>{{ title }}</h4>
  <span>{{ name }}</span>
</template>
<script setup lang="ts">
const props = defineProps<{
	title: string,
	name: string,
	list?: number[] // 可选参数
}>()
console.log(props.title)
</script>

使用ts,失去了为 props 声明默认值的能力。这可以通过 withDefaults 编译器宏解决:

<!-- HomeTop.vue -->
<template>
  <h4>{{ title }}</h4>
  <span>{{ name }}</span>
</template>
<script setup lang="ts">
const props = withDefaults(defineProps<{
	title: string,
	name: 'ikun',
	list?: () => [1, 2, 3] // 默认值
}>())
console.log(props.title)
</script>

子传父

子组件向父组件传值,子组件中定义方法,在父组件中调用。

非TS方式

子组件传递 在子组件中通过defineEmits派发一个事件,同样,defineEmits 也是一个宏函数,不需要定义即可使用。用一个参数接收,然后通过emit('on-click')触发派送

<!-- HomeTop.vue -->
<template>
	<button @click='send'>派发给父组件 </button>
</template>
<script setup>
const emit = defineEmits(['on-click'])

const send = () => {
	emit('on-click', 'zs')
}
</script>

在父组件中,接收子组件传递过来的自定义方法,进行触发。 父组件接收

<template>
  <div id="main">
    <!-- 顶部 -->
    <HomeTop @on-click='getName'></HomeTop>
  </div>
</template>
<script setup>
import HomeTop from '@/components/home/HomeTop.vue'
// 接收参数
const getName = (vale) => {
	console.log(value, '父组件接收参数')
}
</script>

TS方式

子组件传递 在ts环境下,给defineEmits添加泛型,可以对传递的参数类型进行声明。其余的和非TS方式一样

<!-- HomeTop.vue -->
<template>
	<button @click='send'>派发给父组件 </button>
</template>
<script setup lang="ts">

const emit = defineEmits<{
  (e: 'on-click', name: string): void
}>()

const send = () => {
	emit('on-click', 'zs')
}
</script>

兄弟组件

Vue2中兄弟组件通信常见的方式是通过Vue实现一个Event Bus,而在Vue3中是移除了,可以查看[[Vue3中那些实用的小技巧]]这篇文章。

父组件调用子组件方法

ref方式

子组件中定义属性和方法

<!-- HomeTop.vue -->
<template>
	<button @click='send'>派发给父组件 </button>
</template>
<script setup>

const name = 'zs'

const getName = () => {
	return name
}
</script>

在父组件中通过ref来接收,注意的是要在DOM渲染完之后,才能接收子组件的实例。如果是通过click等事件触发后再获取,就不需要等DOM渲染

<template>
  <div id="main">
    <!-- 顶部 -->
    <HomeTop ref='homeTopRef'></HomeTop>
  </div>
</template>
<script setup >
import HomeTop from '@/components/home/HomeTop.vue'

// 这里 homeTopRef 要和子组件上定义的 ref 值一样
const homeTopRef = ref(null)

// DOM渲染完成后获取
nextTick(() => {
  homeTopRef.value.name
})
</script>

defineExpose 【新增】

defineExpose可以向父组件暴露属性方法

<!-- HomeTop.vue -->
<template>
	<button @click='send'>派发给父组件 </button>
</template>
<script setup lang="ts">

const name = 'zs'

const getName = () => {
	return name
}
defineExpose({
	name,
	getName
})
</script>

和第一种方式一样,也是通过ref来接收。在TS环境下,还可以通过泛型约束限制类型,获取更好的提示。

<template>
  <div id="main">
    <!-- 顶部 -->
    <HomeTop ref='homeTopRef' @on-click='getName'></HomeTop>
  </div>
</template>
<script setup lang="ts">
import HomeTop from '@/components/home/HomeTop.vue'

const homeStopwatchRef = ref<InstanceType<typeof HomeStopwatch>>()

// DOM渲染完成后获取
nextTick(() => {
  homeTopRef.value?.name
})
</script>

插槽Slots 【改动】

在子组件中使用一个占位符,父组件可以在这个占位符填充内容内容。

匿名插槽

子组件中插入一个插槽

<template>
  <div>
    <slot></slot>
  </div>
</template>

父组件中使用

<Dialog>
  取消
<Dialog/>

默认内容

子组件定义插槽,在slot中定义默认内容

<template>
  <div>
    <slot>
		  名称  <!-- 默认内容 -->
    </slot>
  </div>
</template>

父组件使用时,没有提供任何插槽内容时,子组件就显示默认内容。如果提供插槽内容,子组件就有显示插槽内容

<Dialog><Dialog/>

具名插槽

一个子组件可以存放多个插槽,为了区分需要使用具名插槽,给每一个插槽取个名字。

没有提供 name 的 <slot> 出口会隐式地命名为default

子组件定义多个具名插槽

<template>
  <div>
    <slot name="header"></slot>
  </div>

  <div>
    <slot name="footer"></slot>
  </div>
</template>

父组件在使用的时候按照name在指定地方插入内容

  <BaseLayout>
    <template v-slot:header>
      <span>header</span>
    </template>

    <template v-slot:footer>
      <span>footer</span>
    </template>
  </BaseLayout>

v-slot 有对应的简写 #,因此 <template v-slot:header> 可以简写为 <template #header>

动态插槽

插槽可以是一个变量名

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

    <template #[dynamicSlotName]>
      ...
    </template>
  </BaseLayout>

作用域插槽

子组件在插槽中绑定数据,向父组件传递参数给<slot>中使用,也就是vue2中的slot-scope

<template>
  <div>
    <slot :userName="name" :text="text"> </slot>
  </div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const name = ref('ikun')
const text = ref('🐔你太美')
</script>

父组件通过一个对象接收

<MyComponent v-slot="slotProps">
  我叫{{ slotProps.userName }} 你真的{{ slotProps.text }}
</MyComponent>

<!-- or -->
<MyComponent v-slot="{userName, text}">
  我叫{{ userName }} 你真的{{ text }}
</MyComponent>

具名作用域插槽

顾名思义,就是有具名插槽和作用域一起使用,也是最常用的一种方式

<template>
  <div>
    <slot name='header' :userName="name"></slot>
  </div>
  <span>
    <slot name='main' :text="text"></slot>
  </span>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const name = ref('ikun')
const text = ref('🐔你太美')
</script>

父组件在具名插槽后面通过一个对象接收

  <MyComponent>
    <template #header="slotProps"> 我叫{{ slotProps.userName }} </template>

    <template #main="{ text }"> 你真的{{ text }}</template>
  </MyComponent>

provide/inject依赖注入

Vue2用途一样,都是用来解决组件嵌套较深的情况下,不方便组件间的通信。

provide用来提供数据,inject用来接收prodive数据。 Vue3全家桶之—— Vue3 Composition API

父组件使用provide提供数据

import { provide, ref } from 'vue'
// 格式
provide(/* 注入名 */ 'message', /* 值 */ 'hello!')

const message = ref('hello')
provide('message', message)

子组件接收

import { inject, type Ref } from 'vue'

const message = inject<Ref<string>>('message')

如果提供的值是一个 ref,注入进来的会是该 ref 对象,而不会自动解包为其内部的值。

全局注入

除了在组件通信上使用,还可以作为全局的依赖注入

// main.ts
import { createApp } from 'vue'

const app = createApp({})

app.provide(/* 注入名 */ 'message', /* 值 */ 'hello!')

defineAsyncComponent异步组件 【新增】

在大型项目中,可能需要将应用分割成小一些的代码块,从而减少主包的体积,加快页面相应速度,这个时候就可以用异步组件defineAsyncComponent

<template>
  <div class="container"></div>
  <AsyncComp></AsyncComp>
</template>

<script setup lang="ts">
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() => import('./components/MyComponent.vue'))
</script>

最后得到的 AsyncComp 是一个外层包装过的组件,仅在页面需要它渲染时才会调用加载内部实际组件的函数。它会将接收到的 props 和插槽传给内部组件,所以你可以使用这个异步的包装组件无缝地替换原始组件,同时实现延迟加载。

加载与错误状态

支持在高级选项处理加载成功和夹在失败状态。

const AsyncComp = defineAsyncComponent({
  // 加载函数
  loader: () => import('./components/MyComponent.vue),

  // 加载异步组件时使用的组件
  loadingComponent: LoadingComponent,
  // 展示加载组件前的延迟时间,默认为 200ms
  delay: 200,

  // 加载失败后展示的组件
  errorComponent: ErrorComponent,
  // 如果提供了一个 timeout 时间限制,并超时了
  // 也会显示这里配置的报错组件,默认值是:Infinity
  timeout: 3000
})

suspense 【新增】

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

Suspense是一个内置组件,用来协调组件在异步加载时渲染的处理。通过它可以实现骨架屏,当组件内容加载时先显示骨架屏内容,加载完毕后显示组件内容。

它有两个插槽,但都直接收一个子节点。

  • defalut:插槽里的内容节点尽可能的展现出来
  • fallback:如果default没有展示,就显示。
  <Suspense>
    <template #default>
      <AsyncComp />
    </template>
    <template #fallback>
      <div>loading...</div>
    </template>
  </Suspense>

AsyncComp组件内容没有加载出来前,先展示loading,加载完毕后,再展示AsyncComp。通常配合defineAsyncComponent来使用。

keep-alive

Vue2使用一样,用来在需要的时候缓存组件实例,避免组件的一个重复加载渲染,提高页面性能。

<KeepAlive include="a,b" exclude="c" :max="10">
  <component :is="view" />
</KeepAlive>

teleport 【新增】

teleport能够将模版渲染至指定的DOM节点,不受父级stylev-show影响,但dataprop数据依旧能够共用。teleport 只改变渲染DOM的结构,它不会影响组件间的逻辑关系。

它和keep-alive一样都是内置组件,可以直接使用。

  • 使用to属性来控制需要传送的目标节点,可以是一个CSS选择器、也可以是一个 DOM 元素对象。
  • 使用disabled属性来禁用

teleport挂载时,传送的 to 目标必须已经存在于 DOM 中。理想情况下,这应该是整个 Vue 应用 DOM 树外部的一个元素。如果目标元素也是由 Vue 渲染的,你需要确保在挂载 teleport之前先挂载该元素。

<Teleport to="body" disabled="true" >
  ...
</Teleport>

v-model 【改动】

Vue组件数据是单向流,子组件不能直接修改父组件里面的数据。而v-model是一个语法糖,通过propsemit组合而成。

  • 使用props默认绑定一个modelValue值到子组件
  • 当数据变化时,通过update:modeValue监听改变后修改数据

父组件绑定数据

<template>
  <MyComponent v-model="isShow"></MyComponent>
</template>

其实相当于,默认是绑定modelValue数据

<MyComponent
  :modelValue="isShow"
  @update:modelValue="newValue => isShow = newValue"
/>

在子组件里通过defineProps接收,在修改的时候通过defineEmits触发emit,父组件修改数据

<template>
  <div v-show="modelValue">显示内容</div>

  <button @click="change">修改</button>
</template>

<script setup lang="ts">
const propData = defineProps<{
  modelValue: boolean
}>()

const emit = defineEmits(['update:modelValue'])
function change() {
  emit('update:modelValue', !propData.modelValue.valueOf())
}
</script>

多个v-mode绑定

可以同时绑定多个,在子组件内部按名称接收修改。

<MyComponent v-model:isShow="isShow" v-model:message="message"></MyComponent>
<template>
  <div v-show="isShow">显示内容</div>
  <br />
  {{ isShow }}
  <button @click="change">修改</button>
  <br />
  <span>{{ message }}</span>
</template>

<script setup lang="ts">
const propData = defineProps<{
  isShow: boolean
  message: string
}>()

const emit = defineEmits(['update:isShow', 'update:message'])
function change() {
  emit('update:isShow', !propData.isShow.valueOf())
  emit('update:message', '子组件改变了数据')
}
</script>

v-mode修饰符

v-mode内置了一些修饰符如.trim.number .lazy,同时也支持自定义修饰符。

<MyComponent v-model:isShow.capitalize="isShow" v-model:message="message"></MyComponent>

子组件默认通过modelModifiers来接收,如果是多个v-mode,则通过绑定的数据名+Modifiers接受,例如:messageModifiers

<template>
  <div v-show="isShow">显示内容</div>
  <br />
  {{ isShow }}
  <button @click="change">修改</button>
  <br />
  <span>{{ message }}</span>
</template>

<script setup lang="ts">
const propData = defineProps<{
  isShow: boolean
  message: string
  messageModifiers?: {
    default: () => {}
  }
}>()

const emit = defineEmits(['update:isShow', 'update:message'])
function change() {
  emit('update:isShow', !propData.isShow.valueOf())
  emit('update:message', '子组件改变了数据')
  console.log('modelModifiers', propData.messageModifiers)  // {capitalize: true}
}
</script>

自定义指令【改动】

Vue内置里一系列指令,除此之外还可以自定义指令。

一个自定义指令由一个包含类似组件生命周期钩子的对象来定义,钩子函数会接收到指令所绑定元素作为其参数。

在模版中直接使用

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

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

将一个自定义指令全局注册

// main.ts
const app = createApp({})

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

指令钩子

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

钩子的参数

  • el:指令绑定到的元素。这可以用于直接操作 DOM
  • binding:一个对象,包含以下属性。
    • value:传递给指令的值。例如在v-my-directive="1 + 1"中,值是 2。
    • oldValue:之前的值,仅在 beforeUpdateupdated 中可用。无论值是否更改,它都可用。
    • arg:传递给指令的参数 (如果有的话)。例如在v-my-directive:foo中,参数是 "foo"。
    • modifiers:一个包含修饰符的对象 (如果有的话)。例如在 v-my-directive.foo.bar 中,修饰符对象是 { foo: true, bar: true }
    • instance:使用该指令的组件实例。
    • dir:指令的定义对象。
  • vnode:代表绑定元素的底层 VNode
  • prevNode:之前的渲染中代表指令所绑定元素的 VNode。仅在 beforeUpdateupdated 钩子中可用。

举例来说,像下面这样使用指令:

<div v-example:foo.bar="baz">

binding 参数会是一个这样的对象:

{
  arg: 'foo',
  modifiers: { bar: true },
  value: /* `baz` 的值 */,
  oldValue: /* 上一次更新时 `baz` 的值 */
}

函数简写

如果指令只需要在 mountedupdated 上实现,可以通过函数简写的形式。

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

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

对象字面量

如果指令需要多个值,在模版上使用时可以传递一个对象,指令在接受的时候可以通过bingding.value.[attr]方式访问。


<script setup>
// 在模板中启用 v-focus
const vDemo = (el, binding) => {
  console.log(binding.value.color) // => "white"
  console.log(binding.value.text) // => "hello!"
}
</script>

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

为Directive标注类型

如果为binding.value定义类型呢?也就是指令的参数。

有两种方式,一种通过Directive传递泛型,还可以通过DirectiveBinding指定bingding.value类型

import { ref } from 'vue'
import type { Directive, DirectiveBinding } from 'vue'

let value = ref<string>('')

type Move = {
  color: string
  text: string
}

const vMove = (el: HTMLElement, binding: DirectiveBinding<Move>) => {
  console.log('binding', binding.value.color)
}

const vHasShow: Directive<HTMLElement, Move> = (el, binding) => {
  console.log('binding', binding.value.text)
}

自定义Hooks

听着很复杂,其实就是和Vue2中的mixins作用是一样的,用来抽离封装一些公共代码逻辑。

mixins存在一些问题,就是引入的mixins会覆盖的问题。

组件的datamethodsfilters如果和mixins里的datamethodsfilters同名会被覆盖掉

mixins的生命周期调用比组件快 Vue3全家桶之—— Vue3 Composition API

还有一点是变量的来源不明确,不利于阅读,维护起来很麻烦。

尽管Vue3中还保留了mixin,但官方推荐还是不要使用,推荐使用组合式函数代替,也就是hook

不推荐 Mixins 在 Vue 3 支持主要是为了向后兼容,因为生态中有许多库使用到。在新的应用中应尽量避免使用 mixin,特别是全局 mixin。

Vue3则提供hooks函数,通过引入Vue的各种钩子实现封装,如onMountedrefwatch等等,这些都是独立,并不会影响到组件。

Vue官方提供了一个hooksVueUse,里面提供了很多各种各样的hook

nextTick

Vue2用途一样,等DOM更新完毕后再执行。nextTick() 可以在状态改变后立即使用,以等待 DOM 更新完成。可以传递一个回调函数作为参数,或者 await 返回的 Promise

import { ref, nextTick } from 'vue'
const count = ref(0)

async function increment() {
  count.value++

  // DOM 还未更新
  console.log(document.getElementById('counter').textContent) // 0

  await nextTick()
  // DOM 此时已经更新
  console.log(document.getElementById('counter').textContent) // 1
}

其他文章

其他文章

转载自:https://juejin.cn/post/7277836494298152994
评论
请登录