vue3学习笔记(总)——ts+组合式API(setup语法糖)

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

文章目录

1. ref全家桶

1.1 ref()

接受一个内部值返回一个响应式的、可更改的 ref 对象此对象只有一个指向其内部值的属性 .value

type M = {
  name: string,
}

const msg1: Ref<string> = ref('字符串')
// const msg1 = ref<string>('字符串')
const msg2 = ref<M>({name: '多多'})

const changeMsg = () =>{
  msg1.value = '已修改'
  msg2.value.name = '小多改变了'
}
  • ref也可以获取dom属性
<div ref="dom">dom内容</div>
// 名字要与ref绑定的名字一样
const dom = ref<HTMLElement | null>(null)

const changeMsg = () => {
  console.log('dom.value?.innerText :>> ', dom.value?.innerText);
  console.log('dom :>> ', dom)
}

在这里插入图片描述

1.2 isRef()以及isProxy()

  • isRef检查某个值是否为 ref
  • isProxy检查一个对象是否是由 reactive()readonly()shallowReactive()shallowReadonly() 创建的代理。

1.3 shallowRef()

ref() 的浅层作用形式。

type M = {
  name: string,
}

const msg2 = shallowRef<M>({name: '多多'})

const changeMsg = () =>{
  // msg2.value.name = '小多改变了' // 视图不会改变
  msg2.value = {
    name: '改变了'
  }
}

1.4 triggerRef()

强制触发依赖于一个浅层 ref 的副作用这通常在对浅引用的内部值进行深度变更后使用。强制更新

注意 ref()和shallowRef()不能一块写不然会影响shallowRef 造成视图更新

const msg1 = ref('字符串')
const msg2 = shallowRef({name: '多多'})

const changeMsg = () =>{
  msg1.value = '改变了'
  msg2.value.name = '小多改变了被影响' // 视图也会改变
}

由于 ref底层调用了triggerRef()所以会造成视图的强制更新

const msg2 = shallowRef({name: '多多'})

const changeMsg = () =>{
  msg2.value.name = '小多改变了'
  triggerRef(msg2)	// 视图强制更新了
}

1.5 customRef()

创建一个自定义的 ref显式声明对其依赖追踪和更新触发的控制方式。

主要应用是防抖

import { customRef } from 'vue'
function MyRef<T>(value: T, delay = 500) {
  let timer: any
  return customRef((track, trigger) => {
    return {
      get() {
        track() /* 收集依赖 */
        return value
      },
      set(newVal) {
        clearTimeout(timer)
        timer = setTimeout(() => {
          console.log('触发了');
          value = newVal
          timer = null
          trigger() /* 触发依赖视图更新 */
        }, delay)
      },
    }
  })
}
const msg1 = MyRef<string>('字符串')
// const msg1 = ref<string>('字符串')
const msg2 = MyRef({ name: '多多' })

const changeMsg = () => {
  // msg1.value = '小多改变了'
  msg2.value = {
    name: '改变'
  }
}

1.6 unref()

如果参数是 ref则返回内部值否则返回参数本身。这是 val = isRef(val) ? val.value : val 计算的一个语法糖。

类型

function unref<T>(ref: T | Ref<T>): T

示例

function useFoo(x: number | Ref<number>) {
  const unwrapped = unref(x)
  // unwrapped 现在保证为 number 类型
}

2. reactive全家桶

2.1 reactive()

返回一个对象的响应式代理。

interface Msg = {
	name: string
}
// ref 支持所有类型reactive 只支持引用类型 Array Object Map Set...
// ref 取值赋值都需要添加.value	reactive 不需要添加.value
const msg1 = ref({name: 'ref---多多'})
const msg2:Msg = reactive({ name: 'reactive---多多' })
// 不推荐
// const msg2 = reactive<Msg>({ name: 'reactive---多多' })

const changeMsg = () => {
  msg1.value.name = 'ref---小多'
  msg2.name = 'reactive---小多'
}


  • reactive proxy 不能直接赋值否则会破坏响应式对象
  • 不推荐使用 reactive() 的泛型参数因为处理了深层次 ref 解包的返回值与泛型参数的类型不同。

解决方案1. 数组可以使用push加解构

<template>
  <el-button @click="add">添加</el-button>
    <hr class="mtb20" />
    <ul>
      <li :key="index" v-for="(item, index) in list">{{ item.name }}</li>
    </ul>
</template>

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

type List = {
  name: string
}

let list: List[] = reactive([])

const add = () => {
  // 模拟后端获取数据
  setTimeout(() => {
    let res: List[] = [
      { name: '多多' },
      { name: '小多' },
      { name: '凡凡' },
      { name: '小凡' },
    ]
    list.push(...res)
  }, 1000)
}
</script>

<style lang="less" scoped></style>

解决方案 2. 变成一个对象把数组作为一个属性去解决

<template>
  <el-button @click="add">添加</el-button>
    <hr class="mtb20" />
    <ul>
      <li :key="index" v-for="(item, index) in list.arr">{{ item.name }}</li>
    </ul>
</template>

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

type List = {
  name: string
}

let list: {arr: List[]} = reactive({
  arr: []
})

