vue3学习笔记

181 阅读7分钟

[TOC]

组件生命周期

onBeforeMount

组件初始化,此次dom还未挂载

onMounted

dom挂载完成

onBeforeUpdate

更新前

onUpdated

更新后

onBeforeUnmount

销毁前

onUnmounted

销毁后

onActivated

激活组件时

onDeactivated

离开组件时

onErrorCaptured

当捕获一个来自子孙组件的异常时激活钩子函数

ref全家桶

ref

将基本类型转化为响应式对象

通过 xxx.value去访问对应的值,在template中会自动解包,即 {{xxx}}即可访问

如果传入的数据不是基本类型,则会在内部调用reactive

import { Ref, ref } from 'vue'
const message: Ref<string> = ref('message')
const hanleClick = () {
  message.value = 'change message'
}

markRaw

将数据不设置为响应式数据,一般用于标记一些不需要监听的对象,比如动态组件的实例。

原理是将对象标记为__v_skip为true,此时将不会走proxy代理

import { Ref, ref, markRef } from 'vue'
const message: Ref<string> = ref('message')
let unRefMessage = markRef(message)

isRef

import { Ref, ref, isRef } from 'vue'
const message: Ref<string> = ref('message')
const notRefMessage: string = '1111'
console.log(isRef(message)) // true
console.log(isRef(notRefMessage)) // false

shallowRef 与 triggerRef

shallowRef:将数据转化成响应式对象,但是非深度监听

如果传入值为基本类型,则与ref一致

如果传入对象,则只有传入的对象是响应式的,对象上的属性是不具有响应式的

如果想更改属性来触发视图上的变化,则可以用triggerRef来强制更新

import { Ref, shallowRef, triggerRef } from 'vue'

type TPeople = { name: string, age: number }

const person: Ref<TPeople> = shallowRef({ name: 'gzhhhh', age: 22 })

const handleChange = () => {
  // ---- 更改对象的属性不会触发视图上的更新
  person.value.name = 'xixixi'
  person.value.age = 22
  triggerRef(person) // 使用triggerRef 来强制视图更新
	
  // ---- 更改value 会触发视图上的更新
  person.value = {
    name: 'xixix',
    age: 111
  }
}

一个对象数据,后面会产生新的对象来替换,我们可以用shallowRef来节省性能

customRef

自定义ref

customRef传入一个工厂函数,有两个参数,trank() 用来收集依赖,trigger() 用来更新视图,须返回一个含有get、set函数的对象

function debounceRef<T>(value: T) {
  return customRef((trank, trigger) => {
    return {
      get() {
        trank() // 收集依赖
        return value
      },
      set(newVal: T) {
        console.log('set');
        value = newVal
        trigger() // 更新视图
      }
    }
  })
}

let message = debounceRef<string>('message1')
console.log(message.value);
message.value = 'change message'
console.log(message.value);

unref

unref():是 val = isRef(val) ? val.value : val 的语法糖

如果参数是一个 ref 则返回它的 value,否则返回参数本身。

toRef、toRefs

toRef:引用某个对象的值,改变ref值会影响到原始数据的值,并且不会触发视图的更新,如果原始数据是响应式的,即用reactive创建的对象,则会触发视图更新

适用场景:让响应式数据和以前的数据关联起来,并且更新响应式数据之后不会触发UI的更新

toRefs:将整个对象的值关联起来,即循环调用toRef

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

type TPeople = { name: string, age: number }

const obj: TPeople = { name: 'gzh', age: 12 }
const reactiveObj: TPeople = reactive({ name: 'gzh', age: 12 })

const nameRef: Ref<string> = toRef(obj, 'name')
const nameReactive: string = toRef(reactiveObj, 'name')

const changeName = () => {
  nameRef.value = 'xixixi' // obj.name = xixi,但是视图不会更新
  nameReactive.value = 'xixixi' // reactiveObj.name = xixi,同时触发视图更新
  console.log(obj);
}
</script>

<template>
  <p>{{ nameRef }}</p>
  <button @click="changeName">改变名字</button>
</template>

reactive全家桶

reactive

reactive:将复杂数据类型变成响应式数据,比如对象和数组

type TBannerItem = {
  id: number,
  imgSrc: string
}
type TBanner = {
  list?: Array<TBannerItem>
}
let banner: TBanner = reactive({
  list: []
})
const fetchBannerData = () => {
  setTimeout(() => {
    const dataList: Array<TBannerItem> = [{
      id: 1,
      imgSrc: 'www.baidu.com'
    }, {
      id: 2,
      imgSrc: 'www.sougou.com'
    }]
    banner.list = dataList // 此时修改会触发视图的更新
  }, 1000)
}
fetchBannerData()

若是直接将整个对象赋值,则不会触发页面的更新

type TBannerItem = {
  id: number,
  imgSrc: string
}

type TBanner = {
  list?: Array<TBannerItem>
}

let banner: TBanner = reactive({
  list: []
})

const fetchBannerData = () => {
  setTimeout(() => {
    const data: TBanner = {
      list: [{
        id: 1,
        imgSrc: 'www.baidu.com'
      }, {
        id: 2,
        imgSrc: 'www.sougou.com'
      }]
    }
    banner = data // 此时直接赋值整个对象,不会触发视图更新,因为此时banner已不是响应式对象
    // banner = reactive(data) // 此种方式同样不会触发视图更新
    console.log(banner);
  }, 1000)
}
fetchBannerData()

注意:直接赋值整个对象不会触发视图更新

shallowReactive

shallowReactive: 监听了第一层属性的值,一旦发生改变,则更新视图;更深层次,虽然值发生了改变,但是视图不会进行更新

type TBannerItem = {
  id: number,
  imgSrc: string
}

type TBanner = {
  name: string
  list: Array<TBannerItem>
}

let banner: TBanner = shallowReactive({
  name: '金刚位广告',
  list: [{
    id: 1,
    imgSrc: 'www.baidu.com'
  }]
})

