MENU

Vue3+TS 笔记

December 1, 2024 • Read: 331 • 编码,前端

前言:本篇笔记内容来自于 B 站:《尚硅谷Vue3入门到实战,最新版vue3+TypeScript前端开发教程》系列视频

链接地址: https://www.bilibili.com/video/BV1Za4y1r7KE/

创建 Vue 工程

NodeJS

安装 18.3 + 的 NodeJS https://nodejs.org/

初始化项目

通过 Vite 初始化一个 Vue 项目 Vite 会在运行目录创建一个 项目同名的目录

PS E:\code> npm create vue@latest

> npx
> create-vue


Vue.js - The Progressive JavaScript Framework

√ 请输入项目名称: ... test_vue3
√ 是否使用 TypeScript 语法? ... 否 / 是
√ 是否启用 JSX 支持? ... 否 / 是
√ 是否引入 Vue Router 进行单页面应用开发? ... 否 / 是
√ 是否引入 Pinia 用于状态管理? ... 否 / 是
√ 是否引入 Vitest 用于单元测试? ... 否 / 是
√ 是否要引入一款端到端(End to End)测试工具? » 不需要
√ 是否引入 ESLint 用于代码质量检测? ... 否 / 是
? 是否引入 Vue DevTools 7 扩展用于调试? (试验阶段) » 否 / 是

之后通过 vscode 打开项目目录.

运行项目

运行前端项目要先看一下项目根目录的 package.json 文件,里面包含了一些项目的信息:

{
  "name": "test_vue3",
  "version": "0.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "run-p type-check \"build-only {@}\" --",
    "preview": "vite preview",
    "build-only": "vite build",
    "type-check": "vue-tsc --build --force"
  },
  "dependencies": {
    "vue": "^3.4.29"
  },
  "devDependencies": {
    "@tsconfig/node20": "^20.1.4",
    "@types/node": "^20.14.5",
    "@vitejs/plugin-vue": "^5.0.5",
    "@vue/tsconfig": "^0.5.1",
    "npm-run-all2": "^6.2.0",
    "typescript": "~5.4.0",
    "vite": "^5.3.1",
    "vue-tsc": "^2.0.21"
  }
}

可以看到 scripts 目录下可以 通过的 dev 命令来启动项目,通过 build 来打包编译。

我们使用 npm run dev 启动项目

项目结构

项目入口是根目录中的 index.html 文件,里面有两个核心操作,声明了 #app 容器,所有前端的内容渲染(挂载)到这个容器上,然后引入了 main.ts 文件

main.ts 文件中引入了 Vue 组件,并创建了一个 App 的组件,挂载了节点为 App 的容器上。

以根目录为例,代码如下:

/index.html 入口文件

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <link rel="icon" href="/favicon.ico">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vite App</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

/src/main.ts 文件

// 引入 createApp 用于创建应用
import {createApp} from 'vue'
// 引入 App 根组件
import App from './App.vue'
// 挂载 组件
createApp(App).mount('#app')

/src/App.vue 文件

<template>
  <div class="app">
    <h1>你好啊!</h1>
  </div>
</template>

<script lang="ts">
  export default {
    name:'App' //组件名
  }
</script>

<style>
  .app {
    background-color: #ddd;
    box-shadow: 0 0 10px;
    border-radius: 10px;
    padding: 20px;
  }
</style>

总结

  • Vite 项目中,index.html 是项目的入口文件,在项目最外层。
  • 加载index.html后,Vite 解析 <script type="module" src="xxx"> 指向的JavaScript
  • Vue3 中是通过 createApp 函数创建一个应用实例。

Demo

src\components\Person.vue 声明一个组件

<template>
    <div class="person">
        <h2>姓名:{{ name }}</h2>
        <h2>年龄:{{ age }}</h2>
        <button @click="showTel">show</button>
    </div>
</template>

<script lang="ts">
    export default {
        name:"Person",
        data(){
            return {
                 name:'张三',
                 age:18,
                 tel:153       
            }
        },
        methods: {
            showTel(){
                    alert(this.tel)
                }
        }
    }
</script>

<style>
    .person{
        background-color:#007acc;
        padding: 20px;
    }
</style>

src\App.vue 根文件

<template>
    <!-- html -->
     <div class="app">
        <h1>123</h1>
        <Person />
     </div>
</template>

<script lang="ts">
    // JS 或 TS
    import Person from "./components/Person.vue"
    
    export default {
        name :'App',
        components: {Person}
    }
</script>

<style>
    /* 样式 */
    .app {
    background: #ddd;
    padding: 20px;
    }
</style>

注:Vue3向下兼容Vue2语法,且Vue3中的模板中可以没有根标签。

Vue3 核心语法

OptionsAPI 和 CompositionAPI

  • Vue2API 设计是 Options (配置、选项)风格的
  • Vue3API 设计是 Composition(组合、组件) 风格的

Options API 弊端

Options类型的 API,数据、方法、计算属性等,是分散在:datamethodscomputed中的,若想新增或者修改一个需求,就需要分别修改:datamethodscomputed,不便于维护和复用。

setup

setup 概述

setup 是 vue3 中一个新的配置项,它的值是一个函数。 setupComposition API 的核心,组件中所用到的:数据、方法、计算属性、监视......等等,均配置在setup中。

特点如下:

  • setup函数返回的对象中的内容,可直接在模板中使用。
  • setup中访问thisundefined
  • setup函数会在beforeCreate之前调用,它是“领先”所有钩子执行的。
<template>
    <div class="person">
        <h2>姓名:{{ name }}</h2>
        <h2>年龄:{{ age }}</h2>
        <button @click="showTel">show</button>
    </div>
</template>

<script lang="ts">
    export default {
        name:"Person",
        setup(){
            // 数据 vue2 中需要写在 data 中 (注意 这时的 name age tel 不是响应式数据)
            let name = 'dbkuaizi'
            let age = 18
            let tel = '1533'

            // 方法 vue2 中需要写在 methods 中
            function showTel() {
                alert(tel)
            }            
            // 返回一个对象 模板可以直接用的
            return {name,age,showTel}

        }
    }
</script>

setup 返回值

  • 若 返回一个对象:则对象中的:属性、方法等,在模板中均可以正常使用
  • 若返回一个函数:则会直接渲染返回的内容,所以可以用作自定义渲染函数。代码如下:
setup(){
  return ()=> 'hello Vue3!'
}

setup 与 Options API 的关系

  • Vue2 的配置(datamethos......)中可以访问到 setup中的属性、方法。
  • 但在setup不能访问到Vue2的配置(datamethos......)。
  • 如果与Vue2冲突,则setup优先。

setup 语法糖

setup 提供了一个语法糖,允许我们把 setup 独立出去,且不用 return,代码如下:

 <template>
    <div class="person">
        <h2>姓名:{{ name }}</h2>
        <h2>年龄:{{ age }}</h2>
        <button @click="showTel">show</button>
    </div>
</template>

<script lang="ts">
    export default {
        name:"Person"
    }
</script>

<script lang="ts" setup>
    // 数据
    let name = 'dbkuaizi'
    let age = 18
    let tel = '1533'

    // 方法
    function showTel() {
        alert(tel)
    }            
</script>

::: alert-info
当不定义组件名时(即 上方代码,省略 9 ~ 13 行 ),会自动通过文件名来推导组件名,例如文件名为 Person.vue 组件名就是 Person
当组件过于复杂时,可能会用一个单独的文件夹来编排代码,例如 /src/compositions/tree/index.vue, 这种场景下 整个 tree 目录都是组件,如果通过文件名推导,这个组件名称就是 index ,肯定不合适,所以需要显式定义组件名。
:::

若想简写 组件名称可以通过 vite 插件来实现:

  1. 安装插件 npm i vite-plugin-vue-setup-extend -D
  2. 编辑 /vite.config.ts 文件
import { defineConfig } from 'vite'
import VueSetupExtend from 'vite-plugin-vue-setup-extend' // 增加

export default defineConfig({
  plugins: [ VueSetupExtend() ] // 增加
})
  1. <script setup lang="ts" name="Person">

补充一种 Vue 3.3 + 支持 不依赖插件的写法

<template>
    <div class="person">
        <h2>姓名:{{ name }}</h2>
        <h2>年龄:{{ age }}</h2>
        <button @click="showTel">show</button>
    </div>
</template>

<script lang="ts" setup>
    defineOptions({name:"Person666"})
    // 数据
    let name = 'dbkuaizi'
    let age = 18
    let tel = '1533'

    // 方法
    function showTel() {
        alert(tel)
    }            
</script>

ref 响应式数据

ref 创建:基本类型响应式数据

  • 作用:定义响应式数据
  • 语法:let xxx =ref(初始值)
  • 返回值:一个 Reflmpt 的实例对象,简称 ref 对象refref 对象value 属性是响应式的

:::alert-info

  • JS 中操作数据 需要 xxx.vlue 不能直接覆盖 ref 对象, 但 模板中不需要 .value 可以直接使用。
  • 对于 let name = ref('dbkuaizi') 来说 name 不是响应式的,name.value 才是。
    :::