const add = () => {
  // 模拟后端获取数据
  setTimeout(() => {
    let res: List[] = [
      { name: '多多' },
      { name: '小多' },
      { name: '凡凡' },
      { name: '小凡' },
    ]
    list.arr = res
  }, 1000)
</script>

<style lang="less" scoped></style>

2.2 readonly()

接受一个对象 (不论是响应式还是普通的) 或是一个 ref返回一个原值的只读代理。

// readonly 无法更改只读 但会受原始数据的影响原始数据改变则相应改变
let msg1 = reactive({ name: '改变' })

const change = () => {
  let copy = readonly(msg1)
  msg1.name = '1111'
  // copy.name = '2222' // 无法更改
  console.log('msg1,copy :>> ', msg1, copy)
}

2.3 shallowReactive() 和 shallowReadonly()

  • shallowReactivereactive() 的浅层作用形式
  • shallowReadonlyreadonly() 的浅层作用形式

3. to系列全家桶

只对响应式对象有效果对普通对象无效

3.1 toRef()

基于响应式对象上的一个属性创建一个对应的 ref。这样创建的 ref 与其源属性保持同步改变源属性的值将更新 ref 的值反之亦然。

let msg1 = reactive({ name: '多多', age: 18 })
let age = toRef(msg1, 'age')

const edit = () => {
  age.value++
}

应用场景 useDemo(value) 需要一个属性但定义的是对象则可以单独把属性取出来使用而不破坏属性的响应性

3.2 toRefs()

将一个响应式对象转换为一个普通对象这个普通对象的每个属性都是指向源对象相应属性的 ref。每个单独的 ref 都是使用 toRef() 创建的。

let msg1 = reactive({ name: '多多', age: 18 })

// toRefs源码类似
const myTORefs = <T extends object>(object: T) => {
  const map: any = {}
  for (const key in object) {
    map[key] = toRef(object, key)
  }
  return map
}

// let { name, age } = msg1 /* 直接解构 不具备响应性更改不会造成视图更新 */
let { name, age } = toRefs(msg1)  /* 使其解构的属性具备响应性 */

const edit = () => {
  name.value = '小多'
  age.value++
}

3.3 toRaw()

根据一个 Vue 创建的代理返回其原始对象。

console.log('msg1, toRaw(msg1) :>> ', msg1, toRaw(msg1));

在这里插入图片描述

4. computed计算属性

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

  1. 函数形式
let price ref<number>(0)
let m = computed<string>(()=>{
   return `$` + price.value
})
  1. 对象形式
let price = ref<number | string>(1)//$0
let mul = computed({
   get: () => {
      return price.value
   },
   set: (value) => {
      price.value = 'set' + value
   }
})

案例购物车总价

<template>
  <table>
    <thead>
      <tr>
        <th align="center">名称</th>
        <th align="center">数量</th>
        <th align="center">价格</th>
        <th align="center">操作</th>
      </tr>
    </thead>
    <tbody>
      <tr :key="index" v-for="(item, index) in shop">
        <td align="center">{{ item.name }}</td>
        <td align="center">
          <button @click="addOrSub(item, false)">-</button> {{ item.num }}
          <button @click="addOrSub(item, true)">+</button>
        </td>
        <td align="center">{{ item.price * item.num }}</td>
        <td align="center"><button @click="del(index)">删除</button></td>
      </tr>
    </tbody>
    <tfoot>
      <td></td>
      <td></td>
      <td></td>
      <td>总价{{ $total }}</td>
    </tfoot>
  </table>
</template>

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

type Shop = {
  name: string
  price: number
  num: number
}

const shop = reactive<Shop[]>([
  {
    name: '苹果',
    price: 10,
    num: 1,
  },
  {
    name: '蛋糕',
    price: 20,
    num: 1,
  },
  {
    name: '面包',
    price: 5,
    num: 1,
  },
])
let $total = ref<number>(0)

$total = computed<number>(() => {
  return shop.reduce((prev, next) => {
    return prev + next.num * next.price
  }, 0)
})

const addOrSub = (item: Shop, flag: boolean): void => {
  if (item.num > 0 && !flag) {
    item.num--
  }
  if (item.num < 99 && flag) {
    item.num++
  }
}

const del = (index: number) => {
  shop.splice(index, 1)
}
</script>

<style lang="less" scoped>
table,
tr,
td,
th {
  border: 1px solid #ccc;
  padding: 20px;
}
</style>

5. watch监听属性

详情可了解Vue3watch 的使用场景及常见问题

5.1 watch()

  • 第一个参数是不同形式的“数据源”它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组

  • 第二个参数是cb回调函数newVal,oldVal,onCleanup

  • 第三个参数是options配置项一个对象

    deep: true // 是否开启深层监听
    immediate: true // 是否立即调用一次
    flush: 'pre ’ | ‘sync’ | ‘post’ // 更新时机
    onTrack函数具备 event 参数调试用。将在响应式 property 或 ref 作为依赖项被追踪时被调用
    onTrigger函数具备 event 参数调试用。将在依赖项变更导致副作用被触发时被调用。

ref监听深层属性需要开启深层监听深层监听引用类型旧值与新值一样

reactive隐性开启深层监听

监听属性单一值需将其变为getter 函数

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

import { watch, reactive, ref } from 'vue'

let msg1 = reactive({
  one: {
    two: {
      three: '内容',
    },
  },
})
let msg2 = ref<string>('测试')
let msg3 = ref<string>('多多')
let msg4 = ref(1)
let msg5 = ref(2)

watch(
  ()=> msg1.one.two.three,
  (newVal, oldVal) => {
    console.log('newVal, oldVal :>> ', newVal, oldVal)
  }
)

watch(
  [msg2, msg3],
  (newVal, oldVal) => {
    console.log('newVal, oldVal :>> ', newVal, oldVal)
  }
)
watch(
  [msg3, ()=> msg4.value + msg5.value],
  (newVal, oldVal) => {
    console.log('newVal, oldVal :>> ', newVal, oldVal)
  }
)

在这里插入图片描述

onCleanup: onCleanup 接受一个回调函数这个回调函数在触发下一次 watch 之前会执行因此可以在这里取消上一次的网络请求亦或做一些内存清理及数据变更等任何操作。
作用场景 监听数据变化发起网络请求时

let count = 2;
const loadData = (data) =>
  new Promise((resolve) => {
    count--;
    setTimeout(() => {
      resolve(`返回的数据为${data}`);
    }, count * 1000);
  });

// 此时如果直接监听两次数据变更时间太短导致最后页面展示的data数据更新为 ’返回的数据为李四‘
// 原因数据每次变化都会发送网络请求但是时间长短不确定所以就有可能导致后发的请求先回来了所以会被先发的请求返回结果给覆盖掉。
setTimeout(() => {
  state.name = '李四';
}, 100);
setTimeout(() => {
  state.name = '王五';
}, 200);

// 第二次更新时间在第一次网络请求结束之前
watch(
  () => state.name,
  (newValue, oldValue, onCleanup) => {
    let isCurrent = true;
    onCleanup(() => {
      // 在下次监听更新之前执行
      isCurrent = false;
    });
    // 模拟网络请求
    loadData(newValue).then((res) => {
      // 取消上次网络请求上次网络请求还没完成就将isCurrent设置为false, 则不会变成第一次网络请求的结果顺序执行第二次监听的结果
      if (isCurrent) {
        data.value = res;
      }
    });
  }
);

5.2 watchEffect()

watch() 是懒执行的仅当数据源变化时才会执行回调。但在某些场景中我们希望在创建侦听器时立即执行一遍回调。举例来说我们想请求一些初始数据然后在相关状态更改时重新请求数据。

配置项
副作用刷新时机 flush 一般使用post

presyncpost
更新时机组件更新前执行强制效果始终同步触发组件更新后执行

其他配置项onTrack函数onTrigger函数

let msg1 = ref('多多测试')
let msg2 = ref('小多')

watchEffect(() => {
  console.log('watchEffect监听 : 默认执行顺序等同于开启立即执行的watch');
  const dom1 = document.querySelector('#dom1')
  console.log('dom1 :>> ', dom1)
  console.log('msg1 :>> ', msg1)
})

watchEffect(() => {
  console.log('watchEffect监听 : flush: "post"');
  const dom1 = document.querySelector('#dom1')
  console.log('post组件更新后执行dom1 :>> ', dom1)
}, {
  flush: 'post'
})

watch(
  msg1,
  (newVal, oldVal) => {
    console.log('watch监听 : ');
    const dom1 = document.querySelector('#dom1')
    console.log('dom1 :>> ', dom1)
    console.log('newVal,oldVal :>> ', newVal, oldVal)
  },
  {
    immediate: true,
  }
)

在这里插入图片描述

  1. watchEffect 默认监听也就是默认第一次就会执行
  2. 不需要设置监听的数据在 effect 函数中用到了哪个数据会自动进行依赖因此不用担心类似 watch 中出现深层属性监听不到的问题
  3. 只能获取到新值由于没有提前指定监听的是哪个数据所以不会提供旧值。

watchEffect监听可能出现的问题
在异步任务无论是宏任务还是微任务中进行的响应式操作watchEffect 无法正确的进行依赖收集。所以后面无论数据如何变更都不会触发 effect 函数。

在这里插入图片描述

解决方法
如果真的需要用到异步的操作可以在外面先取值再放到异步中去使用

在这里插入图片描述
清除副作用

watchEffect((onInvalidate) => {
  console.log('msg1 :>> ', msg1)
  onInvalidate(() => {
    // 第一次不执行
    console.log('before')
  })
})

在这里插入图片描述

停止监听
要手动停止一个侦听器请调用 watch 或 watchEffect 返回的函数

const unwatch = watchEffect(() => {})

// ...当该侦听器不再需要时
unwatch()

5.3 总结

  1. 当监听 Reactive 数据时
    • deep 属性失效会强制进行深度监听
    • 新旧值指向同一个引用导致内容是一样的。
  2. watchsourceRefImpl 类型时
    • 直接监听 state 和 监听 () => state.value 是等效的
    • 如果 ref 定义的是引用类型并且想要进行深度监听需要将 deep 设置为 true。
  3. watchsource 是函数时可以监听到函数返回值的变更。如果想监听到函数返回值深层属性的变化需要将 deep 设置为 true
  4. 如果想监听多个值的变化可以将 source 设置为数组内部可以是 Proxy 对象可以是 RefImpl 对象也可以是具有返回值的函数
  5. 在监听组件 props 时建议使用函数的方式进行 watch并且希望该 prop 深层任何属性的变化都能触发可以将 deep 属性设置为 true
  6. 使用 watchEffect 时注意在异步任务中使用响应式数据的情况可能会导致无法正确进行依赖收集。如果确实需要异步操作可以在异步任务外先获取响应式数据再将值放到异步任务里进行操作。

6. 组件

6.1 组件的生命周期

在这里插入图片描述

  1. beforeCreatecreated 两个生命周期在setup语法糖模式是没有的用setup去代替
  2. onBeforeMount 时读不到dom元素onMouted 以及之后的生命周期可以读取到dom元素。
  3. onBeforeUpdate 获取的是更新之前的domonUpdated 获取的是更新之后的dom
  4. onRenderTrackedonRenderTriggerd 用于调试获取收集依赖

在这里插入图片描述

6.2 全局组件的注册以及批量注册

全局注册

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

const app = createApp(App)
// 全局注册
app.component('MyComponent', MyComponent)

...
app.mount('#app')

批量注册例如elmUI的icon

// main.ts

// 如果您正在使用CDN引入请删除下面一行。
import * as ElementPlusIconsVue from '@element-plus/icons-vue'

const app = createApp(App)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component)
}

