vue3 + ts 快速入门(全)

文章目录

  • 学习链接
  • 1. Vue3简介
    • 1.1. 性能的提升
    • 1.2.源码的升级
    • 1.3. 拥抱TypeScript
    • 1.4. 新的特性
  • 2. 创建Vue3工程
    • 2.1. 基于 vue-cli 创建
    • 2.2. 基于 vite 创建(推荐)
      • vite介绍
      • 创建步骤
      • 项目结构
        • 安装插件
        • 项目结构
        • 总结
    • 2.3. 一个简单的效果
      • Person.vue
      • App.vue
  • 3. Vue3核心语法
    • 3.1. OptionsAPI 与 CompositionAPI
      • Options API 的弊端
      • Composition API 的优势
    • 3.2. 拉开序幕的 setup
      • setup 概述
      • setup 的返回值
      • setup 与 Options API 的关系
      • setup 语法糖
    • 3.3. ref 创建:基本类型的响应式数据
    • 3.4. reactive 创建:对象类型的响应式数据
    • 3.5 ref 创建:对象类型的响应式数据
    • 3.6. ref 对比 reactive
      • 宏观角度
      • 区别
      • 使用原则
    • 3.7 toRefs 与 toRef
      • 现象
      • toRefs&toRef的使用
    • 3.8 computed
    • 3.9 watch
      • 作用
      • 特点
      • 场景
        • * 情况一
        • * 情况二
          • 示例1
          • 示例2
        • * 情况三
        • * 情况四
          • 没有监视的代码
          • 监视reactive定义的对象类型中的某个基本属性
          • 监视reactive定义的对象类型中的某个对象属性
        • * 情况五
    • 3.10 watchEffect
    • 3.11. 标签的 ref 属性
      • 用在普通DOM标签上
      • 用在组件标签上(defineExpose)
    • 3.12 回顾TS
      • main.ts
      • App.vue
      • index.ts
      • Person.vue
    • 3.13 props(defineProps)
      • App.vue
      • index.ts
      • Person.vue
    • 3.14 生命周期
      • App.vue
      • Person.vue
    • 3.15 自定义hooks
      • 未使用hooks前
        • App.vue
        • Person.vue
      • 使用hooks
        • App.vue
        • Person.vue
        • hooks/useSum.ts
        • hooks/useDog.ts
  • 4.路由
    • 4.1 路由的基本理解
    • 4.2 基本切换效果
      • 安装vue-router
      • 配置路由规则router/index.ts
      • 使用router路由管理器main.ts
      • 路由展示区App.vue
      • 路由组件
        • Home.vue
        • New.vue
        • About.vue
      • 路由切换效果图
    • 4.3. 两个注意点
      • About.vue
    • 4.4. 路由器工作模式
    • 4.5. to的两种写法
    • 4.6. 命名路由
    • 4.7 嵌套路由
      • main.ts
      • router/index.ts
      • App.vue
      • News.vue
      • Detail.vue
      • 效果
    • 4.8 路由传参
      • query参数
      • params参数
    • 4.9 路由的props配置
    • 4.10 replace属性
      • 示例
    • 4.11 编程式导航
      • 示例
    • 4.12 重定向
      • 示例
  • 5. pinia
    • 5.1 准备一个效果
      • main.ts
      • App.vue
      • Count.vue
      • LoveTalk.vue
    • 5.2 搭建 pinia 环境
      • 使用步骤
    • 5.3 存储+读取数据
      • store/count.ts
      • store/loveTalk.ts
      • Count.vue
      • LoveTalk.vue
      • App.vue
      • main.ts
    • 5.4 修改数据(三种方式)
      • 第一种方式
        • count.ts
        • Count.vue
      • 第二种方式
        • count.ts
        • Count.vue
      • 第三种方式
        • count.ts
        • Count.vue
    • 5.5 storeToRefs用法
      • LoveTalk.ts
      • LoveTask.vue
      • count.ts
      • Count.vue
    • 5.6 getters用法
      • count.ts
      • Count.vue
    • 5.7 $subscribe的使用
      • loveTalk.ts
      • LoveTalk.vue
    • 5.8 store组合式写法
      • loveTalk.js
      • LoveTalk.vue
  • 6. 组件通信
    • 6.1 props
      • Father.vue
      • Child.vue
    • 6.2 自定义事件
      • Father.vue
      • Child.vue
    • 6.3 mitt
      • emitter.ts
      • Father.vue
      • Child1.vue
      • Child2.vue
    • 6.4 v-model
      • Father.vue
      • AtguiguInput.vue
    • 6.5 $attrs
      • Father.vue
      • Child.vue
      • GrandChild.vue
    • 6.6 r e f s 、 refs、 refsparent、proxy
      • Father.vue
      • Child1.vue
      • Child2.vue
    • 6.7 provide、inject
      • Father.vue
      • Child.vue
      • GrandChild.vue
    • 6.8 pinia
    • 6.9 slot插槽
      • 1. 默认插槽
        • Father.vue
        • Category.vue
      • 2. 具名插槽
        • Father.vue
        • Category.vue
      • 3. 作用域插槽
        • Father.vue
        • Category.vue
  • 7. 其它 API
    • 7.1 shallowRef 与 shallowReactive
      • shallowRef
      • shallowReactive
      • 示例
    • 7.2 readonly 与 shallowReadonly
      • readonly
      • shallowReadonly
      • 示例
    • 7.3 toRaw 与 markRaw
      • toRaw
      • markRaw
      • 示例
    • 7.4 customRef
      • 示例
        • App.vue
        • useMsgRef.ts
  • 8. Vue3新组件
    • 8.1 Teleport传送门
      • 示例
        • App.vue
        • Modal.vue
    • 8.2 Suspense
      • 示例
        • App.vue
        • Child.vue
    • 8.3 全局API转移到应用对象
      • 示例
    • 8.4 其他

学习链接

尚硅谷Vue3入门到实战,最新版vue3+TypeScript前端开发教程

Vue3+Vite4+Pinia+ElementPlus从0-1 web项目搭建

Vue3.2后台管理系统

深入Vue3+TypeScript技术栈 coderwhy

尚硅谷Vue项目实战硅谷甄选,vue3项目+TypeScript前端项目一套通关

Vue3 + vite + Ts + pinia + 实战 + 源码 + electron - 百万播放量哦

1. Vue3简介

  • 2020年9月18日,Vue.js发布版3.0版本,代号:One Piece

  • 经历了:4800+次提交、40+个RFC、600+次PR、300+贡献者

  • 官方发版地址:Release v3.0.0 One Piece · vuejs/core

  • 截止2023年10月,最新的公开版本为:3.3.4

在这里插入图片描述

1.1. 性能的提升

  • 打包大小减少41%

  • 初次渲染快55%, 更新渲染快133%

  • 内存减少54%

1.2.源码的升级

  • 使用Proxy代替defineProperty实现响应式。

  • 重写虚拟DOM的实现和Tree-Shaking

1.3. 拥抱TypeScript

  • Vue3可以更好的支持TypeScript

1.4. 新的特性

  1. Composition API(组合API):

    • setup

    • refreactive

    • computedwatch

  2. 新的内置组件:

    • Fragment

    • Teleport

    • Suspense

  3. 其他改变:

    • 新的生命周期钩子

    • data 选项应始终被声明为一个函数

    • 移除keyCode支持作为 v-on 的修饰符

2. 创建Vue3工程

2.1. 基于 vue-cli 创建

点击查看 Vue-Cli 官方文档,(基于vue-cli创建,其实就是基于webpack来创建vue项目)

备注:目前vue-cli已处于维护模式,官方推荐基于 Vite 创建项目。

## 查看@vue/cli版本,确保@vue/cli版本在4.5.0以上
vue --version

## 安装或者升级你的@vue/cli 
npm install -g @vue/cli

## 执行创建命令
vue create vue_test

##  随后选择3.x
##  Choose a version of Vue.js that you want to start the project with (Use arrow keys)
##  > 3.x
##    2.x

## 启动
cd vue_test
npm run serve

2.2. 基于 vite 创建(推荐)

vite介绍

vite 是新一代前端构建工具,官网地址:https://vitejs.cn,vite的优势如下:

  • 轻量快速的热重载(HMR),能实现极速的服务启动。
  • TypeScriptJSXCSS 等支持开箱即用(不用配置,直接就可以用)。
  • 真正的按需编译,不再等待整个应用编译完成。
  • webpack构建 与 vite构建对比图如下:
    webpack构建 vite构建

创建步骤

具体操作如下(点击查看官方文档)

## 1.创建命令(基于vite创建vue3项目,前提是需要安装nodejs环境)
npm create vue@latest

## 2.具体配置
## 配置项目名称
√ Project name: vue3_test
## 是否添加TypeScript支持
√ Add TypeScript?  Yes
## 是否添加JSX支持
√ Add JSX Support?  No
## 是否添加路由环境
√ Add Vue Router for Single Page Application development?  No
## 是否添加pinia环境
√ Add Pinia for state management?  No
## 是否添加单元测试
√ Add Vitest for Unit Testing?  No
## 是否添加端到端测试方案
√ Add an End-to-End Testing Solution? » No
## 是否添加ESLint语法检查
√ Add ESLint for code quality?  Yes
## 是否添加Prettiert代码格式化
√ Add Prettier for code formatting?  No

构建过程如下:

在这里插入图片描述

访问vue3项目如下:

在这里插入图片描述

项目结构

安装插件

安装官方推荐的vscode插件:

在这里插入图片描述

在这里插入图片描述

项目结构

在这里插入图片描述

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>

main.ts

import './assets/main.css'

// 引入createApp用于创建应用
import { createApp } from 'vue'

// 引入App根组件
import App from './App.vue'

createApp(App).mount('#app')

App.vue

<!-- 自己动手编写的一个App组件 -->
<template>
  <div class="app">
    <h1>你好啊!</h1>
  </div>
</template>

<script lang="ts"> // 添加lang="ts", 里面写ts或js都可以
  
  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在main.ts中是通过 createApp 函数创建一个应用实例。

2.3. 一个简单的效果

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

Person.vue

<template>
  <div class="person">
    <h2>姓名:{{name}}</h2>
    <h2>年龄:{{age}}</h2>
    <button @click="changeName">修改名字</button>
    <button @click="changeAge">年龄+1</button>
    <button @click="showTel">点我查看联系方式</button>
  </div>
</template>

<script lang="ts">
  export default {
    name:'App',
    data() {
      return {
        name:'张三',
        age:18,
        tel:'13888888888'
      }
    },
    methods:{
      changeName(){
        this.name = 'zhang-san'
      },
      changeAge(){
        this.age += 1
      },
      showTel(){
        alert(this.tel)
      }
    },
  }
</script>

App.vue

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

<script lang="ts">
    
  import Person from './components/Person.vue'

  export default {
    name:'App', //组件名
    components:{Person} //注册组件
  }
</script>

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

3. Vue3核心语法

3.1. OptionsAPI 与 CompositionAPI

  • Vue2API设计是Options(配置)风格的。
  • Vue3API设计是Composition(组合)风格的。

Options API 的弊端

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

1.gif2.gif

Composition API 的优势

可以用函数的方式,更加优雅的组织代码,让相关功能的代码更加有序的组织在一起。

3.gif4.gif

3.2. 拉开序幕的 setup

setup 概述

介绍

  • setupVue3中一个新的配置项,值是一个函数。
  • 它是 Composition API “表演的舞台,组件中所用到的:数据、方法、计算属性、监视…等等,均配置在setup中。

特点如下:

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

<script lang="ts">
    
  export default {
      
    name:'Person',
      
    // 生命周期函数
    beforeCreate(){
      console.log('beforeCreate')
    },
      
    setup(){
        
      // 先打印的setup..., 再打印的beforeCreate, 说明了setup函数与beforeCreate生命周期函数的执行顺序
      console.log('setup ...')
        
      // 【setup函数中的this是undefined】
      console.log(this); // undefined

        
      // 数据,原来写在data中【注意:此时的name、age、tel数据都不是响应式数据】
      //                   (不是响应式的意思是:当这些数据变化,并不会触发dom更新,
      //                                     模板中应用这些变量的地方没有重新渲染)
      let name = '张三'
      let age = 18
      let tel = '13888888888'

      
      // 方法,原来写在methods中
      function changeName(){
        name = 'zhang-san'        // 注意:此时这么修改name页面是不变化的
        console.log(name)         // (name确实改了,但name不是响应式的)
      }
        
      function changeAge(){
        age += 1                  // 注意:此时这么修改age页面是不变化的
        console.log(age)          // (age确实改了,但age不是响应式的)
      }
        
      function showTel(){
        alert(tel)
      }

      // 返回一个对象,对象中的内容,模板中可以直接使用(将数据、方法交出去,模板中才可以使用这些交出去的数据、方法)
      return {name,age,tel,changeName,changeAge,showTel}
        
    }
  }
</script>

setup 的返回值

  • 若返回一个对象:则对象中的:属性、方法等,在模板中均可以直接使用**(重点关注)。**
  • 若返回一个函数:则可以直接指定 自定义渲染的内容,代码如下:
<template>
  <div class="person">
      我特么一点都不重要了
  </div>
</template>

<script lang="ts">
  export default {
    name:'Person',
    
    setup(){
      // setup的返回值也可以是一个渲染函数
      // (模板什么的都不重要了,直接在页面上渲染成:你好啊!这几个字)
      // return ()=>'哈哈'
    }
  }
</script>

<style scoped>
  .person {
    background-color: skyblue;
    box-shadow: 0 0 10px;
    border-radius: 10px;
    padding: 20px;
  }
  button {
    margin: 0 5px;
  }
</style>

setup 与 Options API 的关系

  • Vue2 的配置(datamethos…)中可以访问到 setup中的属性、方法。
  • 但在setup不能访问到Vue2的配置(datamethos…)。
  • 如果与Vue2冲突,则setup优先。
<template>
    <div class="person">
        <h2>姓名:{{name}}</h2>
        <h2>年龄:{{age}}</h2>
        <button @click="changeName">修改名字</button>
        <button @click="changeAge">修改年龄</button>
        <button @click="showTel">查看联系方式</button>
        <hr>
        <h2>测试1:{{a}}</h2>
        <h2>测试2:{{c}}</h2>
        <h2>测试3:{{d}}</h2>
        <button @click="b">测试</button>
    </div>
</template>

<script lang="ts">
    export default {
        name:'Person',
        beforeCreate(){
            console.log('beforeCreate')
        },
        data(){
            return {

                a:100,

                // 在data配置项中, 可以使用this.name来使用setup中交出的数据, 因为setup执行时机更早。
                // 但是在setup中不能使用在data中定义的数据
                c:this.name, 

                d:900,

                age:90
            }
        },
        methods:{
            b(){
                console.log('b')
            }
        },

        // setup可以与data、methods等配置项同时存在
        setup(){

            // 数据,原来是写在data中的,此时的name、age、tel都不是响应式的数据
            let name = '张三'
            let age = 18
            let tel = '13888888888'

            // 方法
            function changeName() {
                name = 'zhang-san' // 注意:这样修改name,页面是没有变化的
                console.log(name)  // name确实改了,但name不是响应式的
            }
            function changeAge() {
                age += 1           // 注意:这样修改age,页面是没有变化的
                console.log(age)   // age确实改了,但age不是响应式的
            }
            function showTel() {
                alert(tel)
            }

            // 将数据、方法交出去,模板中才可以使用
            return {name,age,tel,changeName,changeAge,showTel}

            // setup的返回值也可以是一个渲染函数
            // return ()=>'哈哈'
        }
    }
</script>

<style scoped>
    .person {
        background-color: skyblue;
        box-shadow: 0 0 10px;
        border-radius: 10px;
        padding: 20px;
    }
    button {
        margin: 0 5px;
    }
</style>

setup 语法糖

setup函数有一个语法糖,这个语法糖,可以让我们把setup独立出去,代码如下:

<template>

  <div class="person">
      
    <h2>姓名:{{name}}</h2>
    <h2>年龄:{{age}}</h2>
      
    <button @click="changName">修改名字</button>
    <button @click="changAge">年龄+1</button>
    <button @click="showTel">点我查看联系方式</button>
      
  </div>

</template>

<!-- 专门单个弄个script标签, 特地来配置组件的名字 -->
<script lang="ts">
  export default {
    name:'Person',
  }
</script>

<!-- 下面的写法是setup语法糖 -->
<!-- 1. 相当于写了setup函数; 
     2. 相当于自动把其中定义的变量交出去(包括里面引入的其它组件也会交出去, 可以在模板中使用引入的组件))-->
<script setup lang="ts">
    
  console.log(this) // undefined
  
  // 数据(注意:此时的name、age、tel都不是响应式数据)
  let name = '张三'
  let age = 18
  let tel = '13888888888'

  // 方法
  function changName(){
    name = '李四'//注意:此时这么修改name页面是不变化的
  }
  function changAge(){
    console.log(age)
    age += 1 //注意:此时这么修改age页面是不变化的
  }
  function showTel(){
    alert(tel)
  }
</script>

扩展:上述代码,还需要编写一个不写setupscript标签,去指定组件名字,比较麻烦,我们可以借助vite中的插件简化

  1. 第一步:npm i vite-plugin-vue-setup-extend -D
  2. 第二步:vite.config.ts
import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import VueSetupExtend from 'vite-plugin-vue-setup-extend'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    VueSetupExtend(),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
})

  1. 第三步:<script setup lang="ts" name="Person">

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

  • **作用:**定义响应式变量。
  • 语法:let xxx = ref(初始值)
  • **返回值:**一个RefImpl的实例对象,简称ref对象refref对象的value属性是响应式的
  • 注意点:
    • JS中操作数据需要:xxx.value,但模板中不需要.value,直接使用即可。
    • 对于let name = ref('张三')来说,name不是响应式的,name.value是响应式的。
<template>
	<div class="person">
        
        <!-- 模板中直接使用, 不需要.value -->
        <h2>姓名:{{name}}</h2>
        <h2>年龄:{{age}}</h2>
        <h2>电话:{{tel}}</h2>
        
        
        <button @click="changeName">修改名字</button>
        <button @click="changeAge">年龄+1</button>
        <button @click="showTel">点我查看联系方式</button>
    </div>
</template>

<!-- 使用了setup语法糖, 会自动将定义的变量和方法交出去, 以供给模板使用 -->
<script setup lang="ts" name="Person">
    
    // 引入vue中的ref函数
    import { ref } from 'vue'
    
    // name和age是一个RefImpl的实例对象,简称ref对象,它们的value属性是响应式的。
    //(所谓的响应式指的是, 对数据的改变后, 能够让模板中使用该数据的地方得到重新渲染更新)
    // ref是1个函数, 向这个ref函数中传入参数, 返回的是1个RefImpl的实例对象
    let name = ref('张三')
    let age = ref(18)
    
    // tel就是一个普通的字符串,不是响应式的
    let tel = '13888888888'

    function changeName(){
        
        // JS中操作ref对象时候需要.value
        name.value = '李四' // 页面得到刷新
        console.log(name.value)

        // 注意:name不是响应式的,name.value是响应式的,所以如下代码并不会引起页面的更新。
        // name = ref('zhang-san')
    }
    
    function changeAge(){
        // JS中操作ref对象时候需要.value
        age.value += 1      // 页面得到刷新
        console.log(age.value)
    }
    
    function showTel(){
        // tel是普通数据      
        tel += '1'          // tel的确改了, 但页面并未刷新
        alert(tel)
    }