<template>
    <div class="person">
        <h2>姓名:{{ name }}</h2>
        <h2>年龄:{{ age }}</h2>
        <button @click="setAge">set age</button>
        <button @click="showTel">show</button>
    </div>
</template>

<script lang="ts" setup>
    defineOptions({name:"Person"})
    import {ref} from 'vue'

    // name和age是一个RefImpl的实例对象,简称ref对象,它们的value属性是响应式的
    let name = ref('dbkuaizi')
    let age = ref(18)
    // tel 不是响应式的
    let tel = '1533'

    // 方法
    function showTel() {
        alert(tel)
    }            
    // 响应式数据在修改时必须 .value 但在模板中使用时不需要
    function setAge(){
        age.value++
    }
</script>

reactive 创建,对象类型的响应式数据

  • 作用:定义一个响应式的对象,基本类型用 ref 否则会报错
  • 语法:let 响应式对象 = reactive(源对象)
  • 返回值:一个 Proxy 的实例对象,简称响应式对象。

::: alert-info

  • reactive 定义的响应式数据是 "深层次"的。
  • reactive 只能定义响应式类型数据,定义基本类型会报错
  • ref用来定义:基本类型数据对象类型数据
    :::
<template>
    <div class="person">
        <h2>一辆 {{car.name}},价值 {{ car.price }} </h2>
        <button @click="setPrice">setPrice</button>
        <hr>
        <h2>游戏:</h2>
        <ul>
            <li v-for="g in games" :key="g.id">{{ g.name }}</li>
        </ul>
        <button @click="setGames">setGames</button>
    </div>
</template>

<script lang="ts" setup>
    defineOptions({name:"Person"})
    import {reactive,ref} from 'vue'

    let car = reactive({name:'喜德盛',price:1500})
    
    let games = reactive([
        {id:"yx1",name:"仙剑"},
        {id:"yx2",name:"古剑"},
        {id:"yx2",name:"轩辕剑"},
    ])

    function setPrice(){ 
        car.price += 10
    }

    function setGames() {
        games[0].name = '流星蝴蝶剑'
    }
</script>

ref 对比 reactive

宏观角度

  • ref 用来定义:基本类型数据对象类型数据
  • reactive 用来定义:对象类型数据