6.3 defineProps父给子传值

<!-- 父组件 -->
<A :list="[111, 222, 333]" :msg="msgFather"></A>
<el-divider> 无传递值 </el-divider>
<A></A>
// ts版本
// const props = defineProps<{msg:string}>() /* 不需要定义默认值时 */
// 定义默认值需要使用 withDefaults --- ts专有的
const props = withDefaults(defineProps<{ msg: string, list: number[] }>(), {
  msg: '默认值',
  list: () => []
})

// js版本
// const props = defineProps({
//   msg: {
//     type: String,
//     default: '默认值'
//   },
//   list: {
//     type: Array,
//     default: () => []
//   }
// })
// ts
// 也可以将类型声明提取出来传递数据多时推荐
type Props = { msg: string, list: number[] }

const props = withDefaults(defineProps<Props>(), {
  msg: '默认值',
  list: () => []
})

// 也可以使用响应性语法糖结构默认值 ---目前为实验性的需要显式启用
const { msg = '默认值', list= []} = defineProps<Props>()
响应性语法糖

响应性语法糖

6.4 defineEmits子给父传值

<script setup>emit 函数的类型标注也可以通过运行时声明或是类型声明进行

<script setup lang="ts">
// 运行时
const emit = defineEmits(['change', 'update'])

// 基于类型
const emit = defineEmits<{
  (e: 'change', id: number): void
  (e: 'update', value: string): void
}>()
</script>

例子

<!-- 父组件 -->
<A @on-click="getMsg" @change="getMsg2"></A>

<script setup lang="ts">
import { Ref } from 'vue';
import A from '@/components/A.vue'

const getMsg = (data: Ref<string>) => {
  console.log('data :>> ', data);
}

const getMsg2 = (id: number) => {
  console.log('id :>> ', id);
}

</script>
<!-- 子组件 -->
<template>
  <el-button @click="send">给父组件传值</el-button>
</template>

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

const sonMsg = ref('子组件内容')

// ts标注版本
// 第一个参数名字第二个参数传递的参数
const emit = defineEmits<{
  (e: 'on-click', sonMsg:Ref<string>):void
  (e: 'change', id: number):void
}>()

// js版本
// const emit = defineEmits(['on-click', 'change'])

const send = () => {
  emit('on-click', sonMsg)
  emit('change', 1111)
}


</script>

6.5 defineExpose

可以通过 defineExpose 编译器宏来显式指定在 <script setup> 组件中要暴露出去的属性

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

const sonMsg = ref('子组件内容')

const name = ref('多多')

const sonFn = () => {
  console.log('子组件中的方法 :>> ', name);
}

defineExpose({
  name,
  age: 18,
  sonFn,
  fn1: () => console.log('1 :>> ', sonMsg)
})

</script>
<!-- 父组件接收 -->
<template>
  <div class="app-container">
    <div>子给父传递的内容</div>
    <hr class="mtb20" />
    <AVue ref="aRef"></AVue>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import AVue from '@/components/A.vue'

const aRef = ref<InstanceType<typeof AVue> | null>(null)

console.log(aRef.value?.name);	// 组件还没挂载此时为 undefiend
onMounted(()=>{
  console.log(aRef.value?.age)	// 18
  console.log( aRef.value?.fn1());	// 1 :>>  ref('子组件内容')
})

</script>

应用场景 例如elm表单的方法

6.6 递归组件

<!-- Tree组件 -->
<template>
  <div @click.stop="clickTree(item, $event)" :style="{ 'marginLeft': '10px' }" v-for="item in treeData">
    <input v-model="item.checked" type="checkbox"> <span>{{ item.name }}</span>
    <!-- 可以不用定义组件名称直接使用Tree, 但为了防止文件名更改还是自定义名称好 -->
    <TreeItem v-if="item?.children?.length" :treeData="item?.children"></TreeItem>
  </div>

</template>


<script lang="ts">
// 自定义名称
export default {
  name:"TreeItem"
}
</script>

<script setup lang='ts'>
export type TreeType = {
  name: string
  checked: boolean
  children?: TreeType[]
}
defineProps<{
  treeData?: TreeType[]
}>()
/* 使用插件对组件命名 */
// defineOptions({
//   name: 'TreeItem',
// })

const clickTree = (item: TreeType, e: Event) => {
  console.log(item, e);
}

</script>
<!-- 父组件 -->
<template>
    <Tree :treeData="data"></Tree>
</template>

<script setup lang="ts">
import { reactive } from 'vue'
import Tree, {TreeType} from '@/components/Tree.vue'

const data: TreeType[] = reactive([{
  name: '1',
  checked: false
}])

</script>

使用unplugin-vue-define-options进行命名

npm i unplugin-vue-define-options -D
// tsconfig.json
{
  "compilerOptions": {
    // ...
    "types": ["unplugin-vue-define-options/macros-global" /* ... */]
  }
}
// vite
// vite.config.ts
import DefineOptions from 'unplugin-vue-define-options/vite'
import Vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [Vue(), DefineOptions()],
})

6.7 动态组件

<template>
  <div class="tabs">
    <div @click="switchTab(item)" :class="[currentCom == item.com ? 'active' : '']" class="tab" v-for="item in tabsData"> {{ item.name }}</div>
  </div>
  <hr class="mtb20">
  <component :is="currentCom"></component>
</template>

<script setup lang="ts">
import AVue from '@/components/A.vue';
import BVue from '@/components/B.vue';
import CVue from '@/components/C.vue';
import { reactive, shallowRef, markRaw, AllowedComponentProps, ComponentCustomProps, ComponentOptionsMixin, DefineComponent, ExtractPropTypes, VNodeProps } from 'vue';

