前言：本篇笔记内容来自于 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 入口文件 Vite App /src/main.ts 文件// 引入 createApp 用于创建应用 import {createApp} from 'vue' // 引入 App 根组件 import App from './App.vue' // 挂载 组件 createApp(App).mount('#app')/src/App.vue 文件 你好啊！ 总结Vite 项目中，index.html 是项目的入口文件，在项目最外层。加载index.html后，Vite 解析 src\App.vue 根文件 123 <Person /> 注：Vue3向下兼容Vue2语法，且Vue3中的模板中可以没有根标签。Vue3 核心语法OptionsAPI 和 CompositionAPIVue2 的 API 设计是 Options （配置、选项）风格的Vue3 的 API 设计是 Composition（组合、组件） 风格的Options API 弊端Options类型的 API，数据、方法、计算属性等，是分散在：data、methods、computed中的，若想新增或者修改一个需求，就需要分别修改：data、methods、computed，不便于维护和复用。setupsetup 概述setup 是 vue3 中一个新的配置项，它的值是一个函数。 setup 是 Composition API 的核心，组件中所用到的：数据、方法、计算属性、监视......等等，均配置在setup中。特点如下：setup函数返回的对象中的内容，可直接在模板中使用。setup中访问this是undefined。setup函数会在beforeCreate之前调用，它是“领先”所有钩子执行的。 姓名：{{ name }} 年龄：{{ age }} show setup 返回值若 返回一个对象：则对象中的：属性、方法等，在模板中均可以正常使用若返回一个函数：则会直接渲染返回的内容，所以可以用作自定义渲染函数。代码如下：setup(){ return ()=> 'hello Vue3!' }setup 与 Options API 的关系Vue2 的配置（data、methos......）中可以访问到 setup中的属性、方法。但在setup中不能访问到Vue2的配置（data、methos......）。如果与Vue2冲突，则setup优先。setup 语法糖setup 提供了一个语法糖，允许我们把 setup 独立出去，且不用 return，代码如下： 姓名：{{ name }} 年龄：{{ age }} show ::: alert-info当不定义组件名时（即 上方代码，省略 9 ~ 13 行 ），会自动通过文件名来推导组件名，例如文件名为 Person.vue 组件名就是 Person。当组件过于复杂时，可能会用一个单独的文件夹来编排代码，例如 /src/compositions/tree/index.vue， 这种场景下 整个 tree 目录都是组件，如果通过文件名推导，这个组件名称就是 index ，肯定不合适，所以需要显式定义组件名。:::若想简写 组件名称可以通过 vite 插件来实现：安装插件 npm i vite-plugin-vue-setup-extend -D编辑 /vite.config.ts 文件import { defineConfig } from 'vite' import VueSetupExtend from 'vite-plugin-vue-setup-extend' // 增加 export default defineConfig({ plugins: [ VueSetupExtend() ] // 增加 })的 ref 响应式数据ref 创建：基本类型响应式数据作用：定义响应式数据语法：let xxx =ref(初始值)返回值：一个 Reflmpt 的实例对象，简称 ref 对象 或 ref，ref 对象 的 value 属性是响应式的:::alert-infoJS 中操作数据 需要 xxx.vlue 不能直接覆盖 ref 对象, 但 模板中不需要 .value 可以直接使用。对于 let name = ref('dbkuaizi') 来说 name 不是响应式的，name.value 才是。::: 姓名：{{ name }} 年龄：{{ age }} set age show reactive 创建，对象类型的响应式数据作用：定义一个响应式的对象，基本类型用 ref 否则会报错语法：let 响应式对象 = reactive(源对象)返回值：一个 Proxy 的实例对象，简称响应式对象。::: alert-inforeactive 定义的响应式数据是 "深层次"的。reactive 只能定义响应式类型数据，定义基本类型会报错ref用来定义：基本类型数据、对象类型数据；::: 一辆 {{car.name}}，价值 {{ car.price }} setPrice 游戏： {{ g.name }} setGames ref 对比 reactive宏观角度ref 用来定义：基本类型数据、对象类型数据reactive 用来定义：对象类型数据区别ref 创建的变量必须使用 .value 操作，（可以通过 Vue - Official 插件设置自动添加 .value）Vscode 中找到 Vue - Official 插件设置勾选上 Auto Insert Dot Valuereactive 重新分配一个对象，会失去 响应式（可以使用 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 不会破坏响应式关系。:::使用原则：若需要一个基本类型的响应式数据，必须使用ref。若需要一个响应式对象，层级不深，ref、reactive都可以。若需要一个响应式对象，且层级较深，推荐使用 reactive。:::alert-info例如一个表单，有很多表单项，如果表单值是 ref，那就需要在代码中写 很多 .value，增加代码复杂度，reactive 更合适:::toRefs 和 toRef作用：将响应式对象中的属性 转为 ref对象说明：toRefs 与 toRef 功能一致，但 toRefs 可以批量转换 姓名：{{ person.name }} 年龄：{{ person.age }} computed 计算属性说明：根据已有的数据计算出新的数据 （与 Vue2 中计算属性功能一致），计算属性在多次调用、参与计算的值没有发生变化时，不会重复计算。 姓： 名： 全：{{ fullName }} 全：{{ fullName }} 全：{{ fullName }} 全：{{ fullName }} Set Name watch 监控监视 ref 定义的基本类型监视ref定义的【基本类型】数据：直接写数据名即可，监视的是其value值的改变。 求和 Sum 为： {{ sum }} +1 监视 ref 定义的对象类型监视 ref 定义的【对象类型】数据：直接写数据名，监视的是对象的【地址值】，若想监视对象内部的数据，要手动开启深度监视。:::alert-warning若修改的是 ref 定义的对象中的属性，newValue 和 oldValue 都是新值，因为它们是同一个对象。若修改整个 ref 定义的对象，newValue 是新值， oldValue 是旧值，因为不是同一个对象了。::: 姓名：{{ person.name }} 年龄：{{ person.age }} 全改 监视 reactive 定义的对象类型监视 reactive 定义的对象类型数据，默认开启深度监视 姓名：{{ person.name }} 年龄：{{ person.age }} 全改 监视某个属性监视 ref 或 reactive 定义的对象类型数据中的某个属性时，注意如下：若该属性值不是“对象类型”需要写成函数形式若该属性值依然是 “对象类型”，可以直接写，也可以写函数，这里建议写函数结论： 监视的要是对象里的属性，那么最好写函数式，注意点：若是对象监视的是地址值，需要关注对象内部，需要手动开启深度监视。 姓名：{{ person.name }} 年龄：{{ person.age }} 车：{{ person.car.c1 }} 、{{ person.car.c2 }} 修改名称 修改年龄 修改车 修改所有车 监听多个属性监控属性时可以传递一个数组，同时 newVal 也会是一个数组 姓名：{{ person.name }} 年龄：{{ person.age }} 车：{{ person.car.c1 }} 、{{ person.car.c2 }} 修改名称 修改年龄 修改车1 修改所有车 watchEffect官网：立即运行一个函数，同时响应式的追踪其依赖，并在依赖更改时重新执行该函数人话：更智能的 watch，不需要显式指定监控的响应式变量，直接写逻辑即可。在加载时会先执行一次。watch 和 watchEffect 对比都能监听响应式数据的变化，不同的是监听数据变化的方式不同watch 要明确指出监视的数据watchEffect 不用明确指出监视的数据（函数中用到哪些属性，就自动监视哪些属性）。 当前求和为：{{ sum }} 标签 Ref 属性作用：用于注册模板引用，功能类似于带命名空间的 ID ，用于设置组件内的唯一标识用在普通DOM标签上，获取的是DOM节点。用在组件标签上，获取的是组件实例对象。用在 DOM 标签上 666 999 ShowLog 用在组件上 (组件部分参考上方代码) <Person ref="ren" /> 输出组件 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 父组件 <Person :list="users" /> src\components\Person.vue 子组件 {{item.id}} -- {{ item.name }} 声明周期概念：Vue组件实例在创建时要经历一系列的初始化步骤，在此过程中Vue会在合适的时机，调用特定的函数，从而让开发者有机会在特定阶段运行自己的代码，这些特定的函数统称为：生命周期钩子创建阶段：setup挂载阶段：onBeforeMount、onMounted更新阶段：onBeforeUpdate、onUpdated卸载阶段：onBeforeUnmount、onUnmounted常用的钩子：onMounted(挂载完毕)、onUpdated(更新完毕)、onBeforeUnmount(卸载之前):::alert-info生命周期整体分为四个阶段，分别是：创建、挂载、更新、销毁，每个阶段都有两个钩子，一前一后。父子组件的顺序类似于深度有限算法，顺序如下：父组件 挂载前子组件1 挂载前子组件1 挂载后子组件2 挂载前子组件2 挂载后父组件 挂载后::: {{ sum }} 自定义 Hook什么是 Hook?本质上是一个函数，把 setup 函数中使用的 Composition API 进行了封装，把同一功能的逻辑代码、业务数据汇总在了一起，方便后期功能维护。自定义hook的优势：复用代码, 让setup中的逻辑更清楚易懂。业务代码：src\hooks\useDog.ts 将 加载dog的方法、doglist 响应式变量 都放入 useDog 这个文件中注意：文件名称要按照功能来，比如 Dog 相关的功能就叫 useDog.ts，订单相关的东西 就叫 useOrder.tsimport { 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 加载一只狗 路由 Routervue3 中使用路由 需要先安装 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 routersrc\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 代码如下： Vue Router <RouterLink to="/home" active-class="active">首页</RouterLink> <RouterLink to="/news" active-class="active">新闻</RouterLink> <RouterLink to="/about" active-class="active">关于</RouterLink> <RouterView></RouterView> 路由 两个注意点路由组件通常放在 /src/pages 或 /src/views 文件夹，一般组件通常放在 components 文件夹路由组件：靠路由规则渲染出来的 routes:[{path:'/demo',component:Demo}]一般组件：写标签实现一个功能的组件 <Demo/>通过路由切换的组件默认是被卸载掉了，需要的时候再去加载。路由工作模式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 的两种写法字符串的写法 <router-link active-class="active" to="/home">主页</router-link>对象写法 <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 } ]跳转路由： <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 新闻组件 <RouterLink :to="{path:'/news/detail'}">{{ news.title }}</RouterLink> <RouterView></RouterView> 路由传参Query 传参通过 Query 的方式从URL种传参传递参数 <!-- <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> <RouterView></RouterView> ， 获取参数 编号：{{query.id}} 标题：{{query.title}} 内容：{{query.content}} 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传递参数 <!-- <RouterLink :to="`/news/detail/${news.id}/${news.title}/${news.content}`">{{ news.title }}</RouterLink> --> <RouterLink :to="{ name:'xq', params:news }">{{ news.title }}</RouterLink> <RouterView></RouterView> 接收参数 编号：{{route.params.id}} 标题：{{route.params.title}} 内容：{{route.params.content}} 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} }接收参数 编号：{{id}} 标题：{{title}} 内容：{{content}} replace 属性作用：控制路由跳转时操作浏览器历史记录的模式。浏览器的历史记录有两种写入方式：分别为push和replace：push 是追加历史记录（默认值）。replace 是替换当前记录。开启 replace 模式<RouterLink replace .......>News</RouterLink>函数式编程导航大白话：通过 JS 实现 RouterLink 的功能，完成路由跳转场景只能通过 JS 跳转的，例如只有到时间了才能跳转到某个页面， RouterLink 做不到。其他事件触发跳转的，例如鼠标滑过触发跳转。不使用 RouterLink 也想跳转 查看 <RouterLink :to="{ name:'xq', params:news }">{{ news.title }}</RouterLink> <RouterView></RouterView> 路由重定向作用：当匹配到 path 时，重定向到指定路由{ path:'/', redirect:'/home' }Pinia（状态管理）大白话说法：就是组件之间数据的全局共享，例如 登录组件用户登录之后，获取到的用户信息 可以交给 状态管理工具来维护，（类似后端的 Session）安装 Pinia安装库npm i piniasrc\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 目录，是用来保存 状态、业务逻辑 的实体，每个组件都可以 读取、写入 它。它有三个概念：state、getter、action，相当于组件中的：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 当前求和为：{{ countStore.sum }} 1 2 3 加 减 修改数据的三种方式直接修改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中数据。 当前求和为：{{ sum }} {{ text }} 1 2 3 加 减 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. provide 、inject兄弟间、任意组件间1.mitt 2. piniapropsprops 是使用频率最高的父子组件通讯方式，常用于：父 <=> 子父传子，属性值是非函数子传父，属性值是函数父组件 父组件， 我的车：{{ car }} 儿子给的玩具：{{ toy }} <Child :car="car" :getToy="getToy"/> 子组件 子组件 我的玩具：{{ toy }} 父给我的车：{{ car }} 玩具给父亲 自定义事件概述：自定义事件通常用于子传父，简单来说就是子组件定义一个事件，并在需要的时候触发这个事件。接着在父组件使用子组件时，注册对应的自定义事件，并通过回调函数获取子组件的传参。:::alert-info事件传参的本质接收端: 定义一个函数，这个函数接受一个或多个参数，这些参数就是函数调用时发送方发送的参数，发送端：调用接收端定义的函数，并在调用时将需要传递的数据通过传参发送给对方:::注意：需要区分原生事件和自定义事件原生事件：事件名是特定的（click、mosueenter等等）事件对象 $event: 是包含事件相关信息的对象（pageX、pageY、target、keyCode）自定义事件：事件名是任意名称事件对象$event: 是调用emit时所提供的数据，可以是任意类型！！！示例 <Child @send-toy="toy = $event"/> 测试 //子组件中，触发事件： this.$emit('send-toy', 具体数据)父组件 父组件 子给的玩具：{{ toy }} <Child @send-toy="saveToy"/> 子组件 子组件 玩具：{{ toy }} 测试 Mitt与 Vue2 的 $bus、事件发布与订阅（pubsub）功能类似，可以实现任意组件间的通信。mitt 和自定义函数玩法很像，只是通过一个第三方库实现事件的订阅和触发，进而完成数据的交互工作。区别在于自定义组件，只能实现父子之间的数据传递，而第三方库只需要响应的组件引入，就可是实现事件的订阅与触发。安装 mittnpm 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 的本质 <input type="text" :value="userName" @input="userName =(<HTMLInputElement>$event.target).value" >组件上的 v-model本质：:modelValue + @update:modelValue 事件 <AtguiguInput v-model="userName"/> <AtguiguInput :modelValue="userName" @update:modelValue="userName = $event"/>AtguiguInput 组件中 指定 value 的名称如果指定 value 的名称，则可以传递多个 双向绑定变量<AtguiguInput v-model:ming="username" v-model:mima="password"/>AtguiguInput 组件中 可以这样写 <input type="text" :value="ming" @input="emit('update:ming',(<HTMLInputElement>$event.target).value)" > <input type="text" :value="mima" @input="emit('update:mima',(<HTMLInputElement>$event.target).value)" > :::alert-info$event 是啥？ 什么时候使用 $event.target ?对于原生事件（@input 这种），$event 时事件对象，可以 .target对于自定义事件，$event 是触发时 所传递的数据，不能 .target:::defineModel 传参defineModel 是 Vue 3.4+ 开始支持且官方推荐的一种组件双向绑定的写法，可以粗暴的理解为是原始 v-model 的语法糖写法。我们以上面的 v-model 为例 使用 defineModel 可以大幅简化组件内部响应值的写法，Vue 替我们完成了 :modelValue + @update:modelValue 事件。 attrs 祖传孙说明：$attrs 是一个对象，包含了所有父组件传递的标签属性:::alert-warning注意：$attrs 会自动排除 props 中声明的属性(可以认为声明过的 props 被子组件自己“消费”了):::// 父组件 父组件 <Child :a="a" :b="b" :c="c" :d="d" v-bind="{x:100,y:200}" :updateA="updateA"/> // 子组件 ============= 子组件 {{ a }} <GrandChild v-bind="$attrs"/> // 孙组件 ============= 孙组件 b：{{ b }} c：{{ c }} d：{{ d }} x：{{ x }} y：{{ y }} 点我更新A $refs 与 $parent概述: 前面提到 $ref 是组件的唯一标识，作用类似于传统 DOM 中的 ID，所以我们也可以通过 $ref 的方式来 操作对应的组件数据、事件。$refs 值为对象，包含所有被 ref 属性标识的DOM元素或组件实例。$parent 值为对象，当前组件的父组件实例对象。:::alert-info让外部获取数据的前提时通过 defineExpose 向外暴露:::父组件 父组件 房产：{{ house }} 让所有孩子的书变多 <Child1 ref="c1"/> 子组件 子组件1 玩具：{{ toy }} 书籍：{{ book }} 本 干掉父亲的一套房产 provide 与 inject概述：实现父组件与后代组件的直接通信，而不用操作中间组件在祖先组件中通过 provide 配置向后代组件提供数据在后代组件中通过 inject 配置来声明接收数据父组件 使用 provide 提供数据 父组件 资产：{{ money }} 汽车：{{ car }} 资产+1 汽车价格+1 <Child/> 后代组件 通过 inject 获取祖辈组件提供的数据 我是孙组件 资产：{{ money }} 汽车：{{ car }} 点我 其他 APIshallowRef 与 shallowReactiveshallowRef 和 shallowReactive 的作用是创建一个响应式的数据、或响应式的对象，然后只对最顶层的数据做响应式的处理，对更深层的数据不处理。用法：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 与 markRawtoRaw作用：用于获取一个响应式对象的原始对象。（toRaw 返回的对象不再是响应式的，对其进行修改也不会触发视图更新）:::alert-info官网描述：这是一个可以用于临时读取而不影响代理访问/跟踪开销，或是写入而不触发更改的特殊方法。 不建议保持对原始对象的持久引用，需要谨慎使用。::::::alert-info使用场景：在需要讲数据提供给非 Vue 库或外部系统时，使用 toRaw 可以确保它们收到的是普通对象。axios 向服务端传递表单数据时向外部提供数据时（例如下面代码中的 showPerson 方法）lodash 处理函数::: {{person.name}} {{ person.age }} markRaw 作用：标记一个对象，使其永远不会变成响应式的。:::alert-info例如使用 mockjs 时，为了防止误把 mockjs 变为响应式对象，可以使用 markRaw 去标记 mockjs::: customRef 自定义 Ref作用：创建一个自定义的 ref，并对其依赖项跟踪和更新触发进行逻辑控制。人话：自己实现 Ref 响应式数据的效果，因为是自己实现的所以可以在数据的获取时、修改时执行一些逻辑概念梳理：track : 向 Vue 标记这个数据需要持续关注，当数据发生变化时（调用 trigger()） 需要重新通过 get 获取数据trigger : 通知 Vue Set 方法已经对数据修改完成。Ref 和 customRef 的区别：Ref 是自动挡，你只管踩油门，别的不需要管。customRef 是手动挡，离合、换挡都需要自己控制，麻烦一些，但可以在这个过程中实现更多需求自定义 Ref 实现代码： {{ msg }} 通过 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 代码： {{ msg }} Vue3 新组件Teleport 传送门将子组件的 HTML 结构，移动到指定位置。例如下方代码 默认是在 #app 元素中渲染， 使用 to='body' 将元素渲染到 Body 标签中，脱离了 #app 根元素，且不影响交互逻辑<teleport to='body' > 我是一个弹窗 我是弹窗中的一些内容 关闭弹窗 </teleport>Suspense官方文档：https://cn.vuejs.org/guide/built-ins/suspense.html#suspense当子组件中包含了异步任务（例如网络请求）， setup 又是在组件未挂在前执行，若在 setup 中加载异步任务，如果异步任务执行较慢（例如网络请求加载用户信息）。就会造成异步加载时组件无法渲染，空着一块用户体验不好的问题。通过 Suspense 可以实现一个 load 的效果，子组件阻塞时展示一部分内容，加载完成时展示子组件内容。:::alert-warning<Suspense> 是一项实验性功能。骨架屏有专门的解决方案，不要用这个代替骨架屏::: <Suspense> <Child/> 加载中 </Suspense> 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 中不用引入也能用 可以直接使用这个标签 <Child/> 笔记由 两双筷子 整理，来源：https://www.bilibili.com/video/BV1Za4y1r7KE/