区别

  • ref 创建的变量必须使用 .value 操作,(可以通过 Vue - Official 插件设置自动添加 .value
  1. Vscode 中找到 Vue - Official 插件设置
  2. 勾选上 Auto Insert Dot Value
  • reactive 重新分配一个对象,会失去 响应式(可以使用 Object.assign 替换源对象中的属性)
 function setCar(){
    // 这么写直接覆盖了 reactive 对象,car 不再是响应式的了
    // car = {name:'Giant',price:3500}
    // 这么写 car 是一个新的 reactive 对象,而渲染时引用的是最早初始化的 reactive 对象
    // car = reactive({name:'Giant',price:3500})
    
    // 得这么写 不要覆盖原 reactive 对象,Object.assign: 使用后面对象复制到目标对象
    Object.assign(car,{name:'Giant',price:3500})
}

:::alert-warning
注意 如果是 ref 定义的是响应式对象,可以直接通过 xxx.value = 新对象 去赋值,因为 ref.value 是响应式的,所以修改 .value 不会破坏响应式关系。
:::

使用原则:

  1. 若需要一个基本类型的响应式数据,必须使用ref
  2. 若需要一个响应式对象,层级不深,refreactive都可以。
  3. 若需要一个响应式对象,且层级较深,推荐使用 reactive

:::alert-info
例如一个表单,有很多表单项,如果表单值是 ref,那就需要在代码中写 很多 .value,增加代码复杂度,reactive 更合适
:::

toRefs 和 toRef

  • 作用:将响应式对象中的属性 转为 ref对象
  • 说明:toRefstoRef 功能一致,但 toRefs 可以批量转换
<template>
    <div class="person">
        <h2 @click="setName">姓名:{{ person.name }}</h2>
        <h2 @click="setAge">年龄:{{ person.age }}</h2>
    </div>
</template>

<script lang="ts" setup>
    defineOptions({name:"Person"})

    import { reactive,toRefs,toRef } from "vue";
     let person = reactive({
        name:'张三',
        age:20
     })

     //  通过toRefs将person对象中的n个属性批量取出,且依然保持响应式的能力
     let {name,age} = toRefs(person)
     // 通过toRef将person对象中的 age 属性取出,且依然保持响应式的能力
     let age2 = toRef(person,'age')

     function setName(){
        name.value += '!'
     }

     function setAge(){
        age.value += 1
     }

</script>

computed 计算属性

说明:根据已有的数据计算出新的数据 (与 Vue2 中计算属性功能一致),计算属性在多次调用、参与计算的值没有发生变化时,不会重复计算。

<template>
    <div class="person">
        姓:<input type="text" v-model="firstName"> <br>
        名:<input type="text" v-model="lastName"> <br>
        全:<span>{{ fullName }}</span> <br>
        全:<span>{{ fullName }}</span> <br>
        全:<span>{{ fullName }}</span> <br>
        全:<span>{{ fullName }}</span> <br>
        <button @click="setName">Set Name</button>
    </div>
</template>

<script lang="ts" setup>
    defineOptions({ name: "Person" })
    import { ref,computed } from "vue";
    
    let firstName = ref('张')
    let lastName = ref('三')
    // 定义的 fullName 是一个计算属性,且是只读的
    let fullName = computed(() => {
        return firstName.value.slice(0,1).toUpperCase() + firstName.value.slice(1) + '-' + lastName.value
    })

    // 这样定义的是可以修改的
    let fullName2 = computed({
        get(){
            return firstName.value.slice(0,1).toUpperCase() + firstName.value.slice(1) + '-' + lastName.value
        },
        set(val){
            [firstName.value,lastName.value] = val.split('-')
        }
    })

    function setName(){
        // 修改值会触发 computed 中的 set 函数
        fullName2.value = 'li-si'
    }

</script>

watch 监控

监视 ref 定义的基本类型

监视ref定义的【基本类型】数据:直接写数据名即可,监视的是其value值的改变。

<template>
    <div class="person">
       <h2>求和 Sum 为: {{ sum }}</h2>
       <button @click="setSum">+1</button>
    </div>
</template>

<script lang="ts" setup>
    defineOptions({ name: "Person" })
    import { ref,watch } from "vue";
    
    let sum = ref(0)

    function setSum(){
        sum.value++
    }
    
    // 场景1,监控 ref 定义的基本类型数据
    const stopWatch = watch(sum,(newVal,oldVal) => {
        console.log('sum 变化了',newVal,oldVal)
        // 满足条件后 停止监控
        if(newVal >= 10) {stopWatch()}
    })

</script>

监视 ref 定义的对象类型

监视 ref 定义的【对象类型】数据:直接写数据名,监视的是对象的【地址值】,若想监视对象内部的数据,要手动开启深度监视。

:::alert-warning

  • 若修改的是 ref 定义的对象中的属性,newValueoldValue 都是新值,因为它们是同一个对象。
  • 若修改整个 ref 定义的对象,newValue 是新值, oldValue 是旧值,因为不是同一个对象了。
    :::
<template>
    <div class="person">
       <h2 @click="setName">姓名:{{ person.name }}</h2>
       <h2 @click="setAge">年龄:{{ person.age }}</h2>
       <button @click="setPerson">全改</button>
    </div>
</template>

<script lang="ts" setup>
    defineOptions({ name: "Person" })
    import { ref,watch } from "vue";
    // 数据
    let person = ref({name:'张三',age:18})

    function setName(){
        person.value.name += '~'
    }

    function setAge(){
        person.value.age ++
    }

    function setPerson(){
        person.value = {name:'李四',age:28}
    }

    // 这个直接监控 对象的变化,不能监控对象属性的变化
    watch(person,(newVal,oldVal) => {
        console.log("person 变化了",newVal,oldVal)
    })
    
    // 开启了 deep (深度监控) 对象属性也可以监控
    watch(person,(newVal,oldVal) => {
        console.log("person 变化了",newVal,oldVal)
    },{deep:true})

</script>

监视 reactive 定义的对象类型

监视 reactive 定义的对象类型数据,默认开启深度监视

<template>
    <div class="person">
       <h2 @click="setName">姓名:{{ person.name }}</h2>
       <h2 @click="setAge">年龄:{{ person.age }}</h2>
       <button @click="setPerson">全改</button>
    </div>
</template>

<script lang="ts" setup>
    defineOptions({ name: "Person" })
    import { reactive,watch } from "vue";
    // 数据
    let person = reactive({name:'张三',age:18}) 

    function setName(){
        person.name += '~'
    }

    function setAge(){
        person.age ++
    }

    function setPerson(){
        Object.assign(person,{name:'李四',age:28})
    }

    // 监视 reactive 定义的 对象类型数据,默认开启深度监视
    watch(person,(newVal,oldVal)=> {
        console.log('person 变化了',newVal,oldVal)
    })
    
</script>

监视某个属性

监视 refreactive 定义的对象类型数据中的某个属性时,注意如下:

  1. 若该属性值不是“对象类型”需要写成函数形式
  2. 若该属性值依然是 “对象类型”,可以直接写,也可以写函数,这里建议写函数

结论: 监视的要是对象里的属性,那么最好写函数式,注意点:若是对象监视的是地址值,需要关注对象内部,需要手动开启深度监视。

<template>
    <div class="person">
       <h2>姓名:{{ person.name }}</h2>
       <h2>年龄:{{ person.age }}</h2>
       <h2>车:{{ person.car.c1 }} 、{{ person.car.c2 }}</h2>
       <button @click="setName">修改名称</button>
       <button @click="setAge">修改年龄</button>
       <button @click="setCar1">修改车</button>
       <button @click="setCar">修改所有车</button>
    </div>
</template>
<script lang="ts" setup>
    defineOptions({ name: "Person" })
    import { reactive,watch } from "vue";
   let person = reactive({
        name:"张三",
        age:18,  
        car:{
            c1:"奥托",
            c2:"五菱"
        }
   })

   function setName(){
     person.name = '李四'
   }
   
   function setAge(){
     person.age++
   }

   function setCar1(){
     person.car.c1 = '大众'
   }

   function setCar(){
    person.car = {c1:"青桔",c2:"美团"}
   }

   // 监控多个属性
   watch(() => person.name,() => {
     console.log('person.name变化了')
   })

   /**
    * 监控响应式对象的某个属性时,且该属性是对象类型的有以下两种情况
    * 直接写属性,可以监视属性中子属性的变化,例如 person.car.c1 ,但无法监视整个 car 的变化
    * 写函数返回,可以监视 person.car 的变化,但不能监视 person.car.c1 的变化。如果需要监控对象内部,这时可以开启深度监视。
    */ 

   watch(() => person.car,() => {
     console.log('person.car变化了')
   },{deep:true})
</script>

监听多个属性

监控属性时可以传递一个数组,同时 newVal 也会是一个数组

<template>
    <div class="person">
       <h2>姓名:{{ person.name }}</h2>
       <h2>年龄:{{ person.age }}</h2>
       <h2>车:{{ person.car.c1 }} 、{{ person.car.c2 }}</h2>
       <button @click="setName">修改名称</button>
       <button @click="setAge">修改年龄</button>
       <button @click="setCar1">修改车1</button>
       <button @click="setCar">修改所有车</button>
    </div>
</template>
<script lang="ts" setup>
    defineOptions({ name: "Person" })
    import { reactive,watch } from "vue";
   let person = reactive({
        name:"张三",
        age:18,  
        car:{
            c1:"奥托",
            c2:"五菱"
        }
   })

   function setName(){
     person.name = '李四'
   }
   
   function setAge(){
     person.age++
   }

   function setCar1(){
     person.car.c1 = '大众'
   }

   function setCar(){
    person.car = {c1:"青桔",c2:"美团"}
   }

   // 监控响应式对象的某个属性时,且该属性是基本类型的,需要写成函数式
   watch([() => person.name,()=> person.car.c1],(newVal) => {
     // 注意 这里的 newVal 是数组 ['张三', '大众']
     console.log('person.name or person.car.c1 变化了',newVal)
   })
</script>

watchEffect

官网:立即运行一个函数,同时响应式的追踪其依赖,并在依赖更改时重新执行该函数
人话:更智能的 watch,不需要显式指定监控的响应式变量,直接写逻辑即可。在加载时会先执行一次。

watch 和 watchEffect 对比

  • 都能监听响应式数据的变化,不同的是监听数据变化的方式不同
  • watch 要明确指出监视的数据
  • watchEffect 不用明确指出监视的数据(函数中用到哪些属性,就自动监视哪些属性)。
<template>
    <div class="person">
       <h2 @click="changeSum">当前求和为:{{ sum }}</h2>
    </div>
</template>
<script lang="ts" setup>
  defineOptions({ name: "Person" })
  import { ref,watchEffect } from "vue";
  
  let sum = ref(0)

  function changeSum(){
    sum.value+= 5
  }

  // 无需指定监控变量,vue 自动监控,直接写逻辑就行
  // 如果需要监控多个参数,一个一个指定太麻烦了,建议用这个
  watchEffect(()=>{
    if(sum.value > 30) {
      console.log('达到30+ 了')
    }
  })
</script>

标签 Ref 属性

作用:用于注册模板引用,功能类似于带命名空间的 ID ,用于设置组件内的唯一标识

  • 用在普通DOM标签上,获取的是DOM节点。
  • 用在组件标签上,获取的是组件实例对象。

用在 DOM 标签上

<template>
    <div class="person">
       <h2 ref="t1">666</h2>
       <h2 ref="t2">999</h2>
       <button @click="ShowLog">ShowLog</button>
    </div>
</template>
<script lang="ts" setup>
import { ref, useTemplateRef,defineExpose } from 'vue';
  defineOptions({ name: "Person" })
  // Vue3.5+ 可以通过 useTemplateRef 拿到 ref 标识的元素
  let title = useTemplateRef('t1')
 
  // Vue3 默认通过 template 中同名的ref 绑定元素
  let t2 = ref()

  function ShowLog(){
    console.log(title.value,t2.value)
  }
  
  // 可以在组件的最后指定 那些数据可以在外部获取
  defineExpose({title})
</script>

<!-- scoped:局部样式,加上之后尽在当前组件中的标签生效 -->
<style scoped>
  h2{
    color: red;
  }

  .person {
      background-color: #007acc;
      padding: 20px;
  }
</style>

用在组件上 (组件部分参考上方代码)

<template>
    <Person ref="ren" />
    <button @click="showCom">输出组件</button>
</template>

<script lang="ts" setup>
    defineOptions({name:"App"})
    import {ref} from 'vue'
    import Person from "./components/Person.vue"
    let ren = ref()
    function showCom(){
        // 输出组件中 title 变量的内容 <h2 data-v-4cadc14e="">666</h2>
       console.log(ren.value.title)
       // 输出 undefined 因为组件不允许外部获取这个变量
       console.log(ren.value.t2) 
    }
</script>

Props 父传子

在一个父组件中引入子组件,需要向子组件传递一些数据,例如传递一个 list 让子组件进行展示,这里引入了 TS 语法 在传参时规定了类型。

src\types\index.ts ts 结构定义

// 定义一个接口(类似 go 的结构体)
export interface User {
    id:string
    name:string
    age:number
}

// 定义一个自定义类型的 Users
export type Users = Array<User>

src\App.vue 父组件

<template>
    <!-- 这里参数前需要加 ":" 否则只会将 "users" 作为字符串传递 -->
    <Person :list="users" />
</template>

<script lang="ts" setup>
    defineOptions({name:"App"})
    import {reactive, ref} from 'vue'
    import {type Users} from '@/types/index'
    import Person from "./components/Person.vue"

    let users = reactive<Users>([
        {id:"1",name:"666",age:18},
        {id:"2",name:"999",age:15},
    ])
</script>

src\components\Person.vue 子组件

<template>
    <div class="person">
       <h2 v-for="item in list">{{item.id}} -- {{ item.name }}</h2>
    </div>
</template>
<script lang="ts" setup>
  import { defineProps } from 'vue';
  import {type Users} from '@/types'
  defineOptions({ name: "Person" })
  
  // 第一种用法 只接收 然后在模板中使用
  // defineProps(['list'])

  // 第二种用法 限制接受类型 括号里面不用写内容
  // defineProps<{list:Users}>()

  // 第三种用法 限制类型+默认值+必要性 若父组件不传 list 就使用默认值
  let props = withDefaults(defineProps<{list?:Users}>(),{
    list : () => [{id:"1",name:"888",age:18}]
  })

  console.log(props.list)
</script>

<!-- scoped:局部样式,加上之后尽在当前组件中的标签生效 -->
<style scoped>

  .person {
      background-color: #007acc;
      padding: 20px;
  }
</style>

声明周期

概念:Vue组件实例在创建时要经历一系列的初始化步骤,在此过程中Vue会在合适的时机,调用特定的函数,从而让开发者有机会在特定阶段运行自己的代码,这些特定的函数统称为:生命周期钩子

  • 创建阶段:setup
  • 挂载阶段:onBeforeMountonMounted
  • 更新阶段:onBeforeUpdateonUpdated
  • 卸载阶段:onBeforeUnmountonUnmounted
  • 常用的钩子:onMounted(挂载完毕)、onUpdated(更新完毕)、onBeforeUnmount(卸载之前)

:::alert-info
生命周期整体分为四个阶段,分别是:创建、挂载、更新、销毁,每个阶段都有两个钩子,一前一后。
父子组件的顺序类似于深度有限算法,顺序如下:

  • 父组件 挂载前

    • 子组件1 挂载前
    • 子组件1 挂载后
    • 子组件2 挂载前
    • 子组件2 挂载后
  • 父组件 挂载后

:::

<template>
    <div class="person">
      <h2 @click="sum++">{{ sum }}</h2>
    </div>
</template>
<script lang="ts" setup>
  import { defineProps, onBeforeMount, onBeforeUnmount, onBeforeUpdate, onMounted, onUnmounted, onUpdated, ref } from 'vue';
  defineOptions({ name: "Person" })

  let sum = ref(0)

  // 创建
  console.log('创建')

  // 挂载前
  onBeforeMount(() => {
    console.log('挂载前')
  })

  // 挂载后
  onMounted(() => {
    console.log('挂载后')
  })

  // 更新前
  onBeforeUpdate(() => {
    console.log('更新前')
  })
  // 更新后
  onUpdated(() => {
    console.log('更新后')
  })

  // 卸载前
  onBeforeUnmount(() => {
    console.log('卸载前')
  })

  onUnmounted(() => {
    console.log('卸载完毕')
  })
</script>

自定义 Hook

什么是 Hook?
本质上是一个函数,把 setup 函数中使用的 Composition API 进行了封装,把同一功能的逻辑代码、业务数据汇总在了一起,方便后期功能维护。

自定义hook的优势:复用代码, 让setup中的逻辑更清楚易懂。

业务代码:

src\hooks\useDog.ts 将 加载dog的方法、doglist 响应式变量 都放入 useDog 这个文件中

注意:文件名称要按照功能来,比如 Dog 相关的功能就叫 useDog.ts,订单相关的东西 就叫 useOrder.ts

import { defineProps, onMounted,reactive } from 'vue'; 
import axios from 'axios';

export default () => {
    // 存储狗的列表
  let dogList = reactive<string[]>([])

  // 加载一张新图片
  async function getDog() {
     let result = await axios.get('https://dog.ceo/api/breed/pembroke/images/random')
     dogList.push(result.data.message)
  }

  // hooks 中也能写生命周期函数, 挂载后直接加载一只狗
  onMounted(() => {
    getDog()
  })

  // 将工具和方法暴露出去
  return {dogList,getDog}
}

src\components\Person.vue Person 组件,引入并使用 useDog

<template>
    <div class="person">
      <img v-for="dog in dogList" :src="dog" alt="">
      <button @click="getDog">加载一只狗</button>
    </div>
</template>
<script lang="ts" setup>
  defineOptions({ name: "Person" })
  import useDog from '@/hooks/useDog';
  const {dogList,getDog} = useDog()

</script>

路由 Router

vue3 中使用路由 需要先安装 npm i vue-router

基本切换效果

src\router\index.ts 路由组件基本代码:

// 创建一个路由

// 引入路由
import {createRouter,createWebHistory} from "vue-router"
// 引入组件
import Home from "@/components/Home.vue"
import News from "@/components/News.vue"
import About from "@/components/About.vue"

// 创建路由器
const router = createRouter({
    history: createWebHistory(), // 指定工作模式
    routes:[
        {
            path:'/home',
            component: Home
        },
        {
            path:'/news',
            component: News
        },
        {
            path:'/about',
            component: About
        }
    ]
})

// 暴露出去
export default router

src\main.ts 文件代码如下:

// 引入 createApp 用于创建应用
import {createApp} from 'vue'
// 引入 App 根组件
import App from './App.vue'
import router from './router'

// 挂载 组件
const app = createApp(App)
app.use(router)
// 挂载整个app到页面
app.mount('#app')

src\App.vue 代码如下:

<template>
    <div class="app">
        <!-- 标题 -->
        <h2>Vue Router</h2>
        <div class="nav">
            <!-- 
                RouterLink 标签定义 路由跳转连接
                    to 定义跳转地址 不能写 href
                    active-class 定义选中后的 class
            -->
            <RouterLink to="/home" active-class="active">首页</RouterLink>
            <RouterLink to="/news" active-class="active">新闻</RouterLink>
            <RouterLink to="/about" active-class="active">关于</RouterLink>
        </div>
        <div class="main">
            <RouterView></RouterView>
        </div>
    </div>
</template>

<script lang="ts" setup>
    defineOptions({name:"App"})
</script>

<style scoped>
    .app {width: 800px; margin: 0 auto;}
    h2{background-color: bisque;border:1px solid blue;text-align: center;}
    .nav{display: flex;justify-content: space-around;width: 100%;}
    .nav a{display: flex;justify-content: center;width: 200px;text-align: center;line-height: 50px;background-color: rosybrown;}
    .nav a.active{background-color: blueviolet;}
    .main{margin-top: 20px;;border: 1px solid #ccc;width: 800px;height: 300px;}
</style>

路由 两个注意点

  1. 路由组件通常放在 /src/pages/src/views 文件夹,一般组件通常放在 components 文件夹

    • 路由组件:靠路由规则渲染出来的 routes:[{path:'/demo',component:Demo}]
    • 一般组件:写标签实现一个功能的组件 <Demo/>
  2. 通过路由切换的组件默认是被卸载掉了,需要的时候再去加载。

路由工作模式

History 模式

优点:路径中不带 # 直接写路径,从用户角度看 URL 更美观
缺点:生产环境中需要 Web 服务器处理路径问题,否则会出现 404 错误。(注:就是后端说的路由重新)

const router = createRouter({
      history:createWebHistory(), //history模式
      /******/
})

Nginx 配置:

location / {
    root /root/project';
    index index.html
    try_files $uri $uri/ /index.html
}

Hash 模式

特点:兼容性更好,不需要服务器做特殊处理
缺点:URL 中带有 # 不太美观,且 SEO 优化方面相对较差

const router = createRouter({
      history:createWebHashHistory(), //hash模式
      /******/
})

to 的两种写法

字符串的写法

<!-- 第一种:to的字符串写法 -->
<router-link active-class="active" to="/home">主页</router-link>

对象写法

<!-- 第二种:to的对象写法,这种方式方便传参 -->
<router-link active-class="active" :to="{path:'/home'}">Home</router-link>

命名路由

作用:可以简化路由跳转及 params 传参需要用到

路由规则:

routes:[
  {
    name:'zhuye',
    path:'/home',
    component:Home
  },
  {
    name:'xinwen',
    path:'/news',
    component:News,
  },
  {
    name:'guanyu',
    path:'/about',
    component:About
  }
]

跳转路由:

<!--简化后:直接通过名字跳转(to的对象写法配合name属性) -->
<router-link :to="{name:'guanyu'}">跳转</router-link>

嵌套路由

场景:例如系统配置页面,路由是 /setting,但这个页面中需要根据页面内的菜单 再嵌套更具体的配置页面,这时就需要 嵌套路由了(注:嵌套路由本质是 在一个路由组件中嵌套另一个路由组件,如果两个组件之间没有嵌套关系 则不需要使用嵌套路由)。

这里以新闻页面为例,嵌套显示新闻详情

路由规则配置

const router = createRouter({
    history: createWebHistory(), // 指定工作模式
    routes:[
        ...
        {
            path:'/news',
            component: News,
            children:[
                // 子路由 访问路径: /news/detail
                {path: 'detail',component:Detail}
            ]
        },
        ...
    ]
})

src\pages\News.vue 新闻组件

<template>
    <div class="news">
        <ul>
            <li v-for="news in newsList" :key="news.id">
                <RouterLink :to="{path:'/news/detail'}">{{ news.title }}</RouterLink>
            </li>
        </ul>
        <div class="news-conent">
            <RouterView></RouterView>
        </div>
    </div>
</template>

<script lang="ts" setup>
    defineOptions({ name: "News" });
    import { reactive, ref } from "vue";

    const newsList = reactive([
        {id:"1",title:"新闻1",content:"111"},
        {id:"2",title:"新闻2",content:"222"},
        {id:"3",title:"新闻3",content:"333"},
    ]);
</script>

269074414989304.webp

路由传参

Query 传参

通过 Query 的方式从URL种传参

传递参数

<template>
    <div class="news">
        <ul>
            <li v-for="news in newsList" :key="news.id">
                <!-- 第一种写法 模板字符串 -->
                <!-- <RouterLink :to="`/news/detail?id=${news.id}`">{{ news.title }}</RouterLink> -->
                <!-- 第二种写法  -->
                 <RouterLink
                    :to="{
                        path:'/news/detail',
                        query:{
                            id:news.id,
                            title:news.title,
                            content:news.content
                        }
                    }">{{ news.title }}</RouterLink> 
            </li>
        </ul>
        <div class="news-conent">
            <RouterView></RouterView>
        </div>, 
    </div>
</template>

<script lang="ts" setup>
    defineOptions({ name: "News" });
    import { reactive, ref } from "vue";

    const newsList = reactive([
        {id:"1",title:"新闻1",content:"111"},
        {id:"2",title:"新闻2",content:"222"},
        {id:"3",title:"新闻3",content:"333"},
    ]);
</script>

获取参数

<template>
    <ul class="news-list">
        <li>编号:{{query.id}}</li>
        <li>标题:{{query.title}}</li>
        <li>内容:{{query.content}}</li>
    </ul>
</template>

<script lang="ts" setup>
import { toRefs } from 'vue';
import { useRoute } from 'vue-router';

 defineOptions({name:"Detail"})
 let route = useRoute()
 let {query} = toRefs(route)

</script>

Params 参数

在路由种使用 占位符省略 url传参时的 Key

:::alert-warning

  • 传递 params 参数时,若使用 to 的对象写法,必须使用 name 配置项,不能使用 path
  • 传递 params 参数时,需要提前在路由规则中占位,若参数为可选参数,需要使用 ? 结尾,例如: /:xxx?
    :::

路由规则

// 创建一个路由

// 引入路由
import {createRouter,createWebHistory} from "vue-router"
// 引入组件
import Home from "@/pages/Home.vue"
import News from "@/pages/News.vue"
import About from "@/pages/About.vue"
import Detail from "@/pages/Detail.vue"

// 创建路由器
const router = createRouter({
    history: createWebHistory(), // 指定工作模式
    routes:[
        {
            path:'/home',
            component: Home
        },
        {
            path:'/news',
            component: News,
            children:[
                // 子路由
                {
                    // params 传参对象形式必须用 name
                    name:'xq',
                    // content 加 "?" 表示可选参数
                    path: 'detail/:id/:title/:content?',
                    component:Detail
                }
            ]
        },
        {
            path:'/about',
            component: About
        }
    ]
})

// 暴露出去
export default router

传递参数

<template>
    <div class="news">
        <ul>
            <li v-for="news in newsList" :key="news.id">
                <!-- 第一种写法 模板字符串 -->
                <!-- <RouterLink :to="`/news/detail/${news.id}/${news.title}/${news.content}`">{{ news.title }}</RouterLink> -->
                <!-- 第二种写法 -->
                <RouterLink :to="{
                    name:'xq',
                    params:news
                }">{{ news.title }}</RouterLink>
            </li>
        </ul>
        <div class="news-conent">
            <RouterView></RouterView>
        </div>
    </div>