</script>

3.4. reactive 创建:对象类型的响应式数据

  • 作用:定义一个响应式对象(基本类型不要用它,要用ref,否则报错)
  • 语法:let 响应式对象= reactive(源对象)
  • **返回值:**一个Proxy的实例对象,简称:响应式对象。
  • 注意点:reactive定义的响应式数据是“深层次”的。
<template>
<div class="person">

    <h2>汽车信息:一台{{ car.brand }}汽车,价值{{ car.price }}万</h2>

    <h2>游戏列表:</h2>
    <ul>
        <li v-for="g in games" :key="g.id">{{ g.name }}</li>
    </ul>

    <h2>测试:{{ obj.a.b.c.d }}</h2>

    <button @click="changeCarPrice">修改汽车价格</button>
    <button @click="changeFirstGame">修改第一游戏</button>
    <button @click="test">测试</button>

    </div>
</template>

<script lang="ts" setup name="Person">

    import { reactive } from 'vue'

    // 定义数据
    // reactive是1个函数, 向这个reactive函数中传入参数(传入对象或数组), 返回的是1个Proxy的实例对象
    //(Proxy是原生Js就有的函数)
    // reactive函数中传入对象
    let car = reactive({ brand: '奔驰', price: 100 }) 
    console.log('car', car); // car Proxy {brand: '奔驰', price: 100}

    // reactive函数传入数组
    let games = reactive([  
        { id: 'ahsgdyfa01', name: '英雄联盟' },
        { id: 'ahsgdyfa02', name: '王者荣耀' },
        { id: 'ahsgdyfa03', name: '原神' }
    ])

    // reactive定义的响应式数据是 深层次 的
    let obj = reactive({
        a: {
            b: {
                c: {
                    d: 666
                }
            }
        }
    })

    // 修改对象中的属性(修改使用reactive包裹对象后返回的对象)
    function changeCarPrice() {
        car.price += 10
    }

    // 修改数组中的对象的属性(修改使用reactive包裹数组后返回的对象)
    function changeFirstGame() {
        games[0].name = '流星蝴蝶剑'
    }

    function test() {
        obj.a.b.c.d = 999
    }
</script>

3.5 ref 创建:对象类型的响应式数据

  • 其实ref接收的数据可以是:基本类型对象类型
  • ref接收的是对象类型,内部其实也是调用了reactive函数。
<template>
	<div class="person">
        
        <h2>汽车信息:一台{{ car.brand }}汽车,价值{{ car.price }}万</h2>
        
        <h2>游戏列表:</h2>
        <ul>
            <li v-for="g in games" :key="g.id">{{ g.name }}</li>
        </ul>
        
        <h2>测试:{{ obj.a.b.c.d }}</h2>
        
        <button @click="changeCarPrice">修改汽车价格</button>
        
        <button @click="changeFirstGame">修改第一游戏</button>
        
        <button @click="test">测试</button>
    </div>
</template>

<script lang="ts" setup name="Person">

    import { ref,reactive } from 'vue'

    // 使用ref定义对象类型响应式数据
    let car = ref({ brand: '奔驰', price: 100 })

    // 使用reactive定义对象类型响应式数据
    let car2 = reactive({brand: '奔驰', price: 100})

    // reactive只能用来定义对象类型的响应式数据
    // let name = reactive('zhangsan') // 错误, value cannot be made reactive: zhangsan

    // 使用ref定义对象(数组)类型响应式数据
    let games = ref([
        { id: 'ahsgdyfa01', name: '英雄联盟' },
        { id: 'ahsgdyfa02', name: '王者荣耀' },
        { id: 'ahsgdyfa03', name: '原神' }
    ])

    // 使用ref定义对象类型响应式数据也是深层次的
    let obj = ref({
        a: {
            b: {
                c: {
                    d: 666
                }
            }
        }
    })



    // 若ref接收的是对象类型,内部其实也是使用的reactive函数
    console.log(car)       // RefImpl {__v_isShallow: false, dep: undefined, 
                           //          __v_isRef: true, _rawValue: {…}, _value: Proxy}
    console.log(car.value) // Proxy {brand: '奔驰', price: 100}
    console.log(car2)      // Proxy {brand: '奔驰', price: 100}

    function changeCarPrice() {
        // 使用ref函数定义的响应式数据, 在js操作时, 需要带上.value, 才能碰到内部的Proxy对象
        car.value.price += 10
        console.log(car.value.price);
    }

    function changeFirstGame() {
        // 使用ref函数定义的响应式数据, 在js操作时, 需要带上.value, 才能碰到内部的Proxy对象
        games.value[0].name = '流星蝴蝶剑'
        console.log(games.value);   // Proxy {0: {…}, 1: {…}, 2: {…}}
    }

    function test() {
        // 使用ref函数定义的响应式数据, 在js操作时, 需要带上.value, 才能碰到内部的Proxy对象
        obj.value.a.b.c.d = 999
    }
    
</script>

3.6. ref 对比 reactive

宏观角度

  • ref可以定义:基本类型、对象类型的响应式数据

  • reactive只能定义:对象类型的响应式数据

区别

  • ref创建的变量必须使用.value(可以使用volar插件自动添加.value)。

    可以在齿轮->设置->扩展->volar中勾选自动补充value ,它会在使用ref创建的变量时,自动添加上.value

  • reactive重新分配一个新对象,会失去响应式(可以使用Object.assign去整体替换)。

    <template>
        <div class="person">
            <h2>汽车信息:一台{{ car.brand }}汽车,价值{{ car.price }}万</h2>
    
            <button @click="changeBrand">改品牌</button>
            <button @click="changePrice">改价格</button>
    
            <button @click="changeCar">改car</button>
        </div>
    </template>
        
    <script lang="ts" setup name="Person">
    
    import { ref,reactive } from 'vue'
    
    let car = reactive({brand:'奔驰', price:100})
    
    function changeBrand() {
        // 正常修改car的brand, 并且是响应式
        car.brand = '宝马'
    }
    
    function changePrice() {
        // 正常修改car的price, 并且是响应式
        car.price += 10
    }
    
    function changeCar() {
        
        // 错误做法1
        // 不可以直接给reactive重新分配一个新对象,这会让car直接失去响应式
        // car = {brand:'奥托', price:10}
    
        // 错误做法2
        // 这样也不行, 因为模板中用的car是上面定义的响应式对象, 
        // 现在car指向的是1个新的响应式对象, 而模板中压根就没有使用这个新的响应式对象
        // car =  reactive({brand:'奥托', price:10})
    
        // 正确做法(car仍然是响应式的)
        // API介绍: Object.assign(obj1, obj2, obj3, ..), 
        //         将obj2中的每一组属性和值设置到obj1中, 然后obj3的每一组属性和值设置到obj1中
        Object.assign(car, {brand:'奥托', price:10})
    }
    
    </script>
    
    <template>
        <div class="person">
            <h2>汽车信息:一台{{ car.brand }}汽车,价值{{ car.price }}万</h2>
    
            <button @click="changeBrand">改品牌</button>
            <button @click="changePrice">改价格</button>
    
            <button @click="changeCar">改car</button>
        </div>
    </template>
        
    <script lang="ts" setup name="Person">
    
    import { ref,reactive } from 'vue'
    
    let car = ref({brand:'奔驰', price:100})
    
    function changeBrand() {
        // 正常修改car的brand, 并且是响应式
        car.value.brand = '宝马'
    }
    
    function changePrice() {
        // 正常修改car的price, 并且是响应式
        car.value.price += 10
    }
    
    function changeCar() {
        
        // 错误做法1
        // 不能直接给car换了个ref, 因为模板中压根就没有使用这个新的RefImpl对象
        // car = ref({brand:'奥托', price:10})
    
        // 正确做法1(car仍然是响应式的)
        // API介绍: Object.assign(obj1, obj2, obj3, ..), 将obj2中的每一组属性和值设置到obj1中, 
        //         然后obj3的每一组属性和值设置到obj1中
        // Object.assign(car.value, {brand:'奥托', price:10})
    
        // 正确做法2
        //(这里相比于对car使用reactive定义而言, 使用ref定义则可以直接给car.value整体赋值
        // 原因在于car.value获取的是Proxy响应式对象, 凡是对Proxy响应式对象的操作都可以被拦截到)
        car.value = {brand:'奥托', price:10}
        
    }
    
    </script>
    

使用原则

  • 若需要一个基本类型的响应式数据,必须使用ref

  • 若需要一个响应式对象,层级不深,refreactive都可以。

  • 若需要一个响应式对象,且层级较深,推荐使用reactive

3.7 toRefs 与 toRef

  • 作用:将一个响应式对象中的每一个属性,转换为ref对象。
  • 备注:toRefstoRef功能一致,但toRefs可以批量转换。

现象

对响应式对象直接结构赋值,得到的数据不是响应式的

<template>
    <div class="person">

        <h2>姓名:{{ person.name }} {{ name }}</h2>
        <h2>年龄:{{ person.age }}  {{ age }}</h2>

        <button @click="changeName">修改名字</button>
        <button @click="changeAge">修改年龄</button>

    </div>
</template>

<script lang="ts" setup name="Person2">

    import { ref, reactive, toRefs, toRef } from 'vue'

    // 数据
    let person = reactive({ name: '张三', age: 18 })
    console.log(person);                // Proxy {name: '张三', age: 18}

    // 这里的解构赋值其实就等价于: let name = person.name; let age = person.age;
    // 只是记录了此时person.name、person.age的值, 仅此而已
    // 因此, 此处使用结构赋值语法获取的name和age都不是响应式的
    let {name, age } = person
    console.log(name, age);              // 张三 18

    // 方法
    function changeName() {
        name += '~'
        console.log(name, person.name);  // 变化的是name, 而person.name仍然未修改
    }

    function changeAge() {
        age += 1
        console.log(age, person.age);    // 变化的是age, 而person.age仍然未修改
    }

</script>

toRefs&toRef的使用

通过toRefs将person对象中的所有属性都批量取出, 且依然保持响应式的能力

<template>
<div class="person">

    <h2>姓名:{{ person.name }}</h2>
    <h2>年龄:{{ person.age }}</h2>
    <h2>性别:{{ person.gender }}  {{ gender }}</h2>

    <button @click="changeName">修改名字</button>
    <button @click="changeAge">修改年龄</button>
    <button @click="changeGender">修改性别</button>
    
    <button @click="changeGender2">修改性别2</button>

    </div>
</template>

<script lang="ts" setup name="Person">

    import { ref, reactive, toRefs, toRef } from 'vue'

    // 数据
    let person = reactive({ name: '张三', age: 18, gender: '男' })

    // 通过toRefs将person对象中的所有属性都批量取出, 且依然保持响应式的能力
    //(使用toRefs从person这个响应式对象中,解构出name、age, 且name和age依然是响应式的,
    //  name和gender的值是ref类型, 其value值指向的是person.name和person.age,
    //  对name.value和对age.value的修改将会修改person.name和person.age, 并且会页面渲染刷新)
    let { name, age } = toRefs(person)
    console.log(name.value, name);     // '张三' ObjectRefImpl {_object: Proxy, _key: 'name', 
                                       //                 _defaultValue: undefined, __v_isRef: true}
    console.log(age.value, age.value); // 18 ObjectRefImpl {_object: Proxy, _key: 'age', 
                                       //                   _defaultValue: undefined, __v_isRef: true}

    console.log(toRefs(person));       // {name: ObjectRefImpl, age: ObjectRefImpl, 
                                       //  gender: ObjectRefImpl}

    // 通过toRef将person对象中的gender属性取出,且依然保持响应式的能力
    let gender = toRef(person, 'gender')
    console.log(gender, gender.value); // ObjectRefImpl {_object: Proxy, _key: 'gender', 
                                       //               _defaultValue: undefined, __v_isRef: true} '男'

    // 方法
    function changeName() {
        // 此处修改name.value, 将会修改person.name, 并且页面会刷新person.name的值
        name.value += '~'
        console.log(name.value, person.name);
    }

    function changeAge() {
        // 此处修改age.value, 将会修改person.age, 并且页面会刷新person.age的值
        age.value += 1
        console.log(age.value, person.age);
    }

    function changeGender() {
        // 此处修改gender.value, 将会修改person.age, 并且页面会刷新person.gender的值
        gender.value = '女'
        console.log(gender.value, person.gender);
    }

    function changeGender2() {
        // 此处对person.gender的修改, 将会修改上面的let gender = toRef(person, 'gender')
        // 并且页面会刷新person.gender和gender的值
        person.gender = '男'
        console.log(gender.value, person.gender);
    }
</script>

3.8 computed

作用:根据已有数据计算出新数据(和Vue2中的computed作用一致)。

<template>
    <div class="person">

        姓:<input type="text" v-model="firstName"> <br>
        名:<input type="text" v-model="lastName"> <br>

        全名:<span>{{ fullName }}</span> <br>

        <button @click="changeFullName">全名改为: li-si</button>
    </div>
</template>
  
<script setup lang="ts" name="App">

// 引入computed计算属性函数
import { ref, computed } from 'vue'

let firstName = ref('zhang')
let lastName = ref('san')

// 计算属性——只读取,不修改
/* 
// 1. 使用时, 在computed中传入1个函数。在模板中, 直接使用计算属性即可。
// 2. 当计算属性依赖的数据只要发生变化, 它就会重新计算, 如果页面中有使用到该计算属性, 那么就会重新渲染模板 
// 3. 只会计算1次, 后面会使用缓存, 而方法是没有缓存的
let fullName = computed(()=>{
  return firstName.value + '-' + lastName.value
}) 
console.log(fullName); // ComputedRefImpl {dep: undefined, __v_isRef: true, 
                       //                  __v_isReadonly: true, effect: ReactiveEffect, _setter: ƒ, …}
 */

// 计算属性——既读取又修改
let fullName = computed({

    // 读取
    get() {
        // 当firstName或lastName变化时, 计算属性会重新计算, 并刷新页面渲染
        return firstName.value + '-' + lastName.value
    },

    // 修改
    // 当修改计算属性时(或者说给计算属性赋值时, 注意要.value), 此方法会被调用
    set(val) {
        console.log('有人修改了fullName', val)
        firstName.value = val.split('-')[0]
        lastName.value = val.split('-')[1]
    }
})

function changeFullName() {
    // 修改fullName计算属性(会触发计算属性中set方法的调用)
    fullName.value = 'li-si'
}
</script>

3.9 watch

作用

监视数据的变化(和Vue2中的watch作用一致)

特点

Vue3中的watch只能监视以下四种数据

  • ref定义的数据。

  • reactive定义的数据。

  • 函数返回一个值(getter函数,所谓的getter函数就是能返回一个值的函数)。

  • 一个包含上述内容的数组。

场景

我们在Vue3中使用watch的时候,通常会遇到以下几种情况:

* 情况一

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

<template>
    <div class="person">

        <h1>情况一:监视【ref】定义的【基本类型】数据</h1>

        <h2>当前求和为:{{ sum }}</h2>

        <button @click="changeSum">点我sum+1</button>

    </div>
</template>
  
<script lang="ts" setup name="Person">

// 引入watch监视函数
import { ref, watch } from 'vue'

// 数据
let sum = ref(0)

// 方法
function changeSum() {
    sum.value += 1
}

// 监视,情况一:监视【ref】定义的【基本类型】数据
//(注意:这里监视写的是sum, 而不是sum.value哦)
const stopWatch = watch(sum, (newValue, oldValue) => {

    console.log('sum变化了', newValue, oldValue) // 注意: 这里也没带.value哦

    if (newValue >= 10) {

        // 解除监视(即: 当调用此方法后, 不会再监视sum的变化了, 也就是当sum变化时, 当前的监视函数不再执行了)
        stopWatch()

    }
})

</script>
  
<style scoped>
...
</style>
* 情况二

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

注意:

  • 若修改的是ref定义的对象中的属性,newValueoldValue 都是新值,因为它们是同一个对象。

  • 若修改整个ref定义的对象,newValue 是新值, oldValue 是旧值,因为不是同一个对象了。

示例1
<template>
    <div class="person">

        <h1>情况二:监视【ref】定义的【对象类型】数据</h1>

        <h2>姓名:{{ person.name }}</h2>
        <h2>年龄:{{ person.age }}</h2>

        <button @click="changeName">修改名字</button>
        <button @click="changeAge">修改年龄</button>

        <button @click="changePerson">修改整个人</button>

    </div>
</template>
  
<script lang="ts" setup name="Person">

import { ref, watch } from 'vue'

// 数据
let person = ref({
    name: '张三',
    age: 18
})

// 方法
function changeName() {
    person.value.name += '~'   // 当修改person.value.name时, 监视函数未被触发
}

function changeAge() {
    person.value.age += 1      // 当修改person.value.age时, 监视函数也未被触发
}

function changePerson() {
    person.value = { name: '李四', age: 90 }  // 当整体修改person.value时, 此时监视函数被触发
}                                            // (因为监视的是对象的地址值, 所以这里每次修改都会触发监视函数)

/* 
  监视,情况一:监视【ref】定义的【对象类型】数据,监视的是对象的地址值。
  watch的第一个参数是:被监视的数据
  watch的第二个参数是:监视的回调
*/
watch(person, (newValue, oldValue) => {
    
    console.log('person变化了', newValue, oldValue)
    
    // 一直调用changePerson方法, 控制台如下输出
    // person变化了 Proxy {name: '李四', age: 90} Proxy {name: '张三', age: 18}
    // person变化了 Proxy {name: '李四', age: 90} Proxy {name: '李四', age: 90}
    // person变化了 Proxy {name: '李四', age: 90} Proxy {name: '李四', age: 90}
    // person变化了 Proxy {name: '李四', age: 90} Proxy {name: '李四', age: 90}
    // ...
})

</script>
  
<style scoped>
...
</style>
示例2
<template>
    <div class="person">

        <h1>情况二:监视【ref】定义的【对象类型】数据</h1>

        <h2>姓名:{{ person.name }}</h2>
        <h2>年龄:{{ person.age }}</h2>

        <button @click="changeName">修改名字</button>
        <button @click="changeAge">修改年龄</button>

        <button @click="changePerson">修改整个人</button>
    </div>
</template>
  
<script lang="ts" setup name="Person">

import { ref, watch } from 'vue'

// 数据
let person = ref({
    name: '张三',
    age: 18
})

// 方法
function changeName() {
    person.value.name += '~'
                          // 因为开启了深度监视, 当修改person.value.name时, 监视函数被触发
}                         //(但由于原对象并未修改, 所以监视函数中输出的newVal和oldVal是一样的)
                          // 每次调用changeName都修改, 变化如下:
                          // person变化了 Proxy {name: '张三~', age: 18} Proxy {name: '张三~', age: 18}
                          // person变化了 Proxy {name: '张三~~', age: 18} Proxy {name: '张三~~', age: 18}
                          // person变化了 Proxy {name: '张三~~', age: 18} Proxy {name: '张三~~', age: 18}
                          // ...
function changeAge() {
    person.value.age += 1   // 因为开启了深度监视, 当修改person.value.name时, 监视函数被触发
}                           //(但由于原对象并未修改, 所以监视函数中输出的newVal和oldVal是一样的)
                            // 每次调用changeName都修改, 变化如下:
                            // person变化了 Proxy {name: '张三', age: 19} Proxy {name: '张三', age: 19}
                            // person变化了 Proxy {name: '张三', age: 20} Proxy {name: '张三', age: 20}
                            // person变化了 Proxy {name: '张三', age: 21} Proxy {name: '张三', age: 21}
                            // ...
    
