跳至主要內容

apzs...大约 70 分钟

1. Vue3简介

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 创建】(不推荐)

点击查看官方文档open in new window

备注:目前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 是新一代前端构建工具,官网地址:https://vitejs.cnopen in new windowvite的优势如下:

  • 轻量快速的热重载(HMR),能实现极速的服务启动。
  • TypeScriptJSXCSS 等支持开箱即用。
  • 真正的按需编译,不再等待整个应用编译完成。
  • webpack构建 与 vite构建对比图如下: webpack构建 vite构建
### 1.创建命令
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

自己动手编写一个App组件

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

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

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

安装官方推荐的vscode插件:

开发vue3项目时,需要禁用Vetur插件,Vetur插件适用于vue2项目,在vue3项目中会出现各种奇怪的问题

Snipaste_2023-10-08_20-46-34image-20231218085906380

总结:

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

2.3. 【一个简单的效果】

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

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

3. Composition API

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',
    setup(){
      // 数据,原来写在data中(注意:此时的name、age、tel数据都不是响应式数据)
      let name = '张三'
      let age = 18
      let tel = '13888888888'

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

      // 返回一个对象,对象中的内容,模板中可以直接使用
      return {name,age,tel,changeName,changeAge,showTel}
    }
  }
</script>

setup 的返回值

  • 若返回一个对象:则对象中的:属性、方法等,在模板中均可以直接使用**(重点关注)。**
  • 若返回一个函数:则可以自定义渲染内容,代码如下:
setup(){
  return ()=> '你好啊!'
}

setup 与 Options API 的关系

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

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 lang="ts">
  export default {
    name:'Person',
  }
</script>

<!-- 下面的写法是setup语法糖 -->
<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 { defineConfig } from 'vite'
import VueSetupExtend from 'vite-plugin-vue-setup-extend'

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

4. ref和reactive

4.1. 【ref 创建:基本类型的响应式数据】

  • **作用:**定义响应式变量。
  • 语法:let xxx = ref(初始值)
  • **返回值:**一个RefImpl的实例对象,简称ref对象refref对象的value属性是响应式的
  • 注意点:
    • JS中操作数据需要:xxx.value,但模板中不需要.value,直接使用即可。
    • 对于let name = ref('张三')来说,name不是响应式的,name.value是响应式的。
<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 setup lang="ts" name="Person">
  import {ref} from 'vue'
  // name和age是一个RefImpl的实例对象,简称ref对象,它们的value属性是响应式的。
  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(){
    alert(tel)
  }
</script>

4.2. 【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'

// 数据
let car = reactive({ brand: '奔驰', price: 100 })
let games = reactive([
  { id: 'ahsgdyfa01', name: '英雄联盟' },
  { id: 'ahsgdyfa02', name: '王者荣耀' },
  { id: 'ahsgdyfa03', name: '原神' }
])
let obj = reactive({
  a:{
    b:{
      c:{
        d:666
      }
    }
  }
})

function changeCarPrice() {
  car.price += 10
}
function changeFirstGame() {
  games[0].name = '流星蝴蝶剑'
}
function test(){
  obj.a.b.c.d = 999
}
</script>

4.3. 【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 } from 'vue'

// 数据
let car = ref({ brand: '奔驰', price: 100 })
let games = ref([
  { id: 'ahsgdyfa01', name: '英雄联盟' },
  { id: 'ahsgdyfa02', name: '王者荣耀' },
  { id: 'ahsgdyfa03', name: '原神' }
])
let obj = ref({
  a:{
    b:{
      c:{
        d:666
      }
    }
  }
})

console.log(car)

function changeCarPrice() {
  car.value.price += 10
}
function changeFirstGame() {
  games.value[0].name = '流星蝴蝶剑'
}
function test(){
  obj.value.a.b.c.d = 999
}
</script>

4.4. 【ref 对比 reactive】

宏观角度看:

  1. ref用来定义:基本类型数据对象类型数据

  2. reactive用来定义:对象类型数据

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

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

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

4.5. 【toRefs 与 toRef】

  • 作用:将一个响应式对象中的每一个属性,转换为ref对象。
  • 备注:toRefstoRef功能一致,但toRefs可以批量转换。
  • 语法如下:
<template>
  <div class="person">
    <h2>姓名:{{person.name}}</h2>
    <h2>年龄:{{person.age}}</h2>
    <h2>性别:{{person.gender}}</h2>
    <button @click="changeName">修改名字</button>
    <button @click="changeAge">修改年龄</button>
    <button @click="changeGender">修改性别</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对象中的n个属性批量取出,且依然保持响应式的能力
  let {name,gender} =  toRefs(person)
	
  // 通过toRef将person对象中的gender属性取出,且依然保持响应式的能力
  let age = toRef(person,'age')

  // 方法
  function changeName(){
    name.value += '~'
  }
  function changeAge(){
    age.value += 1
  }
  function changeGender(){
    gender.value = '女'
  }
</script>

4.6.【customRef】自定义ref实现防抖

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

实现防抖效果(useSumRef.ts):

import { customRef } from "vue";

export default function(initValue:string,delay:number){
    const msg = customRef((track,trigger)=>{
        let timer:number
        return {
            get(){
                track() // 告诉Vue数据msg很重要,要对msg持续关注,一旦变化就更新
                return initValue
            },
            set(value){
                clearTimeout(timer)
                timer = setTimeout(() => {
                    initValue = value
                    trigger() //通知Vue数据msg变化了
                }, delay);
            }
        }
    })
    return msg
}

组件中使用:

<template>
  <input v-model="name">
  {{name}}
</template>

<script lang="ts" setup name="App">
import useSumRef from "@/utils/useSumRef"
const name = useSumRef('张三',1000)
</script>
GIF 2024-2-14 21-08-31
GIF 2024-2-14 21-08-31

5. Vue3核心语法

5.1. 【computed】

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

只有计算属性值的地址发生改变了才会调用set方法,修改对象的某个属性不会调用set方法(类似于watch的浅度监听)

<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">
  import {ref,computed} from 'vue'

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

  // 计算属性——只读取,不修改
  /* let fullName = computed(()=>{
    return firstName.value + '-' + lastName.value
  }) */


  // 计算属性——既读取又修改
  let fullName = computed({
    // 读取
    get(){
      return firstName.value + '-' + lastName.value
    },
    // 修改
    set(val){
      console.log('有人修改了fullName',val)
      firstName.value = val.split('-')[0]
      lastName.value = val.split('-')[1]
    }
  })

  function changeFullName(){
    fullName.value = 'li-si'
  } 
</script>

如下代码计算属性只有地址发生了改变才会调用set方法,只修改某个属性时不会调用set方法,因此建议计算属性值尽量为基本类型

GIF 2024-2-14 16-29-34
<template>
  <div class="person">
    名字:<input type="text" v-model="person.name"> <br>
    年龄:<input type="number" v-model="person.age"> <br>
    信息:<span>{{personInfo}}</span> <br>
    <button @click="changeName">名字改为:jerry</button>
    <button @click="changeInfo">修改人员信息</button>
  </div>
</template>

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

const person = reactive({name:'tom',age:18})

// 计算属性——既读取又修改
let personInfo = computed({
  // 读取
  get(){
    return {name:person.name+'a',age:person.age+1}
  },
  // 修改
  set(val){
    console.log('有人修改了人员信息',val)
  }
})

function changeName(){
  // 计算属性值的地址没有改变,因此不会调用set方法
  personInfo.value.name = 'jerry'
}

function changeInfo() {
  // 计算属性值的地址发生改变了因此会调用set方法
  personInfo.value = {name:'jerry',age:16}
}

</script>

5.2.【watch】

  • 作用:监视数据的变化(和Vue2中的watch作用一致)
  • 特点:Vue3中的watch只能监视以下四种数据
  1. ref定义的数据。
  2. reactive定义的数据。
  3. 函数返回一个值(getter函数)。
  4. 一个包含上述内容的数组。

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

* 情况一

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

监视ref定义的数据(不管是基本类型还是对象类型),使用watch(sum,(newValue,oldValue)=>{}watch(()=>sum.value,(newValue,oldValue)=>{}效果是一样的

<template>
  <div class="person">
    <h1>情况一:监视【ref】定义的【基本类型】数据</h1>
    <h2>当前求和为:{{sum}}</h2>
    <button @click="changeSum">点我sum+1</button>
  </div>
</template>

<script lang="ts" setup name="Person">
  import {ref,watch} from 'vue'
  // 数据
  let sum = ref(0)
  // 方法
  function changeSum(){
    sum.value += 1
  }
  // 监视,情况一:监视【ref】定义的【基本类型】数据
  const stopWatch = watch(sum,(newValue,oldValue)=>{
    console.log('sum变化了',newValue,oldValue)
    if(newValue >= 10){
      stopWatch()
    }
  })
</script>

* 情况二

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

注意:

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

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

  • 监视ref定义的数据(不管是基本类型还是对象类型),使用watch(sum,(newValue,oldValue)=>{}watch(()=>sum.value,(newValue,oldValue)=>{}效果是一样的

<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 += '~'
  }
  function changeAge(){
    person.value.age += 1
  }
  function changePerson(){
    person.value = {name:'李四',age:90}
  }
  /* 
    监视,情况一:监视【ref】定义的【对象类型】数据,监视的是对象的地址值,若想监视对象内部属性的变化,需要手动开启深度监视
    watch的第一个参数是:被监视的数据
    watch的第二个参数是:监视的回调
    watch的第三个参数是:配置对象(deep、immediate等等.....) 
  */
  watch(person,(newValue,oldValue)=>{
    console.log('person变化了',newValue,oldValue)
  },{deep:true})
  
</script>

* 情况三

监视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 += '~'
  }
  function changeAge(){
    person.age += 1
  }
  function changePerson(){
    Object.assign(person,{name:'李四',age:80})
  }
  function test(){
    obj.a.b.c = 888
  }

  // 监视,情况三:监视【reactive】定义的【对象类型】数据,且默认是开启深度监视的
  watch(person,(newValue,oldValue)=>{
    console.log('person变化了',newValue,oldValue)
  })
  watch(obj,(newValue,oldValue)=>{
    console.log('Obj变化了',newValue,oldValue)
  })
</script>

* 情况四

监视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.car = {c1:'雅迪',c2:'爱玛'}
  }

  // 监视,情况四:监视响应式对象中的某个属性,且该属性是基本类型的,要写成函数式
  /* watch(()=> person.name,(newValue,oldValue)=>{
    console.log('person.name变化了',newValue,oldValue)
  }) */

  // 监视,情况四:监视响应式对象中的某个属性,且该属性是对象类型的,可以直接写,也能写函数,更推荐写函数
  watch(()=>person.car,(newValue,oldValue)=>{
    console.log('person.car变化了',newValue,oldValue)
  },{deep:true})
</script>

* 情况五

监视上述的多个数据

<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:'爱玛'}
  }

  // 监视,情况五:监视上述的多个数据
  watch([()=>person.name,person.car],(newValue,oldValue)=>{
    console.log('person.car变化了',newValue,oldValue)
  },{deep:true})