</template>

<script lang="ts" setup>
    defineOptions({ name: "News" });
    import { reactive, ref } from "vue";

    const newsList = reactive([
        {id:"1",title:"新闻1",content:"111"},
        {id:"2",title:"新闻2",content:"222"},
        {id:"3",title:"新闻3",content:"333"},
    ]);
</script>

接收参数

<template>
    <ul class="news-list">
        <li>编号:{{route.params.id}}</li>
        <li>标题:{{route.params.title}}</li>
        <li>内容:{{route.params.content}}</li>
    </ul>
</template>

<script lang="ts" setup>
import { toRefs } from 'vue';
import { useRoute } from 'vue-router';
    defineOptions({name:"Detail"})
    let route = useRoute()
    console.log(route.params)
</script>

<style scoped>

</style>

Props 参数

作用:让路由组件可以通过 Porps 的方式获取参数

路由写法

 {
    // params 传参对象形式必须用 name
    name:'xq',
    // content 加 "?" 表示可选参数
    path: 'detail/:id/:title/:content?',
    component:Detail,
    // props 布尔值写法,把收到的每一组params参数,作为 props 传递给 Detail 组件
    // props:true,
    // props 函数写法,把函数返回的每一组 Key-Value 作为 props 传递给 Detail 组件
    props: (route) => route.query,
    // props 对象写法,把对象的每一组 Key-Value 作为 props 传递给 Detail 组件
    //props: {a:1,b:2}
}