type Com = DefineComponent<{}, {}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {}, string, VNodeProps & AllowedComponentProps & ComponentCustomProps, Readonly<ExtractPropTypes<{}>>, {}>

// 注意不要使用ref使用shallowRef进行性能优化绕开深度响应对深层对象不做处理
const currentCom = shallowRef(AVue)

const tabsData = reactive([{
  name: 'A组件',
  com: markRaw(AVue)	/* 使用markRaw包裹不进行proxy代理即不进行响应式处理 */
}, {
  name: 'B组件',
  com: markRaw(BVue)
}, {
  name: 'C组件',
  com: markRaw(CVue)
}])

const switchTab = (e: { com: Com; }):void => {
  currentCom.value = e.com
}

</script>

<style lang="less" scoped>
@border: #ccc;

.tabs {
  display: flex;
  align-items: center;
  .tab {
    border: 1px solid @border;
    padding: 15px 15px 20px 15px;
    margin: 0 10px;
    cursor: pointer;
  }
  .active {
    background-color: #7db6eb;
  }
}
</style>

注意 reactive 会进行proxy 代理 而我们组件代理之后毫无用处 节省性能开销 推荐我们使用 shallowRef 或者 markRaw 跳过proxy 代理

markRaw

将一个对象标记为不可被转为代理。返回该对象本身。
类型

function markRaw<T extends object>(value: T): T

示例

const foo = markRaw({})
console.log(isReactive(reactive(foo))) // false

// 也适用于嵌套在其他响应性对象
const bar = reactive({ foo })
console.log(isReactive(bar.foo)) // false

7. 插槽

  1. 插槽内容可以是任意合法的模板内容不局限于文本。例如我们可以传入多个元素甚至是组件
  2. v-slot 有对应的简写 #因此 <template v-slot:header> 可以简写为 <template #header>

7.1 匿名插槽

在这里插入图片描述

<!-- 子组件 -->
<button class="fancy-btn">
  <!-- 插槽出口 -->
  <slot>里面可以填写默认内容</slot> 
</button>
<!-- 父组件 -->
<FancyButton>
  Click me! <!-- 插槽内容 -->
</FancyButton>

<!-- 或 -->
<FancyButton>
   <template v-slot>
      Click me!
    </template>
</FancyButton>

<!-- 或 -->
<FancyButton>
   <template #default>
      Click me!
    </template>
</FancyButton>

7.2 具名插槽

在这里插入图片描述

<!-- 子组件 -->
<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>
<BaseLayout>
  <template v-slot:header>
    <!-- header 插槽的内容放这里 -->
  </template>
  <template #default>
    <!-- main插槽的内容放这里 -->
  </template>
  <template #footer>
    <!-- footer插槽的内容放这里 -->
  </template>
  <!-- 隐式的默认插槽 -->
  ...
</BaseLayout>

注意 当一个组件同时接收默认插槽和具名插槽时所有位于顶级的非 节点都被隐式地视为默认插槽的内容。

7.3 动态插槽

动态指令参数v-slot 上也是有效的即可以定义下面这样的动态插槽名

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

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

<script setup lang="ts">#default
import BaseLayout from '@/components/base-layout.vue';
import { ref} from 'vue';

const dynamicSlotName = ref<string>('header')

</script>

7.4 作用域插槽

使用场景 父组件可以拿到子组件的值在某些场景下插槽的内容可能想要同时使用父组件域内和子组件域内的数据

在这里插入图片描述

  1. v-slot="slotProps" 可以类比这里的函数签名和函数的参数类似我们也可以在 v-slot 中使用解构
<AVue>
    <template #header="headerProps">
      标题: {{ headerProps.headerMsg }}
    </template>
    <template #default="{ index, data }">
      <div>{{ index }} --- {{ data.name }} --- {{ data.age }}</div>
    </template>
    <template #footer="{ footerMsg }">
      底部: {{ footerMsg }}
    </template>
</AVue>
<template>
  <slot name="header" :headerMsg="msg1"></slot>
  <hr class="mtb20">
  <div v-for="(item, index) in dataSlot">
    <slot :index="index" :data="item"></slot>
  </div>
  <hr class="mtb20">
  <slot name="footer" :footerMsg="msg2"></slot>
</template>

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

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

const msg1 = ref<string>('信息1')
const msg2 = ref<string>('信息2')
const dataSlot = reactive<SlotType[]>([
  {
    name: '多多',
    age: 20
  }, {
    name: '小多',
    age: 18
  }, {
    name: '图图',
    age: 19
  }
])
</script>

在这里插入图片描述

8. 内置组件

8.1 异步组件&代码分包&suspense

顶层 await

<script setup> 中可以使用顶层 await。结果代码会被编译成 async setup()

<script setup>
const post = await fetch(`/api/post/1`).then((r) => r.json())
</script>

注意 async setup() 必须与 Suspense 内置组件组合使用Suspense 目前还是处于实验阶段的特性会在将来的版本中稳定。

异步组件以及defineAsyncComponent()方法

defineAsyncComponent
defineAsyncComponent() 定义一个异步组件它在运行时是懒加载的。参数可以是一个异步加载函数或是对加载行为进行更具体定制的一个选项对象。

类型

function defineAsyncComponent(
  source: AsyncComponentLoader | AsyncComponentOptions
): Component

type AsyncComponentLoader = () => Promise<Component>

interface AsyncComponentOptions {
  loader: AsyncComponentLoader
  loadingComponent?: Component
  errorComponent?: Component
  delay?: number
  timeout?: number
  suspensible?: boolean
  onError?: (
    error: Error,
    retry: () => void,
    fail: () => void,
    attempts: number
  ) => any
}

异步组件

异步组件详情

通过 defineAsyncComponent 加载异步配合import 函数模式便可以进行代码分包代码分割详情

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() =>
  import('./components/MyComponent.vue')
)