function changePerson() {
    person.value = { name: '李四', age: 90 }
                            // 当整体修改person.value时, 监视函数被触发
                            //(但由于原对象都改了, 所以监视函数中输出的newVal和oldVal是不一样的)
                            // 每次调用changeName都修改, 变化如下:
}                           // person变化了 Proxy {name: '李四', age: 90} Proxy {name: '张三', age: 18}
                            // person变化了 Proxy {name: '李四', age: 90} Proxy {name: '李四', age: 90}
                            // person变化了 Proxy {name: '李四', age: 90} Proxy {name: '李四', age: 90}
                            // ...
/* 
  监视,情况二:监视【ref】定义的【对象类型】数据,监视的是对象的地址值,
              若想监视对象内部属性的变化,需要手动开启深度监视
  watch的第一个参数是:被监视的数据
  watch的第二个参数是:监视的回调
  watch的第三个参数是:配置对象(deep、immediate等等) 
*/
watch(person, (newValue, oldValue) => {
    console.log('person变化了', newValue, oldValue)
}, { deep: true, immediate: true })

</script>
  
<style scoped>
.person {
    background-color: skyblue;
    box-shadow: 0 0 10px;
    border-radius: 10px;
    padding: 20px;
}

button {
    margin: 0 5px;
}

li {
    font-size: 20px;
}
</style>
* 情况三

监视reactive定义的【对象类型】数据,且默认开启了深度监视。

<template>
<div class="person">

    <h1>情况三:监视【reactive】定义的【对象类型】数据</h1>

    <h2>姓名:{{ person.name }}</h2>
    <h2>年龄:{{ person.age }}</h2>

    <button @click="changeName">修改名字</button>
    <button @click="changeAge">修改年龄</button>
    <button @click="changePerson">修改整个人</button>

    <hr>

    <h2>测试:{{obj.a.b.c}}</h2>

    <button @click="test">修改obj.a.b.c</button>

    </div>
</template>

<script lang="ts" setup name="Person">

    import {reactive,watch} from 'vue'

    // 数据
    let person = reactive({
        name:'张三',
        age:18
    })

    let obj = reactive({
        a:{
            b:{
                c:666
            }
        }
    })

    // 方法
    function changeName(){
        person.name += '~'
        // 每次调用changeName都修改, 变化如下:
        // person变化了 Proxy {name: '张三~', age: 18} Proxy {name: '张三~', age: 18}
        // person变化了 Proxy {name: '张三~~', age: 18} Proxy {name: '张三~~', age: 18}
        // person变化了 Proxy {name: '张三~~~', age: 18} Proxy {name: '张三~~~', age: 18}
        // ...
        //(如上结果, 
        //  1. 证明监视到了person的name 
        //  2. oldVal和newVal是一样的输出, 是因为虽然监测到person的变化, 但oldVal和newVal是同一对象, 从这来说并未改变)
    }

    function changeAge(){
        person.age += 1
        // 每次调用changeAge都修改, 变化如下:
        // person变化了 Proxy {name: '张三', age: 19} Proxy {name: '张三', age: 19}
        // person变化了 Proxy {name: '张三', age: 20} Proxy {name: '张三', age: 20}
        // person变化了 Proxy {name: '张三', age: 21} Proxy {name: '张三', age: 21}
        // ...
        //(如上结果, 
        //  1. 证明监视到了person的age
        //  2. oldVal和newVal是一样的输出, 是因为虽然监测到person的变化, 但oldVal和newVal是同一对象, 从这来说并未改变)
    }

    function changePerson(){
        // 此处注意: 使用reactive函数定义的数据, 不能直接替换, 可以如下方式对person中的属性做批量修改 
        Object.assign(person,{name:'李四',age:80})
        // 多次调用changePerson, 仅有1次监视到到修改, 变化如下:
        // person变化了 Proxy {name: '李四', age: 80} Proxy {name: '李四', age: 80}
        //(如上结果, 
        //  1. 证明监视到了person的name和age的改变
        //  2. oldVal和newVal是一样的输出, 是因为虽然监测到person的变化, 但oldVal和newVal仍是同一对象, 从这来说并未改变)
    }

    function test(){
        obj.a.b.c = 888
        // 此处证明watch监控reactive定义的对象类型数据, 默认是开启了深度监视的
    }

    // 监视,情况三:监视【reactive】定义的【对象类型】数据,且默认是开启深度监视的(隐式创建了深层次的监听, 无法关闭)
    watch(person,(newValue,oldValue)=>{
        console.log('person变化了',newValue,oldValue)
    })

    watch(obj,(newValue,oldValue)=>{
        console.log('Obj变化了',newValue,oldValue)
    })

</script>

<style scoped>
...
</style>
* 情况四

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

  1. 若该属性值不是【对象类型】,需要写成函数形式。
  2. 若该属性值是依然是【对象类型】,可直接编,也可写成函数,建议写成函数。

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

没有监视的代码
<template>
<div class="person">

    <h1>情况四:监视【ref】或【reactive】定义的【对象类型】数据中的某个属性</h1>

    <h2>姓名:{{ person.name }}</h2>

    <h2>年龄:{{ person.age }}</h2>

    <h2>汽车:{{ person.car.c1 }}、{{ person.car.c2 }}</h2>

    <button @click="changeName">修改名字</button>

    <button @click="changeAge">修改年龄</button>

    <button @click="changeC1">修改第一台车</button>

    <button @click="changeC2">修改第二台车</button>

    <button @click="changeCar">修改整个车</button>

    </div>
</template>

<script lang="ts" setup name="Person">

    import { reactive, watch } from 'vue'

    // 数据
    let person = reactive({
        name: '张三',
        age: 18,
        car: {
            c1: '奔驰',
            c2: '宝马'
        }
    })

    // 方法
    function changeName() {
        person.name += '~'
    }

    function changeAge() {
        person.age += 1
    }

    function changeC1() {
        person.car.c1 = '奥迪'
    }

    function changeC2() {
        person.car.c2 = '大众'
    }

    function changeCar() {
        // 注意此处: 因为person是使用reactive定义的, 所以person整体不能改(改是可以改, 但是不再响应式了, 
        //                                                         所以说不能整体直接改), 
        //          但是person里面的car属性可以改, 因此可以如下改
        person.car = { c1: '雅迪', c2: '爱玛' }
    }


</script>

<style scoped>
    ...
</style>
监视reactive定义的对象类型中的某个基本属性
<template>
    <div class="person">

        <h1>情况四:监视【ref】或【reactive】定义的【对象类型】数据中的某个属性</h1>

        <h2>姓名:{{ person.name }}</h2>

        <h2>年龄:{{ person.age }}</h2>

        <h2>汽车:{{ person.car.c1 }}、{{ person.car.c2 }}</h2>

        <button @click="changeName">修改名字</button>

        <button @click="changeAge">修改年龄</button>

        <button @click="changeC1">修改第一台车</button>

        <button @click="changeC2">修改第二台车</button>

        <button @click="changeCar">修改整个车</button>

    </div>
</template>

<script lang="ts" setup name="Person">

    import { reactive, watch } from 'vue'

    // 数据
    let person = reactive({
        name: '张三',
        age: 18,
        car: {
            c1: '奔驰',
            c2: '宝马'
        }
    })

    // 方法
    function changeName() {
        person.name += '~'
        // 一直调用changeName方法, 控制台如下输出
        // person.name变化了 张三~ 张三
        // person.name变化了 张三~~ 张三~
        // person.name变化了 张三~~~ 张三~~
        // ...
    }
    function changeAge() {
        person.age += 1
    }

    function changeC1() {
        person.car.c1 = '奥迪'
    }
    
    function changeC2() {
        person.car.c2 = '大众'
    }
    
    function changeCar() {
        // 注意此处: 因为person是使用reactive定义的, 所以person整体不能改(改是可以改, 但是不再响应式了, 
        //                                                         所以说不能整体直接改), 
        //          但是person里面的car属性可以改, 因此可以如下改
        person.car = { c1: '雅迪', c2: '爱玛' }
    }

    // 监视,情况四:监视响应式对象中的某个属性,且该属性是基本类型的,要写成函数式(不能直接写person.name哦)
    //(如下监视, 将会只监视person的name属性的变化, 
    //  当person的name属性发生变化时, 将会触发监听函数执行, 其它属性变化不会触发监听函数的执行)
    watch(()=> person.name,(newValue,oldValue)=>{
      console.log('person.name变化了',newValue,oldValue)
    }) 
    
    // 错误写法, 因为person的name属性是基本类型, 所以不能直接写为第1个参数, 应该要用函数包一下
    /*watch(person.name,(newValue,oldValue)=>{
      console.log('person.name变化了',newValue,oldValue)
    })*/
    
    // 监视person的car属性中的c1属性
    //(当调用changeC1方法时, 此处能够监测到person.car.c1的改变;
    //  多次调用changeC1方法, 此处只监测到了1次, 因为后面都没改person.car.c1的值;
    //  当调用changeCar方法, 此处能够监测到person.car.c1的改变;
    //  多次调用changeCar方法, 此处只监测到了1次, 因为后面都没改person.car.c1的值;)
    watch(()=> person.car.c1,(newValue,oldValue)=>{
      console.log('person.car.c1变化了',newValue,oldValue)
    })
    
    // 错误写法, 因为person的car.c1属性是基本类型, 所以不能直接写为第1个参数, 应该要用函数包一下
    /*watch(person.car.c1,(newValue,oldValue)=>{
      console.log('person.car.c1变化了',newValue,oldValue)
    })*/

</script>

<style scoped>
...
</style>
监视reactive定义的对象类型中的某个对象属性
<template>
    <div class="person">

        <h1>情况四:监视【ref】或【reactive】定义的【对象类型】数据中的某个属性</h1>

        <h2>姓名:{{ person.name }}</h2>

        <h2>年龄:{{ person.age }}</h2>

        <h2>汽车:{{ person.car.c1 }}、{{ person.car.c2 }}</h2>

        <button @click="changeName">修改名字</button>

        <button @click="changeAge">修改年龄</button>

        <button @click="changeC1">修改第一台车</button>

        <button @click="changeC2">修改第二台车</button>

        <button @click="changeCar">修改整个车</button>

    </div>
</template>

<script lang="ts" setup name="Person">

    import { reactive, watch } from 'vue'

    // 数据
    let person = reactive({
        name: '张三',
        age: 18,
        car: {
            c1: '奔驰',
            c2: '宝马'
        }
    })

    // 方法
    function changeName() {
        person.name += '~'
    }
    function changeAge() {
        person.age += 1
    }

    function changeC1() {
        person.car.c1 = '奥迪'
    }
    function changeC2() {
        person.car.c2 = '大众'
    }
    function changeCar() {
        // 注意此处: 因为person是使用reactive定义的, 所以person整体不能改(改是可以改, 但是不再响应式了, 
        //                                                             所以说不能整体直接改), 
        //          但是person里面的car属性可以改, 因此可以如下改
        person.car = { c1: '雅迪', c2: '爱玛' }
    }

    // 监视,情况四:监视响应式对象中的某个属性,且该属性是对象类型的,可以直接写,也能写函数,更推荐写函数
    // 建议写成函数的形式
    // 当调用changeC1或changeC2方法时, 会触发此处的监测函数执行
    // 当调用changeCar方法时, 会触发此处的监测函数执行
    // 【最佳实践】(函数式来开启对person.car的地址值的监测, 然后deep:true开启对该对象的深度监视)
    watch(() => person.car, (newValue, oldValue) => {
        console.log('person.car变化了', newValue, oldValue)
    }, { deep: true })

    // 如果写成下面这样, 监测的其实是person.car的地址值, 只有在person.car整体改变时, 才会触发此处的监测函数执行
    //  当调用changeC1或changeC2方法时, 不会触发此处的监测函数执行
    /* watch(() => person.car, (newValue, oldValue) => {
        console.log('person.car变化了', newValue, oldValue)
    }) */

    // 如果写成下面这样(直接写的做法), 那么当调用changeCar方法时, 不会触发此处的监测函数执行
    // 当调用changeC1或changeC2方法时, 会触发此处的监测函数执行
    //(因为person.car是person中的对象类型属性, 因此这里可以直接写)
    /* watch(person.car, (newValue, oldValue) => {
        console.log('person.car变化了', newValue, oldValue)
    }) */

</script>

<style scoped>
    ...
</style>
* 情况五

监视上述的多个数据

<template>
<div class="person">

    <h1>情况五:监视上述的多个数据</h1>

    <h2>姓名:{{ person.name }}</h2>

    <h2>年龄:{{ person.age }}</h2>

    <h2>汽车:{{ person.car.c1 }}、{{ person.car.c2 }}</h2>

    <button @click="changeName">修改名字</button>

    <button @click="changeAge">修改年龄</button>

    <button @click="changeC1">修改第一台车</button>

    <button @click="changeC2">修改第二台车</button>

    <button @click="changeCar">修改整个车</button>

    </div>
</template>

<script lang="ts" setup name="Person">

    import {reactive,watch} from 'vue'

    // 数据
    let person = reactive({
        name:'张三',
        age:18,
        car:{
            c1:'奔驰',
            c2:'宝马'
        }
    })

    // 方法
    function changeName(){
        person.name += '~'
    }

    function changeAge(){
        person.age += 1
    }

    function changeC1(){
        person.car.c1 = '奥迪'
    }

    function changeC2(){
        person.car.c2 = '大众'
    }

    function changeCar(){
        person.car = {c1:'雅迪',c2:'爱玛'}
    }

    // 监视,情况五:监视上述的多个数据
    //(person.name是基本类型, 所以要写成函数式; person.car是对象类型, 所以可以直接写;
    // 这里的newVal和oldVal都是数组, 跟监视的2个源相对应; 
    // deep开启深度监视, 不止可以监视地址值, 还包括内部属性的变化;)
    watch([()=>person.name, person.car],(newValue, oldValue)=>{
        console.log('person.car变化了',newValue,oldValue)
    },{deep:true})

</script>

<style scoped>
...
</style>

3.10 watchEffect

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

watch对比watchEffect

  1. 都能监听响应式数据的变化,不同的是监听数据变化的方式不同

  2. watch:要明确指出监视的数据

  3. watchEffect:不用明确指出监视的数据(函数中用到哪些属性,那就监视哪些属性)。

<template>
    <div class="person">
        <h1>需求:水温达到50℃,或水位达到20cm,则联系服务器</h1>
        <h2 id="demo">水温:{{temp}}</h2>
        <h2>水位:{{height}}</h2>
        <button @click="changePrice">水温+1</button>
        <button @click="changeSum">水位+10</button>
    </div>
</template>

<script lang="ts" setup name="Person">
    import {ref,watch,watchEffect} from 'vue'
    // 数据
    let temp = ref(0)
    let height = ref(0)

    // 方法
    function changePrice(){
        temp.value += 10
    }
    function changeSum(){
        height.value += 1
    }

    // 用watch实现,需要明确的指出要监视:temp、height
    watch([temp,height],(value)=>{
        // 从value中获取最新的temp值、height值
        const [newTemp,newHeight] = value
        // 室温达到50℃,或水位达到20cm,立刻联系服务器
        if(newTemp >= 50 || newHeight >= 20){
            console.log('联系服务器')
        }
    })

    // 用watchEffect实现,不用明确的指出要监视变量
    // 1. 它会从监听函数中自动分析需要监视的数据 (而watch则需要指定需要监视的数据)
    // 2. 一上来就会执行1次函数
    const stopWtach = watchEffect(()=>{
        // 室温达到50℃,或水位达到20cm,立刻联系服务器
        if(temp.value >= 50 || height.value >= 20){
            console.log(document.getElementById('demo')?.innerText)
                        console.log('联系服务器')
        }
        // 水温达到100,或水位达到50,取消监视
        if(temp.value === 100 || height.value === 50){
            console.log('清理了')
            stopWtach()
        }
    })
</script>

3.11. 标签的 ref 属性

作用:用于注册模板引用。

  • 用在普通DOM标签上,获取的是DOM节点。

  • 用在组件标签上,获取的是组件实例对象。

用在普通DOM标签上

<template>
    <div class="person">

        <!-- ref标记在普通DOM标签上 -->
        <h1 ref="title1">尚硅谷</h1>
        
        <h2 ref="title2">前端</h2>
        <h3 ref="title3">Vue</h3>

        <input type="text" ref="inpt"> <br><br>

        <button @click="showLog">点我打印内容</button>

    </div>
</template>

<script lang="ts" setup name="Person">

    import {ref} from 'vue'
    
    let title1 = ref()  // 使用ref来获取对应的节点, 其中title1要与对应节点的ref对应的值相同
    let title2 = ref()
    let title3 = ref()

    function showLog(){

        // 通过id获取元素
        const t1 = document.getElementById('title1')

        // 打印内容
        console.log((t1 as HTMLElement).innerText)
        console.log((<HTMLElement>t1).innerText)
        console.log(t1?.innerText)


        // 通过ref获取元素
        console.log(title1.value)
        console.log(title2.value)
        console.log(title3.value)
    }
</script>

用在组件标签上(defineExpose)

defineExpose它属于宏函数,不需要引入

<!-- 父组件App.vue -->
<template>
    
    <!-- ref标记在组件标签上 -->
    <Person ref="ren"/>

    <button @click="test">测试</button>

</template>

<script lang="ts" setup name="App">
    
    // 在setUp中不需要注册Person组件, 直接使用即可
    import Person from './components/Person.vue'
    
    import {ref} from 'vue'

    // 变量名需要与ref标记的值相同
    let ren = ref()

    function test(){
        // 需要子组件通过defineExpose暴露出来的属性或方法, 父组件才可以在这里访问到
        console.log(ren.value.name)
        console.log(ren.value.age)
    }
</script>


<!-- 子组件Person.vue中要使用defineExpose暴露内容 -->
<script lang="ts" setup name="Person">
    
    import {ref,defineExpose} from 'vue'
    
    // 数据
    let name = ref('张三')
    let age = ref(18)
    
    // 使用defineExpose将组件中的数据交给外部
    defineExpose({name,age})
</script>

3.12 回顾TS

main.ts

// 引入createApp用于创建应用
import { createApp } from 'vue'

// 引入App根组件
import App from './App.vue'

createApp(App).mount('#app')

App.vue

<template>
    <Person/>
</template>

<script lang="ts" setup name="App">
    import Person from '@/components/Person.vue'
</script>

index.ts

在src下创建types文件夹,并在这个文件夹中创建如下index.ts文件。

在其中定义接口和自定义泛型

// 定义一个接口,用于限制person对象的具体属性
export interface PersonInter {
    id: string,
    name: string,
    age: number,
    x?: number /* x是可选属性, 该类型中可以有该属性, 也可以无该属性 */
}

// 一个自定义类型
// export type Persons = Array<PersonInter>
export type Persons = PersonInter[] // 与上面等价

Person.vue

注意把vetur这个插件给禁掉, 否则,老是有飘红。就开启本篇中上述的推荐的插件即可。

<template>
    <div class="person">
        
    </div>
</template>

<script lang="ts" setup name="Person">

    // 引入接口 或 自定义类型 的时候, 需要在前面加上type; 
    import { type PersonInter, type Persons } from '@/types'

    // 定义1个变量, 它要符合PersonInter接口
    let person: PersonInter = {id: 'a01', name: 'john', age:60}

    // 定义1个数组, 首先它是个数组, 并且里面元素类型都是符合PersonInter接口的(如果里面有属性名写错会有飘红提示)
    let personList: Array<PersonInter> = [
        {id: 'a01', name: 'john', age:60}
    ]

    // 定义1个数组, 它符合 Persons 自定义类型(如果里面有属性名写错会有飘红提示)
    let personList2: Persons = [
        {id: 'a01', name: 'john', age:60}
    ]