</script>

5.3. 【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实现,不用指明监视哪些数据
      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('清理了')
          // 调用watchEffect返回的函数以停止监视
          stopWtach()
        }
      })
    </script>
    

5.4. 【标签的 ref 属性】

* 方式一:定义同名的ref

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

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

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

用在普通DOM标签上:

<template>
  <div class="person">
    <h1 ref="title">Vue</h1>
    <button @click="showLog">点我打印内容</button>
  </div>
</template>

<script lang="ts" setup name="Person">
import {ref} from 'vue'
// ref="title"中的title与该变量同名,vue会将组件的ref自动设置到该变量上
let title = ref()

function showLog(){
  // 通过ref获取元素
  console.log(title.value)
}
</script>

用在组件标签上:

<!-- 父组件App.vue -->
<template>
  <Person ref="ren"/>
  <button @click="test">测试</button>
</template>

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

  let ren = ref()

  function test(){
    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>
* 方式二:通过函数动态设置ref

通过:ref传入一个函数,可以动态的设置ref

<template>
  <div class="person">
    <template v-for="(item,index) in data" :key="item.label">
      <div :index="index" :value="item.value" :ref="(divRef)=>setRef(divRef,index)">{{ item.label }}</div>
    </template>

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

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

const data = [
  {label: 'tom', value: 18},
  {label: 'jerry', value: 19},
  {label: 'spike', value: 20}
]
let refs = reactive([])

function setRef(divRef:any,index :Number){
  refs[index] = divRef
}

function showLog() {
  // 通过ref获取元素
  console.log(refs)
}
</script>
image-20240214191554560
image-20240214191554560

5.5. 【defineExpose 定义暴露】

组件可以通过defineExpose将该组件的数据方法暴露出去供其他组件使用

<template>
  <div v-if="visible">
    子组件打开,父组件传递的数据为: {{ personInfo }}
  </div>
</template>

<script lang="ts" setup name="Person">
import {ref, defineExpose, reactive} from 'vue'
const visible = ref(false)
const personInfo = reactive({})

const clerarObjectData = (obj) => {
  Object.keys(obj).forEach((key)=>{
    // personInfo[key] = void 0
    delete obj[key]
  })
}

// 数据
const name = ref('张三')
const age = ref(18)
const personRef = ref({name:'张三',age:18})
const personReactive = reactive({name:'张三',age:18})

// 方法
const onOpen = (record) => {
  visible.value = true
  // 清空personInfo对象数据
  clerarObjectData(personInfo)
  // 将record数据赋值给personInfo
  Object.assign(personInfo,record)
}

const onClose = () => {
  visible.value = false
}

/****************************/
/****************************/
// 使用defineExpose将组件中的数据交给外部
defineExpose({name,age,personRef,personReactive,onOpen,onClose})
</script>

父组件获取Person组件暴露出来的数据和方法

可以看到暴露的不管是ref还是reactive都是可以直接使用和修改的,ref对象显示的虽然是RefImpl,但是获取数据并不需要.value

<template>
  <Person ref="ren"/>
  <hr/>
  <button @click="test">测试</button>
</template>

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

let ren = ref()

function test() {
  console.log(ren.value)
  console.log(ren.value.name)
  console.log(ren.value.age)
  console.log(ren.value.personRef)
  console.log(ren.value.personReactive)
  console.log(ren.value.onOpen)
  console.log(ren.value.onClose)
  ren.value.name = '王五'
  console.log(ren.value.name)
  ren.value.onOpen({name: '李四', age: 16})
}
</script>
image-20240214193904091
image-20240214193904091

5.6. 【props】

// 定义一个接口,限制每个Person对象的格式
export interface PersonInter {
id:string,
name:string,
 age:number
}

// 定义一个自定义类型Persons
export type Persons = Array<PersonInter>

App.vue中代码:

<template>
	<Person :list="persons"/>
</template>

<script lang="ts" setup name="App">
import Person from './components/Person.vue'
import {reactive} from 'vue'
 import {type Persons} from './types'

 let persons = reactive<Persons>([
  {id:'e98219e12',name:'张三',age:18},
   {id:'e98219e13',name:'李四',age:19},
    {id:'e98219e14',name:'王五',age:20}
  ])
</script>

Person.vue中代码:

在template里可以使用props.list和list,如果也script定义了list会优先使用script里的list,此时可以使用props.list获取props里的list;在script里只能使用props.list。如下代码展示了props里有list字段同时script里也有list变量的情况

<template>
  <div class="person">
    {{list}}
    <br />
    {{props.list}}
  </div>
</template>

<script lang="ts" setup name="Person">
import {defineProps} from 'vue'
import {type Persons} from '@/types'
const list = [{id:'1',name:'2',age:19}]
const props = defineProps(['list'])
</script>
image-20240219213329582
image-20240219213329582
<template>
<div class="person">
<ul>
  <li v-for="item in list" :key="item.id">
     {{item.name}}--{{item.age}}
   </li>
 </ul>
</div>
</template>

<script lang="ts" setup name="Person">
import {defineProps} from 'vue'
import {type PersonInter} from '@/types'

// 第一种写法:仅接收
// const props = defineProps(['list'])

// 第二种写法:接收+限制类型
// defineProps<{list:Persons}>()

// 第三种写法:接收+限制类型+指定默认值+限制必要性
let props = withDefaults(defineProps<{list?:Persons}>(),{
  list:()=>[{id:'asdasg01',name:'小猪佩奇',age:18}]
})
console.log(props)
</script>

需要注意的是,props传递的不管是ref还是reactive,接收的统统都是reactive对象

App.vue中代码:

<template>
  <Person :list="persons" :personRef="refData" :personReactive="reactiveData"/>
</template>

<script lang="ts" setup name="App">
import Person from '@/components/Person.vue'
import {ref,reactive} from 'vue'
import {type Persons} from '@/types'

let refData = ref({id:'e98219e12',name:'张三',age:18})
let reactiveData = reactive({id:'e98219e13',name:'李四',age:19})

let persons = reactive<Persons>([
  {id:'e98219e12',name:'张三',age:18},
  {id:'e98219e13',name:'李四',age:19},
  {id:'e98219e14',name:'王五',age:20}
])
</script>

Person.vue中代码:

<script lang="ts" setup name="Person">
import {defineProps} from 'vue'
const props = defineProps(['list','personRef','personReactive'])
console.log(props)
</script>
image-20240214200341270
image-20240214200341270

5.7. 【生命周期】

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

  • 规律:

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

  • Vue2的生命周期

    创建阶段:beforeCreatecreated

    挂载阶段:beforeMountmounted

    更新阶段:beforeUpdateupdated

    销毁阶段:beforeDestroydestroyed

  • Vue3的生命周期

    创建阶段:setup

    挂载阶段:onBeforeMountonMounted

    更新阶段:onBeforeUpdateonUpdated

    卸载阶段:onBeforeUnmountonUnmounted

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

  • 示例代码:

    <template>
      <div class="person">
        <h2>当前求和为:{{ sum }}</h2>
        <button @click="changeSum">点我sum+1</button>
      </div>
    </template>
    
    <!-- vue3写法 -->
    <script lang="ts" setup name="Person">
      import { 
        ref, 
        onBeforeMount, 
        onMounted, 
        onBeforeUpdate, 
        onUpdated, 
        onBeforeUnmount, 
        onUnmounted 
      } from 'vue'
    
      // 数据
      let sum = ref(0)
      // 方法
      function changeSum() {
        sum.value += 1
      }
      console.log('setup')
      // 生命周期钩子
      onBeforeMount(()=>{
        console.log('挂载之前')
      })
      onMounted(()=>{
        console.log('挂载完毕')
      })
      onBeforeUpdate(()=>{
        console.log('更新之前')
      })
      onUpdated(()=>{
        console.log('更新完毕')
      })
      onBeforeUnmount(()=>{
        console.log('卸载之前')
      })
      onUnmounted(()=>{
        console.log('卸载完毕')
      })
    </script>
    

5.8. 【自定义hook】

  • 什么是hook?—— 本质是一个函数,把setup函数中使用的Composition API进行了封装,类似于vue2.x中的mixin

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

示例代码:

  • useSum.ts中内容如下:

    import {ref,onMounted} from 'vue'
    
    export default function(){
      let sum = ref(0)
    
      const increment = ()=>{
        sum.value += 1
      }
      const decrement = ()=>{
        sum.value -= 1
      }
      onMounted(()=>{
        increment()
      })
    
      //向外部暴露数据
      return {sum,increment,decrement}
    }		
    
  • useDog.ts中内容如下:

    import {reactive,onMounted} from 'vue'
    import axios,{AxiosError} from 'axios'
    
    export default function(){
      let dogList = reactive<string[]>([])
    
      // 方法
      async function getDog(){
        try {
          // 发请求
          let {data} = await axios.get('https://dog.ceo/api/breed/pembroke/images/random')
          // 维护数据
          dogList.push(data.message)
        } catch (error) {
          // 处理错误
          const err = <AxiosError>error
          console.log(err.message)
        }
      }
    
      // 挂载钩子
      onMounted(()=>{
        getDog()
      })
    	
      //向外部暴露数据
      return {dogList,getDog}
    }
    
  • 组件中具体使用:

    <template>
      <h2>当前求和为:{{sum}}</h2>
      <button @click="increment">点我+1</button>
      <button @click="decrement">点我-1</button>
      <hr>
      <img v-for="(u,index) in dogList.urlList" :key="index" :src="(u as string)"> 
      <span v-show="dogList.isLoading">加载中......</span><br>
      <button @click="getDog">再来一只狗</button>
    </template>
    
    <script lang="ts">
      import {defineComponent} from 'vue'
    
      export default defineComponent({
        name:'App',
      })
    </script>
    
    <script setup lang="ts">
      import useSum from './hooks/useSum'
      import useDog from './hooks/useDog'
    	
      let {sum,increment,decrement} = useSum()
      let {dogList,getDog} = useDog()
    </script>
    

5.9. 【nextTick更新DOM后再执行】

当你在 Vue 中更改响应式状态时,最终的 DOM 更新并不是同步生效的,而是由 Vue 将它们缓存在一个队列中,直到下一个“tick”才一起执行。这样是为了确保每个组件无论发生多少状态改变,都仅执行一次更新。

nextTick() 可以在状态改变后立即使用,以等待 DOM 更新完成。你可以传递一个回调函数作为参数,或者 await 返回的 Promise。

方式一:传递回调函数

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

<script setup>
import { ref, nextTick } from 'vue'

const count = ref(0)

function increment() {
  count.value++

  // DOM 还未更新
  console.log(document.getElementById('counter').textContent) // 0

  nextTick(()=>{
    // DOM 此时已经更新
    console.log(document.getElementById('counter').textContent) // 1
  })
}
</script>

方式二:await 返回的 Promise

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

<script setup>
import { ref, nextTick } from 'vue'

const count = ref(0)

async function increment() {
  count.value++

  // DOM 还未更新
  console.log(document.getElementById('counter').textContent) // 0

  await nextTick()
  // DOM 此时已经更新
  console.log(document.getElementById('counter').textContent) // 1
}
</script>

6. 路由

6.1. 【对路由的理解】

image-20231018144351536

6.2. 【基本切换效果】

  • Vue3中要使用vue-router的最新版本,目前是4版本。

  • 路由配置文件代码如下:

    import { createRouter, createWebHistory } from 'vue-router'
    import Home from '@/pages/Home.vue'
    
    const router = createRouter({
      history: createWebHistory(import.meta.env.BASE_URL),
      routes: [
        {
          path: '/home',
          component: Home
        },
        {
          path: '/news',
          // 路由懒加载 
          component: () => import('@/pages/News.vue')
        },
        {
          path: '/about',
          component: () => import('@/pages/About.vue')
        }
      ]
    })
    export default router
    
  • main.ts代码如下:

    import router from './router/index'
    app.use(router)
    
    app.mount('#app')
    
  • App.vue代码如下

    <template>
      <div class="app">
        <h2 class="title">Vue路由测试</h2>
        <!-- 导航区 -->
        <div class="navigate">
          <router-link to="/home" active-class="active">首页</router-link>
          <router-link to="/news" active-class="active">新闻</router-link>
          <router-link to="/about" active-class="active">关于</router-link>
        </div>
        <!-- 展示区 -->
        <div class="main-content">
          <router-view></router-view>
        </div>
      </div>
    </template>
    
    <script lang="ts" setup name="App">
      import {RouterLink,RouterView} from 'vue-router'  
    </script>
    

6.3. 【两个注意点】

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

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

6.4.【路由器工作模式】

  1. history模式

    优点:URL更加美观,不带有#,更接近传统的网站URL。类似于这样: http://localhost:5173/about

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

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

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

    缺点:URL带有#不太美观,且在SEO优化方面相对较差。类似于这样:http://localhost:5173/#/about

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

6.5. 【to的两种写法】

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

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

6.6. 【命名路由】

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

给路由规则命名:

routes: [
  {
    name: 'zhuye',
    path: '/home',
    component: Home
  },
  {
    name: 'xinwen',
    path: '/news',
    component: () => import('@/pages/News.vue')
  },
  {
    name: 'guanyu',
    path: '/about',
    component: () => import('@/pages/About.vue')
  }
]

跳转路由:

<router-link to="/about">关于</router-link>

<router-link :to="{name:'guanyu'}">关于</router-link>

6.7. 【嵌套路由】

  1. 编写News的子路由:Detail.vue

  2. 配置路由规则,使用children配置项:

const router = createRouter({
  history:createWebHistory(),
  routes:[
    {
      name:'zhuye',
      path:'/home',
      component:Home
    },
    {
      name:'xinwen',
      path:'/news',
      component:() => import('@/pages/News.vue'),
      children:[
        {
          name:'xiang',
          path:'detail',
          component:() => import('@/pages/Detail.vue')
        }
      ]
    },
    {
      name:'guanyu',
      path:'/about',
      component:() => import('@/pages/About.vue')
    }
  ]
})
export default router
  1. 跳转路由(记得要加完整路径):
<router-link to="/news/detail">详情</router-link>
<router-link :to="{path:'/news/detail'}">详情</router-link>
<router-link :to="{name:'xiang'}">详情</router-link>
  1. 记得去News组件中预留一个<router-view>
<template>
  <div class="news">
    <nav class="news-list">
      <router-link v-for="news in newsList" :key="news.id" :to="{path:'/news/detail'}">
        {{news.title}}
      </router-link>
    </nav>
    <div class="news-detail">
      <router-view/>
    </div>
  </div>
</template>

<script setup lang="ts">
import {reactive} from "vue"
const newsList = reactive([
  {id:1,title:'新闻1',content:'内容1'},
  {id:2,title:'新闻2',content:'内容2'},
  {id:3,title:'新闻3',content:'内容3'},
])
</script>

6.8. 【路由传参】

query参数

query参数类似 http://localhost:5173/news/detail?id=1&title=新闻1&content=内容1 这样,使用&分隔键值对

  1. 传递参数
<!-- 跳转并携带query参数(to的字符串写法) -->
<div v-for="news in newsList" :key="news.id">
  <router-link :to="`/news/detail?id=${news.id}&title=${news.title}&content=${news.content}`">
    {{news.title}}
  </router-link>
</div>
<br />
<!-- 跳转并携带query参数(to的对象写法) -->
<div v-for="news in newsList" :key="news.id">
  <router-link
      :to="{
      //name:'xiang', //用name也可以跳转
      path:'/news/detail',
      query:{
        id:news.id,
        title:news.title,
        content:news.content
      }
    }"
  >
    {{ news.title }}
  </router-link>