接收参数

<template>
    <ul class="news-list">
        <li>编号:{{id}}</li>
        <li>标题:{{title}}</li>
        <li>内容:{{content}}</li>
    </ul>
</template>

<script lang="ts" setup>
import { toRefs } from 'vue';
import { useRoute } from 'vue-router';
    defineOptions({name:"Detail"})
   defineProps(['id','title','content'])
</script>

replace 属性

作用:控制路由跳转时操作浏览器历史记录的模式。

浏览器的历史记录有两种写入方式:分别为push和replace:

  • push 是追加历史记录(默认值)。
  • replace 是替换当前记录。

开启 replace 模式

<RouterLink replace .......>News</RouterLink>

函数式编程导航

大白话:通过 JS 实现 RouterLink 的功能,完成路由跳转

场景

  • 只能通过 JS 跳转的,例如只有到时间了才能跳转到某个页面, RouterLink 做不到。
  • 其他事件触发跳转的,例如鼠标滑过触发跳转。
  • 不使用 RouterLink 也想跳转

    <template>
      <div class="news">
          <ul>
              <li v-for="news in newsList" :key="news.id">
                      <button @click="showDetail(news)">查看</button>
                  <RouterLink :to="{
                      name:'xq',
                      params:news
                  }">{{ news.title }}</RouterLink>
              </li>
          </ul>
          <div class="news-conent">
              <RouterView></RouterView>
          </div>
      </div>
    </template>
    
    <script lang="ts" setup>
      defineOptions({ name: "News" });
      import { reactive } from "vue";
    import { useRouter } from "vue-router";
    
      const newsList = reactive([
          {id:"1",title:"新闻1",content:"111"},
          {id:"2",title:"新闻2",content:"222"},
          {id:"3",title:"新闻3",content:"333"},
      ]);
    
      // 注意 这里是路由器 不是路由
      const router = useRouter()
    
      // 通过函数跳转路由
      function showDetail(news:any) {
          router.push({
              name:'xq',
              params:news
          })
      }
    </script>

路由重定向

作用:当匹配到 path 时,重定向到指定路由

{
    path:'/',
    redirect:'/home'
}

Pinia(状态管理)

大白话说法:就是组件之间数据的全局共享,例如 登录组件用户登录之后,获取到的用户信息 可以交给 状态管理工具来维护,(类似后端的 Session)

安装 Pinia

安装库

npm i pinia

src\main.ts 引入

import {createApp} from 'vue'
import App from './App.vue'
// 引入  pinia
import { createPinia } from 'pinia'

const app = createApp(App)
// 创建 pinia
const pinia =createPinia()
app.use(pinia)
app.mount('#app')

存储加读取数据

  • 在 src 目录中添加 store 目录,是用来保存 状态业务逻辑 的实体,每个组件都可以 读取写入 它。
  • 它有三个概念:stategetteraction,相当于组件中的:data(数据)、computed(计算属性)、methods (方法)。

pinia 编码

文件:src/store/count.ts

// 引入defineStore用于创建store
import {defineStore} from 'pinia'

// 定义并暴露一个store
export const useCountStore = defineStore('count',{
  // 动作
  actions:{},
  // 状态
  state(){
    return {
      sum:6
    }
  },
  // 计算
  getters:{}
})

组件中使用

文件:src\components\Count.vue

<template>
    <div class="count">
        <h2>当前求和为:{{ countStore.sum }} </h2>
        <!-- 将选择的值转为数字类型 -->
        <select v-model.number="n">
            <option value="1">1</option>
            <option value="2">2</option>
            <option value="3">3</option>
        </select>
        <button @click="add()">加</button>
        <button @click="sub()">减</button>
    </div>
</template>
    
<script setup lang="ts">
import { ref } from 'vue';

    defineOptions({name:"Count"})
    import {useCountStore} from '@/store/count'

    const countStore = useCountStore()
    
    let n = ref(1) // 用户选择的步长

    function add(){
        countStore.sum+=n.value
    }

    function sub(){
        countStore.sum-=n.value
    }

</script>

修改数据的三种方式

直接修改

countStore.sum = 666

批量修改

修改多条数据可以用这个,只会触发一次修改操作,性能更优

countStore.$patch({
            sum:666,
             text:"Hello,dbkuaizi"
        })

借助 Action 修改

在 store 中可以写一些逻辑,例如 在多个页面有用户注销、下单 这些功能,可以通过 action 维护

import { defineStore } from 'pinia'

export const useCountStore = defineStore('count', {
  /*************/
  actions: {
    //加
    increment(value:number) {
      if (this.sum < 10) {
        //操作countStore中的sum
        this.sum += value
      }
    },
    //减
    decrement(value:number){
      if(this.sum > 1){
        this.sum -= value
      }
    }
  },
  /*************/
})

在组件中使用:

// 使用countStore
const countStore = useCountStore()

// 调用对应action
countStore.increment(n.value)

storeToRefs

  • 借助storeToRefs将store中的数据转为ref对象,方便在模板中使用。
  • 注意:pinia提供的storeToRefs只会将数据做转换,而Vue的toRefs会转换store中数据。
<template>
    <div class="count">
        <h2>当前求和为:{{ sum }} </h2>
        <h3>{{ text }}</h3>
        <!-- 将选择的值转为数字类型 -->
        <select v-model.number="n">
            <option value="1">1</option>
            <option value="2">2</option>
            <option value="3">3</option>
        </select>
        <button @click="add()">加</button>
        <button @click="sub()">减</button>
    </div>
</template>
    