</script>

<style scoped>

</style>

3.13 props(defineProps)

defineProps它属于宏函数,不需要引入

App.vue

<template>
    <!-- Person子组件定义了list属性, 并且限定为Persons类型 -->
    <Person :list="personList" />
</template>

<script lang="ts" setup name="App">

    import Person from '@/components/Person.vue'

    import {reactive} from 'vue'
    import {type Persons} from '@/types'

    let personList = reactive<Persons>([
        { id: 'asudfysafd01', name: '张三', age: 18 },
        { id: 'asudfysafd02', name: '李四', age: 20 },
        { id: 'asudfysaf)d03', name: '王五', age: 22 }
    ])

</script>

index.ts

// 定义一个接口,用于限制person对象的具体属性
export interface PersonInter {
    id: string,
    name: string,
    age: number,
    x?: number /* x是可选属性, 该类型中可以有该属性, 也可以无该属性 */
}

// 一个自定义类型
// export type Persons = Array<PersonInter>
export type Persons = PersonInter[] // 与上面等价

Person.vue

<template>
    <div class="person">
        <ul>
          <!-- 在模板中直接使用list, 不需要加props.list -->
          <li v-for="p in list" :key="p.id">
            {{p.name}} -- {{p.age}}
          </li>
        </ul>
    </div>
</template>

<script lang="ts" setup name="Person">

    import {reactive, withDefaults} from 'vue'

    // 引入接口 或 自定义类型 的时候, 需要在前面加上type; 
    import { type PersonInter, type Persons } from '@/types'

    // 不推荐的写法, 但可用
    let personList:Persons = reactive([
        {id: 'a01', name: 'john', age:60}
    ])

    // 推荐的写法, 意为: personList2这个变量须符合 Persons 类型的规范
    let personList2 = reactive<Persons>([
        {id: 'a01', name: 'john', age:60}
    ])

    // 推荐的写法, 意为: personList3这个变量须符合 PersonInter[] 类型的规范
    let personList3 = reactive<PersonInter[]>([
        {id: 'a01', name: 'john', age:60}
    ])

    // 只接收
    // 定义接收父组件传过来的a属性, 并赋值给props以便于访问。并且defineProps只能使用1次
    /* 
    let props = defineProps(['a', 'b'])
    // 在js代码中使用props.a来访问父组件传过来的a属性对应的值, 在模板中直接使用a来访问父组件传过来的a属性对应的值
    console.log(props.a); 
    */    

    // 接收 + 限制类型 + 限制必要性
    // (list2可不传; list必须传, 并且必须是Persons类型的)
    /* 
    let props = defineProps<{list:Persons, list2?:Persons}>()
    console.log(props.list);  
    */

    // 接收 + 限制类型 + 限制必要性 + 指定默认值
    // (list属性可不传, 如果没有传的话, 就是用下面默认定义的数据)
    const props = withDefaults(defineProps<{list?: Persons}>(),
        {
            list: () => [{id:'A001',name:'张三',age:18}]
        }
    )
    console.log(props.list);

</script>

<style scoped>

</style>

3.14 生命周期

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

  • 规律:

    生命周期整体分为四个阶段,分别是:创建、挂载、更新、销毁,每个阶段都有两个钩子,一前一后。

  • Vue2的生命周期

    创建阶段:beforeCreatecreated

    挂载阶段:beforeMountmounted

    更新阶段:beforeUpdateupdated

    销毁阶段:beforeDestroydestroyed

  • Vue3的生命周期

    创建阶段:setup(替代了之前vue2中的beforeCreate、created)

    挂载阶段:onBeforeMountonMounted

    更新阶段:onBeforeUpdateonUpdated

    卸载阶段:onBeforeUnmountonUnmounted(就对应vue2中的销毁阶段)

  • 常用的钩子:onMounted(挂载完毕)、onUpdated(更新完毕)、onBeforeUnmount(卸载之前)

App.vue

<template>
    <Person v-if="isShow"/>
</template>

<script lang="ts" setup name="App">
    import Person from './components/Person.vue'
    import {ref,onMounted} from 'vue'

    let isShow = ref(true)

    // 挂载完毕(先子组件挂载完毕, 再父挂载完毕)
    onMounted(()=>{
        console.log('父---挂载完毕')
    })

</script>

Person.vue

<template>
    <div class="person">
        <h2>当前求和为:{{ sum }}</h2>
        <button @click="add">点我sum+1</button>
    </div>
</template>

<script lang="ts" setup name="Person">
    import {ref,
            onBeforeMount, onMounted,
            onBeforeUpdate, onUpdated,
            onBeforeUnmount, onUnmounted } from 'vue'

    // 数据
    let sum = ref(0)
    // 方法
    function add(){
        sum.value += 1
    }
    // 创建(替代了之前vue2中的beforeCreate、created)
    console.log('创建')

    // 挂载前(这里面传入的函数由vue3帮我们调用, 这里只是将这个函数注册进去)
    onBeforeMount(()=>{
        // console.log('挂载前')
    })

    // 挂载完毕
    onMounted(()=>{
        console.log('子---挂载完毕')
    })

    // 更新前
    onBeforeUpdate(()=>{
        // console.log('更新前')
    })
    // 更新完毕
    onUpdated(()=>{
        // console.log('更新完毕')
    })
    // 卸载前
    onBeforeUnmount(()=>{
        // console.log('卸载前')
    })
    // 卸载完毕
    onUnmounted(()=>{
        // console.log('卸载完毕')
    })
</script>

3.15 自定义hooks

未使用hooks前

App.vue
<template>
    <Person />
</template>

<script lang="ts" setup name="App">
import Person from './components/Person.vue'
</script>
Person.vue
<template>
    <div class="person">
        
        <h2>当前求和为:{{ sum }},放大10倍后:{{ bigSum }}</h2>
        <button @click="add">点我sum+1</button>
        
        <hr>
        
        <img v-for="(dog, index) in dogList" :src="dog" :key="index">
        <button @click="getDog">再来一只小狗</button>
        
    </div>
</template>

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

    // ---- 求和
    // 数据
    let sum = ref(0)
    let bigSum = computed(() => {
        return sum.value * 10
    })

    // 方法
    function add() {
        sum.value += 1
    }

    // 钩子
    onMounted(() => {
        add()
    })

    // --- 发起请求获取图片
    // 数据
    let dogList = reactive([
        'https://images.dog.ceo/breeds/pembroke/n02113023_4373.jpg'
    ])
    // 方法
    async function getDog() {
        try {
            let result = await axios.get('https://dog.ceo/api/breed/pembroke/images/random')
            dogList.push(result.data.message)
        } catch (error) {
            alert(error)
        }
    }
    // 钩子
    onMounted(() => {
        getDog()
    })


</script>

<style scoped>

</style>

使用hooks

vue3本身就推荐使用组合式api,但是如果各种功能都放到setup里面,显得就有点乱了,所以,使用hooks将单独的功能所使用的各种数据、方法等抽离出去,当需要某个功能时,再引入进来。

hooks中不仅可以定义数据,还可以使用声明周期钩子函数,还可以写计算属性。

App.vue
<template>
    <Person />
</template>

<script lang="ts" setup name="App">
import Person from './components/Person.vue'
</script>
Person.vue
<template>
    <div class="person">
        <h2>当前求和为:{{ sum }},放大10倍后:{{ bigSum }}</h2>
        <button @click="add">点我sum+1</button>
        <hr>
        <img v-for="(dog,index) in dogList" :src="dog" :key="index">
        <br>
        <button @click="getDog">再来一只小狗</button>
    </div>
</template>

<script lang="ts" setup name="Person">

    import useSum from '@/hooks/useSum'

    import useDog from '@/hooks/useDog'

    // 调用函数获得数据
    const {sum,add,bigSum} = useSum()

    // 调用函数获得数据
    const {dogList,getDog} = useDog()

</script>

<style scoped>
    
</style>
hooks/useSum.ts
import { ref ,onMounted,computed} from 'vue'

// 暴露此函数(默认暴露)
export default function () {
    
    // 数据
    let sum = ref(0)
    
    // 这里面也可以写计算属性的哦
    let bigSum = computed(()=>{
        return sum.value * 10
    })

    // 方法
    function add() {
        sum.value += 1
    }

    // 钩子(hooks这里面也能写钩子的哦)
    onMounted(()=>{
        add()
    })

    // 给外部提供东西(要把东西放出去,让外界使用)
    return {sum,add,bigSum}
}
hooks/useDog.ts
import {reactive,onMounted} from 'vue'
import axios from 'axios'

export default function (){
    
    // 数据
    let dogList = reactive([
        'https://images.dog.ceo/breeds/pembroke/n02113023_4373.jpg'
    ])
    
    // 方法
    async function getDog(){
        try {
            let result = await axios.get('https://dog.ceo/api/breed/pembroke/images/random')
            dogList.push(result.data.message)
        } catch (error) {
            alert(error)
        }
    }
    
    // 钩子(hooks这里面也能写钩子的哦)
    onMounted(()=>{
        getDog()
    })
    
    // 向外部提供东西
    return {dogList,getDog}
}

4.路由

4.1 路由的基本理解

在这里插入图片描述

当路由变化,路由器会监听到此变化,就会根据路由规则找到对应的组件,将这个组件展示在路由出口

在这里插入图片描述

4.2 基本切换效果

安装vue-router

# 现在查看package.json,发现安装的版本是【"vue-router": "^4.3.2"】
# 路由器是用来管理路由的, 并且当路径变化时, 根据路由规则将对应的组件 展示在路由出口处
npm install vue-router

配置路由规则router/index.ts

// 创建一个路由器,并暴露出去

// 第一步:引入createRouter
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
    },
  ]
})

// 暴露出去router
export default router

使用router路由管理器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')

路由展示区App.vue

<template>
  <div class="app">
      
    <h2 class="title">Vue路由测试</h2>
      
    <!-- 导航区, 使用<router-link>标签来切换路由路径 -->
    <div class="navigate">
      <RouterLink to="/home" active-class="active">首页</RouterLink>
      <RouterLink to="/news" active-class="active">新闻</RouterLink>
      <RouterLink to="/about" active-class="active">关于</RouterLink>
    </div>
      
    <!-- 展示区 , 使用<Router-view>标签作为路由出口 -->
    <div class="main-content">
      <RouterView></RouterView>
    </div>
      
  </div>
</template>

<script lang="ts" setup name="App">
  import {RouterView,RouterLink} from 'vue-router'

</script>

<style>
    /* App */
  .title {
    text-align: center;
    word-spacing: 5px;
    margin: 30px 0;
    height: 70px;
    line-height: 70px;
    background-image: linear-gradient(45deg, gray, white);
    border-radius: 10px;
    box-shadow: 0 0 2px;
    font-size: 30px;
  }
  .navigate {
    display: flex;
    justify-content: space-around;
    margin: 0 100px;
  }
  .navigate a {
    display: block;
    text-align: center;
    width: 90px;
    height: 40px;
    line-height: 40px;
    border-radius: 10px;
    background-color: gray;
    text-decoration: none;
    color: white;
    font-size: 18px;
    letter-spacing: 5px;
  }
  .navigate a.active {
    background-color: #64967E;
    color: #ffc268;
    font-weight: 900;
    text-shadow: 0 0 1px black;
    font-family: 微软雅黑;
  }
  .main-content {
    margin: 0 auto;
    margin-top: 30px;
    border-radius: 10px;
    width: 90%;
    height: 400px;
    border: 1px solid;
  }
</style>

路由组件

Home.vue
<template>
  <div class="home">
    <img src="http://www.atguigu.com/images/index_new/logo.png" alt="">
  </div>
</template>

<script setup lang="ts" name="Home">

</script>

<style scoped>
  .home {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100%;
  }
</style>
New.vue
<template>
  <div class="news">
    <ul>
      <li><a href="#">新闻001</a></li>
      <li><a href="#">新闻002</a></li>
      <li><a href="#">新闻003</a></li>
      <li><a href="#">新闻004</a></li>
    </ul>
  </div>
</template>

<script setup lang="ts" name="News">
  
</script>

<style scoped>
/* 新闻 */
.news {
  padding: 0 20px;
  display: flex;
  justify-content: space-between;
  height: 100%;
}
.news ul {
  margin-top: 30px;
  list-style: none;
  padding-left: 10px;
}
.news li>a {
  font-size: 18px;
  line-height: 40px;
  text-decoration: none;
  color: #64967E;
  text-shadow: 0 0 1px rgb(0, 84, 0);
}
.news-content {
  width: 70%;
  height: 90%;
  border: 1px solid;
  margin-top: 20px;
  border-radius: 10px;
}
</style>
About.vue
<template>
  <div class="about">
    <h2>大家好,欢迎来到尚硅谷直播间</h2>
  </div>
</template>

<script setup lang="ts" name="About">

</script>

<style scoped>
.about {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
  color: rgb(85, 84, 84);
  font-size: 18px;
}
</style>

路由切换效果图

在这里插入图片描述

4.3. 两个注意点

1、路由组件通常存放在pagesviews文件夹,一般组件通常存放在components文件夹。

2、通过点击导航,视觉效果上“消失” 了的路由组件,默认是被卸载掉的,需要的时候再去挂载

About.vue

当通过切换路由路径的方式而控制About.vue组件的显示和隐藏时,会分别执行onMounted 和 onUnmounted 中定义的函数

<template>

    <div class="about">
        <h2>大家好,欢迎来到尚硅谷直播间</h2>
    </div>

</template>

<script setup lang="ts" name="About">

    import {onMounted,onUnmounted} from 'vue'

	// 挂载时执行的函数
    onMounted(()=>{
        console.log('About组件挂载了')
    })
    
    // 卸载时执行的函数
    onUnmounted(()=>{
        console.log('About组件卸载了')
    })
</script>

<style scoped>
    .about {
        display: flex;
        justify-content: center;
        align-items: center;
        height: 100%;
        color: rgb(85, 84, 84);
        font-size: 18px;
    }
</style>

4.4. 路由器工作模式

  1. history模式

    优点:URL更加美观,不带有#,更接近传统的网站URL

    缺点:后期项目上线,需要服务端配合处理路径问题,否则刷新会有404错误。

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

    优点:兼容性更好,因为不需要服务器端处理路径。

    缺点:URL带有#不太美观,且在SEO优化方面相对较差。

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

4.5. to的两种写法

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

<!-- 第二种:to的对象写法 -->
<router-link active-class="active" :to="{path:'/home'}">Home</router-link>

4.6. 命名路由

作用:可以简化路由跳转及传参(后面就讲)。

给路由规则命名:

// 创建一个路由器,并暴露出去

// 第一步:引入createRouter
import {createRouter,createWebHistory,createWebHashHistory} from 'vue-router'
// 引入一个一个可能要呈现组件
import Home from '@/pages/Home.vue'
import News from '@/pages/News.vue'
import About from '@/pages/About.vue'

// 第二步:创建路由器
const router = createRouter({
  history:createWebHashHistory(), //路由器的工作模式(稍后讲解)
  routes:[ //一个一个的路由规则
    {
      name:'zhuye',
      path:'/home',
      component:Home
    },
    {
      name:'xinwen',
      path:'/news',
      component:News
    },
    {
      name:'guanyu',
      path:'/about',
      component:About
    },
  ]
})

// 暴露出去router
export default router

跳转路由:

<template>
    <div class="app">

        <Header/>

        <!-- 导航区 -->
        <div class="navigate">
            <!--简化前:需要写完整的路径(to的字符串写法) -->
            <RouterLink to="/home" active-class="active">首页</RouterLink>
            <!--简化后:直接通过路由规则中定义的路由的名字(route的name属性)跳转(to的对象写法配合name属性) -->
            <RouterLink :to="{name:'xinwen'}" active-class="active">新闻</RouterLink>
            <RouterLink :to="{path:'/about'}" active-class="active">关于</RouterLink>
        </div>

        <!-- 展示区 -->
        <div class="main-content">
            <RouterView></RouterView>
        </div>
    
    </div>
</template>

<script lang="ts" setup name="App">
    import {RouterView,RouterLink} from 'vue-router'
    import Header from './components/Header.vue'

</script>

4.7 嵌套路由

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')

router/index.ts

当访问/news/detail时,先根据路由规则匹配到News组件,这个News组件应该要展示在App.vue中的路由出口处,然后匹配到子级路由找到Detail.vue,然后将Detail.vue组件展示在News组件的路由出口处。

// 创建一个路由器,并暴露出去

// 第一步:引入createRouter
import {createRouter,createWebHistory,createWebHashHistory} 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:[ //一个一个的路由规则
    {
      name:'zhuye',
      path:'/home',
      component:Home
    },
    {
      name:'xinwen',
      path:'/news',
      component:News,
      children:[
        {
          path:'detail',
          component:Detail
        }
      ]
    },
    {
      name:'guanyu',
      path:'/about',
      component:About
    },
  ]
})

// 暴露出去router
export default router

App.vue

在App.vue中有1个路由出口(一级路由出口)

<template>
  <div class="app">
      
    <Header/>
      
    <!-- 导航区 -->
    <div class="navigate">
      <RouterLink to="/home" active-class="active">首页</RouterLink>
      <RouterLink :to="{name:'xinwen'}" active-class="active">新闻</RouterLink>
      <RouterLink :to="{path:'/about'}" active-class="active">关于</RouterLink>
    </div>
      
    <!-- 展示区 -->
    <div class="main-content">
      <RouterView></RouterView>
    </div>
      
  </div>
</template>

<script lang="ts" setup name="App">
  import {RouterView,RouterLink} from 'vue-router'
  import Header from './components/Header.vue'

</script>

<style>
    /* App */
  .navigate {
    display: flex;
    justify-content: space-around;
    margin: 0 100px;
  }
  .navigate a {
    display: block;
    text-align: center;
    width: 90px;
    height: 40px;
    line-height: 40px;
    border-radius: 10px;
    background-color: gray;
    text-decoration: none;
    color: white;
    font-size: 18px;
    letter-spacing: 5px;
  }
  .navigate a.active {
    background-color: #64967E;
    color: #ffc268;
    font-weight: 900;
    text-shadow: 0 0 1px black;
    font-family: 微软雅黑;
  }
  .main-content {
    margin: 0 auto;
    margin-top: 30px;
    border-radius: 10px;
    width: 90%;
    height: 400px;
    border: 1px solid;
  }
</style>

News.vue

在News.vue中有1个子级路由出口

<template>
  <div class="news">
      
    <!-- 导航区 -->
    <ul>
      <li v-for="news in newsList" :key="news.id">
        <RouterLink to="/news/detail">{{news.title}}</RouterLink>
      </li>
    </ul>
      
    <!-- 展示区 -->
    <div class="news-content">
      <RouterView></RouterView>
    </div>
      
  </div>
</template>

<script setup lang="ts" name="News">
  import {reactive} from 'vue'
  import {RouterView,RouterLink} from 'vue-router'

  const newsList = reactive([
    {id:'asfdtrfay01',title:'很好的抗癌食物',content:'西蓝花'},
    {id:'asfdtrfay02',title:'如何一夜暴富',content:'学IT'},
    {id:'asfdtrfay03',title:'震惊,万万没想到',content:'明天是周一'},
    {id:'asfdtrfay04',title:'好消息!好消息!',content:'快过年了'}
  ])