</div>
  1. 接收参数:

Detail组件可以使用useRoute获取query参数

<template>
  {{route.query}}
</template>

<script setup>
import {useRoute} from 'vue-router'
const route = useRoute()
// 打印query参数
console.log(route.query)
</script>

params参数

params参数类似 http://localhost:5173/news/detail/1/新闻1/内容1 这样,将数据写到路径上

  1. 修改path

需要将路由里的path从detail改为detail/:id/:title/:content

src/router/index.ts

const router = createRouter({
  history:createWebHistory(),
  routes:[
	..
    {
      name:'xinwen',
      path:'/news',
      component:() => import('@/pages/News.vue'),
      children:[
        {
          name:'xiang',
          path:'detail/:id/:title/:content', // 将 `detail`改为`detail/:id/:title/:content`
          component:() => import('@/pages/Detail.vue')
        }
      ]
    },
	..
  ]
})
export default router
  1. 传递参数
<!-- 跳转并携带params参数(to的字符串写法) -->
<div v-for="news in newsList" :key="news.id">
  <router-link :to="`/news/detail/${news.id}/${news.title}/${news.content}`">
    {{news.title}}
  </router-link>
</div>
<br />
<!-- 跳转并携带params参数(to的对象写法) -->
<div v-for="news in newsList" :key="news.id">
  <router-link
      :to="{
        name:'xiang', //用name跳转 (使用对象写法 to中传递params参数 不能使用path只能使用name)
        params:{
          id:news.id,
          title:news.title,
          content:news.content
        }
      }"
  >
    {{news.title}}
  </router-link>
</div>
  1. 接收参数:
<template>
  {{route.params}
</template>

<script setup>
import {useRoute} from 'vue-router'
const route = useRoute()
// 打印params参数
console.log(route.params)
</script>

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

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

存在的问题:

上述写法必须给所有的数据都指定值才能正确匹配到路由,比如http://localhost:5173/news/detail/1/新闻1/内容1这样才能正确访问,此时我们访问http://localhost:5173/news/detail会发现没有显示详情的那个组件

image-20240219211540954
image-20240219211540954

而我们有时可以允许不指定某些数据,比如不指定content,也就是通过http://localhost:5173/news/detail/1/新闻1也能访问到,我们可以将src/router/index.ts文件里指定路由的path从detail/:id/:title/:content改为detail/:id/:title/:content?,这样我们就能通过 http://localhost:5173/news/detail/1/新闻1 访问到该组件了。如果三个都可以为空,则将path写成detail/:id?/:title?/:content?即可,此时也能正确的匹配到路由

image-20240219211638558
image-20240219211638558

6.9. 【路由的props配置】

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

{
	name:'xiang',
	path:'detail/:id?/:title?/:content?',
	component:() => import('@/pages/Detail.vue'),

  // props的对象写法,作用:把对象中的每一组key-value作为props传给Detail组件
  // props:{a:1,b:2,c:3}, 

  // props的布尔值写法,作用:把收到了每一组params参数,作为props传给Detail组件
  // props:true
  
  // props的函数写法,作用:把返回的对象中每一组key-value作为props传给Detail组件
  props(route){
    return route.params
  }
}

Detail组件接收传过来的props参数

<template>
  {{props}}
</template>

<script setup>
import {defineProps} from 'vue'
const props = defineProps(['id','title','content'])
</script>

6.10. 【meta 路由额外信息】

有时我们希望给某个路由增加一些额外的信息,比如这个路由显不显示在菜单上,这个路由里的按钮的颜色等,此时可以使用meta来存储这些额外的信息

import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/pages/Home.vue'

const router = createRouter({
  history:createWebHistory(),
  routes:[
    ...
    {
      name:'guanyu',
      path:'/about',
      component:() => import('@/pages/About.vue'),
      // 使用meta添加额外信息
      meta: {
        isShow: false,
        color: '#fff'
      }
    }
  ]
})
export default router

About组件

<template>
  {{route.meta}}
</template>

<script setup>
import {useRoute} from 'vue-router'
const route = useRoute()
// 打印meta信息
console.log(route.meta)
</script>

6.11. 【重定向】

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

  2. 具体编码:

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

6.12. 【 replace属性】

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

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

    • push是追加历史记录(默认值)此时点击返回会返回到从那个页面进来的url。
    • replace是替换当前记录。
  3. 开启replace模式:

    <router-link to="/news" active-class="active" replace>新闻</router-link>
    

6.13. 【编程式导航】

push和replace

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

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

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

console.log(route.query)
console.log(route.parmas)
console.log(route.meta)
// 获取所有路由信息
console.log(router.getRoutes())

router.push是追加历史记录 router.replace是替换当前记录,传递的参数都和router-link中to的对象写法一样

以下是router.push的代码:(下面有完整代码)

router.push({
  name: 'xiang',
  params: {
    id: 3,
    title: '新闻3',
    content: '内容3'
  }
})

点击新闻1新闻2 后调用编程式导航的push跳到新闻3,此时再返回回到了新闻2

GIF 2024-2-21 10-26-13
GIF 2024-2-21 10-26-13

以下是router.replace的代码:(下面有完整代码)

router.replace({
  name: 'xiang',
  params: {
    id: 3,
    title: '新闻3',
    content: '内容3'
  }
})

点击新闻1新闻2 后调用编程式导航的replace跳到新闻3新闻3会替换掉新闻2,此时再返回就回到了新闻1

GIF 2024-2-21 10-25-12
GIF 2024-2-21 10-25-12

路由的前进与后退

back和forward与浏览器的前进和后退功能一样

// 后退
router.back()
// 前进
router.forward()
GIF 2024-2-21 10-49-40
GIF 2024-2-21 10-49-40

router.go(-1)router.back()功能一样,router.go(1)router.forward()功能一样,router.go(0)和浏览器的刷新按钮功能一样

// go后退1次
router.go(-1)
// go刷新
router.go(0)
// go前进1次
router.go(1)
// go前进2次
router.go(2)
GIF 2024-2-21 10-53-27
GIF 2024-2-21 10-53-27

News组件完整代码:

<template>
  <div class="news">
    <nav class="news-list">
      <!-- 跳转并携带params参数(to的对象写法) -->
      <div v-for="news in newsList" :key="news.id">
        <router-link
            :to="{
              name:'xiang', //用name跳转 (使用对象写法 to中传递params参数 不能使用path只能使用name)
              params:{
                id:news.id,
                title:news.title,
                content:news.content
              }
            }"
        >
          {{ news.title }}
        </router-link>
      </div>
      <!-- push模式 -->
      <button @click="push">push</button>
      <button @click="replace">replace</button>
      <button @click="()=>router.back()">back后退</button>
      <button @click="()=>router.forward()">forward前进</button>
      <button @click="()=>router.go(-1)">go后退1次</button>
      <button @click="()=>router.go(0)">go刷新</button>
      <button @click="()=>router.go(1)">go前进1次</button>
      <button @click="()=>router.go(2)">go前进2次</button>

    </nav>
    <div class="news-detail">
      <router-view/>
    </div>
  </div>
</template>

<script setup lang="ts">
import {reactive} from "vue"
import router from "@/router";

const newsList = reactive([
  {id: 1, title: '新闻1', content: '内容1'},
  {id: 2, title: '新闻2', content: '内容2'},
  {id: 3, title: '新闻3', content: '内容3'},
  {id: 4, title: '新闻4', content: '内容4'},
])

const push = () => {
  router.push({
    name: 'xiang',
    params: {
      id: 3,
      title: '新闻3',
      content: '内容3'
    }
  })
}

const replace = () => {
  router.replace({
    name: 'xiang',
    params: {
      id: 3,
      title: '新闻3',
      content: '内容3'
    }
  })
}

</script>

6.14 动态路由

src/router/index.ts

添加路由

router.addRoute({
  name: 'me',
  path: '/me',
  component: () => import('@/pages/Me.vue')
})

console.log(router.getRoutes())
image-20240221110628384
image-20240221110628384

使用这种方式可以添加路由,但是如果是从服务器返回来的数据,再添加到路由中,就会出现如下的问题,这是因为再打包时发现没有引用Me.vue这个组件,因此并不会将其打包,从而导致找不到组件

const route = JSON.parse('{"name":"me","path":"/me","component":"/src/pages/Me.vue"}')

router.addRoute({
    name: route.name,
    path: route.path,
    component: () => import(route.component)
})

console.log(router.getRoutes())
image-20240221135101929
image-20240221135101929

我们可以使用import.meta.glob/src/pages包及其子包的vue文件都导入进来,这样就可以使用动态路由了

const modules = import.meta.glob('/src/pages/**/**.vue')

const route = JSON.parse('{"name":"me","path":"/me","component":"/src/pages/Me.vue"}')

router.addRoute({
    name: route.name,
    path: route.path,
    component: modules[route.component]
})

console.log(router.getRoutes())

删除路由

有几个不同的方法来删除现有的路由:

  • 通过添加一个名称冲突的路由。如果添加与现有途径名称相同的途径,会先删除路由,再添加路由:

    router.addRoute({ path: '/about', name: 'about', component: About })
    // 这将会删除之前已经添加的路由,因为他们具有相同的名字且名字必须是唯一的
    router.addRoute({ path: '/other', name: 'about', component: Other })
    
  • 通过调用 router.addRoute() 返回的回调:

    const removeRoute = router.addRoute(routeRecord)
    removeRoute() // 删除路由如果存在的话
    

    当路由没有名称时,这很有用。

  • 通过使用 router.removeRoute() 按名称删除路由:

    router.addRoute({ path: '/about', name: 'about', component: About })
    // 删除路由
    router.removeRoute('about')
    

    需要注意的是,如果你想使用这个功能,但又想避免名字的冲突,可以在路由中使用 Symbol 作为名字。

当路由被删除时,所有的别名和子路由也会被同时删除

添加嵌套路由

要将嵌套路由添加到现有的路由中,可以将路由的 name 作为第一个参数传递给 router.addRoute(),这将有效地添加路由,就像通过 children 添加的一样:

router.addRoute({ name: 'admin', path: '/admin', component: Admin })
router.addRoute('admin', { path: 'settings', component: AdminSettings })

这等效于:

router.addRoute({
  name: 'admin',
  path: '/admin',
  component: Admin,
  children: [{ path: 'settings', component: AdminSettings }],
})

查看现有路由

Vue Router 提供了两个功能来查看现有的路由:

6.15 路由守卫

路由守卫官方open in new window写的挺好的,下面大部分抄的官方的

全局前置守卫

你可以使用 router.beforeEach 注册一个全局前置守卫:

const router = createRouter({ ... })

router.beforeEach((to, from) => {
  // ...
  // 返回 false 以取消导航
  return false
})

当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于等待中

每个守卫方法接收两个参数:

  • to: 即将要进入的目标
  • from: 当前导航正要离开的路由