// 完整写法
const AsyncComp = defineAsyncComponent({
  // 加载函数
  loader: () => import('./Foo.vue'),

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

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

Suspense

Suspense详情

<Suspense> 是一个内置组件用来在组件树中协调对异步依赖的处理。它让我们可以在组件树上层等待下层的多个嵌套异步依赖项解析完成并可以在等待时渲染一个加载状态。

<Suspense> 可以等待的异步依赖有两种

  1. 带有异步 setup() 钩子的组件。这也包含了使用 <script setup> 时有顶层 await 表达式的组件。
  2. 异步组件。
<Suspense>
  <!-- 具有深层异步依赖的组件 -->
  <Dashboard />

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

案例可见vue3笔记案例——Suspense使用之骨架屏

vue3笔记案例——Suspense使用之骨架屏

8.2 Transition&TransitionGroup动画组件

详情见vue3学习笔记之Transition&TransitionGroup

8.3 Teleport传送组件

运用场景 一个组件模板的一部分在逻辑上从属于该组件但从整个应用视图的角度来看它在 DOM 中应该被渲染在整个 Vue 应用外部的其他地方。例如 全屏的模态框

主要解决问题 因为Teleport节点挂载在其他指定的DOM节点下完全不受父级style样式影响

基本使用

<Teleport> 接收一个 to prop 来指定传送的目标。to 的值可以是一个 CSS 选择器字符串也可以是一个 DOM 元素对象

<!-- 标签名 -->
<Teleport to="body">
  <div>xxx</div>
</Teleport>

<!-- 类名 -->
<Teleport to=".xxx">
  <div>xxx</div>
</Teleport>

<!-- id名 -->
<Teleport to="#xxx">
  <div>xxx</div>
</Teleport>

案例可见vue3笔记案例——Teleport使用之模态框

vue3笔记案例——Teleport使用之模态框

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

禁用 Teleport

:disabled 设置为 trueteleport 不生效可以传递一个 props 动态控制 teleport

<teleport :disabled="true" to='body'>
	<A></A>
</teleport>

8.4 KeepAlive缓存组件

<KeepAlive> 是一个内置组件它的功能是在多个组件间动态切换时缓存被移除的组件实例

<!-- 非活跃的组件将会被缓存 -->
<!-- 基本 -->
<keep-alive>
  <component :is="view"></component>
</keep-alive>
 
<!-- 多个条件判断的子组件 -->
<keep-alive>
  <ComA v-if="a > 1"></ComA>
  <ComB v-else></ComB>
</keep-alive>
 
<!-- 和 `<transition>` 一起使用 -->
<transition>
  <keep-alive>
    <component :is="view"></component>
  </keep-alive>
</transition>

注意 在 DOM 模板中使用时它应该被写为 <keep-alive>

包含/排除(include/exclude)

<KeepAlive> 默认会缓存内部的所有组件实例但我们可以通过 includeexclude prop 来定制该行为。这两个 prop 的值都可以是一个以英文逗号分隔的字符串、一个正则表达式或是包含这两种类型的一个数组

<!-- 它会根据组件的 name 选项进行匹配所以组件如果想要条件性地被 KeepAlive 缓存就必须显式声明一个 name 选项。 -->
<!-- 只缓存A,B组件 -->
<!-- 以英文逗号分隔的字符串 -->
<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>

<!-- 不缓存C组件 -->
<KeepAlive :exclude="['C']">
  <component :is="view" />
</KeepAlive>

最大缓存实例数(max)

我们可以通过传入 max prop 来限制可被缓存的最大组件实例数。<KeepAlive> 的行为在指定了 max 后类似一个 LRU 缓存如果缓存的实例数量即将超过指定的那个最大数量则最久没有被访问的缓存实例将被销毁以便为新的实例腾出空间。

<KeepAlive :max="10">
  <component :is="activeComponent" />
</KeepAlive>

缓存实例的生命周期

当一个组件实例从 DOM 上移除但因为被 <KeepAlive> 缓存而仍作为组件树的一部分时它将变为不活跃状态而不是被卸载。当一个组件实例作为缓存树的一部分插入到 DOM 中时它将重新被激活

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

// 缓存组件中
// 只执行一次可以在此生命周期中执行只需初始化一次的内容
onMounted(()=>{
  console.log('初始化')
})

// 类似普通组件中的onMounted每次激活都会执行
onActivated(()=>{
  // 调用时机为首次挂载
  // 以及每次从缓存中被重新插入时
  console.log('onActivated————初始化')
})

// 类似普通组件中的onUnmounted但不会卸载组件而是将组件变为不活跃状态并缓存
onDeactivated(()=>{
  // 在从 DOM 上移除、进入缓存
  // 以及组件卸载时调用
  console.log('onDeactivated————组件失活')
})

// 不执行
onUnmounted(()=>{
  console.log('组件卸载')
})
</script>

注意

  • onActivated 在组件挂载时也会调用并且 onDeactivated 在组件卸载时也会调用。
  • 这两个钩子不仅适用于 <KeepAlive> 缓存的根组件也适用于缓存树中的后代组件。

案例

示例图
在这里插入图片描述

代码

<template>
    <label><input type="radio" v-model="current" :value="A" /> A</label>
    <label class="mlr10"><input type="radio" v-model="current" :value="B" /> B</label>
    <label><input type="radio" v-model="current" :value="C" /> C</label>
    <hr class="mtb20"/>
    <!-- 不对C组件进行缓存 -->
    <KeepAlive :exclude="['C']">
      <component :is="current"></component>
    </KeepAlive>
</template>

<script setup lang="ts">
import { shallowRef } from 'vue'
import A from '@/components/A.vue'
import B from '@/components/B.vue'
import C from '@/components/C.vue'

const current = shallowRef(A)

</script>
<!-- A组件 -->
<template>
  <p>组件: A</p>
  <div class="mtb20">
    <span :style="{ 'marginRight': '1rem' }">count: {{ count }}</span>
    <el-button @click="count++"> + </el-button>
  </div>
</template>

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

const count = ref<number>(0)

</script>


<!-- B组件 -->
<template>
  <p>组件: B</p>
  <div class="mtb20">
    <span :style="{ 'marginRight': '1rem' }">信息: {{ msg }}</span>
    <input v-model="msg" />
  </div>
</template>

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

const msg = ref<string>('')

</script>


<!-- C组件 -->
<template>
  <p>组件: C</p>
  <div class="mtb20">
    <span :style="{ 'marginRight': '1rem' }">不缓存信息: {{ msg }}</span>
    <input v-model="msg" />
  </div>
</template>

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

const msg = ref<string>('')

</script>

9. 依赖注入Provide/Inject

一个父组件相对于其所有的后代组件会作为依赖提供者。任何后代的组件树无论层级有多深都可以注入由父组件提供给整条链路的依赖。

在这里插入图片描述

9.1 Provide提供

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

// 第一个参数是注入名字符串或 Symbol第二个参数是值任意类型
provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
</script>

除了在一个组件中提供依赖我们还可以在整个应用层面提供依赖

import { createApp } from 'vue'

const app = createApp({})

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

在应用级别提供的数据在该应用内的所有组件中都可以注入。这在你编写插件时会特别有用因为插件一般都不会使用组件形式来提供值。

9.2 Inject (注入)

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

const message = inject<string>('message')
</script>

注入默认值

// 如果没有祖先组件提供 "message"
// `value` 会是 "这是默认值"
const value = inject<string>('message', '这是默认值')

在一些场景中默认值可能需要通过调用一个函数或初始化一个类来取得。为了避免在用不到默认值的情况下进行不必要的计算或产生副作用我们可以使用工厂函数来创建默认值

const value = inject('key', () => new ExpensiveClass())

如果默认值是函数不想被当作工厂函数则需要传递第三个参数 false

const value = inject('key', () => {}, false)

和响应式数据配合使用

建议尽可能将任何对响应式状态的变更都保持在供给方组件中。

<!-- 在供给方组件内 -->
<script setup lang="ts">
import { provide, ref } from 'vue'

const location = ref<string>('North Pole')

function updateLocation():void {
  location.value = 'South Pole'
}

provide('location', {
  location,
  updateLocation
})
</script>
<!-- 在注入方组件 -->
<template>
  <button @click="updateLocation">{{ location }}</button>
</template>

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

type Location = {
  location: Ref<string>
  updateLocation: () => void
}

const { location, updateLocation } = inject('location') as Location

</script>

不想要后代组件更改祖先组件提供的数据使用 readonly() 包裹使其变为只读

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

const num = ref<number>(1)

provide('num', readonly(num))

</script>

子孙组件使用提供的数据时类型为 unknow解决方法

  • 使用 as 断言const num = inject('num') as Ref<number>
  • 使用 !num!.value
  • 注入时使用默认值const num = inject('num', ref(1))
案例

在这里插入图片描述

<!-- 祖先组件 -->
<template>
  <div class="f24 fw700 mtb20">祖先组件</div>
  <el-button @click="num++">数字+</el-button>
  <el-button @click="num--">数字-</el-button>
  <span class="ml1">{{ num }}</span>
  <div class="mtb20">
    <label class="mr1"><input v-model="color" value="red" type="radio"> 红色</label>
    <label class="mr1"><input v-model="color" value="yellow" type="radio"> 黄色</label>
    <label class="mr1"><input v-model="color" value="blue" type="radio"> 蓝色</label>
  </div>
  <div class="box"></div>
  <hr class="mtb20">
  <A></A>
</template>

<script setup lang="ts">
import { provide, ref, readonly } from 'vue';
import A from '@/components/A.vue';

const color = ref<string>('red')
const num = ref<number>(1)

provide('color', color)
provide('num', readonly(num))

</script>

<style lang="less" scoped>
.box {
  width: 100px;
  height: 100px;
  background-color: v-bind(color); // v-bind可以直接绑定setup中的变量
}
</style>
<!-- 父亲组件 -->
<template>
  <div class="f24 fw700 mtb20">父亲组件</div>
  <div class="box"></div>
  <hr class="mtb20">
  <B></B>
</template>

<script setup lang="ts">
import { inject, Ref } from 'vue';
import B from '@/components/B.vue';

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

</script>

<style lang="less" scoped>
.box {
  width: 100px;
  height: 100px;
  background-color: v-bind(color);
}
</style>
<!-- 子孙组件 -->
<template>
  <div class="f24 fw700 mtb20">子孙组件</div>

  <el-button @click="changeColor">改变颜色</el-button>
  <el-button @click="changeNum">改变数字</el-button>

  <div class="box mtb20"></div>
  <div>数字{{ num }}</div>
</template>

<script setup lang="ts">
import { inject, Ref } from 'vue';
const color = inject<Ref<string>>('color')
const num = inject('num') as Ref<number>


// 不推荐在后代组件中直接修改祖先组件提供的数据应该在祖先组件写修改方法并一起provide

const changeColor = () => {
  color!.value = 'green'
}

const changeNum = () => {
  num.value++
}

</script>

<style lang="less" scoped>
.box {
  width: 100px;
  height: 100px;
  background-color: v-bind(color);
}
</style>

9.3 使用 Symbol 作注入名

如果你正在构建大型的应用包含非常多的依赖提供或者你正在编写提供给其他开发者使用的组件库建议最好使用 Symbol 来作为注入名以避免潜在的冲突。

我们通常推荐在一个单独的文件中导出这些注入名 Symbol

// keys.js
import { InjectionKey } from 'vue'

export const myInjectionKey = Symbol() as InjectionKey<string>
// 在供给方组件中
import { provide, InjectionKey } from 'vue'
import { myInjectionKey } from './keys.js'


provide(myInjectionKey , 'foo') // 若提供的是非字符串值会导致错误
// 注入方组件
import { inject } from 'vue'
import { myInjectionKey } from './keys.js'

const foo = inject<string>(myInjectionKey) // 类型string | undefined此时数据类型为unknow
// 方法一定义一个默认值
const foo = inject<string>('foo', 'bar') // 类型string

// 方法二as类型断言
const foo = inject('foo') as string

10. 兄弟组件传参以及Mitt

兄弟组件传参的几种方式

  1. 借助父组件传参父组件充当桥梁缺点是处理复杂繁琐逻辑结构不清晰
  2. 发布订阅模式event-bus缺点vue3取消了

10.1 event-bus

简易的一个bus.ts

// bus.ts
type BusClass<T> = {
  emit: (name: T) => void
  on: (name: T, callback: Function) => void
}

// 定义参数类型
type BusParams = string | number | symbol

type List = {
  [key: BusParams]: Array<Function>
}

class Bus<T extends BusParams> implements BusClass<T> {
  list: List
  constructor() {
    this.list = {}
  }
  emit(name: T, ...args: Array<any>){
    let eventName: Array<Function> = this.list[name]
    eventName.forEach(e => {
      e.apply(this, args)
    });
  }
  on(name: T, callback: Function){
    let fn: Array<Function> = this.list[name] || []
    fn.push(callback)
    this.list[name] = fn
  }
}

export default new Bus()
<!-- A组件 -->
<template>
  <el-button @click="emitB">派发</el-button>
</template>

<script setup lang="ts">
import Bus from '@/utils/Bus'

let flag = true

const emitB = () => {
  flag = !flag
  Bus.emit('emit-flag', flag)
}

</script>

<style lang="less" scoped>

</style>

<!-- B组件 -->
<template>
  <div>接收{{ flag }}</div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import Bus from '@/utils/Bus'

const flag = ref<boolean>(true)

Bus.on('emit-flag', (e: boolean) => {
  flag.value = e
})

</script>

<style lang="less" scoped>

</style>

10.2 Mitt

github地址Mitt

安装

npm install mitt -S

main.ts挂载全局属性

import { createApp } from 'vue'
import mitt from 'mitt'
...

const app = createApp({})

const Mitt = mitt()
// TS
// 由于必须要拓展ComponentCustomProperties类型才能获得类型提示
declare module 'vue' {
  export interface ComponentCustomProperties {
    $bus: typeof Mitt
  }
}

app.config.globalProperties.$bus = Mitt
...
app.mount('#app')

使用

派发emit

import { getCurrentInstance } from 'vue';

const instance = getCurrentInstance()

let flag = true

const emitB = () => {
  flag = !flag
  instance?.proxy?.$bus.emit('on-flag', flag)
}

接收on

import { getCurrentInstance, ref } from 'vue';

const instance = getCurrentInstance()

const flag = ref<boolean>(true)

instance?.proxy?.$bus.on('on-flag', (e) => {
  flag.value = e as boolean
})

// *监听所有事件回调函数参数一事件名 参数二传递值
instance?.proxy?.$bus.on('*', (type, e) => {
  console.log('type,e :>> ', type, e);
})

删除off

...
const Bus = (e: any) => {
  flag.value = e as boolean
}

instance?.proxy?.$bus.off('on-flag', Bus)

// 删除全部事件
instance?.proxy?.$bus.all.clear()

11. TSX

vue3中使用tsx详情可见vue3学习笔记之TSX的使用

12. v-model

<input v-model="text">
<!-- v-model实际上是以下的简写版本 -->
<input
  :value="text"
  @input="event => text = event.target.value">

12.1 组件中的v-model

<CustomInput v-model="searchText" />
<!-- 等价于 -->
<CustomInput
  :modelValue="searchText"
  @update:modelValue="newValue => searchText = newValue"
/>
<!-- CustomInput.vue -->
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>

<template>
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

组件中 v-model 的实现是通过 definePropsdefineEmits 结合实现的

案例v-model的实现

<template>
  <!-- 父组件 -->
  <el-button @click="isShow = !isShow">显示/隐藏</el-button> {{ isShow }}
  <div>msg: {{ msg }}</div>
  <hr class="mtb20">
  <A v-model="isShow" v-model:textVal="msg"></A>
</template>

<script setup lang="ts">
import A from '@/components/A.vue';

const isShow = ref(true)

const msg = ref('多多')

</script>
<!-- A组件 -->
<template>
  <div v-if="modelValue">
    <el-button @click="close">隐藏</el-button>
    <input @input="changeInput" :value="textVal" type="text">
  </div>
</template>

<script setup lang="ts">

interface Props {
  modelValue: boolean
  textVal: string
}

defineProps<Props>()

const emit = defineEmits<{
  (e: 'update:modelValue', flag: boolean): void
  (e: 'update:textVal', msg: string): void
}>()

const changeInput = (e: Event) => {
  const val = (e.target as HTMLInputElement).value
  emit('update:textVal', val)
}

const close = () => {
  emit('update:modelValue', false)
}
</script>

12.2 内置修饰符

.lazy

默认情况下v-model 会在每次 input 事件后更新数据 (IME 拼字阶段的状态例外)。你可以添加 lazy 修饰符来改为在每次 change 事件后更新数据

<!-- 在 "change" 事件后同步更新而不是 "input" -->
<input v-model.lazy="msg" />

<!-- 等价于 -->
<input
  :value="msg"
  @change="event => msg = event.target.value">

.number

如果你想让用户输入自动转换为数字你可以在 v-model 后添加 .number 修饰符来管理输入

<input v-model.number="age" />

<!-- 类似于 -->
<input
  :value="msg"
  @input="event => msg = parseFloat(event.target.value)">

注意 number 修饰符会在输入框有 type="number" 时自动启用。

.trim

如果你想要默认自动去除用户输入内容中两端的空格你可以在 v-model 后添加 .trim 修饰符

<input v-model.trim="msg" />

<!-- 等价于 -->
<input
  :value="msg"
  @input="event => msg = event.target.value.trim()">

注意 number 修饰符会在输入框有 type="number" 时自动启用。

12.3 自定义修饰符Modifiers

基本使用

xxxModifiers

//定义
interface Props {
  modelValue: boolean
  textVal: string
  modelModifiers?: {
    noClose: boolean
  }
  textValModifiers?: {
    isDD: boolean
    capitalize: boolean
  }
}

const props = defineProps<Props>()

// 使用
const noClose = props?.modelModifiers?.noClose

例子

interface Props {
  modelValue: boolean
  textVal: string
  modelModifiers?: {
    noClose: boolean
  }
  textValModifiers?: {
    isDD: boolean
    capitalize: boolean
  }
}

const props = defineProps<Props>()

const emit = defineEmits<{
  (e: 'update:modelValue', flag: boolean): void
  (e: 'update:textVal', msg: string): void
}>()

const changeInput = (e: Event) => {
  let val = (e.target as HTMLInputElement).value
  const isDD = props?.textValModifiers?.isDD
  const capitalize = props?.textValModifiers?.capitalize
  if(isDD) {
    val = `多多的${val}`
  }
  if(capitalize) {
    val = val.charAt(0).toUpperCase() + val.slice(1)
  }
  emit('update:textVal', val)
}

const close = () => {
  const val = props?.modelModifiers?.noClose ? true : false
  emit('update:modelValue', val)
}
<A v-model.noClose="isShow" v-model:textVal.capitalize="msg"></A>

13. 全局API

官方文档

13.1 app.config.globalProperties

用法

const app = createApp()

// TS
// 由于必须要拓展ComponentCustomProperties类型才能获得类型提示
declare module 'vue' {
  export interface ComponentCustomProperties {
    msg: string
  }
}

app.config.globalProperties.msg = 'hello'
<!-- template中直接使用 -->
<template>
  <div>
    {{msg}}
  </div>
</template>
//js中
// 使用一
const instance = getCurrentInstance()
const msg = instance?.proxy?.msg

// 使用二
const { appContext } = <ComponentInternalInstance>getCurrentInstance()
const msg = appContext.config.globalProperties.msg

console.log(msg);

13.2 nextTick()

等待下一次 DOM 更新刷新的工具方法。

<script setup>
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
}
</script>

<template>
  <button id="counter" @click="increment">{{ count }}</button>
</template>

EventLoop

从一道题浅说 JavaScript 的事件循环

事件循环中先执行宏任务再清空里面所有微任务然后执行新的宏任务

在这里插入图片描述

异步任务分为宏任务微任务

常见的宏任务script(整体代码)、setTimeout、setInterval、I/O、UI交互事件、setImmediate(Node.js 环境)、ajax、callback
常见的微任务promise.then()中、MutaionObserver、process.nextTick(node环境)

注意promise本身是同步的但它的方法是异步的且属于微任务

console.log(1) // 直接执行

setTimeout(() => {
  console.log(2) // 进入宏任务队列
})

new Promise((resolve) => {
  console.log(3) //直接执行
  resolve()
})
  .then(() => {
    console.log(4) // 进入微任务队列
  })
  .then(() => {
    console.log(5)
  })

console.log(6) // 直接执行

// 执行顺序1 3 6 4 5 2

上述执行顺序看似先执行的微任务实际上是由于script整体代码本身是一个宏任务所以在执行同步任务后先清空微任务在执行新的宏任务

console.log(1) // 直接执行

// 进入宏任务队列
setTimeout(() => {
  console.log(2)
}, 10)

// 进入宏任务队列
setTimeout(() => {
  new Promise((resolve) => {
    console.log(3) //直接执行
    resolve()
  }).then(() => {
    console.log(4) // 进入微任务队列
  })
})

new Promise((resolve) => {
  console.log(5) //直接执行
  // 进入微任务队列
  resolve()
  console.log(6) //直接执行
  // 进入宏任务队列
  setTimeout(() => console.log(7))
})
  .then(() => {
    setTimeout(() => console.log(8))
  })
  .then(() => {
    console.log(9)
  })

console.log(10) // 直接执行

// 1 5 6 10 9 3 4 7 8 2

一次执行一个宏任务清空完宏任务里的微任务后执行下一个即先跳过其他宏任务以及里面的微任务

14. 自定义指令

<script setup> 中任何以 v 开头的驼峰式命名的变量都可以被用作一个自定义指令。

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

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

在没有使用 <script setup> 的情况下自定义指令需要通过 directives 选项注册

export default {
  setup() {
    /*...*/
  },
  directives: {
    // 在模板中启用 v-focus
    focus: {
      /* ... */
    }
  }
}

14.1 指令钩子

一般常用 mountedupdatedunmounted 三个钩子函数

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 钩子中可用。
<template>
  <div v-color:height.bg="{ width: '240px', height: '200px', color: 'red' }"></div>
</template>

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

const vColor: Directive = {
  mounted(el, binding) {
    el.style.width = binding.value?.width
    if (binding.modifiers?.bg) {
      el.style.backgroundColor = binding.value?.color
    } else {
      el.style.color = binding.value?.color
    }
    if (binding.arg === 'height') {
      el.style.height = binding.value?.width
    } else {
      el.style.height = binding.value?.height
    }
  }
}

</script>

14.2 简写形式

对于自定义指令来说一个很常见的情况是仅仅需要在 mountedupdated 上实现相同的行为除此之外并不需要其他钩子。这种情况下我们可以直接用一个函数来定义指令如下所示

<div v-color="color"></div>
// 全局
app.directive('color', (el, binding) => {
  // 这会在 `mounted` 和 `updated` 时都调用
  el.style.color = binding.value
})

// 组件
const vColor: Directive = (el, binding) => {
  // 这会在 `mounted` 和 `updated` 时都调用
  el.style.color = binding.value
}

案例简单实现权限指令dome

<template>
  <!-- 父组件 -->
  <el-button v-has-show="'index:add'">新 增</el-button>
  <el-button v-has-show="'index:edit'">修 改</el-button>
  <el-button v-has-show="'index:delete'">删 除</el-button>

</template>

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

// mock登陆返回的用户信息
localStorage.setItem('userId', 'dd')

// mock后台返回的权限数组
// *:*:* ===> 用户id:当前信息页:权限
const permission = [
  'dd:index:add',
  'dd:index:edit',
  'dd:index:delete',
  'xd:index:add',
  'xd:index:edit'
]

const userId = localStorage.getItem('userId')

const vHasShow: Directive = (el, binding) => {
  if (!permission.includes(`${userId}:${binding.value}`)) {
    // 隐藏
    el.parentNode && el.parentNode.removeChild(el)
  }
}

</script>

案例自定义指令实现拖拽效果

在这里插入图片描述

<div v-move class="box"></div>

move.ts

import type { Directive } from 'vue'

const vMove: Directive = (el) => {
  let moveEl = (el?.firstElementChild as HTMLElement) || el;
  const mouseDown = (e: MouseEvent) => {
    el.style.position = 'absolute'
    el.style.zIndex = 999
    // 点击位置相对于盒子的偏移量 = 鼠标位置 - 盒子偏移量
    const x = e.clientX - el.offsetLeft
    const y = e.clientY - el.offsetTop
    /* 
      边界值设定
      左/上边界 = x/y鼠标偏移量
      下/右边界 = 浏览器宽高 - 盒子宽高
    */
    const bottomLimit = window.innerHeight - el.clientHeight
    const rightLimit = window.innerWidth - el.clientWidth

    const move = (e) => {
      // 移动位置以左上角为基准 = 新鼠标位置 - 鼠标初始盒子偏移量
      const moveY = e.clientY - y < 0 ? y : (e.clientY - y > bottomLimit ? bottomLimit : e.clientY - y + 'px')
      const moveX = e.clientX - x < 0 ? x : (e.clientX - x > rightLimit ? rightLimit : e.clientX - x + 'px')
      el.style.top = moveY
      el.style.left = moveX
    }
    // 监听鼠标移动事件
    document.addEventListener('mousemove', move)
    // 鼠标抬起取消鼠标移动事件的监听
    document.addEventListener('mouseup', () => {
      document.removeEventListener('mousemove', move)
    })
  }

  moveEl.addEventListener('mousedown', mouseDown)
}

export default vMove

main.ts
全局注册指令

import { createApp } from 'vue'
import vMove from '@/directive/move'
...

const app = createApp({})

app.directive('move', vMove)

app.mount('#app')

15. 组合式函数——“vue的hooks”

官网地址组合式函数

vueUse库

15.1 基本使用

命名

组合式函数约定用驼峰命名法命名并以 “use” 作为开头。

输入参数

尽管其响应性不依赖 ref组合式函数仍可接收 ref 参数。如果编写的组合式函数会被其他开发者使用你最好在处理输入参数时兼容 ref 而不只是原始的值。unref() 工具函数会对此非常有帮助

import { unref } from 'vue'

function useFeature(maybeRef) {
  // 若 maybeRef 确实是一个 ref它的 .value 会被返回
  // 否则maybeRef 会被原样返回
  const value = unref(maybeRef)
}

返回值

推荐的约定是组合式函数始终返回一个包含多个 ref 的普通的非响应式对象而不是一个 reactive 对象这样该对象在组件中被解构为 ref 之后仍可以保持响应性

// x 和 y 是两个 ref
const { x, y } = useMouse()

mouse.ts

import useEventListener from "./eventListener"

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

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

  useEventListener(window, 'mousemove', update)
  // 通过返回值暴露所管理的状态
  return { x, y }
}