</script>

<style scoped>
/* 新闻 */
.news {
  padding: 0 20px;
  display: flex;
  justify-content: space-between;
  height: 100%;
}
.news ul {
  margin-top: 30px;
  list-style: none;
  padding-left: 10px;
}
.news li>a {
  font-size: 18px;
  line-height: 40px;
  text-decoration: none;
  color: #64967E;
  text-shadow: 0 0 1px rgb(0, 84, 0);
}
.news-content {
  width: 70%;
  height: 90%;
  border: 1px solid;
  margin-top: 20px;
  border-radius: 10px;
}
</style>

Detail.vue

<template>
  <ul class="news-list">
    <li>编号:xxx</li>
    <li>标题:xxx</li>
    <li>内容:xxx</li>
  </ul>
</template>

<script setup lang="ts" name="About">

</script>

<style scoped>
  .news-list {
    list-style: none;
    padding-left: 20px;
  }

  .news-list>li {
    line-height: 30px;
  }
</style>

效果

可以看到在App.vue中有1个路由出口,在News.vue中也有1个路由出口

在这里插入图片描述

4.8 路由传参

query参数

1.定义路由规则

const router = createRouter({
  history:createWebHistory(), //路由器的工作模式(稍后讲解)
  routes:[ //一个一个的路由规则
    {
      name:'zhuye',
      path:'/home',
      component:Home
    },
    {
      name:'xinwen',
      path:'/news',
      component:News,
      children:[
        {
          name:'xiang',
          path:'detail',
          component:Detail
        }
      ]
    },
    {
      name:'guanyu',
      path:'/about',
      component:About
    }
  ]
})

2.传递参数

<!-- 跳转并携带query参数(to的字符串写法) -->
<router-link to="/news/detail?a=1&b=2&content=欢迎你">
	跳转
</router-link>
				
<!-- 跳转并携带query参数(to的对象写法) -->
<RouterLink 
  :to="{
    //name:'xiang', //用name也可以跳转
    path:'/news/detail',
    query:{
      id:news.id,
      title:news.title,
      content:news.content
    }
  }"
>
  {{news.title}}
</RouterLink>

3.接收参数:

import {useRoute} from 'vue-router'
import {toRefs} from 'vue' 

const route = useRoute()

// 从1个响应式对象直接解构属性(route是响应式对象),会丢失响应式
// 然后试图在模板中使用此query, 发现点击不同的新闻时数据没有变化, 因为在解构时这里已经丢失了响应式了
// 应该使用toRefs
// const {query} = route 

// 应该如下使用toRefs
// 然后在模板中使用, 发现点击不同的新闻时, 数据有了变化
const {query} = toRefs(route)

// 打印query参数
console.log(route.query)

params参数

  1. 定义路由规则,并定义路由路径params参数
const router = createRouter({
    history:createWebHistory(), //路由器的工作模式(稍后讲解)
    routes:[ //一个一个的路由规则
        {
            name:'zhuye',
            path:'/home',
            component:Home
        },
        {
            name:'xinwen',
            path:'/news',
            component:News,
            children:[
                {
                    name:'xiang',
					// 添加路径参数来占位
                    path:'detail/:id/:title/:content?', // 这里加个问号的意思是可传可不传, 否则必须传
                    component:Detail
                }
            ]
        },
        {
            name:'guanyu',
            path:'/about',
            component:About
        }
    ]
})
  1. 传递参数
<!-- 跳转并携带params参数(to的字符串写法) -->
<RouterLink :to="`/news/detail/001/新闻001/内容001`">{{news.title}}</RouterLink>
				
<!-- 跳转并携带params参数(to的对象写法) -->
<RouterLink 
  :to="{
    name:'xiang', // 用name跳转, 注意这里不能用path, 并且下面的params的属性对应的值不能是对象或数组
    params:{
      id:news.id,
      title:news.title,
      content:news.title
    }
  }"
>
  {{news.title}}
</RouterLink>
  1. 接收参数:
// useRoute是hooks钩子
import {useRoute} from 'vue-router'

const route = useRoute()

// 从1个响应式对象直接解构属性(route是响应式对象),会丢失响应式
// 然后试图在模板中使用此query, 发现点击不同的新闻时数据没有变化, 因为在解构时这里已经丢失了响应式了
// 应该使用toRefs
// const {params} = route 

// 应该如下使用toRefs
// 然后在模板中使用, 发现点击不同的新闻时, 数据有了变化
const {params} = toRefs(route)

// 打印params参数
console.log(route.params)

备注1:传递params参数时,若使用to的对象写法,必须使用name配置项,不能用path

备注2:传递params参数时,需要提前在规则中占位。

4.9 路由的props配置

作用:让路由组件更方便的收到参数(可以将路由参数作为props传给组件)

{
    name:'xiang',
    path:'detail/:id/:title/:content',
    component:Detail,
        
    // 第一种写法:将路由收到的【所有params参数】作为props传给路由组件
    // props的布尔值写法,作用:把收到了每一组params参数,作为props传给Detail组件,
    // (类似于: <Detail :id='xx' :title='xx' :content='xx' />)
    // 这样在Detail组件中通过defineProps(['id','title','content'])声明属性, 
    // 然后在模板中直接使用id,title,content就可以访问这些属性了
    // props:true

    // 第二种写法:函数写法,可以自己决定将什么作为props给路由组件
    // props的函数写法,作用:把返回的对象中每一组key-value作为props传给Detail组件
    // 这里的形参可以不叫route, 换成其它任何名字都代表路由对象
    // 这样在Detail组件中通过defineProps(['k'])声明属性, 
    // 然后在模板中直接使用k就可以访问k属性对应的值了, route.query中的属性也是一样
    props(route){
        return {...route.query, k:'v'}
    }
    
    // 第三种写法:对象写法,可以自己决定将什么作为props给路由组件
    // props的对象写法,作用:把对象中的每一组key-value作为props传给Detail组件
    // props:{a:1,b:2,c:3}, 
    
    // 以上写法请注意, 都是在指定Detail作为路由组件展示在路由出口时, 给该【路由组件】传递的props, 
    // 注意与直接使用<Detail/>标签的形式的【一般组件】区别开来
}

4.10 replace属性

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

  2. 浏览器的历史记录有两种写入方式:分别为pushreplace

    • push是追加历史记录(默认值)。
    • replace是替换当前记录。
  3. 开启replace模式:

    <RouterLink replace to='/news/detail/1'>News</RouterLink>
    

示例

<template>
    <div class="app">
        <Header/>
        <!-- 导航区 -->
        <div class="navigate">
            <RouterLink to="/home" active-class="active">首页</RouterLink>
            <RouterLink replace :to="{name:'xinwen'}" active-class="active">新闻</RouterLink>
            <RouterLink replace :to="{path:'/about'}" active-class="active">关于</RouterLink>
        </div>
        <!-- 展示区 -->
        <div class="main-content">
            <RouterView></RouterView>
        </div>
    </div>
</template>

4.11 编程式导航

路由组件的两个重要的属性:$route$router变成了两个hooks

import {useRoute,useRouter} from 'vue-router'

const route = useRoute()
const router = useRouter()

console.log(route.query)
console.log(route.parmas)

// <RouterLink to=''/>标签中的to属性能怎么写, 那么router.push(..)中的参数就能怎么写
console.log(router.push) 
console.log(router.replace)

示例

<template>
  <div class="news">
    <!-- 导航区 -->
    <ul>
      <li v-for="news in newsList" :key="news.id">
        <button @click="showNewsDetail(news)">查看新闻</button>
        <RouterLink 
          :to="{
            name:'xiang',
            query:{
              id:news.id,
              title:news.title,
              content:news.content
            }
          }"
        >
          {{news.title}}
        </RouterLink>
      </li>
    </ul>
    <!-- 展示区 -->
    <div class="news-content">
      <RouterView></RouterView>
    </div>
  </div>
</template>

<script setup lang="ts" name="News">
  import {reactive} from 'vue'
  import {RouterView,RouterLink,useRouter} from 'vue-router'

  const newsList = reactive([
    {id:'asfdtrfay01',title:'很好的抗癌食物',content:'西蓝花'},
    {id:'asfdtrfay02',title:'如何一夜暴富',content:'学IT'},
    {id:'asfdtrfay03',title:'震惊,万万没想到',content:'明天是周一'},
    {id:'asfdtrfay04',title:'好消息!好消息!',content:'快过年了'}
  ])

  const router = useRouter()

  interface NewsInter {
    id:string,
    title:string,
    content:string
  }

  function showNewsDetail(news:NewsInter){
    router.replace({
      name:'xiang',
      query:{
        id:news.id,
        title:news.title,
        content:news.content
      }
    })
  }

</script>

<style scoped>
/* 新闻 */
.news {
  padding: 0 20px;
  display: flex;
  justify-content: space-between;
  height: 100%;
}
.news ul {
  margin-top: 30px;
  /* list-style: none; */
  padding-left: 10px;
}
.news li::marker {
  color: #64967E;
}
.news li>a {
  font-size: 18px;
  line-height: 40px;
  text-decoration: none;
  color: #64967E;
  text-shadow: 0 0 1px rgb(0, 84, 0);
}
.news-content {
  width: 70%;
  height: 90%;
  border: 1px solid;
  margin-top: 20px;
  border-radius: 10px;
}
</style>

4.12 重定向

  1. 作用:将特定的路径,重新定向到已有路由。

  2. 具体编码:

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

示例

// 创建一个路由器,并暴露出去

// 第一步:引入createRouter
import {createRouter,createWebHistory,createWebHashHistory} 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:[ //一个一个的路由规则
        {
            name:'zhuye',
            path:'/home',
            component:Home
        },
        {
            name:'xinwen',
            path:'/news',
            component:News,
            children:[
                {
                    name:'xiang',
                    path:'detail',
                    component:Detail,

                    props(route){
                        return route.query
                    }

                }
            ]
        },
        {
            name:'guanyu',
            path:'/about',
            component:About
        },
        {
            path:'/',
            // 使用重定向, 当用户访问/时, 跳转到/home
            // 即: 让指定的路径重新定位到另一个路径
            redirect:'/home'
        }
    ]
})

// 暴露出去router
export default router

5. pinia

5.1 准备一个效果

pinia_example

main.ts

// 引入createApp用于创建应用
import {createApp} from 'vue'

// 引入App根组件
import App from './App.vue'

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

// 挂载整个应用到app容器中
app.mount('#app')

App.vue

<template>
  <Count/>
  <br>
  <LoveTalk/>
</template>

<script setup lang="ts" name="App">
  import Count from './components/Count.vue'
  import LoveTalk from './components/LoveTalk.vue'
</script>

Count.vue

<template>
    <div class="count">
        <h2>当前求和为:{{ sum }}</h2>
        
        <!-- 如果不写.number, 那么绑定所获取的值是字符串 -->
        <!-- 当然也可以这样使用v-bind来绑定, 如: <option :value="1">1</option> -->
        <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="minus"></button>
    </div>
</template>

<script setup lang="ts" name="Count">
    import { ref } from "vue";
    // 数据
    let sum = ref(1) // 当前求和
    let n = ref(1) // 用户选择的数字

    // 方法
    function add(){
        sum.value += n.value
    }
    function minus(){
        sum.value -= n.value
    }
</script>

<style scoped>
    .count {
        background-color: skyblue;
        padding: 10px;
        border-radius: 10px;
        box-shadow: 0 0 10px;
    }
    select,button {
        margin: 0 5px;
        height: 25px;
    }
</style>

LoveTalk.vue

<template>
    <div class="talk">
        <button @click="getLoveTalk">获取一句土味情话</button>
        <ul>
            <li v-for="talk in talkList" :key="talk.id">{{talk.title}}</li>
        </ul>
    </div>
</template>

<script setup lang="ts" name="LoveTalk">
    import {reactive} from 'vue'
    import axios from "axios";
    import {nanoid} from 'nanoid'
    // 数据
    let talkList = reactive([
        {id:'ftrfasdf01',title:'今天你有点怪,哪里怪?怪好看的!'},
        {id:'ftrfasdf02',title:'草莓、蓝莓、蔓越莓,今天想我了没?'},
        {id:'ftrfasdf03',title:'心里给你留了一块地,我的死心塌地'}
    ])
    // 方法
    async function getLoveTalk(){
        // 发请求,下面这行的写法是:连续解构赋值+重命名
        let {data:{content:title}} = await 
                                     axios.get('https://api.uomg.com/api/rand.qinghua?format=json')
        // 把请求回来的字符串,包装成一个对象
        let obj = {id:nanoid(),title}
        // 放到数组中
        talkList.unshift(obj)
    }
</script>

<style scoped>
    .talk {
        background-color: orange;
        padding: 10px;
        border-radius: 10px;
        box-shadow: 0 0 10px;
    }
</style>

5.2 搭建 pinia 环境

使用步骤

第一步:npm install pinia(此处安装的版本是:“pinia”: “^2.1.7”,)

第二步:操作src/main.ts

import { createApp } from 'vue'

import App from './App.vue'

/* 引入createPinia,用于创建pinia */
import { createPinia } from 'pinia'

/* 创建pinia */
const pinia = createPinia()

const app = createApp(App)

/* 使用插件 */
app.use(pinia)

app.mount('#app')

此时开发者工具中已经有了pinia选项

5.3 存储+读取数据

  1. Store是一个保存:状态业务逻辑 的实体,每个组件都可以读取写入它。

  2. 它有三个概念:stategetteraction,相当于组件中的: datacomputedmethods

store/count.ts

import { defineStore } from 'pinia'

// defineStore返回的值的命名 格式为: use{文件名}Store
export const useCountStore = defineStore('count', /* 建议这里的名字与文件名保持一直, 首字母小写 */{
    // 真正存储数据的地方
    state() { // 这个只能写成1个函数
        return {
            sum: 6
        }
    }
})

store/loveTalk.ts

import {defineStore} from 'pinia'

export const useTalkStore = defineStore('talk',{
    // 真正存储数据的地方
    state(){
        return {
            talkList:[
                {id:'ftrfasdf01',title:'今天你有点怪,哪里怪?怪好看的!'},
                {id:'ftrfasdf02',title:'草莓、蓝莓、蔓越莓,今天想我了没?'},
                {id:'ftrfasdf03',title:'心里给你留了一块地,我的死心塌地'}
            ]
        }
    }
})

Count.vue

<template>
    <div class="count">
        
        <!-- 直接使用countStore -->
        <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="minus"></button>
    </div>
</template>

<script setup lang="ts" name="Count">
    import { ref, reactive } from "vue";
    import { useCountStore } from '@/store/count'

    const countStore = useCountStore()

    // 以下两种方式都可以拿到state中的数据
    // console.log('@@@',countStore.sum) // 注意: 这里后面不要写.value哦, 因为会自动拆包
    // console.log('@@@',countStore.$state.sum) // 也可以通过$state拿到sum

    /*   
      let obj = reactive({
        a:1,
        b:2,
        c:ref(3)
      })
      let x = ref(9)
      console.log(obj.a)
      console.log(obj.b)
      console.log(obj.c) // 注意, 这里最后面就不用.value了
    */


    // 数据
    let n = ref(1) // 用户选择的数字

    // 方法
    function add() {

    }
    function minus() {

    }
</script>

<style scoped>
    .count {
        background-color: skyblue;
        padding: 10px;
        border-radius: 10px;
        box-shadow: 0 0 10px;
    }

    select,
    button {
        margin: 0 5px;
        height: 25px;
    }
</style>

LoveTalk.vue

<template>
    <div class="talk">
        <button @click="getLoveTalk">获取一句土味情话</button>
        <ul>
            <li v-for="talk in talkStore.talkList" :key="talk.id">{{talk.title}}</li>
        </ul>
    </div>
</template>

<script setup lang="ts" name="LoveTalk">
    import {reactive} from 'vue'
    import axios from "axios";
    import {nanoid} from 'nanoid'
    import {useTalkStore} from '@/store/loveTalk'

    const talkStore = useTalkStore()

    // 方法
    async function getLoveTalk(){
        // 发请求,下面这行的写法是:连续解构赋值+重命名
        // let {data:{content:title}} = await 
        //                        axios.get('https://api.uomg.com/api/rand.qinghua?format=json')
        // 把请求回来的字符串,包装成一个对象
        // let obj = {id:nanoid(),title}
        // 放到数组中
        // talkList.unshift(obj)
    }
</script>

<style scoped>
    .talk {
        background-color: orange;
        padding: 10px;
        border-radius: 10px;
        box-shadow: 0 0 10px;
    }
</style>

App.vue

<template>
    <Count/>
    <br>
    <LoveTalk/>
</template>

<script setup lang="ts" name="App">
    import Count from './components/Count.vue'
    import LoveTalk from './components/LoveTalk.vue'
</script>

main.ts

import {createApp} from 'vue'
import App from './App.vue'
// 第一步:引入pinia
import {createPinia} from 'pinia'

const app = createApp(App)
// 第二步:创建pinia
const pinia = createPinia()
// 第三步:安装pinia
app.use(pinia)
app.mount('#app')

5.4 修改数据(三种方式)

第一种方式

count.ts
import {defineStore} from 'pinia'

export const useCountStore = defineStore('count',{
    // 真正存储数据的地方
    state(){
        return {
            sum:6,
            school:'atguigu',
            address:'宏福科技园'
        }
    }
})
Count.vue
<template>
    <div class="count">
        <h2>当前求和为:{{ countStore.sum }}</h2>

        <button @click="add"></button>
    </div>
</template>

<script setup lang="ts" name="Count">

    import { ref, reactive } from "vue";

    // 引入useCountStore
    import { useCountStore } from '@/store/count'

    // 使用useCountStore,得到一个专门保存count相关的store
    const countStore = useCountStore()

    // 数据
    let n = ref(1) // 用户选择的数字

    // 方法
    function add() {
        // 第一种修改方式, 直接拿到countStore去改, 注意: 这和vuex不同, vuex是不能直接修改的
        countStore.sum += 1
        countStore.school = '尚硅谷'
        countStore.address = '北京'
    }
</script>

<style scoped>
    .count {
        background-color: skyblue;
        padding: 10px;
        border-radius: 10px;
        box-shadow: 0 0 10px;
    }

    select,
    button {
        margin: 0 5px;
        height: 25px;
    }
</style>

第二种方式

count.ts
import {defineStore} from 'pinia'

export const useCountStore = defineStore('count',{
    // 真正存储数据的地方
    state(){
        return {
            sum:6,
            school:'atguigu',
            address:'宏福科技园'
        }
    }
})
Count.vue
<template>
    <div class="count">
        
        <h2>当前求和为:{{ countStore.sum }}</h2>
        
        <h3>欢迎来到:{{ countStore.school }},坐落于:{{ countStore.address }}</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="minus"></button>
    </div>
</template>

<script setup lang="ts" name="Count">
    
    import { ref,reactive } from "vue";
    
    // 引入useCountStore
    import {useCountStore} from '@/store/count'
    
    // 使用useCountStore,得到一个专门保存count相关的store
    const countStore = useCountStore()

    // 数据
    let n = ref(1) // 用户选择的数字
    
    // 方法
    function add(){
        
        // 第二种修改方式(如果很多数据都要统一一次性发生变化,推荐使用$patch)
        countStore.$patch({
            sum:888,
            school:'尚硅谷',
            address:'北京'
        })

    }
    
    function minus(){

    }