可以返回的值如下:

  • false: 取消当前的导航。如果浏览器的 URL 改变了(可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址。
  • 一个路由地址open in new window: 通过一个路由地址重定向到一个不同的地址,如同调用 router.push(),且可以传入诸如 replace: truename: 'home' 之类的选项。它会中断当前的导航,同时用相同的 from 创建一个新导航。
 router.beforeEach(async (to, from) => {
   if (
     // 检查用户是否已登录
     !isAuthenticated &&
     // ❗️ 避免无限重定向
     to.name !== 'Login'
   ) {
     // 将用户重定向到登录页面
     return { name: 'Login' }
   }
 })

如果遇到了意料之外的情况,可能会抛出一个 Error。这会取消导航并且调用 router.onError()open in new window 注册过的回调。

如果什么都没有,undefined 或返回 true则导航是有效的,并调用下一个导航守卫

以上所有都同 async 函数 和 Promise 工作方式一样:

router.beforeEach(async (to, from) => {
  // canUserAccess() 返回 `true` 或 `false`
  const canAccess = await canUserAccess(to)
  if (!canAccess) return '/login'
})

可选的第三个参数 next

在之前的 Vue Router 版本中,还可以使用 第三个参数 next 。这是一个常见的错误来源,我们经过 RFCopen in new window 讨论将其移除。然而,它仍然是被支持的,这意味着你可以向任何导航守卫传递第三个参数。在这种情况下,确保 next 在任何给定的导航守卫中都被严格调用一次。它可以出现多于一次,但是只能在所有的逻辑路径都不重叠的情况下,否则钩子永远都不会被解析或报错。这里有一个在用户未能验证身份时重定向到/login错误用例

// BAD
router.beforeEach((to, from, next) => {
  if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
  // 如果用户未能验证身份,则 `next` 会被调用两次
  next()
})

下面是正确的版本:

// GOOD
router.beforeEach((to, from, next) => {
  if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
  else next()
})

全局解析守卫

你可以用 router.beforeResolve 注册一个全局守卫。这和 router.beforeEach 类似,因为它在每次导航时都会触发,不同的是,解析守卫刚好会在导航被确认之前、所有组件内守卫 (beforeRouteEnter,后面会讲) 和异步路由组件被解析之后调用。这里有一个例子,确保用户可以访问自定义 metaopen in new window 属性 requiresCamera 的路由:

router.beforeResolve(async to => {
  if (to.meta.requiresCamera) {
    try {
      await askForCameraPermission()
    } catch (error) {
      if (error instanceof NotAllowedError) {
        // ... 处理错误,然后取消导航
        return false
      } else {
        // 意料之外的错误,取消导航并把错误传给全局处理器
        throw error
      }
    }
  }
})

router.beforeResolve 是获取数据或执行任何其他操作(如果用户无法进入页面时你希望避免执行的操作)的理想位置。

全局后置钩子

你也可以注册全局后置钩子,然而和守卫不同的是,这些钩子不会接受 next 函数也不会改变导航本身:

router.afterEach((to, from) => {
  sendToAnalytics(to.fullPath)
})

它们对于分析、更改页面标题、声明页面等辅助功能以及许多其他事情都很有用。

它们也反映了 navigation failuresopen in new window 作为第三个参数:

router.afterEach((to, from, failure) => {
  if (!failure) sendToAnalytics(to.fullPath)
})

了解更多关于 navigation failures 的信息在它的指南open in new window中。

在守卫内的全局注入

从 Vue 3.3 开始,你可以在导航守卫内使用 inject() 方法。这在注入像 pinia storesopen in new window 这样的全局属性时很有用。在 app.provide() 中提供的所有内容都可以在 router.beforeEach()router.beforeResolve()router.afterEach() 内获取到:

// main.ts
const app = createApp(App)
app.provide('global', 'hello injections')

// router.ts or main.ts
router.beforeEach((to, from) => {
  const global = inject('global') // 'hello injections'
  // a pinia store
  const userStore = useAuthStore()
  // ...
})

路由独享的守卫

你可以直接在路由配置上定义 beforeEnter 守卫:

const routes = [
  {
    path: '/users/:id',
    component: UserDetails,
    beforeEnter: (to, from) => {
      // reject the navigation
      return false
    },
  },
]

beforeEnter 守卫 只在进入路由时触发,不会在 paramsqueryhash 改变时触发。例如,从 /users/2 进入到 /users/3 或者从 /users/2#info 进入到 /users/2#projects都不会触发。它们只有在 从一个不同的 路由导航时,才会被触发。

你也可以将一个函数数组传递给 beforeEnter,这在为不同的路由重用守卫时很有用:

function removeQueryParams(to) {
  if (Object.keys(to.query).length)
    return { path: to.path, query: {}, hash: to.hash }
}

function removeHash(to) {
  if (to.hash) return { path: to.path, query: to.query, hash: '' }
}

const routes = [
  {
    path: '/users/:id',
    component: UserDetails,
    beforeEnter: [removeQueryParams, removeHash],
  },
  {
    path: '/about',
    component: UserDetails,
    beforeEnter: [removeQueryParams],
  },
]

请注意,你也可以通过使用路径 meta 字段open in new window和全局导航守卫来实现类似的行为。

组件内的守卫

最后,你可以在路由组件内直接定义路由导航守卫(传递给路由配置的)

可用的配置 API

你可以为路由组件添加以下配置:

  • beforeRouteEnter
  • beforeRouteUpdate
  • beforeRouteLeave
const UserDetails = {
  template: `...`,
  beforeRouteEnter(to, from) {
    // 在渲染该组件的对应路由被验证前调用
    // 不能获取组件实例 `this` !
    // 因为当守卫执行时,组件实例还没被创建!
  },
  beforeRouteUpdate(to, from) {
    // 在当前路由改变,但是该组件被复用时调用
    // 举例来说,对于一个带有动态参数的路径 `/users/:id`,在 `/users/1` 和 `/users/2` 之间跳转的时候,
    // 由于会渲染同样的 `UserDetails` 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
    // 因为在这种情况发生的时候,组件已经挂载好了,导航守卫可以访问组件实例 `this`
  },
  beforeRouteLeave(to, from) {
    // 在导航离开渲染该组件的对应路由时调用
    // 与 `beforeRouteUpdate` 一样,它可以访问组件实例 `this`
  },
}

beforeRouteEnter 守卫 不能 访问 this,因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建。

不过,你可以通过传一个回调给 next 来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数:

beforeRouteEnter (to, from, next) {
  next(vm => {
    // 通过 `vm` 访问组件实例
  })
}

注意 beforeRouteEnter 是支持给 next 传递回调的唯一守卫。对于 beforeRouteUpdatebeforeRouteLeave 来说,this 已经可用了,所以不支持 传递回调,因为没有必要了:

beforeRouteUpdate (to, from) {
  // just use `this`
  this.name = to.params.name
}

这个 离开守卫 通常用来预防用户在还未保存修改前突然离开。该导航可以通过返回 false 来取消。

beforeRouteLeave (to, from) {
  const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
  if (!answer) return false
}
使用组合式 API

你可以通过 onBeforeRouteUpdateonBeforeRouteLeave 分别添加 update 和 leave 守卫,组合式API没有enter守卫,可以使用如下方式使用enter守卫。

<script>
import {defineComponent, ref} from "vue"

const data = ref(false)
export default defineComponent({
  name: "About",
  beforeRouteEnter(to, from, next) {
    data.value = true
    // 不能使用setup里定义的变量
    // data2.value = false
    next();
  },
})
</script>

<script setup>
import {onBeforeRouteLeave, onBeforeRouteUpdate} from "vue-router"
// 普通的script里导入了ref,这里就不用再导入ref了
const data2 = ref(true)
onBeforeRouteUpdate((to, from, next) => {
  next()
})

onBeforeRouteLeave((to, from, next) => {
  // setup语法糖里的可以直接使用别的普通script里的变量
  data.value = false
  data2.value = true
  next()
})
</script>

完整的导航解析流程

  1. 导航被触发。
  2. 在失活的组件里调用 beforeRouteLeave 守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫(2.2+)。
  5. 在路由配置里调用 beforeEnter
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter
  8. 调用全局的 beforeResolve 守卫(2.5+)。
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 触发 DOM 更新。
  12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

6.16 缓存路由

缓存组件

官方写的挺好的,这里直接抄的官方的

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

基本使用

在组件基础章节中,我们已经介绍了通过特殊的 <component> 元素来实现动态组件open in new window的用法:

<component :is="activeComponent" />

默认情况下,一个组件实例在被替换掉后会被销毁。这会导致它丢失其中所有已变化的状态——当这个组件再一次被显示时,会创建一个只带有初始状态的新实例。

在下面的例子中,你会看到两个有状态的组件——A 有一个计数器,而 B 有一个通过 v-model 同步 input 框输入内容的文字展示。尝试先更改一下任意一个组件的状态,然后切走,再切回来:

你会发现在切回来之后,之前已更改的状态都被重置了。

在切换时创建新的组件实例通常是有意义的,但在这个例子中,我们的确想要组件能在被“切走”的时候保留它们的状态。要解决这个问题,我们可以用 <KeepAlive> 内置组件将这些动态组件包装起来:

<!-- 非活跃的组件将会被缓存! -->
<KeepAlive>
  <component :is="activeComponent" />
</KeepAlive>

现在,在组件切换时状态也能被保留了:

DOM 内模板open in new window中使用时,它应该被写为 <keep-alive>

包含/排除

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

<!-- 以英文逗号分隔的字符串 -->
<KeepAlive include="a,b">
  <component :is="view" />
</KeepAlive>

<!-- 正则表达式 (需使用 `v-bind`) -->
<KeepAlive :include="/a|b/">
  <component :is="view" />
</KeepAlive>

<!-- 数组 (需使用 `v-bind`) -->
<KeepAlive :include="['a', 'b']">
  <component :is="view" />
</KeepAlive>

它会根据组件的 nameopen in new window 选项进行匹配,所以组件如果想要条件性地被 KeepAlive 缓存,就必须显式声明一个 name 选项。

在 3.2.34 或以上的版本中,使用 <script setup> 的单文件组件会自动根据文件名生成对应的 name 选项,无需再手动声明。

最大缓存实例数

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

template

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

缓存实例的生命周期

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

一个持续存在的组件可以通过 onActivated()open in new windowonDeactivated()open in new window 注册相应的两个状态的生命周期钩子:

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

onActivated(() => {
  // 调用时机为首次挂载
  // 以及每次从缓存中被重新插入时
})

onDeactivated(() => {
  // 在从 DOM 上移除、进入缓存
  // 以及组件卸载时调用
})
</script>

请注意:

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

缓存路由

RouterView 插槽

RotuerView 组件暴露了一个插槽,可以用来渲染路由组件:

<router-view v-slot="{ Component }">
  <component :is="Component" />
</router-view>

上面的代码等价于不带插槽的 <router-view />,但是当我们想要获得其他功能时,插槽提供了额外的扩展性。

KeepAlive & Transition

当在处理 KeepAliveopen in new window 组件时,我们通常想要保持路由组件活跃,而不是 RouterView 本身。为了实现这个目的,我们可以将 KeepAlive 组件放置在插槽内:

<router-view v-slot="{ Component }">
  <keep-alive>
    <component :is="Component" />
  </keep-alive>
</router-view>

类似地,插槽允许我们使用一个 Transitionopen in new window 组件来实现在路由组件之间切换时实现过渡效果:

<router-view v-slot="{ Component }">
  <transition>
    <component :is="Component" />
  </transition>
</router-view>

我们也可以在 Transition 组件内使用 KeepAlive 组件:

<router-view v-slot="{ Component }">
  <transition>
    <keep-alive>
      <component :is="Component" />
    </keep-alive>
  </transition>
</router-view>

关于更多 RouterView 组件和 Transition 组件之间的互动,请参考 Transitionsopen in new window 指南。

传递 props 和插槽

我们可以利用其插槽给路由组件传递 props 或插槽:

<router-view v-slot="{ Component }">
  <component :is="Component" some-prop="a value">
    <p>Some slotted content</p>
  </component>
</router-view>

实践中通常不会这么做,因为这样会导致所有路由组件都使用相同的 props 和插槽。请查阅传递 props 给路由组件open in new window获取其他传递 props 的方式。

模板引用

使用插槽可以让我们直接将模板引用open in new window放置在路由组件上:

<router-view v-slot="{ Component }">
  <component :is="Component" ref="mainContent" />
</router-view>

而如果我们将引用放在 <router-view> 上,那引用将会被 RouterView 的实例填充,而不是路由组件本身。

7. pinia

7.1【准备一个效果】

pinia_example

7.2【搭建 pinia 环境】

第一步:npm install pinia

第二步:操作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选项

7.3【存储+读取数据】

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

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

  3. 具体编码:src/store/count.ts

    // 引入defineStore用于创建store
    import {defineStore} from 'pinia'
    
    // 定义并暴露一个store
    export const useCountStore = defineStore('count',{
      // 动作
      actions:{},
      // 状态
      state(){
        return {
          sum:6
        }
      },
      // 计算
      getters:{}
    })
    
  4. 具体编码:src/store/talk.ts

    // 引入defineStore用于创建store
    import {defineStore} from 'pinia'
    
    // 定义并暴露一个store
    export const useTalkStore = defineStore('talk',{
      // 动作
      actions:{},
      // 状态
      state(){
        return {
          talkList:[
            {id:'yuysada01',content:'你今天有点怪,哪里怪?怪好看的!'},
         		{id:'yuysada02',content:'草莓、蓝莓、蔓越莓,你想我了没?'},
            {id:'yuysada03',content:'心里给你留了一块地,我的死心塌地'}
          ]
        }
      },
      // 计算
      getters:{}
    })
    
  5. 组件中使用state中的数据

    <template>
      <h2>当前求和为:{{ sumStore.sum }}</h2>
    </template>
    
    <script setup lang="ts" name="Count">
      // 引入对应的useXxxxxStore	
      import {useSumStore} from '@/store/sum'
      
      // 调用useXxxxxStore得到对应的store
      const sumStore = useSumStore()
    </script>
    
    <template>
    	<ul>
        <li v-for="talk in talkStore.talkList" :key="talk.id">
          {{ talk.content }}
        </li>
      </ul>
    </template>
    
    <script setup lang="ts" name="Count">
      import axios from 'axios'
      import {useTalkStore} from '@/store/talk'
    
      const talkStore = useTalkStore()
    </script>
    

7.4.【修改数据】(三种方式)

  1. 第一种修改方式,直接修改

    countStore.sum = 666
    
  2. 第二种修改方式:批量修改

    countStore.$patch({
      sum:999,
      school:'atguigu'
    })
    
  3. 第三种修改方式:借助action修改(action中可以编写一些业务逻辑)

    import { defineStore } from 'pinia'
    
    export const useCountStore = defineStore('count', {
      /*************/
      actions: {
        //加
        increment(value:number) {
          if (this.sum < 10) {
            //操作countStore中的sum
            this.sum += value
          }
        },
        //减
        decrement(value:number){
          if(this.sum > 1){
            this.sum -= value
          }
        }
      },
      /*************/
    })
    
  4. 组件中调用action即可

    // 使用countStore
    const countStore = useCountStore()
    
    // 调用对应action
    countStore.incrementOdd(n.value)
    

7.5.【storeToRefs】

  • 借助storeToRefsstore中的数据转为ref对象,方便在模板中使用。
  • 注意:pinia提供的storeToRefs只会将数据做转换,而VuetoRefs会转换store中数据。
<template>
	<div class="count">
		<h2>当前求和为:{{sum}}</h2>
	</div>
</template>

<script setup lang="ts" name="Count">
  import { useCountStore } from '@/store/count'
  /* 引入storeToRefs */
  import { storeToRefs } from 'pinia'

	/* 得到countStore */
  const countStore = useCountStore()
  /* 使用storeToRefs转换countStore,随后解构 */
  const {sum} = storeToRefs(countStore)
</script>

7.6.【getters】

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

  2. 追加getters配置。

    // 引入defineStore用于创建store
    import {defineStore} from 'pinia'
    
    // 定义并暴露一个store
    export const useCountStore = defineStore('count',{
      // 动作
      actions:{
        /************/
      },
      // 状态
      state(){
        return {
          sum:1,
          school:'atguigu'
        }
      },
      // 计算
      getters:{
        bigSum:(state):number => state.sum *10,
        upperSchool():string{
          return this. school.toUpperCase()
        }
      }
    })
    
  3. 组件中读取数据:

    const {increment,decrement} = countStore
    let {sum,school,bigSum,upperSchool} = storeToRefs(countStore)
    

7.7.【$subscribe】

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

talkStore.$subscribe((mutate,state)=>{
  console.log('LoveTalk',mutate,state)
  localStorage.setItem('talk',JSON.stringify(talkList.value))
})

7.8. 【store组合式写法】

import {defineStore} from 'pinia'
import axios from 'axios'
import {nanoid} from 'nanoid'
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}
})