eventListener.ts

// 按照惯例组合式函数名以“use”开头
export default function useEventListener (target: any, event: string, callback: Function) {

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

组件

<template>
  <div>x: {{ x }}----y: {{ y }}</div>
</template>

<script setup lang="ts">
import useBase64 from '@/hooks/useBase64'

const { x, y } = useMouse()

</script>

16. 插件

一个插件可以是一个拥有 install() 方法的对象也可以直接是一个安装函数本身。安装函数会接收到安装它的应用实例和传递给 app.use() 的额外选项作为参数

const myPlugin = {
  install(app, options) {
    // 配置此应用
  }
}
import { createApp } from 'vue'

const app = createApp({})

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

插件没有严格定义的使用范围但是插件发挥作用的常见场景主要包括以下几种

  1. 通过 app.component()app.directive() 注册一到多个全局组件或自定义指令。
  2. 通过 app.provide() 使一个资源可被注入进整个应用。
  3. app.config.globalProperties 中添加一些全局实例属性或方法
  4. 一个可能上述三种都包含了的功能库 (例如 vue-router)。

17. 样式穿透及CSS 新特性

详情见vue3学习笔记之样式穿透(:deep)及CSS 新特性(:soltted、:gloabl、v-bind、mouldCSS)

18. h函数

创建虚拟 DOM 节点 (vnode)。

类型

// 完整参数签名
function h(
  type: string | Component,
  props?: object | null,
  children?: Children | Slot | Slots
): VNode

// 省略 props
function h(type: string | Component, children?: Children | Slot): VNode

type Children = string | number | boolean | VNode | null | Children[]

type Slot = () => Children

type Slots = { [name: string]: Slot }

第一个参数既可以是一个字符串 (用于原生元素) 也可以是一个 Vue 组件定义。第二个参数是要传递的 prop第三个参数是子节点。

当创建一个组件的 vnode 时子节点必须以插槽函数进行传递。如果组件只有默认槽可以使用单个插槽函数进行传递。否则必须以插槽函数的对象形式来传递。

为了方便阅读当子节点不是插槽对象时可以省略 prop 参数。

import { h } from 'vue'

// 除了 type 外其他参数都是可选的
h('div')
h('div', { id: 'foo' })

// attribute 和 property 都可以用于 prop
// Vue 会自动选择正确的方式来分配它
h('div', { class: 'bar', innerHTML: 'hello' })

// class 与 style 可以像在模板中一样
// 用数组或对象的形式书写
h('div', { class: [foo, { bar }], style: { color: 'red' } })

// 事件监听器应以 onXxx 的形式书写
h('div', { onClick: () => {} })

// children 可以是一个字符串
h('div', { id: 'foo' }, 'hello')

// 没有 prop 时可以省略不写
h('div', 'hello')
h('div', [h('span', 'hello')])

// children 数组可以同时包含 vnode 和字符串
h('div', ['hello', h('span', 'hello')])

案例

<template>
  <ComA text="这是props传递的内容" @on-click="getBtn">
    <template #default>
      插槽内容
    </template>
  </ComA>
</template>

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

type Props = {
  [key: string]: any
}

const ComA = (props: Props, ctx: any) => {
  return h(
    'div',
    {
      class: 'text-center bg-green500 text-white w-20 h-10 lh-10',
      onClick: () => {
        ctx.emit('on-click', '我点击了')
      }
    },
    // props.text // props传递内容
    ctx.slots.default() // 插槽传递内容
  )
}

const getBtn = (str: string) => {
  console.log(str);
}
</script>

19. 环境变量及proxy代理

19.1 环境变量

环境变量详情请看vite+vue3环境变量的配置

19.2 proxy代理

vite.config.ts

export default defineConfig({
  plugins: [vue()],
  server:{
     proxy:{
        '/api':{
            target:"http://localhost:3001/", //跨域地址
            changeOrigin:true, //支持跨域
            rewrite:(path) => path.replace(/^\/api/, "")//重写路径,替换/api
        }
     }
  }
})
fetch('/api/user')
阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: vue