setTimeout(() => {
  banner.name = '金刚位广告1'
  banner.list[0].imgSrc = 'www.sougou.com' // dom挂载前,此时改变会触发视图更新
}, 2000)

const handleChangeBannber = () => {
  banner.list[0].imgSrc = 'www.jd.com' // dom挂载后,此时改变不会触发视图更新,但是对象会变化
  console.log(banner);
}

<p>{{ banner }}</p>
<button @click="handleChangeBannber">改变深层次的属性</button>

注意:dom挂载前深层次属性改变也会触发视图更新,dom挂载后在深层次属性改变不会再触发视图的更新

toRaw

将响应式对象变成普通对象,即改变数据不会触发视图更新

const person: TPeople = { name: 'gzh', age: 24 }
const reactivePerson = reactive(person)
const rawPerson = toRaw(reactivePerson)

console.log(rawPerson === person); // true
console.log(rawPerson === reactivePerson); // false

const changePerson1 = () => {
  rawPerson.name = 'gzhhhh' // 不会触发视图更新
  console.log(person); // { name: 'gzhhhh', age: 24 }
  console.log(rawPerson); // { name: 'gzhhhh', age: 24 }
  console.log(reactivePerson); // Proxy { name: 'gzhhhh', age: 24 }
  reactivePerson.name = 'gzhssss' // 会触发视图更新
  console.log(person); // { name: 'gzhssss', age: 24 }
  console.log(rawPerson); // { name: 'gzhssss', age: 24 }
  console.log(reactivePerson); // Proxy { name: 'gzhssss', age: 24 }
}

setTimeout(() => {
  changePerson1()
}, 2000);

watch

监听响应式数据的变化

类型声明

// 侦听单一源
function watch<T>(
  source: WatcherSource<T>,
  callback: (
    value: T,
    oldValue: T,
    onInvalidate: InvalidateCbRegistrator
  ) => void,
  options?: WatchOptions
): StopHandle

// 侦听多个源
function watch<T extends WatcherSource<unknown>[]>(
  sources: T
  callback: (
    values: MapSources<T>,
    oldValues: MapSources<T>,
    onInvalidate: InvalidateCbRegistrator
  ) => void,
  options? : WatchOptions
): StopHandle

监听基本类型

将需要监听值传入watch方法即可,watch接收三个参数

第一个: 需要监听值

第二个:值变化后的回调函数,有两个参数,第一个是新值,第二个是旧值

第三个:一个配置对象,可选值: deep:boolean表示是否立即执行监听,immediate:boolean表示是否需要深度监听

使用方法基本与vue2.x相似

需要注意:

如果ref传入的复杂类型,需要将deep设置为true才会进行深度监听

监听多个值的写法

监听单个值
<script setup lang="ts">
import { ref, watch, Ref } from 'vue'
const message: Ref<string> = ref('hello, world')
// 监听单个值
watch(message, (nVal: string, oVal?: string) => {
  console.log(nVal); // 新值
  console.log(oVal); // 旧值
})
</script>
<template>
	<input type="text" v-model="message" />
</template>
监听多个值
<script setup lang="ts">
import { ref, watch, Ref } from 'vue'
const name: Ref<string> = ref('gzhhh')
const age: Ref<number> = ref(24)
// 监听多个值
watch([name, age], (nVal:Array<string|number>, oVal: Array<string|number|undefined>) => {
  console.log(nVal); // 新值 [name, age]
  console.log(oVal); // 旧值 [name, age]
}, {
  immediate: true
})
</script>
<template>
	<input type="text" v-model.number="age" />
</template>

监听复杂类型

监听reactive响应式对象

注意:

回调的oVal的值是新值

监听对象上某个数据的写法

监听整个对象
<script setup lang="ts">
import { reactive, watch } from 'vue'
type TPerson = {
  name: string,
  age: number,
  hobby: Array<{ name: string, percent: number }>
}
const person: TPerson = reactive({
  name: '',
  age: 0,
  hobby: [{
    name: '',
    percent: 0
  }]
})

// 监听整个对象,默认进行深度监听,即deep参数为true
watch(person, (nVal: TPerson, oVal?: TPerson) => {
  console.log(nVal);
  console.log(oVal); // 注意:此处的name会变成新的值,官方bug
  console.log(oVal?.hobby[0].name); // 注意:此处的name会变成新的值,官方bug
}, {
  immediate: true
})

// 监听对象的某个属性
watch(() => person.hobby[0].name, (nVal, oVal) => {
  console.log(nVal); // 新值
  console.log(oVal); // 旧值
})
</script>
<template>
  <input type="text" v-model="person.hobby[0].name" />
</template>
监听对象上的某个属性
<script setup lang="ts">
import { reactive, watch } from 'vue'
type TPerson = {
  name: string,
  age: number,
  hobby: Array<{ name: string, percent: number }>
}
const person: TPerson = reactive({
  name: '',
  age: 0,
  hobby: [{
    name: '',
    percent: 0
  }]
})
// 监听对象的某个属性
watch(() => person.hobby[0].name, (nVal, oVal) => {
  console.log(nVal); // 新值
  console.log(oVal); // 旧值
})
</script>
<template>
  <input type="text" v-model="person.hobby[0].name" />
</template>

watchEffect

与watch一样,用来监听响应式数据的变化。

类型声明

function watchEffect(
  effect: (onInvalidate: InvalidateCbRegistrator) => void,
  options?: WatchEffectOptions
): StopHandle

基本用法

监听基本类型(ref)
<script setup lang="ts">
import { ref, watchEffect, Ref } from 'vue'

const message: Ref<string> = ref('watchEffect')
const age: Ref<number> = ref(24)

// 加载时会调用一次, 用与收集依赖
watchEffect(() => {
  console.log(`${message.value} --- watchEffect`); // 改变message会触发此处的watchEffect
  console.log(age); // 改变age不会触发此处的watchEffect
})
const handleChangeMessage = () => {
  message.value = 'watchEffect -- update'
}
const handleChangeAge = () => {
  age.value = 111
}
</script>
<template>
  <p>{{ message }}</p>
  <p>{{ age }}</p>
  <button @click="handleChangeAge">改变age</button>
  <button @click="handleChangeMessage">改变message</button>