defineAsyncComponent

defineExpose

onRouteLeave

8. 组件通信

① 父传子

使用props只能查看数据,不能修改数据。

父组件传给子组件的数据,当该数据在父组件的中发生改变的时候,子组件中的数据不会因其的改变而去改变。是因为父组件传给子组件的数据是通过子组件的mounted阶段进行的处理,才能通过data渲染到页面上,从而更新。但是数据发生变化的时候,mounted阶段已经过了,所以data捕获不到数据的更新,从而无法更新。

参考链接:https://www.jb51.net/article/265304.htm

1、父组件传递方式

<template>
  <div class="hello">
  我是父组件
  <!-- 父组件通过:变量(这里是info)绑定值 -->
   <Child :info="parentMsg" :bar="1"></Child>
  </div>
</template>
 
<script setup>
import Child from './Child.vue'
import {ref} from 'vue'
const parentMsg=ref('父组件传递值是a')
 
</script>
 
<style scoped>
 
</style>

2、子组件接收方式和使用

<template>
    <!-- info是父组件传递过了的值 -->
    <div>我是子组件拿到了父组件info的值是=>{{info}}</div>
    <div>我是子组件拿到了父组件props.bar的值是=>{{props.bar}}</div>
    <!-- <template>可以直接使用pros里的数据(<script>里不行) -->
    <div>我是子组件拿到了父组件bar的值是=>{{bar}}</div>
    <button @click="alertBar">点击</button>
</template>
   
<script setup>
  //defineProps是Vue3提供方法,不需要引入直接使用(当然引入了也不会报错)
  import { toRefs, defineProps } from 'vue'
  const props = defineProps({
    //子组件接收父组件传递过来的值
    info: String,
    bar: {
       type: Number,      //参数的类型
       required: false,   //是否是必须的参数
       default: 0,        //默认数据
    }
  })
  //使用父组件传递过来的值(不添加这一行也行)
  const {info} =toRefs(props)

  const alertBar = ()=>{
    // console.log(bar); // bar is not defined
    console.log(props.bar) // 在<script>里必须带上props,在<template>可以直接使用pros里的数据
    alert(props.bar)
  }
</script>

3、效果图

image-20230614213822296
image-20230614213822296

② 子传父

vue3中子组件向父组件传递值和vue2.x的区别是vue2.x使用的是 $emit 而vue3使用的是emit,它们的传值一样都是方法加值,即vue2.x的是this.$emit('方法名','传递的值(根据需要传或者不传)'),vue3的setup语法糖的是defineEmits。vue3的子传父方式如下所示:

1、子组件的传递方式

<template>
  <div>
    <button @click="clickChild">点击子组件</button>
  </div>
</template>
 
<script setup>
// 使用defineEmits创建名称,接受一个数组
const emit = defineEmits(['clickChild','click'])
const clickChild = () => {
  let param1 = { content: 'b' }
  //传递给父组件
  emit('clickChild', param1)
  let param2 = { content: 'c' }
  emit('click', param2)
}
</script>

2、父组件接收与使用

<Child @click="clickEven"></Child>

vue2中这种写法是自定义事件,可以通过.native修饰符变为原生DOM事件 vue3中直接写则为原生DOM事件;如果在子组件使用defineEmits(['clickChild','click'])定义了click并且使用emit('click', param2)则为自定义事件;如果直接为defineEmits(['clickChild'])并且使用emit('click', param2)则为自定义事件,不过会报警告

<template>
  <div class="hello">
  <h1>我是父组件</h1>
  <!-- clickChild是子组件绑定的事件,click是父组件接受方式 -->
   <Child  @clickChild="clickEven" @click="clickMsg"></Child>
 <h1>子组件传递的值是 {{result}}</h1>
 </div>
</template>
 
<script setup>
import Child from './Child.vue'
import {ref} from 'vue'
const result=ref('')
const clickEven=(param1)=>{
  console.log(param1);
  result.value=param1.content
}
const clickMsg=(param2)=>{
  console.log(param2);
}
</script>

3、效果图

image-20230614223735480
image-20230614223735480

③ 定义暴露属性和方法(非常常用)

使用这种方式既可以查看数据也能修改数据

父组件获取并修改子组件数据和方法

当时用语法糖时,需要将组建的属性及方法通过defineExpose导出,父组件才能访问到数据,否则拿不到子组件的数据

父组件

父组件在使用子组件时可以添加ref="testRef",并且定义const testRef = ref(),这样就

<template>
  <div class="hello">
    <h1>我是父组件</h1>
    <Child ref="testRef"></Child>
    <button @click="getSonHander">获取子组件中的数据</button>
  </div>
</template>
 
<script setup>
import Child from './Child.vue'
import { ref } from 'vue'
const testRef = ref()
const getSonHander = () => {
  console.log('获取子组件中的性别', testRef.value.sex);
  console.log('获取子组件中的其他信息', testRef.value.info)
  testRef.value.info.age += 1
  console.log("修改子组件的年龄",testRef.value.info.age);
  console.log("调用子组件的方法==>");
  testRef.value.say()
}
</script>

子组件

<template>
  <div>
    <div> 我是子组件 </div>
    <div>性别:{{ sex }}</div>
  </div>
</template>
 
<script setup>
import { reactive, ref, defineExpose } from "vue";
let sex = ref('男')
let info = reactive({ like: '王者荣耀', age: 18 })
let say = () => {
  console.log("我是子组件");
}
defineExpose({ sex, info, say })
</script>

效果图

image-20230618181037465
image-20230618181037465

子组件获取并修改父组件属性和方法

父组件

<template>
  <div>
    <h1> 我是父组件</h1>
    <div>性别:{{ sex }}</div>
    <Child></Child>
  </div>
</template>
 
<script setup>
import { reactive, ref, defineExpose } from "vue";
import Child from './Child.vue'
let sex = ref('男')
let info = reactive({ like: '钓鱼', age: 38 })
let say = () => {
  console.log("我是父组件");
}
defineExpose({ sex, info, say })
</script>

子组件

要想获得父组件实例vc,调用方法时参数名必须为$parent

<template>
  <div class="hello">
    <div>我是子组件</div>
    <button @click="getFather($parent)">获取父组件中的数据</button>
  </div>
</template>
 
<script setup>
import { ref } from 'vue'
const testRef = ref()
const getFather = ($parent) => {
  console.log('获取父组件中的性别', $parent.sex);
  console.log('获取父组件中的其他信息', $parent.info)
  $parent.info.age += 1
  console.log("修改父组件的年龄",$parent.info.age);
  console.log("调用父组件的方法==>");
  $parent.say()
}
</script>
image-20230618180756271
image-20230618180756271

④ 全局事件总线

全局事件总线可以实现任意组件通信,在vue2中可以根据VM与VC关系推出全局事件总线。