<script setup lang="ts">
    defineOptions({name:"Count"})
    import { ref } from 'vue';
    import {useCountStore} from '@/store/count'
    import { storeToRefs } from 'pinia';

    const countStore = useCountStore()
    // storeToRefs 只会关注 store 中的 state 数据部分,不会堆 方法进行 ref 包裹
    const {sum,text} = storeToRefs(countStore)
    
    let n = ref(1) // 用户选择的步长

    function add(){
        countStore.increment(n.value)
    }

    function sub(){
        countStore.sum-=n.value
    }

</script>

Getters

作用:当 state 中的数据需要经过处理后再使用,可以通过 getters 配置

import {defineStore} from 'pinia'

export const useCountStore = defineStore('count',{
    state(){
        return {
            sum:6,
            text:"hello word"
        }
    },
    // 类似于计算属性
    getters: {
        // 使用箭头函数
        bigSum:state => state.sum * 10,
        // 使用 普通函数写法 需要指定返回值类型
        upperText():string{
            return this.text.toUpperCase()
        }
    }
})

$subscribe 监听数据变化

 // 数据
const talkStore = useTalkStore()

// 通过 store 的 $subscribe() 方法侦听 state 及其变化
talkStore.$subscribe((mutate,state)=>{
    console.log(mutate,state)
    localStorage.setItem('talkList',JSON.stringify(state.talkList))
})

Store 组合式写法

import {defineStore} from 'pinia'
import axios from 'axios';
import { nanoid } from 'nanoid';

// 选项式
export const useTalkStore = defineStore('talk',{
    state(){
        return {
            talkList:JSON.parse(localStorage.getItem('talkList') as string) || [] 
        }
    },
    actions:{
        // 方法
    async getTalk(){
        // 发送请求 拿到响应值通过连续解构 赋值+重命名给 title
        let {data:{content:title}} = await axios.get("https://api.uomg.com/api/rand.qinghua?format=json")
        // 放入数组中
        this.talkList.push({id: nanoid(),title})
        }
    }
})

// 组合式
import { reactive } from 'vue';

export const useTalkStore2 = defineStore('talk',()=>{
    
    const talkList = reactive(JSON.parse(localStorage.getItem('talkList') as string) || [] )

    async function getTalk(){
        // 发送请求 拿到响应值通过连续解构 赋值+重命名给 title
        let {data:{content:title}} = await axios.get("https://api.uomg.com/api/rand.qinghua?format=json")
        // 放入数组中
        talkList.push({id: nanoid(),title})
    }

    return {talkList,getTalk}
}); 

组件通信

组件关系传递方式
父传子1.props
2.v-model
3.$refs
4.默认插槽、具名插槽
子传父1. props
2.自定义事件
3.v-model
4.$parent
5.作用域插槽
祖传孙、孙传祖1. $attrs
2. provideinject
兄弟间、任意组件间1.mitt
2. pinia

props

props 是使用频率最高的父子组件通讯方式,常用于:父 <=> 子

  • 父传子,属性值是非函数
  • 子传父,属性值是函数

父组件

<template>
  <div class="father">
    <h3>父组件,</h3>
        <h4>我的车:{{ car }}</h4>
        <h4>儿子给的玩具:{{ toy }}</h4>
        <Child :car="car" :getToy="getToy"/>
  </div>
</template>

<script setup lang="ts" name="Father">
    import Child from './Child.vue'
    import { ref } from "vue";
    // 数据
    const car = ref('奔驰')
    const toy = ref()
    // 方法
    function getToy(value:string){
        toy.value = value
    }
</script>

子组件

<template>
  <div class="child">
    <h3>子组件</h3>
        <h4>我的玩具:{{ toy }}</h4>
        <h4>父给我的车:{{ car }}</h4>
        <button @click="getToy(toy)">玩具给父亲</button>
  </div>
</template>

<script setup lang="ts" name="Child">
    import { ref } from "vue";
    const toy = ref('奥特曼')
    
    defineProps(['car','getToy'])
</script>

自定义事件

概述:自定义事件通常用于子传父,简单来说就是子组件定义一个事件,并在需要的时候触发这个事件。接着在父组件使用子组件时,注册对应的自定义事件,并通过回调函数获取子组件的传参。

:::alert-info
事件传参的本质

  • 接收端: 定义一个函数,这个函数接受一个或多个参数,这些参数就是函数调用时发送方发送的参数,
  • 发送端:调用接收端定义的函数,并在调用时将需要传递的数据通过传参发送给对方
    :::