</template>

注意:

  1. 在组件初始化的时候就会调用一次,用于收集依赖,此后,只有当依赖的值发生变化才会重新执行
  2. 在回调里需要加上xxx.value才会被收集到依赖,像上面例子,age不会被收集,当age值改变时不会触发watchEffect
  3. 可以写多个watchEffect,最好将相关变化的依赖项写到同一个。
监听复杂数据类型(reative)
<script setup lang="ts">
import { watchEffect, reactive } from 'vue'
type TPerson = {
  name: string,
  age: number,
}
const person: TPerson = reactive({
  name: 'gzh',
  age: 0,
})
watchEffect(() => {
  // console.log(person); // 如果声明的是person,则改变name与age的值不会触发此处的watchEffect
  console.log(person.name);
})
const handleChangePersonName = () => {
  person.name = 'gzhhhh'
}
</script>
<template>
  <p>{{ person }}</p>
  <button @click="handleChangePersonName">改变PersonName</button>
</template>

注意:

  1. 在监听复杂数据类型时,在watchEffect需要引用对象属性才会被收集到依赖中
进阶用法
onInvalidate回调

在依赖值发生变化的时候,会触发这个回调,而且其在watchEffect中是最先执行,不论写得顺序。

可以用于清除副作用,比如:防抖等

const age: Ref<number> = ref(24)
watchEffect((onInvalidate) => {
  console.log(`${age.value} --- watchEffect`);
  onInvalidate(() => {
    console.log('此处回调永远在当前watchEffect中最先执行')
  })
})
/**
 * 此处回调永远在当前watchEffect中最先执行
 * 111 --- watchEffect
 */
const handleChangeAge = () => {
  age.value = 111
}

注意:组件初始化过程执行的watchEffect,不会调用onInvalidate回调,只有当依赖值发生变化或者执行停止监听的函数 才会。

停止watchEffect监听

watchEffect返回一个函数,可以让组件停止对相应依赖的监听

const age: Ref<number> = ref(24)
const stop = watchEffect((onInvalidate) => {
  console.log(`${age.value} --- watchEffect`);
})
const handleStopWatch = () => {
  stop() // 即停止监听,当age值发生变化,不会再执行上面的watchEffect
}
额外配置参数

watchEffect第二个参数为一个配置对象,其属性为:

flush: 'pre'|'sync'|post 用于指定触发时机,pre:组件更新前执行,sync:强制效果始终同步触发, post:组件更新后执行

onTrigger:(event: DebuggerEvent)用于开发环境调试,回调的参数为对应依赖的响应式对象

与watch对比

  • 第一点我们可以从示例代码中看到 watchEffect 不需要指定监听的属性,他会自动的收集依赖, 只要我们回调中引用到了 响应式的属性, 那么当这些属性变更的时候,这个回调都会执行,而 watch 只能监听指定的属性而做出变更(v3开始可以同时指定多个)。
  • 第二点就是 watch 可以获取到新值与旧值(更新前的值),而 watchEffect 是拿不到的。
  • 第三点是 watchEffect 如果存在的话,在组件初始化的时候就会执行一次用以收集依赖(与computed同理),而后收集到的依赖发生变化,这个回调才会再次执行,而 watch 不需要,因为他一开始就指定了依赖。

父子组件传值

父传子

父组件通过v-bind传递数据,通过defineProps接收数据

父组件

<script setup lang="ts">
import { ref, Ref, reactive } from 'vue';
import PassValue from './components/PassValue.vue'
const title: Ref<string> = ref('我是标题党')
const list: Array<number> = reactive([1, 2, 3, 4])
</script>

<template>
  <PassValue :title="title" :dataList="list" />
</template>

子组件

<script setup lang="ts">
import { withDefaults } from 'vue'
type props = {
  title?: string,
  dataList?: Array<number>
}
  
// 非TS写法
// defineProps({
//  title: {
//		type: string,
// 		required: true,
//    default: '我是默认标题'
//  },
//  dataList: Array
//})
  
// 不需默认值的写法
// defineProps<props>()

// 需要默认值需要用withDefaults
withDefaults(defineProps<props>(), {
  title: '我是默认标题',
  dataList: () => [], // 复杂数据类型需要函数返回,防止组件作用域污染
})
</script>
<template>
  <h1>{{ title }}</h1>
  <ul>
    <li v-for="item of dataList" :key="item">{{ item }}</li>
  </ul>
</template>

注意:

  1. 需要默认值需要将defineProps传入withDefaults函数
  2. 注意TS写法与非TS写法

子传父

子组件通过defineEmits 派发事件给父组件,父组件用v-on监听事件,获取回调中的数据

子组件

<script setup lang="ts">
// 返回一个函数,用于触发对应的事件
const emits = defineEmits(['on-click', /** 可以写多个emit事件 */])

const handleClick = () => {
  // 第一个参数为要派发的事件名称
  emits('on-click', {
    name: 'PassValue',
    remark: '我是第一个参数'
  }, '我是第2个参数', '我是第3个参数') // 可以传递多个参数
}
</script>
<template>
  <button @click="handleClick">派发事件给父组件啦</button>
</template>

父组件

<script setup lang="ts">
import PassValue from './components/PassValue.vue'
const onClickCb = (args1: { name?: string, remark?: string }, arg2?: string, arg3?: string) => {
  console.log(args1); // {name: 'PassValue', remark: '我是第一个参数'}
  console.log(arg2); // 我是第2个参数
  console.log(arg3); //我是第3个参数
}
</script>

<template>
  <PassValue @on-click="onClickCb" />
</template>

父组件获取子组件实例

父组件使用ref获取实例,子组件通过defineExpose传递想要暴露的数据给父组件

父组件