但是在vue3中没有Vue构造函数,也就没有Vue.prototype.以及组合式API写法没有this,

那么在Vue3想实现全局事件的总线功能就有点不现实啦,如果想在Vue3中使用全局事件总线功能,可以使用插件mitt实现。

mitt:官网地址:https://www.npmjs.com/package/mitt

安装

npm install --save mitt

src\bus\index.ts

//引入mitt插件:mitt一个方法,方法执行会返回bus对象
import mitt from 'mitt';
const $bus = mitt();
export default $bus;

组件1:

<template>
    <h3>我是子组件1:曹植</h3>
</template>

<script setup lang="ts">
import $bus from "@/bus";
//组合式API函数
import { onMounted } from "vue";
//组件挂载完毕的时候,当前组件绑定一个事件,接受将来兄弟组件传递的数据
onMounted(() => {
  //第一个参数:即为事件类型  第二个参数:即为事件回调
  $bus.on("car", (car) => {
    console.log(car);
  });
});
</script>

组件2:

<template>
  <h2>我是子组件2:曹丕</h2>
  <button @click="handler">点击我给兄弟送一台法拉利</button>
</template>

<script setup lang="ts">
//引入$bus对象
import $bus from '@/bus';
//点击按钮回调
const handler = () => {
  $bus.emit('car', { car: "法拉利" });
}
</script>

⑤ v-model父子组件通讯

传统父子组件通讯方式

传统方式我们需要在父组件中定义一个let money = ref(10000);,然后在使用子组件的标签上添加:modelValue="money"绑定给子组件,并且使用@update:modelValue接收子组件的自定义事件(这里的自定义事件名字叫update:modelValue,中间的:仅仅做分割作用,没有什么特殊的含义),然后指定这个自定义事件的处理函数为handler,并在handler方法里添加修改money的值

<template>
  <div>
    <h1>v-model:钱数{{ money }}</h1>
    <Child :modelValue="money" @update:modelValue="handler"></Child>
  </div>
</template>

<script setup>
import { ref } from "vue";
import Child from "./Child.vue";
//父组件的数据钱数
let money = ref(10000);
//自定义事件的回调
const handler = (num) => {
  //将来接受子组件传递过来的数据
  money.value = num;
};
</script>

在子组件里,我们需要使用let props = defineProps(["modelValue"])接收父组件传递的值,使用let $emit = defineEmits(['update:modelValue'])声明自定义事件,然后使用$emit('update:modelValue',props.modelValue+1000)触发自定义事件以改变modelValuemoney)的值

<template>
  <div class="child">
    <h3>钱数:{{ modelValue }}</h3>
    <button @click="handler">父子组件数据同步</button>
  </div>
</template>

<script setup>
//接受props
let props = defineProps(["modelValue"]);
let $emit = defineEmits(['update:modelValue']);
//子组件内部按钮的点击回调
const handler = ()=>{
   //触发自定义事件
   $emit('update:modelValue',props.modelValue+1000);
}
</script>

v-model方式

想使用v-model的方式,我们可以删掉:modelValue="money" @update:modelValue="handler",修改为v-model="money"就行了,这个父组件的handler方法也可以删掉了

使用v-model来实现父子组件传递数据时,会给子组件传递一个名为modelValueprops,子组件可以使用let props = defineProps(["modelValue"])接收这个props,如果我们想要修改数据,自定义事件必须声明为let $emit = defineEmits(['update:modelValue'])(也就是update:+props里的名字),然后使用$emit('update:modelValue',props.modelValue+1000)触发更新操作,此时就会将这个触发自定义事件的第二个参数的值(也就是props.modelValue+1000)赋给modelValue对应的变量(也就是父组件的money

<template>
  <div>
    <h1>v-model:钱数{{ money }}</h1>
    <!-- <Child :modelValue="money" @update:modelValue="handler"></Child> -->
    <Child v-model="money"></Child>
  </div>
</template>

<script setup>
import { ref } from "vue";
import Child from "./Child.vue";
//父组件的数据钱数
let money = ref(10000);
//自定义事件的回调
// const handler = (num) => {
//   //将来接受子组件传递过来的数据
//   money.value = num;
// };
</script>

子组件不需要做任何改变

<template>
  <div class="child">
    <h3>钱数:{{ modelValue }}</h3>
    <button @click="handler">父子组件数据同步</button>
  </div>
</template>

<script setup>
//接受props
let props = defineProps(["modelValue"]);
let $emit = defineEmits(['update:modelValue']);
//子组件内部按钮的点击回调
const handler = ()=>{
   //触发自定义事件
   $emit('update:modelValue',props.modelValue+1000);
}
</script>

v-model绑定多个数据

如果想要绑定多个数据,可以使用v-model:pageNo="pageNo",也就是v-model:+自定义名字

<template>
  <div>
    <h2>当前页:{{pageNo}} 每页大小:{{pageSize}}</h2>
    <Child2 v-model:pageNo="pageNo" v-model:pageSize="pageSize"></Child2>
  </div>
</template>

<script setup>
//引入子组件
import { ref } from "vue";
import Child2 from "./Child2.vue";
//父亲的数据
let pageNo = ref(1);
let pageSize = ref(3);
</script>

子组件使用let props = defineProps(["pageNo", "pageSize"])接收父组件传过来的两个自定义事件,然后使用let $emit = defineEmits(["update:pageNo", "update:pageSize"])来声明自定义事件(也就是update:+自定义名称),然后使用$emit("update:pageNo", props.pageNo + 3)$emit('update:pageSize', pageSize + 4)更新相应数据的值

<template>
  <div class="child2">
    <h1>同时绑定多个v-model</h1>
    <button @click="handler">pageNo{{ pageNo }}</button>
    <button @click="$emit('update:pageSize', pageSize + 4)">
      pageSize{{ pageSize }}
    </button>
  </div>
</template>

<script setup>
let props = defineProps(["pageNo", "pageSize"]);
let $emit = defineEmits(["update:pageNo", "update:pageSize"]);
//第一个按钮的事件回调
const handler = () => {
  $emit("update:pageNo", props.pageNo + 3);
};
</script>

⑥ useAttrs(扩充原组件功能)

父组件

注意要先安装mybatis-plus并在src\main.js中使用ElementPlus

// 引入实例化上下文的api方法createApp
import { createApp } from 'vue'
// 引入element ui
import ElementPlus from 'element-plus'
// 引入element ui的样式
import 'element-plus/dist/index.css'
// 引入App组件
import App from './App.vue'
// 创建app
const app = createApp(App)
app.use(ElementPlus) // 使用element-ui
// 挂载
app.mount('#app');
<template>
  <div>
    <h1>useAttrs</h1>
    <el-button type="primary" size="small" :icon="Edit"></el-button>
    <!-- 自定义组件 -->
    <HintButton type="primary" size="small" :icon="Edit" title="编辑按钮"></HintButton>
  </div>
</template>

<script setup>
import {  Edit } from "@element-plus/icons-vue";
import HintButton from "./HintButton.vue";
</script>

优化前的子组件

解释一下<el-button :="$attrs"></el-button>是什么意思:

我们使用<el-button v-bind:type="$attrs.type" v-bind:size="$attrs.size"></el-button>这种方式可以简化为

<el-button v-bind="{ type: $attrs.type, size: $attrs.size }"></el-button>,然后又可以简化为

<el-button v-bind="$attrs"></el-button>,然后又可以简化为

<el-button :="$attrs"></el-button>,这样我们就继承了el-button所有可能的配置

<template>
  <div :title="$attrs.title">
     <el-button :="$attrs"></el-button>
  </div>
</template>

<script setup>
//vue3框架提供一个方法useAttrs方法,它可以获取组件身上的属性与事件!!!
//引入useAttrs方法:获取组件标签身上属性与事件
import {useAttrs} from 'vue';
//此方法执行会返回一个对象
let $attrs = useAttrs();
</script>

优化后的子组件

我们可以使用defineProps接收自己定义的props,由于defineProps优先级比useAttrs优先级高,因此我们定义的title会存在props里、并且useAttrs()里就没有title

<template>
  <div :title="title">
     <el-button :="$attrs"></el-button>
  </div>
</template>

<script setup>
//引入useAttrs方法:获取组件标签身上属性与事件
import {useAttrs} from 'vue';
//此方法执行会返回一个对象
let $attrs = useAttrs();
//props与useAttrs方法都可以获取父组件传递过来的属性与属性值
//但是props接受了useAttrs方法就获取不到了
let props =defineProps(['title']);
</script>

vue3框架提供的useAttrs()方法,它可以获取组件身上的属性与事件,因此我们输出useAttrs()能看到这些属性和事件

<template>
  <div>
    <h1>useAttrs</h1>
    <el-button type="primary" size="small" :icon="Edit"></el-button>
    <!-- 自定义组件 -->
    <HintButton type="primary" size="small" :icon="Edit" title="编辑按钮" @click="handler" @xxx="handler"></HintButton>
  </div>
</template>

<script setup>
//vue3框架提供一个方法useAttrs方法,它可以获取组件身上的属性与事件!!!
//图标组件
import {  Edit } from "@element-plus/icons-vue";
import HintButton from "./HintButton.vue";
//按钮点击的回调
const handler = ()=>{
  alert(12306);
}
</script>

HintButton组件里输出$attrs可以看到即显示了size属性和type属性(我们自定义的title被放在了props里了),又显示了onClick事件和OnXxx事件

image-20230618150520708
image-20230618150520708

provideinject(隔代组件通讯)

使用provideinject允许隔代获取数据,也可以隔代修改数据

img
img

父组件有一个 provide("name",value) 选项来提供数据,后代组件有一个 inject("name") 选项来开始使用这些数据

祖父组件

<template>
  <div class="box">
    <h1>我是祖父组件,我的代步工具是:{{car}}</h1>
    <hr />
    <Child></Child>
  </div>
</template>

<script setup>
import Parent from "./Parent.vue";
//vue3提供provide(提供)与inject(注入),可以实现隔辈组件传递数据
import { ref, provide } from "vue";
let car = ref("法拉利");
//祖先组件给后代组件提供数据
//两个参数:第一个参数就是提供的数据key
//第二个参数:祖先组件提供数据
provide("TOKEN", car);
</script>

父组件

<template>
  <div>
     <h2>我是父组件</h2>
     <Child></Child>
  </div>
</template>

<script setup>
import Child from './Child.vue';
</script>

子组件

<template>
  <div>
    <h3>我是孙子组件</h3>
    <p>我的代步工具是:{{car}}</p>
    <button @click="updateCar">败家</button>
  </div>
</template>

<script setup>
import {inject} from 'vue';
//注入祖先组件提供数据
//需要参数:即为祖先提供数据的key
let car = inject('TOKEN');
const updateCar = ()=>{
   car.value  = '自行车';
}
</script>

点击败家之前:

image-20230618182639773

点击败家之后

image-20230618182650313

⑧ Pinia(组合式)

安装

npm install pinia

src/sotre/index.js

// 大仓库
import { createPinia } from 'pinia'
// 创建大仓库
const pinia = createPinia()
// 对外暴露
export default pinia

src\main.js

// 引入pinia
import pinia from './sotre'
app.use(pinia)

src\store\modules\todo.js

组合式与选择式的方式区别主要是在仓库的写法上,别的没什么不同。仓库的写法非常像vue文件里组合式API的写法

//定义组合式API仓库
import { defineStore } from "pinia";
import { ref, computed,watch} from 'vue';
//创建小仓库
let useTodoStore = defineStore('todo', () => {
    let arr = ref([1,2,3,4,5]);

    const total = computed(() => {
        return arr.value.reduce((prev, next) => {
            return prev + next;
        }, 0)
    })
    //务必要返回一个对象:属性与方法可以提供给组件使用
    return {
        arr,
        total,
        updateTodo() {
            arr.value.push(1);
        }
    }
});

export default useTodoStore;

使用:

<template>
  <div>
    <div>数组:{{ todoStore.arr }}</div>
    <div>长度:{{todoStore.total}}</div>
    <button @click="updateTodo">点我更新数据</button>
  </div>
</template>

<script setup>
//引入组合式API函数仓库
import useTodoStore from "@/store/modules/todo";
let todoStore = useTodoStore();

//点击p段落去修改仓库的数据
const updateTodo = () => {
  todoStore.updateTodo();
};
</script>

⑧ 插槽

默认插槽

父组件

<template>
  <h1>我是父组件</h1>
  <Child>
      <div>子组件,你好</div>
  </Child>
</template>

<script name="App" setup>
import Child from "./Child.vue";
</script>

子组件

<template>
  <div>
    <h2>我是子组件</h2>
    <slot></slot>
  </div>
</template>
image-20230618153548174
image-20230618153548174

具名插槽

<template>
  <h1>我是父组件</h1>
  <Child>
      <template  v-slot:msg1>
        <div>子组件,你好</div>
      </template>
      <template  #msg2>
        <div>子组件,你好!!!</div>
      </template>
  </Child>
</template>

<script name="App" setup>
import Child from "./Child.vue";
</script>
<template>
  <div>
    <h2>我是子组件</h2>
    <slot name="msg1"></slot>
    <slot name="msg2"></slot>
  </div>
</template>
image-20230618153822127
image-20230618153822127

作用域插槽

作用域插槽:就是可以传递数据的插槽,子组件可以将数据回传给父组件,父组件可以决定这些回传的数据是以何种的结构或者外观在子组件内部去展示。

父组件

<template>
  <h1>我是父组件</h1>
  <Child :todos="todos">
    <template v-slot="todo">
      <h3>{{ todo }}</h3>
    </template>
  </Child>
</template>
 
<script setup>
import Child from './Child.vue'
import { ref } from 'vue'

const todos = ref([
  { id: 1, title: '吃饭', done: true },
  { id: 2, title: '睡觉', done: false },
  { id: 3, title: '打游戏', done: true },
])

</script>

子组件

<template>
  <div>
    <h3>作用域插槽</h3>
    <ul>
      <li :style="{color:item.done?'green':'red'}" v-for="(item,index) in todos">
        <!-- {{ item.title }} -->
        <!-- 作用域插槽:可以将数据传给父组件 -->
        <slot :$row="item" :$index="index"></slot>
      </li>
    </ul>
  </div>
</template>
 
<script setup>
const props = defineProps(["todos"])
</script>
image-20230702171955667
image-20230702171955667

9. 动态/懒 加载组件

9.1、动态加载组件

vue3中不能根据字符串指定组件,is的类型需要是组件类型

<template>
  <component :is="view" :data="user" />
</template>

<script setup>
import {ref} from 'vue'
import Foo from './Foo.vue'
import Bar from './Bar.vue'
const view = Math.random() > 0.5 ? Foo : Bar
// 传递props数据(其他同理)
const user = ref({username:'zhangsan'})
</script>

vue2可以使用字符串来动态指定组件,但是需要引入所有可能的组件

<template>
  <component :is="view" :data="user" />
</template>

<script>
import Foo from './Foo.vue'
import Bar from './Bar.vue'

export default {
  components: { Foo, Bar },
  data() {
    return {
      view: Math.random() > 0.5 ? 'Foo' : 'Bar',
      user: {username:'zhangsan'}
    }
  }
}
</script>

如果在 <component> 标签上使用 v-model,模板编译器会将其扩展为 modelValue prop 和 update:modelValue 事件监听器,就像对任何其他组件一样。但是,这与原生 HTML 元素不兼容,例如 <input><select>。因此,在动态创建的原生元素上使用 v-model 将不起作用:

<template>
  <!-- 由于 'input' 是原生 HTML 元素,因此这个 v-model 不起作用 -->
  <component :is="tag" v-model="username" />
  {{username}}
</template>

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

const tag = ref('input')
const username = ref('tom')
</script>

9.2、异步加载组件

基本用法

在大型项目中,我们可能需要拆分应用为更小的块,并仅在需要时再从服务器加载相关组件。Vue 提供了 defineAsyncComponentopen in new window 方法来实现此功能:

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() => {
  return new Promise((resolve, reject) => {
    // ...从服务器获取组件
    resolve(/* 获取到的组件 */)
  })
})
// ... 像使用其他一般组件一样使用 `AsyncComp`