</script>

<style scoped>
    .count {
        background-color: skyblue;
        padding: 10px;
        border-radius: 10px;
        box-shadow: 0 0 10px;
    }
    select,button {
        margin: 0 5px;
        height: 25px;
    }
</style>

第三种方式

count.ts
import {defineStore} from 'pinia'

export const useCountStore = defineStore('count',{
    
    // actions里面放置的是一个一个的方法,用于响应组件中的“动作”
    // (使用actions的意义在于可以将对组件共享数据统一操作的逻辑抽取放到这里)
    actions:{
        
        increment(value){ // value是调用方传过来的值
            
            console.log('increment被调用了',value)
            
            if( this.sum < 10){
                // 修改数据(this是当前的store)
                this.sum += value
            }
        }
    },
    
    // 真正存储数据的地方
    state(){
        return {
            sum:6,
            school:'atguigu',
            address:'宏福科技园'
        }
    }
})
Count.vue
<template>
    <div class="count">
        
        <h2>当前求和为:{{ countStore.sum }}</h2>
        
        <h3>欢迎来到:{{ countStore.school }},坐落于:{{ countStore.address }}</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="minus"></button>
    </div>
</template>

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

    // 引入useCountStore
    import {useCountStore} from '@/store/count'

    // 使用useCountStore,得到一个专门保存count相关的store
    const countStore = useCountStore()

    // 数据
    let n = ref(1) // 用户选择的数字

    // 方法
    function add(){

        // 第三种修改方式(直接调用count.ts中定义的actions方法)
        const result = countStore.increment(n.value)

        console.log('result', result); // result undefined

    }

    function minus(){

    }
</script>

<style scoped>
    .count {
        background-color: skyblue;
        padding: 10px;
        border-radius: 10px;
        box-shadow: 0 0 10px;
    }
    select,button {
        margin: 0 5px;
        height: 25px;
    }
</style>

5.5 storeToRefs用法

  • 借助storeToRefsstore中的数据转为ref对象,方便在模板中使用。
  • 注意:pinia提供的storeToRefs只会将数据做转换,而VuetoRefs会转换store中数据(虽然能实现功能,单不建议使用哦)。

LoveTalk.ts

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

export const useTalkStore = defineStore('talk',{
    actions:{
        async getATalk(){
            // 发请求,下面这行的写法是:连续解构赋值+重命名
            let {data:{content:title}} = await 
                                     axios.get('https://api.uomg.com/api/rand.qinghua?format=json')
            // 把请求回来的字符串,包装成一个对象
            let obj = {id:nanoid(),title}
            // 放到数组中
            this.talkList.unshift(obj)
        }
    },
    // 真正存储数据的地方
    state(){
        return {
            talkList:[
                {id:'ftrfasdf01',title:'今天你有点怪,哪里怪?怪好看的!'},
                {id:'ftrfasdf02',title:'草莓、蓝莓、蔓越莓,今天想我了没?'},
                {id:'ftrfasdf03',title:'心里给你留了一块地,我的死心塌地'}
            ]
        }
    }
})

LoveTask.vue

<template>
    <div class="talk">
        <button @click="getLoveTalk">获取一句土味情话</button>
        <ul>
            <li v-for="talk in talkList" :key="talk.id">{{talk.title}}</li>
        </ul>
    </div>
</template>

<script setup lang="ts" name="LoveTalk">
    import {useTalkStore} from '@/store/loveTalk'
    import { storeToRefs } from "pinia";

    const talkStore = useTalkStore()
    
    // 这里如果直接这样解构写: const {talkList} = taskStore; 那么此时这里的talkList就已经丢失了响应式
    // 这里虽然也可以写:  const {talkList} = toRefs(taskStore); 虽然可以维持talkList的响应式, 但代价过大,
    //                 (toRefs会把talkStore中的全部数据包括函数,state啥的都给包了一遍)
    // 所以最好使用storeToRefs, 因为storeToRefs只会关注sotre中数据,不会对方法进行ref包裹
    const {talkList} = storeToRefs(talkStore)

    // 方法
    function getLoveTalk(){
        talkStore.getATalk()
    }
</script>

<style scoped>
    .talk {
        background-color: orange;
        padding: 10px;
        border-radius: 10px;
        box-shadow: 0 0 10px;
    }
</style>

count.ts

import {defineStore} from 'pinia'

export const useCountStore = defineStore('count',{
    // actions里面放置的是一个一个的方法,用于响应组件中的“动作”
    actions:{
        increment(value:number){
            console.log('increment被调用了',value)
            if( this.sum < 10){
                // 修改数据(this是当前的store)
                this.sum += value
            }
        }
    },
    // 真正存储数据的地方
    state(){
        return {
            sum:1,
            school:'atguigu',
            address:'宏福科技园'
        }
    }
})

Count.vue

<template>
    <div class="count">
        <h2>当前求和为:{{ sum }}</h2>
        <h3>欢迎来到:{{ school }},坐落于:{{ address }}</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="minus"></button>
    </div>
</template>

<script setup lang="ts" name="Count">
    
    import { ref,reactive,toRefs } from "vue";
    
    import {storeToRefs} from 'pinia'
    
    // 引入useCountStore
    import {useCountStore} from '@/store/count'
    
    // 使用useCountStore,得到一个专门保存count相关的store
    const countStore = useCountStore()
    
    // storeToRefs只会关注sotre中数据,不会对方法进行ref包裹
    const {sum,school,address} = storeToRefs(countStore)
    
    // console.log('!!!!!',storeToRefs(countStore))

    // 数据
    let n = ref(1) // 用户选择的数字
    
    // 方法
    function add(){
        countStore.increment(n.value)
    }
    function minus(){
        countStore.sum -= n.value
    }
</script>

<style scoped>
    .count {
        background-color: skyblue;
        padding: 10px;
        border-radius: 10px;
        box-shadow: 0 0 10px;
    }
    select,button {
        margin: 0 5px;
        height: 25px;
    }
</style>

5.6 getters用法

概念:当state中的数据,需要经过处理后再使用时,可以使用getters配置。

count.ts

import {defineStore} from 'pinia'

export const useCountStore = defineStore('count',{
    
  // actions里面放置的是一个一个的方法,用于响应组件中的“动作”
  actions:{
    increment(value:number){
      console.log('increment被调用了',value)
      if( this.sum < 10){
        // 修改数据(this是当前的store)
        this.sum += value
      }
    }
  },
    
  // 真正存储数据的地方
  state(){
    return {
      sum:3,
      school:'atguigu',
      address:'宏福科技园'
    }
  },
    
  getters:{
    bigSum:state => state.sum * 10,
    upperSchool():string{
      return this.school.toUpperCase()
    }
  }
})

Count.vue

<template>
    <div class="count">
        <h2>当前求和为:{{ sum }},放大10倍后:{{ bigSum }}</h2>
        <h3>欢迎来到:{{ school }},坐落于:{{ address }},大写:{{ upperSchool }}</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="minus"></button>
    </div>
</template>

<script setup lang="ts" name="Count">
    
    import { ref,reactive,toRefs } from "vue";
    
    import {storeToRefs} from 'pinia'
    
    // 引入useCountStore
    import {useCountStore} from '@/store/count'
    
    // 使用useCountStore,得到一个专门保存count相关的store
    const countStore = useCountStore()
    
    // storeToRefs只会关注sotre中数据,不会对方法进行ref包裹, 并且同时维持解构属性结果的响应式
    // (可以直接解构出state和getters中定义的数据)
    const {sum,school,address,bigSum,upperSchool} = storeToRefs(countStore)
    
    // console.log('!!!!!',storeToRefs(countStore))

    // 数据
    let n = ref(1) // 用户选择的数字
    
    // 方法
    function add(){
        countStore.increment(n.value)
    }
    function minus(){
        countStore.sum -= n.value
    }
</script>

<style scoped>
    .count {
        background-color: skyblue;
        padding: 10px;
        border-radius: 10px;
        box-shadow: 0 0 10px;
    }
    select,button {
        margin: 0 5px;
        height: 25px;
    }
</style>

5.7 $subscribe的使用

通过 store 的 $subscribe() 方法侦听 state 及其变化

loveTalk.ts

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

export const useTalkStore = defineStore('talk',{
  actions:{
      
    async getATalk(){
        
      // 发请求,下面这行的写法是:连续解构赋值+重命名
      let {data:{content:title}} = await axios
                                         .get('https://api.uomg.com/api/rand.qinghua?format=json')
      
      // 把请求回来的字符串,包装成一个对象
      let obj = {id:nanoid(),title}
      
      // 放到数组中
      this.talkList.unshift(obj)
    }
  },
    
  // 真正存储数据的地方
  state(){
    return {
      talkList:JSON.parse(localStorage.getItem('talkList') as string) || []
    }
  }
})

LoveTalk.vue

<template>
    <div class="talk">
        <button @click="getLoveTalk">获取一句土味情话</button>
        <ul>
            <li v-for="talk in talkList" :key="talk.id">{{ talk.title }}</li>
        </ul>
    </div>
</template>

<script setup lang="ts" name="LoveTalk">
    import { useTalkStore } from '@/store/loveTalk'
    import { storeToRefs } from "pinia";

    const talkStore = useTalkStore()
    const { talkList } = storeToRefs(talkStore)

    talkStore.$subscribe((mutate, state) => {
        // 注意: 箭头函数中没有this
        console.log('talkStore里面保存的数据发生了变化', mutate, state)

        // 实现页面刷新时, 这里的talkList不丢失, 因为在loveTalk.ts中会取localStorage中读取talkList数据
        localStorage.setItem('talkList', JSON.stringify(state.talkList))
    })

    // 方法
    function getLoveTalk() {
        talkStore.getATalk()
    }
</script>

<style scoped>
    .talk {
        background-color: orange;
        padding: 10px;
        border-radius: 10px;
        box-shadow: 0 0 10px;
    }
</style>

5.8 store组合式写法

loveTalk.js

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

/* export const useTalkStore = defineStore('talk',{
  actions:{
    async getATalk(){
      // 发请求,下面这行的写法是:连续解构赋值+重命名
      let {data:{content:title}} = await 
                                   axios.get('https://api.uomg.com/api/rand.qinghua?format=json')
      // 把请求回来的字符串,包装成一个对象
      let obj = {id:nanoid(),title}
      // 放到数组中
      this.talkList.unshift(obj)
    }
  },
  // 真正存储数据的地方
  state(){
    return {
      talkList:JSON.parse(localStorage.getItem('talkList') as string) || []
    }
  }
})
 */

import {reactive} from 'vue'
export const useTalkStore = defineStore('talk',()=>{
    
    // talkList就是state
    const talkList = reactive(
        JSON.parse(localStorage.getItem('talkList') as string) || []
    )

    // getATalk函数相当于action
    async function getATalk(){
        
        // 发请求,下面这行的写法是:连续解构赋值+重命名
        let {data:{content:title}} = await 
                                     axios.get('https://api.uomg.com/api/rand.qinghua?format=json')
        
        // 把请求回来的字符串,包装成一个对象
        let obj = {id:nanoid(),title}
        
        // 放到数组中
        talkList.unshift(obj)
    }
    
    return {talkList,getATalk}
})

LoveTalk.vue

<template>
    <div class="talk">
        <button @click="getLoveTalk">获取一句土味情话</button>
        <ul>
            <li v-for="talk in talkList" :key="talk.id">{{talk.title}}</li>
        </ul>
    </div>
</template>

<script setup lang="ts" name="LoveTalk">
    import {useTalkStore} from '@/store/loveTalk'
    import { storeToRefs } from "pinia";

    const talkStore = useTalkStore()
    const {talkList} = storeToRefs(talkStore)

    talkStore.$subscribe((mutate,state)=>{
        console.log('talkStore里面保存的数据发生了变化',mutate,state)
        localStorage.setItem('talkList',JSON.stringify(state.talkList))
    })

    // 方法
    function getLoveTalk(){
        talkStore.getATalk()
    }
</script>

<style scoped>
    .talk {
        background-color: orange;
        padding: 10px;
        border-radius: 10px;
        box-shadow: 0 0 10px;
    }
</style>

6. 组件通信

6.1 props

概述:props是使用频率最高的一种通信方式,常用与 :父 ↔ 子

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

这种不适合父子孙中父给孙组件传递数据,或者兄弟组件也可以找到同1个父组件来实现兄弟组件通信

Father.vue

<template>
    <div class="father">
        
        <h3>父组件</h3>
        
        <h4>汽车:{{ car }}</h4>
        
        <h4 v-show="toy">子给的玩具:{{ toy }}</h4>
        
        <Child :car="car" :sendToy="getToy" />
        
    </div>
</template>

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

<style scoped>
    .father {
        background-color: rgb(165, 164, 164);
        padding: 20px;
        border-radius: 10px;
    }
</style>

Child.vue

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

<script setup lang="ts" name="Child">
    
    import { ref } from 'vue'
    
    // 数据
    let toy = ref('奥特曼')
    
    // 声明接收props
    defineProps(['car', 'sendToy'])
    
</script>

<style scoped>
    .child {
        background-color: skyblue;
        padding: 10px;
        box-shadow: 0 0 10px black;
        border-radius: 10px;
    }
</style>

6.2 自定义事件

Father.vue

<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,e:any) {
        console.log('saveToy', value, e)
        toy.value = value
    }
    
</script>

<style scoped>
    .father {
        background-color: rgb(165, 164, 164);
        padding: 20px;
        border-radius: 10px;
    }

    .father button {
        margin-right: 5px;
    }
</style>

Child.vue

<template>
    <div class="child">
        <h3>子组件</h3>
        <h4>玩具:{{ toy }}</h4>
        
        <!-- 在模板中可以使用$event来代表事件对象 -->
        <button @click="emit('send-toy', toy, $event)">测试</button>
    </div>
</template>

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

<style scoped>
    .child {
        margin-top: 10px;
        background-color: rgb(76, 209, 76);
        padding: 10px;
        box-shadow: 0 0 10px black;
        border-radius: 10px;
    }
</style>

6.3 mitt

概述:与消息订阅与发布(pubsub)功能类似,可以实现任意组件间通信。

安装mittnpm install mitt,版本是:“mitt”: “^3.0.1”

emitter.ts

// 引入mitt
import mitt from 'mitt'

// 调用mitt得到emitter,emitter能:绑定事件、触发事件
const emitter = mitt()

/* 
// 绑定事件
emitter.on('test1',()=>{
  console.log('test1被调用了')
})
emitter.on('test2',()=>{
  console.log('test2被调用了')
})

// 触发事件
setInterval(() => {
  emitter.emit('test1')
  emitter.emit('test2')
}, 1000);

setTimeout(() => {
  // emitter.off('test1')
  // emitter.off('test2')
  emitter.all.clear()
}, 3000); 
*/


// 暴露emitter
export default emitter

Father.vue

<template>
    <div class="father">
        <h3>父组件</h3>
        <Child1/>
        <Child2/>
    </div>
</template>

<script setup lang="ts" name="Father">
    import Child1 from './Child1.vue'
    import Child2 from './Child2.vue'
</script>

<style scoped>
    .father{
        background-color:rgb(165, 164, 164);
        padding: 20px;
        border-radius: 10px;
    }
    .father button{
        margin-left: 5px;
    }
</style>

Child1.vue

<template>
    <div class="child1">
        <h3>子组件1</h3>
        <h4>玩具:{{ toy }}</h4>
        <button @click="emitter.emit('send-toy',toy)">玩具给弟弟</button>
    </div>
</template>

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

    // 数据
    let toy = ref('奥特曼')
</script>

<style scoped>
    .child1{
        margin-top: 50px;
        background-color: skyblue;
        padding: 10px;
        box-shadow: 0 0 10px black;
        border-radius: 10px;
    }
    .child1 button{
        margin-right: 10px;
    }
</style>

Child2.vue

<template>
    <div class="child2">
        <h3>子组件2</h3>
        <h4>电脑:{{ computer }}</h4>
        <h4>哥哥给的玩具:{{ toy }}</h4>
    </div>
</template>

<script setup lang="ts" name="Child2">
    
    import { ref, onUnmounted } from 'vue'
    
    import emitter from '@/utils/emitter';
    
    // 数据
    let computer = ref('联想')
    let toy = ref('')

    // 给emitter绑定send-toy事件
    emitter.on('send-toy', (value: any) => {
        toy.value = value
    })
    
    // 在组件卸载时解绑send-toy事件
    onUnmounted(() => {
        emitter.off('send-toy')
    })
</script>

<style scoped>
    .child2 {
        margin-top: 50px;
        background-color: orange;
        padding: 10px;
        box-shadow: 0 0 10px black;
        border-radius: 10px;
    }
</style>

6.4 v-model

Father.vue

<template>
    <div class="father">
        <h3>父组件</h3>
        <h4>{{ username }}</h4>
        <h4>{{ password }}</h4>

        <!-- v-model用在html标签上 -->
        <!-- <input type="text" v-model="username"> -->
        <!-- <input type="text" :value="username" 
                                @input="username = (<HTMLInputElement>$event.target).value"> -->

        <!-- v-model用在组件标签上 -->
        <!-- <AtguiguInput v-model="username"/> -->
        <!-- 上面这行等价于下面这行 -->
        <!-- $event到底是啥? 啥时候能.target
             对于原生事件, $event就是事件对象 ===> 能.target
             对于自定义事件, $event就是触发事件时, 所传递的数据 ===> 不能.target
        -->
        <!-- 
            <AtguiguInput 
                :modelValue="username" 
                @update:modelValue="username = $event"
            /> 
        -->

        <!-- 修改modelValue -->
        <AtguiguInput v-model:ming="username" v-model:mima="password"/>
    </div>
</template>

<script setup lang="ts" name="Father">
    import { ref } from "vue";
    import AtguiguInput from './AtguiguInput.vue'
    // 数据
    let username = ref('zhansgan')
    let password = ref('123456')
</script>

<style scoped>
    .father {
        padding: 20px;
        background-color: rgb(165, 164, 164);
        border-radius: 10px;
    }
</style>

AtguiguInput.vue

<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>

<style scoped>
    input {
        border: 2px solid black;
        background-image: linear-gradient(45deg,red,yellow,green);
        height: 30px;
        font-size: 20px;
        color: white;
    }
</style>

6.5 $attrs

  1. 概述:$attrs用于实现**当前组件的父组件,向当前组件的子组件**通信(祖→孙)。

  2. 具体说明:$attrs是一个对象,包含所有父组件传入的标签属性。

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

    (就是父组件给子组件通过标签的属性方式传递给子组件,子组件使用props的方式只接收了部分属性,其它没有接收的属性可以通过子组件的$attrs来访问)

Father.vue

<template>
    <div class="father">
        
        <h3>父组件</h3>
        
        <h4>a:{{a}}</h4>
        <h4>b:{{b}}</h4>
        <h4>c:{{c}}</h4>
        <h4>d:{{d}}</h4>
        
        <!-- v-bind="{x:100,y:200}就等价:  :x=100 :y=200 -->
        <Child :a="a" :b="b" :c="c" :d="d" :e="e" 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)
    let e = ref(5)

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

<style scoped>
    .father{
        background-color: rgb(165, 164, 164);
        padding: 20px;
        border-radius: 10px;
    }
</style>

Child.vue