<script setup lang="ts">
import { ref, Ref } from 'vue';
import PassValue from './components/PassValue.vue'
const passValueRef = ref(null) // 此处变量名需与组件ref属性值一致
const onClickCb = () => {
  console.log(passValueRef.value.message);
}
</script>
<template>
  <PassValue ref="passValueRef" @on-click="onClickCb" />
</template>

子组件

<script setup lang="ts">
import { ref, Ref } from 'vue'
const message: Ref<string> = ref('我是子组件的数据')
defineExpose({
  message
})
</script>
<template>
  <button @click="handleClick">派发事件给父组件啦</button>
</template>

注意:

  1. 获取子组件的方式,变量名需与子组件的ref属性值一致;(感觉这种获取方式有点离谱)
  2. 与vue2不同,父组件不能直接通过实例获取子组件的数据,需要子组件通过defineExpose抛出变量。(这种做法更安全,子组件的数据不是父组件想改就能改)

组件注册方式

局部注册

直接import XxxComp from 'xxxComp'

全局注册

  1. 使用createApp()返回的实例的component方法

    createApp(App).component('xxx-xxx', xxxComp)
    
  2. 动态注册全局组件

    // 目录
    |____asyncComp
    | |____AsyncButton
    | | |____index.vue
    |____syncComp
    | |____MyButton
    | | |____index.vue
    |____index.ts
    
    // index.ts
    import { defineAsyncComponent } from 'vue';
    const syncCompoents: Record<string, { [key: string]: any }> = import.meta.globEager('./syncComp/**/*.vue')
    const asyncCompoents = import.meta.glob('./asyncComp/*/*.vue')
    
    const isEmptyObject = (obj:Object = {}):boolean => {
      return Object.keys(obj).length === 0
    }
    
    const getCompoentName = (str: string = ''):string => {
      if (!str) return ''
      const patten: RegExp = /^\.\/.*\/(.*)\/.*\.vue$/
      const name:any = str.match(patten)
      return name[1].replace(/([A-Z])/g,"-$1").slice(1).toLowerCase()
    }
    
    export default function install(app: any) {
      if (!isEmptyObject(syncCompoents)) {
        for (const [key, value] of Object.entries(syncCompoents)) {
          const name: string = getCompoentName(key)
          app.component(name, value.default);
        }
      }
      if (!isEmptyObject(asyncCompoents)) {
        for (const [key, value] of Object.entries(asyncCompoents)) {
          const name = getCompoentName(key)
          app.component(name, defineAsyncComponent(value));
        }
      }
    }
    
    // main.ts
    import { createApp } from 'vue'
    import components from '@/components/globalComp';
    import App from './App.vue'
    
    const app = createApp(App)
    app.use(components)
    app.mount('#app')
    

    注意:

    1. 使用vite,所以遍历目前采用import.meta.globEager 或者 import.meta.glob

      import.meta.globEager为直接导入,用于一般组件

      import.meta.glob为动态导入,构建时,会分离为独立的 chunk,用于异步组件

    2. 异步组件需要用defineAsyncComponent包裹

动态组件

使用<component :is="componentName">

<template>
  <div>
    <span
      v-for="(item, index) of tabList"
      :key="item.name"
      @click="handleChangeTab(index)"
    >{{ item.name }}</span> &nbsp;&nbsp;
  </div>
  <component :is="current.comName"></component>
</template>

<script setup lang="ts">
import { ref, Ref, reactive, markRaw } from 'vue'
import TabA from './TabA.vue';
import TabB from './TabB.vue';
type TTabs = {
  name: string,
  comName: any
}
type TCom = Pick<TTabs, 'comName'>
const tabList = reactive<TTabs[]>([
  {
    name: 'TabA',
    comName: markRaw(TabA) // 此处用markRaw标记组件不会走proxy代理,减少性能的损耗
  },
  {
    name: 'TabB',
    comName: markRaw(TabB)
  }
])
const current = reactive<TCom>({
  comName: tabList[0].comName
})

const handleChangeTab = (index: number) => {
  current.comName = tabList[index].comName
}
</script>

注意:

​ 使用setup语法糖必须is必须传入对象的实例,不能传入字符串

异步组件

使用defineAsyncComponent 定义的组件为异步组件,打包时会单独拆分为一个文件,当用到时才会加载对应的文件,经常与<Suspense>搭配使用。

子组件

<template>
  <ul>
    <li v-for="item in dataList" :key="item.name">{{ item }}</li>
  </ul>
</template>
<script setup lang="ts">
import { reactive } from 'vue';
type TData = {
  name: string
  age: number
}
const getDataList = (): Promise<Array<TData>> => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const dataList: Array<TData> = [{
        name: 'gzh',
        age: 18
      }, {
        name: 'gzhhh',
        age: 24
      }, {
        name: 'gzhhhhh',
        age: 30
      }]
      resolve(dataList)
    }, 5000)
  })
}
// 在setup顶层使用await, 父组件使用时要用<Suspense></Suspense>包裹,不然不会展示
const originData = await getDataList()
const dataList = reactive<Array<TData>>(originData)
</script>

父组件

<template>
  <Suspense>
    <template #default>
      <AsyncComp />
    </template>
    <template #fallback>loading...</template>
  </Suspense>
</template>
<script setup lang="ts">
import { reactive, defineAsyncComponent } from 'vue';
import TodoList from './TodoList.vue';
// 使用defineAsyncComponent定义组件为异步组件
const AsyncComp = defineAsyncComponent(() => import('./AsyncComp.vue'))
</script>

注意:

  1. 在setup顶层使用await ,在父组件使用时,要使用Suspense组件包裹
  2. Suspense 的 默认插槽#default 是放置组件的,#fallback是等待子组件await过程展示的组件,一般用于加载动画等。

插槽

默认插槽

slot 无指定name即为默认插槽

子组件ChildrenComp

<div class="header">
    <slot>Header</slot>
</div>

父组件

<ChildrenComp>
 	<div>我是默认插槽</div>