如你所见,defineAsyncComponent 方法接收一个返回 Promise 的加载函数。这个 Promise 的 resolve 回调方法应该在从服务器获得组件定义时调用。你也可以调用 reject(reason) 表明加载失败。

ES 模块动态导入open in new window也会返回一个 Promise,所以多数情况下我们会将它和 defineAsyncComponent 搭配使用。类似 Vite 和 Webpack 这样的构建工具也支持此语法 (并且会将它们作为打包时的代码分割点),因此我们也可以用它来导入 Vue 单文件组件:

import { defineAsyncComponent } from 'vue'

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

最后得到的 AsyncComp 是一个外层包装过的组件,仅在页面需要它渲染时才会调用加载内部实际组件的函数。它会将接收到的 props 和插槽传给内部组件,所以你可以使用这个异步的包装组件无缝地替换原始组件,同时实现延迟加载。

与普通组件一样,异步组件可以使用 app.component() 全局注册open in new window

app.component('MyComponent', defineAsyncComponent(() => import('./Foo.vue')))

可以直接在父组件中直接定义它们:

<template>
  <Foo />
</template>

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

const Foo = defineAsyncComponent(() =>import('./Foo.vue'))
</script>

加载与错误状态

异步操作不可避免地会涉及到加载和错误状态,因此 defineAsyncComponent() 也支持在高级选项中处理这些状态:

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

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

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

如果提供了一个加载组件,它将在内部组件加载时先行显示。在加载组件显示之前有一个默认的 200ms 延迟——这是因为在网络状况较好时,加载完成得很快,加载组件和最终组件之间的替换太快可能产生闪烁,反而影响用户感受。

如果提供了一个报错组件,则它会在加载器函数返回的 Promise 抛错时被渲染。你还可以指定一个超时时间,在请求耗时超过指定时间时也会渲染报错组件。

【Suspense】异步组件渲染加载内容

  • 等待异步组件时渲染一些额外内容,让应用有更好的用户体验
  • 使用步骤:
    • 异步引入组件
    • 使用Suspense包裹组件,并配置好defaultfallback
import { defineAsyncComponent,Suspense } from "vue";
const Foo = defineAsyncComponent(()=>import('./Foo.vue'))
<template>
    <div class="app">
        <h3>我是App组件</h3>
        <Suspense>
          <template v-slot:default>
            <Foo/>
          </template>
          <template v-slot:fallback>
            <h3>加载中.......</h3>
          </template>
        </Suspense>
    </div>
</template>

10. Vue3新功能

10.1. 【Teleport】组件指定位置

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

如下是将弹窗放到body里的示例:

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

10.2.【全局API转移到应用对象】

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

10.3.【其他】

  • 过渡类名 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

    ......

11. 其它 API(很少使用)

11.1.【shallowRef 与 shallowReactive 】

shallowRef

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

  2. 用法:

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

shallowReactive

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

  2. 用法:

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

总结

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

11.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. 特点:

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

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

11.3.【toRaw 与 markRaw】

toRaw

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

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

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

  2. 具体编码:

    import { reactive,toRaw,markRaw,isReactive } from "vue";
    
    /* toRaw */
    // 响应式对象
    let person = reactive({name:'tony',age:18})
    // 原始对象
    let rawPerson = toRaw(person)
    
    
    /* markRaw */
    let citysd = markRaw([
      {id:'asdda01',name:'北京'},
      {id:'asdda02',name:'上海'},
      {id:'asdda03',name:'天津'},
      {id:'asdda04',name:'重庆'}
    ])
    // 根据原始对象citys去创建响应式对象citys2 —— 创建失败,因为citys被markRaw标记了
    let citys2 = reactive(citys)
    console.log(isReactive(person))
    console.log(isReactive(rawPerson))
    console.log(isReactive(citys))
    console.log(isReactive(citys2))
    

markRaw

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

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

  2. 编码:

    /* markRaw */
    let citys = markRaw([
      {id:'asdda01',name:'北京'},
      {id:'asdda02',name:'上海'},
      {id:'asdda03',name:'天津'},
      {id:'asdda04',name:'重庆'}
    ])
    // 根据原始对象citys去创建响应式对象citys2 —— 创建失败,因为citys被markRaw标记了
    let citys2 = reactive(citys)
    
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.0.0-alpha.8