注意:需要区分原生事件和自定义事件

  • 原生事件:

    • 事件名是特定的(clickmosueenter等等)
    • 事件对象 $event: 是包含事件相关信息的对象(pageXpageYtargetkeyCode
  • 自定义事件:

    • 事件名是任意名称
    • 事件对象$event: 是调用emit时所提供的数据,可以是任意类型!!!

示例

<!--在父组件中,给子组件绑定自定义事件:-->
<Child @send-toy="toy = $event"/>

<!--注意区分原生事件与自定义事件中的$event-->
<button @click="toy = $event">测试</button>
//子组件中,触发事件:
this.$emit('send-toy', 具体数据)

父组件

<template>
  <div class="father">
    <h3>父组件</h3>
        <h4 v-show="toy">子给的玩具:{{ toy }}</h4>
        <!-- 给子组件 Child 绑定事件 -->
    <Child @send-toy="saveToy"/>
  </div>
</template>

<script setup lang="ts" name="Father">
  import Child from './Child.vue'
    import { ref } from "vue";
    // 数据
    let toy = ref('')
    // 用于保存传递过来的玩具
    function saveToy(value:string){
        console.log('saveToy',value)
        toy.value = value
    }
</script>

子组件

<template>
  <div class="child">
    <h3>子组件</h3>
        <h4>玩具:{{ toy }}</h4>
        <!-- 点击时,通过 emit 调用自定义事件 send-toy,并传递 toy 参数 -->
        <button @click="emit('send-toy',toy)">测试</button>
  </div>
</template>

<script setup lang="ts" name="Child">
    import { ref } from "vue";
    // 数据
    let toy = ref('奥特曼')
    // 声明事件
    const emit = defineEmits(['send-toy'])
</script>

Mitt

与 Vue2 的 $bus、事件发布与订阅(pubsub)功能类似,可以实现任意组件间的通信。

mitt 和自定义函数玩法很像,只是通过一个第三方库实现事件的订阅和触发,进而完成数据的交互工作。区别在于自定义组件,只能实现父子之间的数据传递,而第三方库只需要响应的组件引入,就可是实现事件的订阅与触发。

安装 mitt

npm i mitt

新建文件:src\utils\emitter.ts 用于存放 mitt 初始化 全局变量

// 引入mitt 
import mitt from "mitt";

// 创建emitter
const emitter = mitt()

/*
  // 绑定事件
  emitter.on('abc',(value)=>{
    console.log('abc事件被触发',value)
  })
  emitter.on('xyz',(value)=>{
    console.log('xyz事件被触发',value)
  })

  setInterval(() => {
    // 触发事件
    emitter.emit('abc',666)
    emitter.emit('xyz',777)
  }, 1000);

  setTimeout(() => {
    // 清理事件
    emitter.all.clear()
  }, 3000); 
*/

// 创建并暴露mitt
export default emitter

数据接收端

绑定 send-toy 事件,

import emitter from "@/utils/emitter";
import { onUnmounted } from "vue";

// 绑定事件
emitter.on('send-toy',(value)=>{
  console.log('send-toy事件被触发',value)
})

onUnmounted(()=>{
  // 解绑事件
  emitter.off('send-toy')
})

发送端触发

import emitter from "@/utils/emitter";

function sendToy(){
  // 触发事件
  emitter.emit('send-toy',toy.value)
}

v-model 传参

:::alert-warning
这一节讲的是 UI 组件库,如何通过 v-model 实现父组件中响应式变量的双向绑定的底层原理,实际开发中如果不写底层组件 很少会用到这个。
但是这个知识点很重要,需要理解底层原理。
:::

概述:实现父子组件间的双向通信

v-model 的本质

<!-- 使用 v-model 指令 -->
<input type="text" v-model="userName">

<!-- v-model的本质是下面这行代码
     本质上是两个操作,一个是数据展示时 通过 :value 绑定,然后在更新时通过 @input 触发
 -->
<input 
  type="text" 
  :value="userName" 
  @input="userName =(<HTMLInputElement>$event.target).value"
>

组件上的 v-model

本质::modelValue + @update:modelValue 事件

<!-- 组件标签上使用v-model指令 -->
<AtguiguInput v-model="userName"/>

<!-- 组件标签上v-model的本质 -->
<AtguiguInput :modelValue="userName" @update:modelValue="userName = $event"/>

AtguiguInput 组件中

<template>
  <div class="box">
    <!--将接收的value值赋给input元素的value属性,目的是:为了呈现数据 -->
        <!--给input元素绑定原生input事件,触发input事件时,进而触发update:model-value事件-->
    <input 
       type="text" 
       :value="modelValue" 
       @input="emit('update:model-value',$event.target.value)"
    >
  </div>
</template>

<script setup lang="ts" name="AtguiguInput">
  // 接收props
  defineProps(['modelValue'])
  // 声明事件
  const emit = defineEmits(['update:modelValue'])
</script>

指定 value 的名称

如果指定 value 的名称,则可以传递多个 双向绑定变量

<AtguiguInput v-model:ming="username" v-model:mima="password"/>

AtguiguInput 组件中 可以这样写

<template>
  <input 
    type="text" 
    :value="ming"
    @input="emit('update:ming',(<HTMLInputElement>$event.target).value)"
  >
  <br>
  <input 
    type="text" 
    :value="mima"
    @input="emit('update:mima',(<HTMLInputElement>$event.target).value)"
  >
</template>

<script setup lang="ts" name="AtguiguInput">
  defineProps(['ming','mima'])
  const emit = defineEmits(['update:ming','update:mima'])
</script>

:::alert-info
$event 是啥? 什么时候使用 $event.target ?

  • 对于原生事件(@input 这种),$event 时事件对象,可以 .target
  • 对于自定义事件,$event 是触发时 所传递的数据,不能 .target
    :::

defineModel 传参

defineModel 是 Vue 3.4+ 开始支持且官方推荐的一种组件双向绑定的写法,可以粗暴的理解为是原始 v-model 的语法糖写法。

我们以上面的 v-model 为例 使用 defineModel 可以大幅简化组件内部响应值的写法,Vue 替我们完成了 :modelValue + @update:modelValue 事件。

<template>
  <input  type="text" v-model="ming">
  <br>
  <input type="text" v-model="mima">
</template>

<script setup lang="ts" name="AtguiguInput">
  // 如果使用的元素中没有指定 value 名称 就可以直接获取
  let val = defineModel()
  // 如果指定了 value 的名称 或者需要传递多个,则传递一个 绑定的value名称即可
  let ming = defineModel('ming')
  let mima = defineModel('mima')
</script>

attrs 祖传孙

说明:$attrs 是一个对象,包含了所有父组件传递的标签属性

:::alert-warning
注意:$attrs 会自动排除 props 中声明的属性(可以认为声明过的 props 被子组件自己“消费”了)
:::

// 父组件
<template>
  <div class="father">
    <h3>父组件</h3>
        <Child :a="a" :b="b" :c="c" :d="d" v-bind="{x:100,y:200}" :updateA="updateA"/>
  </div>
</template>

<script setup lang="ts" name="Father">
    import Child from './Child.vue'
    import { ref } from "vue";
    let a = ref(1)
    let b = ref(2)
    let c = ref(3)
    let d = ref(4)

    function updateA(value){
        a.value = value
    }
</script>


// 子组件 =============
<template>
    <div class="child">
        <h3>子组件</h3>
        <h4> {{ a }} </h4>
        <!-- 继续向下传递父组件传递过来的、在当前组件中没有 defineProps 接收的参数 -->
        <GrandChild v-bind="$attrs"/>
    </div>
</template>

<script setup lang="ts" name="Child">
    import GrandChild from './GrandChild.vue'
    defineProps(['a'])
</script>

// 孙组件 =============

<template>
    <div class="grand-child">
        <h3>孙组件</h3>
        <h4>b:{{ b }}</h4>
        <h4>c:{{ c }}</h4>
        <h4>d:{{ d }}</h4>
        <h4>x:{{ x }}</h4>
        <h4>y:{{ y }}</h4>
        <button @click="updateA(666)">点我更新A</button>
    </div>
</template>

<script setup lang="ts" name="GrandChild">
    defineProps(['b','c','d','x','y','updateA'])
</script>

$refs 与 $parent

概述: 前面提到 $ref 是组件的唯一标识,作用类似于传统 DOM 中的 ID,所以我们也可以通过 $ref 的方式来 操作对应的组件数据、事件。

  • $refs 值为对象,包含所有ref 属性标识的DOM元素或组件实例。
  • $parent 值为对象,当前组件的父组件实例对象。

:::alert-info
让外部获取数据的前提时通过 defineExpose 向外暴露
:::

父组件

<template>
    <div class="father">
        <h3>父组件</h3>
        <h4>房产:{{ house }}</h4>
        <button @click="getAllChild($refs)">让所有孩子的书变多</button>
        <Child1 ref="c1"/>
    </div>
</template>

<script setup lang="ts" name="Father">
    import Child1 from './Child1.vue'
    import { ref,reactive } from "vue";

    // 数据
    let house = ref(4)
    // 遍历所有被 ref 标记的标签
    function getAllChild(refs:{[key:string]:any}){
        console.log(refs)
        for (let key in refs){
            refs[key].book += 3
        }
    }
    
    // 向外部提供数据
    defineExpose({house})
</script>

子组件

<template>
  <div class="child1">
    <h3>子组件1</h3>
        <h4>玩具:{{ toy }}</h4>
        <h4>书籍:{{ book }} 本</h4>
        <button @click="minusHouse($parent)">干掉父亲的一套房产</button>
  </div>
</template>

<script setup lang="ts" name="Child1">
    import { ref } from "vue";
    // 数据
    let toy = ref('奥特曼')
    let book = ref(3)

    // 方法
    function minusHouse(parent:any){
        parent.house -= 1
    }

    // 把数据交给外部
    defineExpose({toy,book})

</script>

provide 与 inject

概述:实现父组件与后代组件的直接通信,而不用操作中间组件

  • 在祖先组件中通过 provide 配置向后代组件提供数据
  • 在后代组件中通过 inject 配置来声明接收数据

父组件 使用 provide 提供数据

<template>
  <div class="father">
    <h3>父组件</h3>
    <h4>资产:{{ money }}</h4>
    <h4>汽车:{{ car }}</h4>
    <button @click="money += 1">资产+1</button>
    <button @click="car.price += 1">汽车价格+1</button>
    <Child/>
  </div>
</template>

<script setup lang="ts" name="Father">
  import Child from './Child.vue'
  import { ref,reactive,provide } from "vue";
  // 数据
  let money = ref(100)
  let car = reactive({
    brand:'奔驰',
    price:100
  })
  
  // 用于更新money的方法
  function updateMoney(value:number){
    money.value += value
  }
  
// 提供数据
  provide('moneyContext',{money,updateMoney})
  provide('car',car)
</script>

后代组件 通过 inject 获取祖辈组件提供的数据

<template>
  <div class="grand-child">
    <h3>我是孙组件</h3>
    <h4>资产:{{ money }}</h4>
    <h4>汽车:{{ car }}</h4>
    <button @click="updateMoney(6)">点我</button>
  </div>
</template>

<script setup lang="ts" name="GrandChild">
  import { inject } from 'vue';
  // 注入数据 注: inject 第二个参数是默认值,当注入的参数不存在时,使用该默认值
  let {money,updateMoney} = inject('moneyContext',{money:0,updateMoney:(x:number)=>{}})
  let car = inject('car')

slot 插槽

529024723291845.webp

在父组件中使用子组件,可以将不确定、由父组件决定内容的部分定义为插槽,可以简单的理解为预览内容的占位符。

默认插槽

父组件中:

<Category title="今日热门游戏">
    <ul>
    <li v-for="g in games" :key="g.id">{{ g.name }}</li>
    </ul>
</Category>

子组件中:

<template>
    <div class="item">
    <h3>{{ title }}</h3>
    <!-- 默认插槽 -->
    <slot>默认内容</slot>
    </div>
</template>

具名插槽

说明:默认插槽只能有一个,具名插槽可以有多个

父组件中: 通过 v-slot#插槽名 指定插槽名称

<Category title="今日热门游戏">
    <template v-slot:s1>
    <ul>
        <li v-for="g in games" :key="g.id">{{ g.name }}</li>
    </ul>
    </template>
    <template #s2>
    <a href="">更多</a>
    </template>
</Category>

子组件中: 通过 name 设置插槽名称

<template>
    <div class="item">
    <h3>{{ title }}</h3>
    <slot name="s1"></slot>
    <slot name="s2"></slot>
    </div>
</template>

作用域插槽

概念:数据在组件的自身,但根据数据生成的结构需要组件的使用者来决定。

大白话:数据在子组件中,但根据数据生成的结构,却由父亲决定。

场景:

  1. 前端 UI 库的数据表格组件,数据表格的渲染是组件内部实现的,但使用时想自定义列的单元格内容,就可以通过作用域插槽实现。
  2. 前端 UI 库的弹窗组件,弹窗的内容可以通过作用域插槽实现

父组件:

<Game v-slot="params">
<!-- <Game v-slot:default="params"> -->
<!-- <Game #default="params"> -->
<ul>
    <li v-for="g in params.games" :key="g.id">{{ g.name }}</li>
</ul>
</Game>

子组件:

<template>
<div class="category">
    <h2>今日游戏榜单</h2>
    <slot :games="games" a="哈哈"></slot>
</div>
</template>

<script setup lang="ts" name="Category">
import {reactive} from 'vue'
let games = reactive([
    {id:'asgdytsa01',name:'英雄联盟'},
    {id:'asgdytsa02',name:'王者荣耀'},
    {id:'asgdytsa03',name:'红色警戒'},
    {id:'asgdytsa04',name:'斗罗大陆'}
])
</script>

其他 API

shallowRef 与 shallowReactive

shallowRefshallowReactive 的作用是创建一个响应式的数据、或响应式的对象,然后只对最顶层的数据做响应式的处理,对更深层的数据不处理。

用法:

let myVar = shallowRef(initialValue);

const myObj = shallowReactive({ ... });

:::alert-info

总结

通过使用 shallowRef() 和 shallowReactive() 来绕开深度响应。浅层式 API 创建的状态只在其顶层是响应式的,对所有深层的对象不会做任何处理,避免了对每一个内部属性做响应式所带来的性能成本,这使得属性的访问变得更快,可提升性能。
:::

readDonly 与 shallowReadonly

说明:创建响应式数据的深只读副本,效果类似于数据库的视图 (可以读,原变量修改,这个数据可以跟着变,但不能修改这个数据)。

特点:

  • 对象的所有嵌套属性都将变为只读。
  • 任何尝试修改这个对象的操作都会被阻止(在开发模式下,还会在控制台中发出警告)。

应用场景:

  • 创建不可变的状态快照。
  • 保护全局状态或配置不被修改。
let sum1 = ref(0)
let sum2 = readonly(sum1)
  
const original = reactive({ ... });
const readOnlyCopy = readonly(original);

shallowReadonly

与 readDonly 效果类似,但只作用于对象的顶层属性,对象内部的嵌套属性仍然是可变的。

特点:

  • 只将对象的顶层属性设置为只读,对象内部的嵌套属性仍然是可变的。
  • 适用于只需保护对象顶层属性的场景。
const original = reactive({ ... });
const shallowReadOnlyCopy = shallowReadonly(original);

toRaw 与 markRaw

toRaw
作用:用于获取一个响应式对象的原始对象。(toRaw 返回的对象不再是响应式的,对其进行修改也不会触发视图更新)

:::alert-info
官网描述:这是一个可以用于临时读取而不影响代理访问/跟踪开销,或是写入而不触发更改的特殊方法。 不建议保持对原始对象的持久引用,需要谨慎使用。
:::

:::alert-info
使用场景:在需要讲数据提供给非 Vue 库或外部系统时,使用 toRaw 可以确保它们收到的是普通对象。

  • axios 向服务端传递表单数据时
  • 向外部提供数据时(例如下面代码中的 showPerson 方法)
  • lodash 处理函数
    :::
<template>
    <h2>{{person.name}}</h2>
    <h2>{{ person.age }}</h2>
</template>

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

    defineOptions({name:"App"})

    // 响应式对象
    let person = reactive({
        name:'tony',
        age:18
    })
    // 原始对象
    let rawPerson = toRaw(person)

    // 通过 toRaw 可以向外部提供一个非响应式的原始类型
    // 不用担心 showPerson 对 persion 的操作影响到
    showPerson(toRaw(person))

    function showPerson(p:any) {
        p.age += 1 // 如何修改也不影响 person
        console.log(p)
    }
</script>

markRaw

作用:标记一个对象,使其永远不会变成响应式的。

:::alert-info
例如使用 mockjs 时,为了防止误把 mockjs 变为响应式对象,可以使用 markRaw 去标记 mockjs
:::

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

    defineOptions({name:"App"})

    // 用markRow 标记一下
    let person = markRaw({
        name:'tony',
        age:18
    })

    let person2 = reactive(person)
    console.log(person)
    // person2 依旧是原始对象
    console.log(person2)

</script>

customRef 自定义 Ref

作用:创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行逻辑控制。
人话:自己实现 Ref 响应式数据的效果,因为是自己实现的所以可以在数据的获取时、修改时执行一些逻辑

概念梳理:

  • track : 向 Vue 标记这个数据需要持续关注,当数据发生变化时(调用 trigger()) 需要重新通过 get 获取数据
  • trigger : 通知 Vue Set 方法已经对数据修改完成。
  • RefcustomRef 的区别:

    • Ref 是自动挡,你只管踩油门,别的不需要管。
    • customRef 是手动挡,离合、换挡都需要自己控制,麻烦一些,但可以在这个过程中实现更多需求

自定义 Ref 实现代码:

<template>
    <h2>{{ msg }}</h2>
    <input type="text" v-model="msg">
</template>

<script setup lang="ts">
import { customRef, markRaw, reactive, ref } from 'vue';
defineOptions({name:"App"})
    // 使用 vue 提供的默认的 ref 定义响应式数据,数据修改 页面自动更新 (自动挡)
    // let msg = ref("你好")

    // 使用 Vue 提供的 customRef 定义响应式数据(手动挡)
    let initMsg = '你好'
    // track(跟踪、订阅)、trigger(触发、发布)
    let msg = customRef((track,trigger) => {
        return {
            // get 在 msg 读取时调用
            get(){
                // 向 Vue 标记 msg 数据很重要,需要持续关注,一旦数据发生变化,就去更新
                track()
                return initMsg
            },
            // set 在 msg 修改时调用
            set(val){
                initMsg = val
                // 通知 vue 数据发生了变化
                trigger()
            },
        }
    }) 

</script>

通过 Hooks 封装

useMsgRef.ts 代码

import {customRef} from 'vue'

// 封装成 hooks 传入默认值,延迟时间
export default function(initValue:string,delay:number) {
    // 定义一个定时器,实现防抖的效果
    let timer:number
    // track(跟踪、订阅)、trigger(触发、发布)
    let msg = customRef((track,trigger) => {
        return {
            // get 在 msg 读取时调用
            get(){
                // 向 Vue 标记 msg 数据很重要,需要持续关注,一旦数据发生变化,就去更新
                track()
                return initValue
            },
            // set 在 msg 修改时调用
            set(val){
                // 使用定时器 延时指定时间触发
                clearTimeout(timer)
                timer = setTimeout(() => {
                    initValue = val
                    // 通知 vue 数据发生了变化
                    trigger()                    
                }, delay);
            },
        }
    }) 
    return {msg}
}

App.Vue 代码:

<template>
    <h2>{{ msg }}</h2>
    <input type="text" v-model="msg">
</template>

<script setup lang="ts">
import {ref } from 'vue';
import useMsgRef from './useMsgRef';
defineOptions({name:"App"})
    // 使用 vue 提供的默认的 ref 定义响应式数据,数据修改 页面自动更新 (自动挡)
    // let msg = ref("你好")

    // 使用 useMsgRef 定义响应式数据,且有延迟更新的效果
    let {msg} = useMsgRef('hello',1000)

</script>

Vue3 新组件

Teleport 传送门

将子组件的 HTML 结构,移动到指定位置。

例如下方代码 默认是在 #app 元素中渲染, 使用 to='body' 将元素渲染到 Body 标签中,脱离了 #app 根元素,且不影响交互逻辑

<teleport to='body' >
    <div class="modal" v-show="isShow">
      <h2>我是一个弹窗</h2>
      <p>我是弹窗中的一些内容</p>
      <button @click="isShow = false">关闭弹窗</button>
    </div>
</teleport>

Suspense

官方文档:https://cn.vuejs.org/guide/built-ins/suspense.html#suspense

当子组件中包含了异步任务(例如网络请求), setup 又是在组件未挂在前执行,若在 setup 中加载异步任务,如果异步任务执行较慢(例如网络请求加载用户信息)。就会造成异步加载时组件无法渲染,空着一块用户体验不好的问题。

通过 Suspense 可以实现一个 load 的效果,子组件阻塞时展示一部分内容,加载完成时展示子组件内容。

:::alert-warning

  1. <Suspense> 是一项实验性功能。
  2. 骨架屏有专门的解决方案,不要用这个代替骨架屏
    :::
<template>
    <Suspense>
        <!-- 加载完成显示组件内容  -->
        <template #default>
            <!-- 子组件有个异步操作,需要等请求完了之后 才能完成 Setup 的调用 -->
            <Child/>    
        </template>
        <!-- 加载时显示 -->
        <template #fallback>
            加载中
        </template>
    </Suspense>
</template>

<script setup lang="ts">
    import { Suspense } from 'vue';
    import Child from './Child.vue';
    defineOptions({name:"App"})
</script>

Commponent 全局组件

main.ts 入口文件中注册全局组件

import {createApp} from 'vue'
import App from './App.vue'
import Child from './Child.vue'

// 创建应用
const app = createApp(App)

// 注册全局组件
app.component("Child",Child)

app.mount('#app')

App.vue 中不用引入也能用 可以直接使用这个标签

<template>
    <Child/>    
</template>

<script setup lang="ts">
    defineOptions({name:"App"})
</script>

笔记由 两双筷子 整理,来源:https://www.bilibili.com/video/BV1Za4y1r7KE/