</ChildrenComp>

结果

<div class="header">
    <div>我是默认插槽</div>
</div>

具名插槽

slot指定name值,父组件插入对应插槽时要指定对应的name

子组件

<div class="header">
    <slot name="header">Header</slot>
</div>

父组件

<ChildrenComp>
 	<div v-slot:header>我是具名插槽</div>
</ChildrenComp>
<!-- 或者采用缩写的方式 -->
<ChildrenComp>
 	<div #header>我是具名插槽</div>
</ChildrenComp>

结果

<div class="header">
    <div>我是具名插槽</div>
</div>

作用域插槽

将子组件作用域的值抛出给父组件访问

子组件

// 使用v-bind将数据抛出
<div class="center">
 <slot v-for="(item, index) in dataList" :key="item.id" :data="item" :index="index"></slot>
</div>

父组件

<template v-slot:default="{ data }">
  <div>{{ data }}</div>
</template>
<!-- 或者 缩写 -->
<template #default="{ data }">
  <div>{{ data }}</div>
</template>
<!-- 或者 当插入默认插槽时 缩写 -->
<template v-slot="{ data }">
  <div>{{ data }}</div>
</template>

动态插槽

动态指定插槽名

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

transition 动画组件

基本用法

transition指定name值,vue内部会在对应的时机加上类名,具体类名如下:

  • name-enter-from: 可以理解为:元素隐藏到显示的第一个状态;比如:我想让元素从下面开始进入布局,这时候name-enter-from就可以下面属性transform:translateY(-200%)
  • name-enter-active:元素在隐藏到显示过渡添加的类名,在过渡/动画完成之后移除,这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数
  • name-enter-to:定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时 v-enter-from 被移除),在过渡/动画完成之后移除。
  • name-leave-from:定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。
  • name-leave-active:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。
  • name-leave-to:离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时 v-leave-from 被移除),在过渡/动画完成之后移除。

一般我们只需记住name-enter-from,name-enter-active,name-leave-active,name-leave-to即可,name-enter-toname-leave-from一般与元素最终样式保持一致

<template>
  <div class="ani">
    <button @click="handleClick">改变</button>
    <div class="dialog">
      <div class="dialog-mask" v-show="flag"></div>
      <transition name="fade">
        <div class="dialog-body" v-show="flag">
          <button @click="handleClick">关闭</button>
        </div>
      </transition>
    </div>
  </div>
</template>

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

const flag: Ref<boolean> = ref(false)

const handleClick = () => {
  flag.value = !flag.value
}

</script>
<style scoped>
.dialog-mask {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  background: rgba(0, 0, 0, 0.185);
}
.dialog-body {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  margin: auto;
  width: 400px;
  height: 300px;
  border-radius: 20px;
  background: #fff;
  box-shadow: 0 0 2px #fff;
  transform-origin: center;
}

 /* 动画方式 */
.fade-enter-active {
  animation: fade-in 1s ease-in-out;
}
.fade-leave-active {
  animation: fade-in 1s ease-out reverse;
}
@keyframes fade-in {
  0% {
    transform: scale(0.7);
    opacity: 0;
  }
  50% {
    transform: scale(1.1);
    opacity: 1;
  }
  100% {
    transform: scale(1);
    opacity: 1;
  }
}
 /* 类名方式 */
/* .fade-enter-active {
  transition: all 0.3s ease-in;
}
.fade-leave-active {
  transition: all 0.1s ease-out;
}
.fade-enter-to {
  transform: translate(0, 10%);
}
.fade-enter-from {
  transform: translate(0, -200%);
}
.fade-leave-to {
  transform: scale(1.1);
  opacity: 0;
} */
</style>

除了类名,transition组件还提供了过渡的钩子

<transition
  @before-enter="beforeEnter"
  @enter="enter"
  @after-enter="afterEnter"
  @enter-cancelled="enterCancelled"
  @before-leave="beforeLeave"
  @leave="leave"
  @after-leave="afterLeave"
  @leave-cancelled="leaveCancelled"
  :css="false"
>
  <!-- ... -->
</transition>
methods: {
  // --------
  // 进入时
  // --------

  beforeEnter(el) {
    // ...
  },
  // 当与 CSS 结合使用时
  // 回调函数 done 是可选的
  enter(el, done) {
    // ...
    done()
  },
  afterEnter(el) {
    // ...
  },
  enterCancelled(el) {
    // ...
  },

  // --------
  // 离开时
  // --------

  beforeLeave(el) {
    // ...
  },
  // 当与 CSS 结合使用时
  // 回调函数 done 是可选的
  leave(el, done) {
    // ...
    done()
  },
  afterLeave(el) {
    // ...
  },
  // leaveCancelled 只用于 v-show 中
  leaveCancelled(el) {
    // ...
  }
}

注意:

多个元素之间的过渡需要加上mode属性,可选值:in-out新增元素先进入,移除元素再移除,out-inin-out相反。

列表过渡使用transition-group

Provide/Inject

用于父组件向子孙组件传递数据,即在父组件使用Provide定义的数据,子孙组件都能Inject访问

父组件

<script setup lang="ts">
import { provide, Ref, ref } from 'vue';
import AnimationComp from './AnimationComp.vue';
provide<Ref<boolean>>('flag', ref(false))
</script>
<template>
  <AnimationComp></AnimationComp>
</template>

子组件

<template>
   <p>injectFlag {{ injectFlag }}</p>
		<button @click="handleClick">触发</button>
</template>
<script setup lang="ts">
import { Ref, ref, reactive, inject } from 'vue';
// 如果要修改值需赋上默认值,也就是第二个参数,不然ts会报错
const injectFlag = inject<Ref<boolean>>('flag', ref(false))
const handleClick = () => {
  injectFlag.value = !injectFlag.value
}
</script>

注意:

​ 默认情况下,通过Provide定义的数据不是响应式的,除非用refreactive定义的数据

EventBus 的简单实现