<template>
    <div class="child">

        <h3>子组件</h3>

        <h4>{{ e }}</h4>

        <!-- Father组件传给Child组件的属性,但是Father组件没有使用props接收的属性,就存在$attrs中 -->
        <h4>{{ $attrs }}</h4>

        <!-- Father组件传给Child组件的属性,但是Father组件没有使用props接收的属性,全部传递给GrandChild组件-->
        <GrandChild v-bind="$attrs"/>

    </div>
</template>

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

<style scoped>
    .child{
        margin-top: 20px;
        background-color: skyblue;
        padding: 20px;
        border-radius: 10px;
        box-shadow: 0 0 10px black;
    }
</style>

GrandChild.vue

<template>
	<div class="grand-child">
        
		<h3>孙组件</h3>
        
		<h4>a:{{ a }}</h4>
		<h4>b:{{ b }}</h4>
		<h4>c:{{ c }}</h4>
		<h4>d:{{ d }}</h4>
		<h4>x:{{ x }}</h4>
		<h4>y:{{ y }}</h4>

		<!-- Father组件通过Child组件的v-bind="$attr"将函数传给GrandChild组件,
		     这样GrandChild组件就可以通过此函数传递数据给Father组件了 -->
		<button @click="updateA(6)">点我将爷爷那的a更新</button>
        
	</div>
</template>

<script setup lang="ts" name="GrandChild">
    // 接收Father组件传递过来并由Child组件通过v-bind="$attr"中转过来的属性
	defineProps(['a','b','c','d','x','y','updateA'])
</script>

<style scoped>
	.grand-child{
		margin-top: 20px;
		background-color: orange;
		padding: 20px;
		border-radius: 10px;
		box-shadow: 0 0 10px black;
	}
</style>

6.6 r e f s 、 refs、 refsparent、proxy

  1. 概述:

    • $refs用于 :父→子。
    • $parent用于:子→父。
  2. 原理如下:

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

Father.vue

<template>
    <div class="father">

        <h3>父组件</h3>

        <h4>房产:{{ house }}</h4>

        <button @click="changeToy">修改Child1的玩具</button>

        <button @click="changeComputer">修改Child2的电脑</button>

        <!-- 在模板中可以直接使用$refs -->
        <button @click="getAllChild($refs)">让所有孩子的书变多</button>

        <button @click="getAllChild2()">让c1孩子的书变多2</button>
        <button @click="getAllChild3()">让c1孩子的书变多3</button>

        <Child1 ref="c1"/>

        <Child2 ref="c2"/>

    </div>
</template>

<script setup lang="ts" name="Father">


    import Child1 from './Child1.vue'
    
    import Child2 from './Child2.vue'
    
    import { ref,reactive } from "vue";
    
    import { getCurrentInstance } from 'vue';
    
    const proxy = getCurrentInstance()
    
    let c1 = ref()
    let c2 = ref()

    // 注意点:当访问obj.c的时候,底层会自动读取value属性,因为c是在obj这个响应式对象中的
    /* 
	let obj = reactive({
		a:1,
		b:2,
		c:ref(3)
	})
	let x = ref(4)

	console.log(obj.a)
	console.log(obj.b)
	console.log(obj.c)
	console.log(x) 
	*/


    // 数据
    let house = ref(4)

    // 方法
    function changeToy(){
        // 必须要Child1组件通过defineExpose将toy属性暴露出来, 这样Father组件才能访问到并修改此toy属性
        c1.value.toy = '小猪佩奇'
    }
    
    function changeComputer(){
        c2.value.computer = '华为'
    }
    
    function getAllChild(refs:{[key:string]:any}){
        console.log(refs)
        for (let key in refs){
            // 这里不需要refs[key].value.book += 3, 是因为refs本身就是个响应式对象, 它会自动解包
            refs[key].book += 3
        }
    }
    
    function getAllChild2(){
        // 使用getCurrentInstance来访问感觉更加方便
        console.log(proxy);
        console.log(proxy.refs);   // {c1: Proxy(Object), c2: Proxy(Object)}
        console.log(proxy.parent); // {uid: 0, vnode: {…}, type: {…}, parent: null, 
                                   //                                 appContext: {…}, …}
        console.log(proxy.attrs);  // {__vInternal: 1}

        proxy.refs.c1.book += 2
    }
    
    function getAllChild3(){
        // console.log($refs);      // 注意, 在vue3的setup语法糖中不能直接访问到$refs
        // console.log(this.$refs); // 注意, 在vue3的setup语法糖中不能直接访问到$refs
        console.log(this.proxy);    // 这个等价于getCurrentInstance()返回的值
        console.log(this.proxy == proxy);    // true
        console.log(this.c1);       // 这里可以直接访问到ref='c1'标识的组件
        this.c1.book += 2
    }
    
    // 向外部提供数据
    defineExpose({house})
    
</script>

<style scoped>
    .father {
        background-color: rgb(165, 164, 164);
        padding: 20px;
        border-radius: 10px;
    }

    .father button {
        margin-bottom: 10px;
        margin-left: 10px;
    }
</style>

Child1.vue

<template>
    
  <div class="child1">
      
    <h3>子组件1</h3>
      
		<h4>玩具:{{ toy }}</h4>
      
		<h4>书籍:{{ book }} 本</h4>

		<button @click="minusHouse($parent)">干掉父亲的一套房产</button>
      
		<button @click="minusHouse2()">干掉父亲的一套房产2</button>
      
		<button @click="minusHouse3()">干掉父亲的一套房产3</button>
  </div>
    
</template>

<script setup lang="ts" name="Child1">
	import { ref,getCurrentInstance } from "vue";

	const proxy = getCurrentInstance()

	// 数据
	let toy = ref('奥特曼')
	let book = ref(3)

	// 方法
	function minusHouse(parent:any){
		// 需要Father组件通过defineExpose将house属性暴露出来, 这里才可以访问到
		parent.house -= 1
	}

	function minusHouse2(){
		// 需要Father组件通过defineExpose将house属性暴露出来, 这里才可以访问到
		console.log(proxy);
		console.log(proxy.parent);
		console.log(proxy.parent.exposed);
		proxy.parent.exposed.house.value -= 1
	}

	function minusHouse3(){
		// 需要Father组件通过defineExpose将house属性暴露出来, 这里才可以访问到
		console.log(this); // Proxy(Object) {proxy: {…}, minusHouse: ƒ, minusHouse2: ƒ, …
		console.log(this.parent); // undefined
		console.log(this.proxy);  // 这个等价于getCurrentInstance()返回的值
		console.log(this.proxy == proxy); // true
	}

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

</script>

<style scoped>
	.child1{
		margin-top: 20px;
		background-color: skyblue;
		padding: 20px;
		border-radius: 10px;
    box-shadow: 0 0 10px black;
	}
</style>

Child2.vue

<template>
    <div class="child2">
        
        <h3>子组件2</h3>
        
        <h4>电脑:{{ computer }}</h4>
        
        <h4>书籍:{{ book }} 本</h4>
        
    </div>
</template>

<script setup lang="ts" name="Child2">
    
    import { ref } from "vue";
    
    // 数据
    let computer = ref('联想')
    
    let book = ref(6)
    
    // 把数据交给外部
    defineExpose({ computer, book })
    
</script>

<style scoped>
    .child2 {
        margin-top: 20px;
        background-color: orange;
        padding: 20px;
        border-radius: 10px;
        box-shadow: 0 0 10px black;
    }
</style>

6.7 provide、inject

  1. 概述:实现祖孙组件直接通信

  2. 具体使用:

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

Father.vue

<template>
    <div class="father">

        <h3>父组件</h3>

        <h4>银子:{{ money }}万元</h4>

        <h4>车子:一辆{{car.brand}}车,价值{{car.price}}万元</h4>

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

    function updateMoney(value:number){
        money.value -= value
    }

    // 向后代提供数据
    provide('moneyContext',{money,updateMoney})

    // (注意数据的后面不要.value, 否则不具备响应式)
    provide('car',car)

</script>

<style scoped>
    .father {
        background-color: rgb(165, 164, 164);
        padding: 20px;
        border-radius: 10px;
    }
</style>

Child.vue

<template>
    <div class="child">
        <h3>我是子组件</h3>
        <GrandChild/>
    </div>
</template>

<script setup lang="ts" name="Child">
    import GrandChild from './GrandChild.vue'
</script>

<style scoped>
    .child {
        margin-top: 20px;
        background-color: skyblue;
        padding: 20px;
        border-radius: 10px;
        box-shadow: 0 0 10px black;
    }
</style>

GrandChild.vue

<template>
    <div class="grand-child">
        
        <h3>我是孙组件</h3>
        
        <h4>银子:{{ money }}</h4>
        
        <h4>车子:一辆{{car.brand}}车,价值{{car.price}}万元</h4>
        
        <button @click="updateMoney(6)">花爷爷的钱</button>
        
    </div>
</template>

<script setup lang="ts" name="GrandChild">
    
    import { inject } from "vue";

    let {money,updateMoney} = inject('moneyContext',{money:0,updateMoney:(param:number)=>{}})

    // 第二个参数的含义是: 如果没有提供car, 那么就把第二个参数作为默认值(这样可以避免使用car时模板中红色波浪线)
    let car = inject('car',{brand:'未知',price:0})
    
</script>

<style scoped>
    .grand-child{
        background-color: orange;
        padding: 20px;
        border-radius: 10px;
        box-shadow: 0 0 10px black;
    }
</style>

6.8 pinia

直接参考pinia章节即可。

6.9 slot插槽

1. 默认插槽

在这里插入图片描述

Father.vue
<template>
    <div class="father">
        
        <h3>父组件</h3>
        
        <div class="content">
            
            <Category title="热门游戏列表">
                <ul>
                    <li v-for="g in games" :key="g.id">{{ g.name }}</li>
                </ul>
            </Category>
            
            <Category title="今日美食城市">
                <img :src="imgUrl" alt="">
            </Category>
            
            <Category title="今日影视推荐">
                <video :src="videoUrl" controls></video>
            </Category>
            
        </div>
    </div>
</template>

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

    let games = reactive([
        {id:'asgytdfats01',name:'英雄联盟'},
        {id:'asgytdfats02',name:'王者农药'},
        {id:'asgytdfats03',name:'红色警戒'},
        {id:'asgytdfats04',name:'斗罗大陆'}
    ])
    
    let imgUrl = ref('https://z1.ax1x.com/2023/11/19/piNxLo4.jpg')
    
    let videoUrl = ref('http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4')

</script>

<style scoped>
    .father {
        background-color: rgb(165, 164, 164);
        padding: 20px;
        border-radius: 10px;
    }
    .content {
        display: flex;
        justify-content: space-evenly;
    }
    img,video {
        width: 100%;
    }
</style>
Category.vue
<template>
    
    <div class="category">
        
        <h2>{{title}}</h2>
        
        <!-- 1. 如果父组件在使用当前组件时, 父组件标签中没有传入内容, 那么这里就显示“默认内容” 
		     2. 如果这里这里写多个slot, 那么父组件标签中传入的内容就会在每个slot地方都展示一遍
             3. 其实, 这里省略了name属性, 它的默认值为default, 
                     即这里相当于: <slot name="default">默认内容</slot>-->
        <slot>默认内容</slot>
        
        <!-- 这里同样会再展示一遍 -->
        <slot name="default">默认内容</slot>
        
    </div>
    
</template>

<script setup lang="ts" name="Category">
    
    defineProps(['title'])
    
</script>

<style scoped>
    .category {
        background-color: skyblue;
        border-radius: 10px;
        box-shadow: 0 0 10px;
        padding: 10px;
        width: 200px;
        height: 300px;
    }
    h2 {
        background-color: orange;
        text-align: center;
        font-size: 20px;
        font-weight: 800;
    }
</style>

2. 具名插槽

Father.vue
<template>
    <div class="father">
        <h3>父组件</h3>
        <div class="content">

            <Category>
                <!-- v-slot只能用在组件标签上 或者 <template>标签中 -->
                <template v-slot:s2>
                    <ul>
                        <!-- Category标签中的内容可以直接使用Father组件中的数据 -->
                        <li v-for="g in games" :key="g.id">{{ g.name }}</li>
                    </ul>
                </template>
                <template v-slot:s1>
                    <h2>热门游戏列表</h2>
                </template>
            </Category>

            <!-- 还可以直接把v-slot直接写在组件上, 它将会把内部的所有内容都塞到s2的插槽中 -->
            <Category v-slot:s2>
                <ul>
                    <!-- Category标签中的内容可以直接使用Father组件中的数据 -->
                    <li v-for="g in games" :key="g.id">{{ g.name }}</li>
                </ul>
            </Category>

            <Category>
                <template v-slot:s2>
                    <img :src="imgUrl" alt="">
                </template>
                <template v-slot:s1>
                    <h2>今日美食城市</h2>
                </template>
            </Category>

            <!-- 简写写法 -->
            <Category>
                <template #s2>
                     <!-- Category标签中的内容可以直接使用Father组件中的数据 -->
                    <video video :src="videoUrl" controls></video>
                </template>
                <template #s1>
                    <h2>今日影视推荐</h2>
                </template>
            </Category>
        </div>
    </div>
</template>

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

    let games = reactive([
        {id:'asgytdfats01',name:'英雄联盟'},
        {id:'asgytdfats02',name:'王者农药'},
        {id:'asgytdfats03',name:'红色警戒'},
        {id:'asgytdfats04',name:'斗罗大陆'}
    ])
    let imgUrl = ref('https://z1.ax1x.com/2023/11/19/piNxLo4.jpg')
    let videoUrl = ref('http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4')

</script>

<style scoped>
    .father {
        background-color: rgb(165, 164, 164);
        padding: 20px;
        border-radius: 10px;
    }
    .content {
        display: flex;
        justify-content: space-evenly;
    }
    img,video {
        width: 100%;
    }
    h2 {
        background-color: orange;
        text-align: center;
        font-size: 20px;
        font-weight: 800;
    }
</style>
Category.vue
<template>
    <div class="category">
        <slot name="s1">默认内容1</slot>
        <slot name="s2">默认内容2</slot>
    </div>
</template>

<script setup lang="ts" name="Category">

</script>

<style scoped>
    .category {
        background-color: skyblue;
        border-radius: 10px;
        box-shadow: 0 0 10px;
        padding: 10px;
        width: 200px;
        height: 300px;
    }
</style>

3. 作用域插槽

理解:数据在组件的自身,但根据数据生成的结构需要组件的使用者来决定。(新闻数据在News组件中,但使用数据所遍历出来的结构由App组件决定)

Father.vue
<template>
    <div class="father">
        <h3>父组件</h3>
        <div class="content">
            
            <Game>

                <!-- 这里的params可以拿到所有子组件中传给<slot>插槽标签的所有属性和对应的值 -->
                <!-- 形成的效果就是: 结构是由父组件决定的, 而数据的提供者是子组件
                                   (至于子组件的这个数据哪来的就不用管了, 反正就是有); 
                     或者换句话说: 父组件通过插槽的方式“直接”访问到了子组件通过插槽传递的数据;-->
                <!-- 这里默认其实是:  v-slot:default="params"-->
                <template v-slot="params">
                    <ul>
                        <li v-for="y in params.youxi" :key="y.id">
                            {{ y.name }}
                        </li>
                    </ul>
                </template>
            </Game>

            <Game>
                <template v-slot="params">
                    <ol>
                        <li v-for="item in params.youxi" :key="item.id">
                            {{ item.name }}
                        </li>
                    </ol>
                </template>
            </Game>

            <Game>
                <template #default="{youxi}">
                    <h3 v-for="g in youxi" :key="g.id">{{ g.name }}</h3>
                </template>
            </Game>

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

<script setup lang="ts" name="Father">
    import Game from './Game.vue'
</script>

<style scoped>
    .father {
        background-color: rgb(165, 164, 164);
        padding: 20px;
        border-radius: 10px;
    }
    .content {
        display: flex;
        justify-content: space-evenly;
    }
    img,video {
        width: 100%;
    }
</style>
Category.vue
<template>
    <div class="game">
        
        <h2>游戏列表</h2>
        
        <!-- 给插槽提供数据 -->
        <slot :youxi="games" x="哈哈" y="你好"></slot>
    </div>
</template>

<script setup lang="ts" name="Game">
    
    import {reactive} from 'vue'
    
    let games = reactive([
        {id:'asgytdfats01',name:'英雄联盟'},
        {id:'asgytdfats02',name:'王者农药'},
        {id:'asgytdfats03',name:'红色警戒'},
        {id:'asgytdfats04',name:'斗罗大陆'}
    ])
    
</script>

<style scoped>
    .game {
        width: 200px;
        height: 300px;
        background-color: skyblue;
        border-radius: 10px;
        box-shadow: 0 0 10px;
    }
    h2 {
        background-color: orange;
        text-align: center;
        font-size: 20px;
        font-weight: 800;
    }
</style>

7. 其它 API

7.1 shallowRef 与 shallowReactive

shallowRef

  1. 作用:创建一个响应式数据,但只对顶层属性进行响应式处理。

  2. 用法:

    let myVar = shallowRef(initialValue);
    
  3. 特点:只跟踪引用值的变化,不关心值内部的属性变化。

shallowReactive

  1. 作用:创建一个浅层响应式对象,只会使对象的最顶层属性变成响应式的,对象内部的嵌套属性则不会变成响应式的

  2. 用法:

    const myObj = shallowReactive({ ... });
    
  3. 特点:对象的顶层属性是响应式的,但嵌套对象的属性不是。

总结

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

示例

<template>
    <div class="app">

        <h2>求和为:{{ sum }}</h2>
        <h2>名字为:{{ person.name }}</h2>
        <h2>年龄为:{{ person.age }}</h2>
        <h2>汽车为:{{ car }}</h2>

        <button @click="changeSum">sum+1</button>
        <button @click="changeName">修改名字</button>
        <button @click="changeAge">修改年龄</button>
        <button @click="changePerson">修改整个人</button>

        <span>|</span>

        <button @click="changeBrand">修改品牌</button>
        <button @click="changeColor">修改颜色</button>
        <button @click="changeEngine">修改发动机</button>
    </div>
</template>

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

    let sum = shallowRef(0)
    let person = shallowRef({
        name: '张三',
        age: 18
    })

    /* 如果使用ref来定义sum和person, 那么下面的方法被调用时, 数据都会发生改变, 并且都会有响应式;
       但因为使用shallowRef定义, 因此只有第1层修改才会数据发生改变, 具有响应式, 
      (第1层指的是xxx.value, 不能再点下去了, 否则就不是第1层了)*/
    function changeSum() {
        sum.value += 1                       // 数据发生改变, 有响应式
    }
    function changeName() {
        person.value.name = '李四'           // 数据未发生改变
    }
    function changeAge() {
        person.value.age += 1                // 数据未发生改变
    }
    function changePerson() {
        person.value = { name: 'tony', age: 100 } // 数据发生改变, 有响应式
    }

    /* ****************** */

    /* 如果使用reactive来定义car, 那么下面的方法被调用时, 数据都会发生改变, 并且都会有响应式;
       但因为使用shallowReactive定义, 因此只有第1层修改才会数据发生改变, 具有响应式, 
      (第1层指的是brand和options, 不能再点下去了, 否则就不是第1层了)*/
    let car = shallowReactive({
        brand: '奔驰',
        options: {
            color: '红色',
            engine: 'V8'
        }
    })

    function changeBrand() {
        car.brand = '宝马'
    }

    function changeColor() {
        car.options.color = '紫色'
    }

    function changeEngine() {
        car.options.engine = 'V12'
    }

</script>

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

    button {
        margin: 0 5px;
    }