// 1. 能够监听事件,并且一个事件可以被多个地方监听
// 2. 触发事件,传入对应的事件名称能够触发已经监听的地方,并且传递参数


type TParams = string | number | symbol

interface IBus {
  deps: TEventName,
  emit: (name: TParams, ...args: Array<any>) => void,
  on: (name: TParams, callback: Function) => void
}

type TEventName = {
  [key: TParams]: Array<Function>
}

class Bus implements IBus {
  deps: TEventName;
  constructor() {
    this.deps = {}
  }
  emit(name: TParams, ...args: Array<any>) {
    let fn:Array<Function> = this.deps[name] || []
    fn.forEach((item:Function) => {
      item.apply(this, args)
    })
  }
  on(name: TParams,  callback: Function) {
    let fn:Array<Function> = this.deps[name] || []
    fn.push(callback)
    this.deps[name] = fn
  }
}

export default new Bus

自动引入插件

自动引入相关插件的依赖,不需要import

unplugin-auto-import/vite

指令相关

v-model(破坏性更新)

实现双向绑定指令;

vue3 与vue2 v-model区别:

  1. vue3 实现v-model的方式是<Child @update:modelValue = "var = $event" :modelValue="var" />, vue2的实现方式是<input @input="var = $event.target.value" :value="var" />, 默认值不一样了。
  2. vue3 将 v-mode :value.sync合并了,所以vue3去掉了:value.sync,取而代之的是v-model:title
  3. vue3 支持自定义修饰符v-model:title.sync

使用方法:

父组件

<template>
  <VModelChildComp v-model="isShow" v-model:title.replace="title" />
</template>
<script setup lang="ts">
import { provide, Ref, ref } from 'vue';
import VModelChildComp from './VModelChildComp.vue';

const isShow: Ref<boolean> = ref(false)
const title: Ref<string> = ref('我是父标题')
</script>

子组件

<template>
  <h2>{{ title }}</h2>
  <button @click="handleClosed">关闭</button>
  <div class="vmodel">{{ modelValue ? '展示' : '关闭' }}</div>
</template>

<script setup lang="ts">
type Props = {
  modelValue: boolean,
  title: string,
  // 默认的值modelValue的修饰符
  modelModifiers?: {
  	good: boolean
	}
  // 修饰符
  titleModifiers?: {
    replace: boolean
    old: boolean
  }
}
const props = defineProps<Props>()

const emits = defineEmits(['update:modelValue', 'update:title'])

const handleClosed = () => {
  console.log(props.titleModifiers); // { replace: true } 这是old父组件没传就是undefined
  const tempTitle = '! 我是子标题 !'
  emits('update:title', props.titleModifiers?.replace ? tempTitle..replaceAll('!', '') : tempTitle)
  emits('update:modelValue', !props.modelValue)
}
</script>
自定义指令(破坏性更新)

使用指令可以获取到dom节点,从而进行一系列操作。

一个自定义指令被定义为一个包含类似于组件的生命周期钩子的对象。

指令有以下几个参数,可以钩子在DirectiveBinding参数中获取到对应的值:

  • 指令参数: v-xxx:param
  • 指令修饰符:v-xxx:param.x.y,可以指定多个修饰符
  • 指令传递的值:v-xxx:param="value"
组件内部指令
<template>
  <div class="box" v-style:border-left="'1px solid #000'">
    <div class="header">
      <h3>我是头部</h3>
    </div>
    <div class="body" v-style:color.background="'blue'">我是body</div>
  </div>
</template>

<script setup lang="ts">
import { Directive, DirectiveBinding } from 'vue';

const vStyle: Directive<HTMLDivElement, any> = {
  created() {
    // 元素初始化时被调用
    console.log(`vMove ---- created`);
  },
  beforeMount() {
    // 元素挂载前被调用
    console.log(`vMove ---- beforeMount`);
  },
  mounted(el, binding: DirectiveBinding<string>) {
    console.log(`vMove ---- mounted`);
    console.log(binding.arg); // color
    console.log(binding.modifiers); // {background: true}
    console.log(binding.value); // green
    if (binding.arg === 'color') {
      const prop: string = Object.keys(binding.modifiers)[0] || 'color'
      el.style.setProperty(prop, binding.value)
    } else {
      el.style.setProperty(binding.arg as string, binding.value)
    }
    // 元素挂载后被调用
    // el.style.background = binding.value
  },
  beforeUpdate() {
    // 元素更新前被调用
    console.log(`vMove ---- beforeUpdate`);
  },
  updated() {
    // 元素更新后被调用
    console.log(`vMove ---- updated`);
  },
  beforeUnmount() {
    // 元素卸载前被调用
    console.log(`vMove ---- beforeUnmount`);
  },
  unmounted() {
    // 元素卸载后被调用
    console.log(`vMove ---- unmounted`);
  },
}
</script>
<style scoped>
.box {
  position: absolute;
  width: 400px;
  height: 300px;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  border: 1px solid #ccc;
}
.header {
  height: 60px;
  line-height: 60px;
  background: salmon;
  text-align: center;
  color: #fff;
}
.header h3 {
  cursor: pointer;
}
.body {
  height: calc(100% - 60px);
  background: rgb(237, 202, 202);
}
</style>

指令简化形式:

对于自定义指令来说,需要在 mountedupdated 上实现相同的行为、又并不关心其他钩子的情况很常见。此时我们可以将指令定义成一个下面这样的函数:

import { Directive, DirectiveBinding } from 'vue'
const vColor: Directive<HTMLElement, string> = (el: HTMLElement, binding: DirectiveBinding<string>) => {
  	// 这会在 `mounted` 和 `updated` 时都调用
  	el.style.color = binding.value 
}
全局自定义指令
const app = createApp({})

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

自动化注册全局自定义指令

目录

- src
	- directives
		- index.ts
		- v-focus.ts

v-focus.ts

import { Directive, DirectiveBinding } from 'vue';

const vFocus: Directive<HTMLFormElement, any> = (el: HTMLFormElement) => {
  el.focus()
}

export default vFocus

index.ts

const directives = import.meta.globEager('./**/v-*.ts') // 遍历当前文件夹中v-*.ts的文件,支持二级目录

const getDirectiveName = (str: string): string => {
  if (!str) return ''
  const patten: RegExp = /v\-(.*)\.ts$/ // v-[name]
  const name: RegExpMatchArray | null = str.match(patten)
  return name ? name[1] : ''
}

export default function install(app: any) {
  for (const [key, module] of Object.entries(directives)) {
    const name = getDirectiveName(key)
    app.directive(name, module.default)
  }
}

main.ts

import directives from '@/directive/index'
const app = createApp(App)
app.use(directives)

hooks

hooks 类似于vue2的mixins,一般用于对公共逻辑的封装,对外抛出对应的数据,有利于代码的解耦

官方提供的hooks
  • useAttrs:在子组件 获取父组件传递过来没有被props接收的数据
  • useSlot:获取插槽数据
  • ...
自定义hooks

hooks/useMouse.ts

import { reactive, onMounted, onBeforeUnmount } from 'vue';
type TCoordinates = {
  x: number,
  y: number
}
export default (): TCoordinates => {
  const coordinates = reactive<TCoordinates>({
    x: 0,
    y: 0
  })
  const update = (e: MouseEvent) => {
    coordinates.x = e.pageX
    coordinates.y = e.pageY
  }
  onMounted(() => {
    document.addEventListener('mousemove', update) 
  })
  onBeforeUnmount(() => {
    document.removeEventListener('mousemove', update)
  })

  return coordinates
}
export type Coordinates = TCoordinates

App.vue


<script setup lang="ts">
import { provide, Ref, ref, toRef, toRefs } from 'vue';
const coordinates: Coordinates = useMouse()
// 注意:因为useMouse返回的值是用reactive创建的,所以采用解构的方式只返回了值,并不是响应式数据,所以需要toRefs转化成响应式的
// const { x, y } = toRefs<Coordinates>(useMouse()) 
</script>
<template>
  <div>
    坐标x: {{ coordinates.x }}
    坐标x: {{ coordinates.y }}
  </div>
</template>

全局函数与变量

vue3挂载全局函数与变量是挂载到const app = createApp(App)实例下的app.config.gobalProperties对象

挂载一个全局工具函数

const app = createApp(App)
app.config.globalProperties.$utils = {
  isObject<T>(obj: T): boolean {
    return Object.prototype.toString.call(obj) === '[object Object]' && Object.keys(obj).length > 0
  },
}

在模板中使用

<template>
  {{ $utils.isObject({name:1}) }}
</template>

script中使用

import { getCurrentInstance } from 'vue'
const { appContext: { config } } = getCurrentInstance()
console.log(config.globalProperties.$utils.isObject({}))

全局插件

通过全局api去调用插件的方式,常见的有loading

<template>
  <div class="global-loading" v-show="isShow">
    <div class="loading-content">
		加载中...
  	</div>
  </div>
</template>

<script setup lang="ts">
import { ref, Ref } from 'vue';
const isShow: Ref<boolean> = ref(false)
const show = () => {
  if (isShow.value) return
  isShow.value = true
}
const hide = () => {
  if (!isShow.value) return
  isShow.value = false
}
defineExpose({
  show,
  hide
})
</script>
import { createVNode, render, App, VNode } from 'vue';

import LoadingComp from './index.vue'

export default function install(app: App) {
  const loadingVNode:VNode = createVNode(LoadingComp) // 先将SFC组件转成vnode
  render(loadingVNode, document.body) // 再将vnode渲染到对应的节点
  app.config.globalProperties.$loading = loadingVNode.component?.exposed // 挂载到全局函数
}

pinia === vuex5

pinia是下一代的vuex,是一个全局状态管理的工具,支持options Api 与 Composition Api的方式定义,pinia去掉了vuex的mutations,统一使用actions去修改store的状态

基本使用

import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'
// 创建pinia实例
const store = createPinia()
const app = createApp(App)
// 注册到vue
app.use(store).mount('#app')

./store/index.ts

import { defineStore } from 'pinia'
import { StoreEnum } from './store-name';

export type TUser = {
  id: number,
  name: string,
  age: number
}

// 可定义多个store, 需要将唯一名称传入defineStore第一个参数
export const useUserStore = defineStore(StoreEnum.USERSTORE, {
  state: (): TUser => ({
    id: 0,
    name: '',
    age: 0
  }),
  getters: {
    formatAge: (state): string => `$---${state.age}`
  },

  actions: {
    addAge(age?: number) {
      this.age = age ? age : this.age + 1
    },
    fetchUser(id: number):Promise<TUser> {
      return new Promise<TUser>((resolve) => {
        setTimeout(() => {
          const user: TUser = {
            id,
            name: 'gzhh',
            age: 24
          }
          this.$state = user
          resolve(user)
        }, 1000)
      })
    }
  }
})

./store/store-name.ts

export const enum StoreEnum {
  USERSTORE = 'User'
}

在组件中使用

<template>
  <div class="pinia-demo">
    <button @click="handleAddAge">增加年龄</button>
    <button @click="handleReset">重置</button>
    {{ user.formatAge }}
    {{ user.name }}
  </div>
</template>

<script setup lang="ts">
import { onMounted } from 'vue'
import { useUserStore, TUser } from '@/store'

const user = useUserStore()

onMounted(async () => {
  const currUser: TUser = await user.fetchUser(1)
  console.log(currUser)
})

user.$subscribe((...args) => {
  console.log(args);
})
// 监听action触发
user.$onAction((context) => {
  console.log(context);
  context.after((state) => {
    console.log('====>', state);
  })
})
const handleAddAge = () => {
  // 方式1
  // user.age++

  // 方式2
  // user.$patch({
  //   age: ++user.age
  // })

  // // 方式3
  // user.$patch((state) => {
  //   state.name = 'gzhhhh'
  //   state.age++
  // })

  // // 方式4
  // user.$state = {
  //   name: 'gzhhh',
  //   age: ++user.age
  // }

  // // 方式5
  user.addAge()
}