</style>

7.2 readonly 与 shallowReadonly

readonly

  1. 作用:用于创建一个对象的深只读副本。

  2. 用法:

    const original = reactive({ ... });
    const readOnlyCopy = readonly(original);
    
  3. 特点:

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

    • 创建不可变的状态快照。
    • 保护全局状态或配置不被修改。

shallowReadonly

  1. 作用:与 readonly 类似,但只作用于对象的顶层属性。

  2. 用法:

    const original = reactive({ ... });
    const shallowReadOnlyCopy = shallowReadonly(original);
    
  3. 特点:

    • 只将对象的顶层属性设置为只读,对象内部的嵌套属性仍然是可变的。

    • 适用于只需保护对象顶层属性的场景。

示例

<template>
    <div class="app">

        <h2>当前sum1为:{{ sum1 }}</h2>

        <h2>当前sum2为:{{ sum2 }}</h2>


        <button @click="changeSum1">点我sum1+1</button>

        <button @click="changeSum2">点我sum2+1</button>

        <!-- ******************* -->

        <h2>当前car1为:{{ car1 }}</h2>

        <h2>当前car2为:{{ car2 }}</h2>


        <button @click="changeBrand2">修改品牌(car2)</button>

        <button @click="changeColor2">修改颜色(car2)</button>

        <button @click="changePrice2">修改价格(car2)</button>
    </div>
</template>

<script setup lang="ts" name="App">
    import { ref, reactive, readonly, shallowReadonly } from "vue";

    let sum1 = ref(0)

    // 这里要传入1个响应式对象, 注意不要.value
    // 当sum1数据发生变化的时候, sum2也会发生变化, 但不能直接改sum2, 因为sum2只读,
    // (这样就可以达到一种保护数据的目的)
    let sum2 = readonly(sum1)


    function changeSum1() {
        sum1.value += 1
    }

    function changeSum2() {
        sum2.value += 1 // sum2是不能修改的
    }

    /******************/

    let car1 = reactive({
        brand: '奔驰',
        options: {
            color: '红色',
            price: 100
        }
    })

    // 这里要传入1个响应式对象
    // 当car1数据发生变化的时候, car2也会发生变化, 
    // 但不能直接改car2的第一层属性, 因为这里使用的是shallowReadOnly, 意味着car2的第一层属性都只读,
    // 这里也可以使用readOnly, 这就意味着car2的任何属性都不能改了
    // (这样就可以达到一种保护数据的目的)
    let car2 = shallowReadonly(car1)

    function changeBrand2() {
        car2.brand = '宝马'
    }

    function changeColor2() {
        // 由于car2是对car1使用了shallowReadOnly, 因此这里是允许改的
        car2.options.color = '绿色'
    }

    function changePrice2() {
        car2.options.price += 10
    }
</script>

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

    button {
        margin: 0 5px;
    }
</style>

7.3 toRaw 与 markRaw

toRaw

  1. 作用:用于获取一个响应式对象的原始对象toRaw 返回的对象不再是响应式的,不会触发视图更新

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

  3. 何时使用? 在需要将响应式对象传递给非 Vue 的库或外部系统时,使用 toRaw 可以确保它们收到的是普通对象

markRaw

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

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

示例

<template>
    <div class="app">

        <h2>姓名:{{ person.name }}</h2>
        <h2>年龄:{{ person.age }}</h2>
        <button @click="person.age += 1">修改年龄</button>

        {{ rawPerson }}
        <!-- 这里修改rawPerson不会影响到person的数据的变化, 
             并且由于rawPerson不是响应式数据, 因此上面的{{ rawPerson }}也不会变化 -->
        <button @click="rawPerson.age += 1">修改年龄rawPerson</button>

        <hr>

        <h2>{{ car2 }}</h2>
        <button @click="car2.price += 10">点我价格+10</button>

    </div>
</template>

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

    /* toRaw */
    let person = reactive({
        name:'tony',
        age:18
    })

    // 用于获取一个响应式对象的原始对象
    let rawPerson = toRaw(person)

    console.log('响应式对象',person)  // Proxy(Object) {name: 'tony', age: 18}
    console.log('原始对象',rawPerson) // {name: 'tony', age: 18}

    console.log('------------------------');

    /* markRaw */
    // 如果这里没加markRaw, 那么这里的这个car就可以作为响应式对象的源头
    // 加上了markRaw之后, 就意味着car永远不能作为响应式对象的源头, 只能是1个原始的对象, 不能做成1个响应式对象
    let car = markRaw({brand:'奔驰',price:100})
    let car2 = reactive(car) // 这里的car2不是响应式的了

    // 从输出看, 其实就是加了个标记__v_skip: true, 当遇到这个标记时, 就不对这个对象做响应式处理
    console.log(car)  // {brand: '奔驰', price: 100, __v_skip: true}
    console.log(car2) // {brand: '奔驰', price: 100, __v_skip: true}

    // 例如使用mockjs时,为了防止误把mockjs变为响应式对象,可以使用 markRaw 去标记mockjs
    let mockJs = markRaw(mockjs)


</script>

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

7.4 customRef

作用:创建一个自定义的ref,并对其依赖项跟踪和更新触发进行逻辑控制。

示例

App.vue
<template>
    <div class="app">
        <h2>{{ msg }}</h2>
        <input type="text" v-model="msg">
    </div>
</template>

<script setup lang="ts" name="App">
    import {ref} from 'vue'
    import useMsgRef from './useMsgRef'

    // 使用Vue提供的默认ref定义响应式数据,数据一变,页面就更新
    //                               (这是vue给我们提供的功能, 也是承诺)
    // let msg = ref('你好')

    // 使用useMsgRef来定义一个响应式数据且有延迟效果
    let {msg} = useMsgRef('你好',1000)

</script>

<style scoped>
    .app {
        background-color: #ddd;
        border-radius: 10px;
        box-shadow: 0 0 10px;
        padding: 10px;
    }
    button {
        margin:0 5px;
    }
</style>
useMsgRef.ts
import { customRef } from "vue";

export default function (initValue: string, delay: number) {

    // 使用Vue提供的customRef定义响应式数据
    let timer: number

    // track(跟踪)、trigger(触发)
    let msg = customRef((track, trigger) => {

        return {

            // get何时调用?—— msg被读取时
            get() {

                track() // 告诉Vue数据msg很重要,你要对msg进行持续关注,一旦msg变化就去更新

                console.log('get');

                return initValue
            },

            // set何时调用?—— msg被修改时
            set(value) {

                console.log('set');

                clearTimeout(timer)

                timer = setTimeout(() => {

                    initValue = value

                    trigger() // 通知Vue一下数据msg变化了

                }, delay);

            }
        }
    })

    return { msg }
}

8. Vue3新组件

8.1 Teleport传送门

什么是Teleport?—— Teleport 是一种能够将我们的组件html结构移动到指定位置的技术。

示例

这个示例有个奇怪的地方(css还有这种操作的),给outer加上filter之后,fixed定位就变成相对于父元素定位了,而不是body定位,这时,使用teleport可以解决这个问题,因为它把dom都传送走了,当然,teleport不仅可以适用于这种情况,也可用于其它场景。

App.vue
<template>
    <div class="outer">
        
        <h2>我是App组件</h2>
        
        <img src="http://www.atguigu.com/images/index_new/logo.png" alt="">
        
        <br>
        
        <!-- 遮罩 -->
        <Modal/>
        
    </div>
</template>

<script setup lang="ts" name="App">
    import Modal from "./Modal.vue";
</script>

<style>
    .outer{
        background-color: #ddd;
        border-radius: 10px;
        padding: 5px;
        box-shadow: 0 0 10px;
        width: 400px;
        height: 400px;
        filter: saturate(200%);
    }
    img {
        width: 270px;
    }
</style>
Modal.vue
<template>
    
    <button @click="isShow = true">展示弹窗</button>
    
    <!-- 数据用的还是当前组件的, 但渲染的地方被传送到了body那里;
         to这里写的是选择器哦;
	-->
    <teleport to='body'>
        
        <div class="modal" v-show="isShow">
            
            <h2>我是弹窗的标题</h2>
            
            <p>我是弹窗的内容</p>
            
            <button @click="isShow = false">关闭弹窗</button>
            
        </div>
        
    </teleport>
    
</template>

<script setup lang="ts" name="Modal">
    
    import {ref} from 'vue'
    
    let isShow = ref(false)
    
</script>

<style scoped>
    .modal {
        width: 200px;
        height: 150px;
        background-color: skyblue;
        border-radius: 10px;
        padding: 5px;
        box-shadow: 0 0 5px;
        text-align: center;
        position: fixed;
        left: 50%;
        top: 20px;
        margin-left: -100px;
    }
</style>

8.2 Suspense

  • 等待异步组件时渲染一些额外内容,让应用有更好的用户体验
  • 使用步骤:
    • 异步引入组件
    • 使用Suspense包裹组件,并配置好defaultfallback

示例

App.vue
<template>
    <div class="app">
        
        <h2>我是App组件</h2>
        
        <Child/>
        
        <Suspense>
            
            <template v-slot:default>
                <Child/>
            </template>
            
            <!-- 当组件未加载完成时, 显示的临时内容 -->
            <template v-slot:fallback>
                <h2>加载中......</h2>
            </template>
            
        </Suspense>
    </div>
</template>

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

<style>
    .app {
        background-color: #ddd;
        border-radius: 10px;
        padding: 10px;
        box-shadow: 0 0 10px;
    }
</style>
Child.vue
<template>
    <div class="child">
        <h2>我是Child组件</h2>
        <h3>当前求和为:{{ sum }}</h3>
    </div>
</template>

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

    let sum = ref(0);


    // 当下面多了这行请求数据的异步代码时, Child组件将不会展示出来(setup顶层最外面有async),
    // 需要父组件在使用时, 借助Suspense组件才能展示Child组件
    let {data:{content}} = await axios.get('https://api.uomg.com/api/rand.qinghua?format=json')
    console.log('content',content)

    /*   
    // 使用这种方式, 可以不借助Suspense组件也能展示Child组件
    let content = (async function() {
        let {data:{content}} = await axios.get('https://api.uomg.com/api/rand.qinghua?format=json')
        return content
    })(); 
    */

</script>

<style scoped>
    .child {
        background-color: skyblue;
        border-radius: 10px;
        padding: 10px;
        box-shadow: 0 0 10px;
    }
</style>

8.3 全局API转移到应用对象

  • app.component
  • app.config
  • app.directive
  • app.mount
  • app.unmount
  • app.use

示例

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

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

// 全局注册组件, 然后所有的地方都可以使用Hello这个组件了
app.component('Hello',Hello)

// 全局挂载
// 类似于vue2的Vue.prototype.x=99, 然后所有的组件中都可以使用x了
app.config.globalProperties.x = 99

// 解决全局挂载x的时候, ts报错的问题
declare module 'vue' {
    interface ComponentCustomProperties {
        x:number
    }
}

// 全局注册指令, 然后所有的组件中都可以使用v-beauty了, 如: <h1 v-beauty="sum">好开心</h1>
app.directive('beauty',(element,{value})=>{
    element.innerText += value
    element.style.color = 'green'
    element.style.backgroundColor = 'yellow'
})

// 挂载应用
app.mount('#app')

// 卸载应用
setTimeout(() => {
    app.unmount()
}, 2000);

8.4 其他

  • 过渡类名 v-enter 修改为 v-enter-from、过渡类名 v-leave 修改为 v-leave-from

  • keyCode 作为 v-on 修饰符的支持。

  • v-model 指令在组件上的使用已经被重新设计,替换掉了 v-bind.sync。

  • v-ifv-for 在同一个元素身上使用时的优先级发生了变化。

  • 移除了$on$off$once 实例方法。

  • 移除了过滤器 filter

  • 移除了$children 实例 propert

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/591434.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

11个2024年热门的AI编码助手

大家好&#xff0c;人工智能&#xff08;AI&#xff09;领域的大型语言模型&#xff08;LLMs&#xff09;已经逐渐发展成熟&#xff0c;并且深入到了我们日常的工作当中。在众多AI应用中&#xff0c;编码助手尤为突出&#xff0c;是开发人员编写更高效、准确无误代码的必备辅助…

docker原理

Docker原理 在前面我们学习了Docker&#xff0c;接下来我们探究一下Docker的底层技术原理 Linux 命名空间&#xff08;namespace&#xff09;、控制组&#xff08;cgroups&#xff09;和 联合文件系统&#xff08;UnionFS&#xff09; 三大技术支撑了目前 Docker 的实现&…

STM32入门学习之DMA

1.直接存储访问DMA(Direct Memory Access)&#xff1a;DMA传输不需要CPU的参与&#xff0c;直接在内存和I/O设备间开辟了一条新的数据传输通道&#xff0c;不仅提高数据传输的速率&#xff0c;还因为不需要CPU的干预&#xff0c;从而提高了CPU的利用率。(注&#xff1a;文中的资…

OpenCV如何在图像中寻找轮廓(60)

返回:OpenCV系列文章目录&#xff08;持续更新中......&#xff09; 上一篇&#xff1a;OpenCV如何模板匹配(59) 下一篇 :OpenCV检测凸包(61) 目标 在本教程中&#xff0c;您将学习如何&#xff1a; 使用 OpenCV 函数 cv::findContours使用 OpenCV 函数 cv::d rawContours …

基于SSM的校园短期闲置资源置换平台(有报告)。Javaee项目。ssm项目。

演示视频&#xff1a; 基于SSM的校园短期闲置资源置换平台&#xff08;有报告&#xff09;。Javaee项目。ssm项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&#xff0c;通过…

英语语法动词和动词的虚拟语气

动词 谓语动词的三大功能&#xff1a;时、态、气 1、时 和 态 的组合构成了英语的 16 种时态。 2、气 表示的是动作的情感和假设。

uboot-网络配置

文章目录 一、网络简介二、修改PHY芯片地址三、删除 uboot 中 74LV595 的驱动代码1.删除宏定义&#xff0c;添加ENET1和ENET2复位引脚&#xff0c;宏定义2.删除内容如下 四、添加 I.MX6U-ALPHA 开发板网络复位引脚驱动 一、网络简介 &#x1f4a6;I.MX6UL/ULL 内部有个以太网 …

perl:用 MIDI::Simple 生成midi文件,用 pygame 播放 mid文件

在 csdn.net 下载 strawberry-perl-5.32.1.1-64bit.zip 解压安装在 D:\Strawberry\ 运行 cpan install MIDI::Simple D:\Strawberry\c\bin\gmake.exe test -- OK Running make install for CONKLIN/MIDI-Perl-0.84.tar.gz Installing D:\Strawberry\perl\site\lib\MIDI.pm I…

算法打卡day40

今日任务&#xff1a; 1&#xff09;139.单词拆分 2&#xff09;多重背包理论基础&#xff08;卡码网56携带矿石资源&#xff09; 3&#xff09;背包问题总结 4&#xff09;复习day15 139单词拆分 题目链接&#xff1a;139. 单词拆分 - 力扣&#xff08;LeetCode&#xff09; …

【数据库原理及应用】期末复习汇总高校期末真题试卷

试卷 一、填空题 1.________是位于用户与操作系统之间的一层数据管理软件。 2.数据库系统的三级模式结构是指________、________、________。 3.数据库系统的三种数据模型是________ 、________、________。 4.若关系中的某一属性组的值能唯一地标识一个元组&#xff0c;则…

项目管理-项目进度管理3/3

项目管理&#xff1a;每天进步一点点~ 活到老&#xff0c;学到老 ヾ(◍∇◍)&#xff89;&#xff9e; 何时学习都不晚&#xff0c;加油 项目进度管理&#xff1a;需掌握 ITTO, 搞懂计算图&#xff0c;问题和解决方案。 项目进度管理6个过程&#xff0c;包括&#xff08;口…

Qt5.15.2安装Android开发环境。

下载Java 8&#xff0c;不要下Java 20 jdk8 安装跟着默认走就行&#xff1a;C:\Program Files\Java 需要将QtCreator的sdk_definitions.json文件修改一下 “cmdline-tools;latest” 修改为 “cmdline-tools;6.0” 在一个非中文路径&#xff0c;建立一个android-sdk-windows空…

MATLAB 微积分

MATLAB 微积分 MATLAB提供了多种方法来解决微分和积分问题&#xff0c;求解任意程度的微分方程式以及计算极限。最重要的是&#xff0c;您可以轻松求解复杂函数的图&#xff0c;并通过求解原始函数及其导数来检查图上的最大值&#xff0c;最小值和其他文具点。 本章将讨论微…

AD中如何器件带动导线一起旋转

选中器件和导线&#xff0c;右键点击联合&#xff0c;从选中的器件生成联合 点击屏幕右上角的小齿轮&#xff08;设置按钮&#xff09;&#xff0c;选择下图所示的旋转步进为45度&#xff08;或其他&#xff09;&#xff0c;器件拖拽设置为Connected Tracks 之后就可以按住空格…

从零开始搭建一个vue项目

从零开始搭建一个vue项目 一、环境准备 1.1 安装node.js 选择合适的LTS版本&#xff0c;然后下载安装&#xff0c;安装地址&#xff1a;https://nodejs.org/en/download 在命令行中查看已安装的node.js版本 node -v v14.14.01.2 切换为淘宝的镜像源 解决国内下载慢的问题,…

【数据结构(邓俊辉)学习笔记】向量06——位图

文章目录 0.概述1.结构2.实现3. 应用3.1 去重3.2 筛法 0.概述 位图&#xff08;Bitmap&#xff09;是一种特殊的序列结构&#xff0c;可用以动态地表示由一组&#xff08;无符号&#xff09;整数构成的集合。 test() 判断k 是否存在集合S中。set() 将k 加入到集合S中。clear…

免费APP分发平台 - 一个指南和解析

数字化时代的APP分发平台 随着数字化进程的加速免费APP分发平台 - 一个指南和解析&#xff0c;移动应用&#xff08;APP&#xff09;市场正迅速扩大。在这个充满竞争的市场中免费APP分发平台 - 一个指南和解析&#xff0c;一个优秀的APP分发平台能够帮助开发者和商家更有效地触…

【matlab基础知识】(三)二维曲线绘制plot

x[-pi:0.0001:pi]; 选择较小步距 ysin(tan(x))-tan(sin(x));plot(x,y) 条件和函数值做一个点乘 x[-2:0.02:2];y1.1*sign(x).*(abs(x)>1.1)x.*(abs(x)<1.1);plot(x,y) 颜色&#xff0c;线形&#xff0c;曲线上的标志 由于0.01cosx波动太小&#xff0c;所以plotyy绘制多…

蓝桥杯练习系统(算法训练)ALGO-949 勇士和地雷阵

资源限制 内存限制&#xff1a;256.0MB C/C时间限制&#xff1a;1.0s Java时间限制&#xff1a;3.0s Python时间限制&#xff1a;5.0s 问题描述 勇士们不小心进入了敌人的地雷阵&#xff08;用n行n列的矩阵表示&#xff0c;*表示某个位置埋有地雷&#xff0c;-表示某个…

可视化大屏C位图:智慧场馆/场所图

Hello&#xff0c;我是大千UI工场&#xff0c;本期可视化大屏的焦点图&#xff08;C位&#xff09;分享将场馆作为焦点图的情形&#xff0c;欢迎友友们关注、评论&#xff0c;如果有订单可私信。 智慧场馆是指通过物联网、大数据、人工智能等技术手段&#xff0c;将传统场馆与…