const handleReset = () => {
  user.$reset()
}

</script>

改变state的方式有5中方式:

  1. 直接调用属性更改: user.age++

  2. 调用$patch批量更改

    user.$patch({
    	age: ++user.age,
      name: 'gzhhh'
    })
    
  3. 调用$patch工厂函数批量更改

    user.$patch((state) => {
      state.name = 'gzhhh'
      state.age = 24
    })
    
  4. 直接修改store的state对象

    user.$state = {
    	name: 'gzhhh',
    	age: ++user.age
    }
    
  5. 调用actions方法修改(推荐),项目大时更好维护

     user.$state = {
        name: 'gzhhh',
        age: ++user.age
     }
    

pinia提供监听store值更改的钩子$subscribe, 类似于组件的watchEffect,可以用于处理数据持久化

const stopUserSubscribe = user.$subscribe((mutation, state) => {
  console.log(mutation)
  console.log(state);
}, {detached: false})

mutation主要包含三个属性值:

  • events:当前state改变的具体数据,包括改变前的值和改变后的值等等数据
  • storeId:是当前store的id — user
  • type:用于记录这次数据变化是通过什么途径

detached: 布尔值,默认是 false,正常情况下,当订阅所在的组件被卸载时,订阅将被停止删除,如果设置detached值为 true 时,即使所在组件被卸载,订阅依然在生效。

pinia插件机制

pinia提供了插件机制,供我们拓展pinia的功能

import { createApp } from 'vue'
import { createPinia, PiniaPluginContext } from 'pinia'
import { StoreEnum } from '@/store/store-name';
const store = createPinia()
const app = createApp(App)

const __privateKey__ = '__pinia__'
const setLocalStorage = (key: string, value: any) => {
  localStorage.setItem(key, JSON.stringify(value))
}
const getLocalStorage = (key: string): object => {
  return localStorage.getItem(key) ? JSON.parse(localStorage.getItem(key) as string) : {}
}
const piniaLocalStoragePlugin = (options: any) => {
  return (context: PiniaPluginContext) => {
    // 每一个store被使用时,会触发一次这个函数,可以在这个函数进行拓展
    const key: string = __privateKey__ + context.store.$id
    context.store.$state = getLocalStorage(key)
    context.store.$subscribe((mutations, state) => {
      if(options.persistenceStore.includes(mutations.storeId)) {
        setLocalStorage(key, state)
      }
    })
  }
}
// pinia注册插件
store.use(piniaLocalStoragePlugin({
  persistenceStore: [StoreEnum.USERSTORE]
}))
app.use(store).mount('#app')

vue-router 4.x

快速上手

  1. npm install vue-router -S

  2. 新建src/router/index.ts

    import { createRouter , createWebHashHistory, RouteRecordRaw } from 'vue-router';
    
    const routes: Array<RouteRecordRaw> = [{
      path: '/login',
      component: () => import('../views/Login.vue'),
      children: [{
      	// 子路由
        path: 'login-detail/:id/:name',
        name: 'LoginDetail',
        component: () => import('../views/LoginDetail.vue')
      }]
    }, {
      path: '/register',
      component: () => import('../views/Register.vue')
    }]
    
    const router = createRouter({
      history: createWebHashHistory(),
      routes  
    })
    
    export default router
    
  3. main.ts中注册

    import Router from '@/router';
    app.use(Router).mount('#app')
    
  4. App.vue 中使用router-view组件承接路由组件

    <router-view></router-view>

路由编程式导航

<script>
  import { useRouter, useRoute } from 'vue-router'
  const router = useRouter()
  const route = useRoute()
  
  const toDetail = () => {
    router.push('/login/login-detail')
  }
  console.log(route.query) // 当前路由的query参数
  console.log(route.params) // 当前路由的params参数
</script>

router 是 整个路由对象

route 是 当前路由相关数据的对象

路由传参的三种方式

  1. 通过query的方式

    router.push({
      path: '/login/login-detail', // path也可换成name
    	query: {
        id: 1,
        name: 'gzhhh'
      }
    })
    
    // 子组件接收
    console.log(route.query.id)
    console.log(route.query.name)
    
    • 通过query的方式传参,页面刷新不会丢失参数
    • 声明路由可以通过path name
  2. 通过params的方式

    router.push({
      name: 'LoginDetail', // path也可换成name
    	params: {
        id: 1,
        name: 'gzhhh'
      }
    })
    
    // 子组件接收
    console.log(route.params.id)
    console.log(route.params.name)
    
    • 通过params方式是将参数存在内存中,所以刷新便会丢失参数
    • 声明路由必须使用name
  3. 通过url的方式

    //路由定义文件
    {
      	// 子路由
        path: 'login-detail/:id/:name',
        name: 'LoginDetail',
        component: () => import('../views/LoginDetail.vue')
    }
    
    router.push({
      name: 'LoginDetail', // path也可换成name
    	params: {
        id: 1,
        name: 'gzhhh'
      }
    })
    
    // 子组件接收
    console.log(route.params.id)
    console.log(route.params.name)
    
    • 通过占位符的方式传递参数,刷新之后不会丢失参数
    • 声明路由必须使用name
    • 必须现在定义路由的path字段加上对应的占位符

命名视图

指定组件展示在哪个

{
  path: '/register',
  component: () => import('../views/Register.vue'),
  children: [{
    path: 'register-detail',
    name: 'RegisterDetail',
    components: {
      default: () => import('../views/RegisterDetailDetault.vue') //默认的路由名字
      a: () => import('../views/RegisterDetailA.vue'), //a 指定路由的名字
      b: () => import('../views/RegisterDetailB.vue')  //b 指定路由的名字
    }
  }
<router-view></router-view> // 展示默认的组件
<router-view name="b"></router-view> // 展示b组件
<router-view name="a"></router-view> // 展示c组件