Vue3

渐进式框架

  • 无需构建步骤,渐进式增强静态HTML
  • 在任何页面中作为 Web Components 嵌入
  • 单页面应用(SPA)
  • 全栈/服务器端渲染(SSR)
  • jamstack/ 静态站点生成(SSG)
  • 开发桌面端,移动端,WebGL,甚至是命令行终端界面

目前,在开发中,Vue有两大版本Vue2和Vue3,Vue 2 已于 2023 年 12 月 31 日达到终止支持时间。它不再会有新增功能、更新或问题修复。不过,它依然可以从所有现有的分发渠道 (CDN、包管理器、GitHub 等) 上获得。

Vue API 风格

Vue的组件可以按两种不同风格书写:
选项式API (Options API)
组合式API

选项式API (Options API)

在选项式 API 中,组件的配置通过多个选项对象来定义,比如 datamethodscomputedwatch 等。选项式定义的属性都会暴露在函数内部的this上,他会指向当前的组件实例,vue2使用便是这种写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<templates>
<div>
{{count}}
</div>
</templates>
<script>
export default{
name:"App",
data(){
return{
count: 0
}
},
methods:{
addCount(){
this.count++
}
},
mounted(){
console.log(this.count)
}
}
</script>

优点

  • 易于理解:对初学者友好,层次明确,更易于阅读。
  • 结构清晰:同类功能集中在一起,便于管理。

缺点

  • 逻辑复用困难:如果组件比较复杂,逻辑往往会分散到不同的选项中,导致复用和维护变得困难。
  • 类型推导 weaker:在 TypeScript 中,类型推导可能比组合式 API 差。

组合式API (Composition API)

组合式 API 使用 setup 函数来构建组件状态和行为,将相关逻辑组合在一起,适合更复杂的组件。vue3官方推荐使用组合式API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<templates>
<div>
{{count}}
</div>
</templates>
<script setup> // 这里的setup 是语法糖的写法
import {ref,onMounted} from 'vue'
const count = ref(0)
function addCount(){
count.value++
}
onMounted(()=>{
console.log(count.value)
})
</script>

优点

  • 逻辑复用:可以通过组合函数轻松复用多个组件之间的逻辑。
  • 更好的类型推导:在使用 TypeScript 时,可以更好地支持类型推导。
  • 灵活性高:开发者可以随意组织代码结构,响应式状态管理更加自由。

缺点

  • 学习曲线:对于初学者来说,理解 refreactivecomputed 等组合式 API 可能会稍显复杂。
  • 过度组合:可能导致逻辑分散,需要开发者自行管理代码结构。

环境准备

安装 Node.js

Vue 3 的官方推荐环境要求 Node.js 版本为 15 以上。尽管 Vue 3 最低支持 Node.js 版本是 12,但为了获得更好的性能和支持,建议使用更高版本,特别是 LTS(长期支持)版本。

Vue.js 需要 Node.js 环境。因此,首先需要安装 Node.js。你可以从 Node.js 官方网站 下载并安装适合你操作系统的版本。建议使用 LTS(长期支持版)版本。

创建 Vue3项目

1
$ npm init vue@latest
1
$ pnpm create vue@latest
1
$ yarn create vue@latest
1
$ bun create vue@latest

启动开发服务器

使用以下命令启动开发服务器:

1
2
3
$ cd <your-project-name>
$ npm install
$ npm run dev
1
2
3
$ cd <your-project-name>
$ pnpm install
$ pnpm run dev
1
2
3
$ cd <your-project-name>
$ yarn
$ yarn dev
1
2
3
$ cd <your-project-name>
$ bun install
$ bun run dev

插播 - npm /pnpm/ cnpm 区别

npmpnpmcnpm 是用于 JavaScript 和 Node.js 项目的包管理工具。它们之间的主要区别在于性能、存储方式和使用场景。下面将详细讲解这三者的区别及使用方法。

npm(Node Package Manager)

概述

  • npm 是 Node.js 的默认包管理工具,用于安装和管理 JavaScript 依赖包。
  • npm 会将所有依赖包安装到项目的 node_modules 文件夹中。

特点

  • 兼容性好,几乎所有 Node.js 项目都支持。
  • 缓存机制,减少重复下载的时间,但还是每次安装时会将所有依赖包下载到本地。

基本使用

  • 安装包:

    1
    $ npm install package-name  
  • 卸载包:

    1
    $ npm uninstall package-name  
  • 全局安装:

    1
    $ npm install -g package-name  
  • 查看已安装包:

    1
    $ npm list  

pnpm(Performant NPM)

pnpm 安装的依赖会在一个中央位置存储,并通过硬链接的方式在项目中引用。这种机制使得多个项目可以共享相同的依赖版本,从而节省磁盘空间和提高安装速度。然而,这种方式也可能引发一些依赖问题,

概述

  • pnpm 是一个高效的包管理工具,旨在解决 npm 的性能问题。
  • pnpm 通过使用硬链接(Hard Links)来共享依赖包,减少磁盘使用并加快安装速度。

特点

  • 更快的安装速度,尤其是在有多个项目共享同一依赖时。
  • 节省磁盘空间,多个项目可以共享同一版本的依赖包。

基本使用

  • 安装包:

    1
    $ pnpm add package-name  
  • 卸载包:

    1
    $ pnpm remove package-name  
  • 全局安装:

    1
    $ pnpm add -g package-name  
  • 查看已安装包:

    1
    $ pnpm list  

cnpm(Chinese NPM)

概述

  • cnpm 是 npm 的一个镜像,主要为了在中国境内加速 npm 包的下载。
  • 由于 npm 官方服务器在国内访问速度较慢,因此 cnpm 通过来自淘宝的镜像源提供更快的包下载体验。

特点

  • 加速 npm 包的下载速度,使用较高的稳定性。
  • 兼容 npm 的命令,可以无缝替换。

基本使用

  • 安装包:

    1
    $ cnpm install package-name  
  • 卸载包:

    1
    $ cnpm uninstall package-name  
  • 全局安装:

    1
    $ cnpm install -g package-name  
  • 查看已安装包:

    1
    $ cnpm list  

npm和pnpm区别

npmpnpm 在依赖管理和安装方式上有一些显著的区别,尤其是在全局与局部安装、依赖管理和存储结构方面。

全局与局部安装

npm

  • 使用 npm install -g package-name 命令可以全局安装包,这会将包安装在全局的 node_modules 目录中,通常位于系统的某个特定路径下(如 /usr/local/lib/node_modules)。
  • 局部安装通过 npm install package-name 命令,这会将包安装在当前项目的 node_modules 目录中。

pnpm

  • pnpm 也支持全局和局部安装,命令与 npm 相同,即通过 pnpm add -g package-name 安装全局包。
  • 局部安装时,pnpm 会将依赖包安装在当前项目的 node_modules 中,但它采用了硬链接的机制来引用存储在一个集中位置的依赖。
依赖管理和存储结构

npm

  • npm 会将每个项目的所有依赖都下载到项目的 node_modules 目录中,通常会出现嵌套的目录结构(即深层次嵌套),这可能导致很多重复的依赖,尤其是当不同项目依赖于同一库的不同版本时。
  • 每个项目的依赖是相互独立的,依赖冲突可能会导致不同版本在不同项目中共存,但这也增加了磁盘占用。

pnpm

  • pnpm 将所有的依赖存储在一个全局的集中位置(通常位于 .pnpm-store),并通过硬链接的方式引用这些依赖到各个项目的 node_modules 中。
  • 这种方法让 pnpm 节省了磁盘空间,并且提高了安装速度,因为相同的依赖不需要被重复下载。
  • pnpm 也会保持一定的项目隔离,确保每个项目都能使用其声明的依赖版本,避免全局共享的潜在冲突。
性能与效率

npm

  • 虽然 npm 在使用上非常简单,但在处理大型项目和复杂依赖时,可能会变得较慢,并且占用较多的磁盘空间。

pnpm

  • pnpm 的硬链接机制使得安装速度更快,尤其在处理多个项目时,依赖的重用大大减少了下载和安装时间。
  • 由于避免了重复文件,pnpm 在磁盘空间上的使用效率也优于 npm
锁定文件和版本冲突

npm

  • 使用 package-lock.json 文件来锁定依赖版本,确保每次安装时依赖版本的一致性。

pnpm

  • 使用 pnpm-lock.yaml 文件来锁定依赖版本,类似于 npm 的方式,但由于硬链接的特性,pnpm 在处理矛盾依赖时会更加智能。
npm/pnpm总结
  • npm 是一个相对简单和直接的包管理工具,适合快速的开发需求和较小的项目。
  • pnpm 则是一种更为高效和节省空间的选择,特别适合需要管理大量依赖的项目或多个项目之间共享依赖的场景。

npm /pnpm/ cnpm总结

  • npm:默认的包管理工具,使用广泛,但在网络较差的地方可能会慢。
  • pnpm:注重性能和磁盘空间,适合大型项目和多个项目共享的场景。
  • cnpm:是 npm 的镜像,专为中国用户加速 npm 包下载,适合网络环境较差的用户。

选择建议

  • 如果你在中国,尤其是网络环境较差,建议使用 cnpm 来加速下载。
  • 如果你更加关注性能和磁盘使用,可以考虑使用 pnpm
  • 若只需基本的包管理,使用 npm 即可。

目录结构

setup

setup概述

在 Vue 3 中,setup 是一个新的组件选项,是 Composition API 的核心部分。它在组件实例创建之前调用,并用于初始化组件的响应式状态、计算属性和方法

setup函数中的this是undefind

setup函数在所有生命周期之前执行,甚至在optionsAPI中data()更早执行,所以data能够获取setup定义的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<template>  
<div>
<h1>{{ message }}</h1>
<button @click="increment">Increment</button>
</div>
</template>

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

export default {
setup() {
// 响应式状态
const message = ref('Hello, Vue 3!');
const count = ref(0);

// 方法
const increment = () => {
count.value++;
message.value = `Count: ${count.value}`;
};

// 返回值,供模板使用
return {
message,
increment,
count,
};
},
};
</script>
  1. 响应式引用:使用 ref 创建基本类型的响应式引用,使用 reactive 创建对象的响应式状态。
  2. 方法和计算属性:可以在 setup 中定义方法,直接在模板中调用。同时,可以使用 computed 来创建计算属性。
  3. Props 和 Contextsetup 接收两个参数:propscontextprops 是组件的属性,而 context 包含了一些组件上下文的信息,例如 attrs, slotsemit

setup函数中使用 Props 和 Context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>  
<div>
<h1>{{ props.message }}</h1>
<button @click="emit('customEvent')">Emit Custom Event</button>
</div>
</template>

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

export default {
props: {
message: String,
},
setup(props, { emit }) {
return {
props,
emit,
};
},
};
</script>

setup语法糖

在script 中加 setup属性

  • setup语法糖可以不再 export
  • setup语法糖可以不再 手动return

setup(props)<script setup> 都是 Vue 3 中用于定义组件逻辑的方式,但它们在使用方式和特性上有一些区别。

setup()

  • setup(props) 是 Vue 3 Composition API 的一部分,用于定义组件的逻辑部分。它是在 Vue 组件内部使用的一个函数,接收一个参数 props,用于访问组件接收到的属性。
  • 可以在 setup 函数内部使用 Vue 3 的响应式 API,如 refreactive 等,来创建和管理组件的状态。
  • 可以定义组件的方法和计算属性。
  • 可以通过 context 参数访问组件实例、插槽、注入等。
  • 通常需要配合 defineComponentdefineProps 等函数使用,以声明组件的类型和 props

<script setup>

  • <script setup> 是 Vue 3 的语法糖,可以更简洁地定义组件的逻辑。它省略了传统的 <script> 区块中的一些冗长的代码,特别是在使用 Composition API 时。
  • <script setup> 自动处理 props 的声明和接收,不需要显式使用 setup(props) 函数来定义。
  • 可以直接使用 Vue 3 的响应式 API(如 refreactive)来创建组件的响应式数据和方法,无需返回一个对象。
  • 不需要显式导出组件的选项和方法,提高了代码的可读性和编写效率。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>  
<div>
<h1>{{ title }}</h1>
<p>{{ description }}</p>
</div>
</template>

<script setup>
import { defineProps } from 'vue';

// 定义 props
const props = defineProps({
title: {
type: String,
required: true
},
description: {
type: String,
default: 'This is a default description.'
}
});
</script>

区别

  • **传统的 setup(props)**:
    • 需要显式地定义 setup 函数来处理组件的逻辑和 props
    • 可以更灵活地控制组件的导出内容。
    • 需要手动处理 props 的声明和类型检查。
  • **<script setup>**:
    • 是一种更简洁、更直观的方式来定义组件逻辑和处理 props
    • 自动处理 props 的声明和接收,不需要手动设置 setup 函数的参数。
    • 不需要显式导出组件的选项和方法,减少了重复代码。

ref - 基本类型响应式数据

  1. 创建响应式值

    1
    const count = ref(0);  

    使用 ref 创建一个响应式引用,count 就是一个包含原始值的响应式对象。

  2. 访问和修改值

    • 访问值时使用.value属性:

      1
      console.log(count.value); // 输出当前值  
    • 修改值时也使用.value属性:

      1
      count.value = 10; // 更新 count 的值  
  3. 在模版中使用
    在 Vue 模版中,无需使用 .value,可以直接使用变量名:

    1
    <p>{{ count }}</p> <!-- 直接使用 count -->  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>  
<div>
<h1>{{ message }}</h1>
<input v-model="message" placeholder="Type something..." />
<button @click="increaseCounter">Increase Counter</button>
<p>Counter: {{ counter }}</p>
</div>
</template>

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

// 使用 ref 创建响应式数据
const message = ref('Hello, Vue 3!');
const counter = ref(0);

// 修改引用值的函数
const increaseCounter = () => {
counter.value++;
};
</script>

reactive - Object类型响应式数据

reactive 是另一个用于创建响应式对象的重要工具。与 ref 专注于单个基本类型或引用类型的响应式数据不同,reactive 主要用于创建一个响应式的对象或数组。

  1. 创建响应式对象

    1
    2
    3
    4
    const state = reactive({  
    message: 'Hello, Vue 3!',
    counter: 0,
    });

    使用 reactive 创建的 state 是一个包含多个属性的响应式对象。

  2. 访问和修改属性

    • 可以通过state.message和state.counter直接访问和修改对象的属性,而不需要使用.value:

      1
      2
      console.log(state.message); // 访问 message  
      state.counter++; // 修改 counter
  3. 在模板中使用
    在 Vue 模板中,可以直接使用对象的属性:

    1
    <h1>{{ state.message }}</h1>  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>  
<div>
<h1>{{ state.message }}</h1>
<input v-model="state.message" placeholder="Type something..." />
<button @click="incrementCounter">Increase Counter</button>
<p>Counter: {{ state.counter }}</p>
</div>
</template>

<script setup>
import { reactive } from 'vue';

// 使用 reactive 创建响应式对象
const state = reactive({
message: 'Hello, Vue 3!',
counter: 0,
});

// 修改对象属性的函数
const incrementCounter = () => {
state.counter++;
};
</script>

ref - Object类型响应式数据

ref 也可以用来定义响应式对象类型的数据。虽然 reactive 更常用来处理对象和数组,但某些特定情况下使用 ref 也是合适的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>  
<div>
<h1>{{ user.name }}</h1>
<p>Age: {{ user.age }}</p>
<input v-model="user.name" placeholder="Enter name" />
<input v-model.number="user.age" type="number" placeholder="Enter age" />
</div>
</template>

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

// 使用 ref 创建响应式对象
const user = ref({
name: 'Alice',
age: 25,
});

// 注意:修改对象的属性可以直接访问 .value
// 例如,user.value.name = "New Name" 需要在逻辑中使用
// 直接把值赋给引用的对象形式也同样有效。
</script>

操作 ref 中的对象

1
2
3
4
5
6
7
8
<script>
// 访问属性
console.log(user.value.name); // 'Alice'

// 修改属性
user.value.name = 'Bob';
user.value.age = 30;
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 结合watch实现
<script>
import { ref, watch } from 'vue';

const user = ref({
name: 'Alice',
age: 25,
});

// 监视对象属性变化
watch(
() => user.value,
(newValue, oldValue) => {
console.log('User changed:', newValue);
},
{ deep: true } // 观察深层变化
);
</script>

使用 ref 定义对象类型的响应式数据是可行的,但在处理多层结构时,reactive 通常更为直观。

ref 和 reactive

在 Vue 3 中,refreactive 是两个用于创建响应式数据的方法,适用于不同的情况和数据结构-

  1. ref

    • 用途ref 用于创建一个响应式的基本数据类型(如字符串、数字、布尔值等)或者包裹单个对象/数组。

    • 特点

      • 返回一个包含 .value 属性的对象,访问和赋值时需要使用 .value
      • 对于基本数据类型,使用 ref 可以将它们变成响应式的数据。
      • 支持深度响应,即如果你将一个对象传递给 ref,并且在这个对象中更改某个属性,视图会自动更新。
  • 示例
1
2
3
4
5
6
7
8
import { ref } from 'vue';  

const count = ref(0); // 基本数据类型的响应式引用
const user = ref({ name: 'Alice', age: 25 }); // 也可以用于对象

// 访问和修改
count.value++;
user.value.name = 'Bob';
  1. reactive
  • 用途reactive 用于创建一个响应式的对象或数组,直接操作该对象的属性。
  • 特点
    • 返回一个代理对象,访问和修改属性时,不需要使用 .value
    • 常用于创建包含多个属性的复杂对象或数组。
    • 适用于结构较复杂的状态管理。
  • 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
import { reactive } from 'vue';  

const state = reactive({
count: 0,
user: {
name: 'Alice',
age: 25,
},
});

// 访问和修改
state.count++;
state.user.name = 'Bob';
  1. 总结对比
特性 ref reactive
用途 基本数据类型和单个对象的响应式处理 复杂对象和数组的响应式处理
访问方式 使用 .value 直接访问属性
深度响应性 支持 支持
适用场景 简单状态或需要引用的单一对象 复杂对象或需要操作多个属性的状态
  1. 使用
  • 使用 ref
    • 当需要处理基本数据类型或者单个对象,并希望明确访问路径时。
    • 对于需要动态更新的单一值(如表单输入等)。
  • 使用 reactive
    • 当需要管理一个包含多个相关属性的对象时。
    • reactive 重新分配一个新对象,会失去响应式。(Object.assign())
    • 当需要对复杂状态进行操作和管理时,比如全局状态或大型组件的内部状态。
  1. 底层原理

    Vue 3 的响应式系统的底层原理主要依赖于代理(Proxy)和依赖收集机制来实现反应式数据。

    这些机制允许 Vue 追踪数据变化,并在数据变化时自动更新 DOM。

    • Proxy

    Vue 3 使用 JavaScript 的 Proxy 对象来创建响应式数据。与 Vue 2 的 Object.defineProperty 方法相比,Proxy 提供了更强大的功能,比如可以监听对整个对象的操作,包括属性的增加、删除等。这让 Vue 3 的响应式系统更加灵活和高效。

    • 响应式转换

    当你使用 reactiveref 创建响应式数据时,Vue 会将原始对象包装在一个 Proxy 对象中。Proxy 拦截对对象的访问和修改操作(如 getset):

    get 拦截:访问对象属性时,Vue 会记录这个属性的依赖关系。这样,当该属性发生变化时,Vue 能够知道哪些组件需要重新渲染。

    set 拦截:修改对象属性时,Vue 会触发更新逻辑,通知依赖于该属性的组件。

    • 依赖收集

    依赖收集是 Vue 响应式系统的一个重要概念。每当访问一个响应式属性时,Vue 会将当前活动的副作用(通常就是组件的渲染过程)记录为依赖。该依赖会被存储在一个“依赖列表”中。这样,当属性值发生变化时,Vue 会自动调用这些依赖,触发组件的重新渲染。

    依赖收集的实现是通过存储一个“依赖集合”,并在 get 拦截中为当前组件注册这个依赖。

    • 触发更新

    当响应式数据的值改变时,set 拦截会触发更新机制,标记相关的依赖为“需要更新状态”。然后,Vue 会通过调度机制将相关的组件重新渲染以反映最新的数据变化。

toRef 和toRefs

toReftoRefs 是 Vue 3 中用于处理响应式对象的两个非常实用的 API。它们主要用于在组合 API 中方便地转换响应式对象的属性,使其保持响应性。

toRef

toRef 用于将一个响应式对象中的单个属性转换为一个响应式引用(ref)。这允许你在组件中单独使用对象的某个属性,同时保持它的响应性。

用法示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup>
import { reactive, toRef } from 'vue';

const state = reactive({
count: 0,
name: 'Vue'
});

// 将 state.count 转换为一个响应式的 ref
const countRef = toRef(state, 'count');

// 现在你可以使用 countRef 作为一个响应式引用
countRef.value++; // 修改 count 的值
console.log(state.count); // 输出 1
</script>

toRefs

toRefs 用于将响应式对象的所有属性转换为响应式引用(ref)。这通常在需要将响应式对象的多个属性解构出来时非常有用,确保解构后仍然保持响应性。

用法示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<script setup>
import { reactive, toRefs } from 'vue';

const state = reactive({
count: 0,
name: 'Vue',
});

// 将整个 state 对象的所有属性转换为响应式的 refs
/*
const { count, name } = state;
相当于
let name = state.name
let count = state.count
这样的name,count 并不是响应式的,加上toRefs就能够解决
*/
const { count, name } = toRefs(state);

// 现在你可以使用 count 和 name,作为响应式属性
count.value++; // 修改 count 的值
name.value = 'Vue 3'; // 修改 name 的值

console.log(state.count); // 输出 1
console.log(state.name); // 输出 'Vue 3'
</script>

总结

  • toRef 用于将一个响应式对象中的单个属性转换为响应式引用(ref)。
  • toRefs 用于将一个响应式对象的所有属性转换为响应式引用(ref),便于解构使用。

html标签中的ref属性

作用:用于注册模版引用
用在普通标签上,获取的是DOM节点
用在组件标签上,获取的是组件实例对象

Vue2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>  
<div>
<input type="text" ref="textInput">
<button @click="focusTextInput">Focus Input</button>
</div>
</template>

<script>
export default {
methods: {
focusTextInput() {
// 在方法中通过 this.$refs 访问 ref 属性
this.$refs.textInput.focus();
}
}
};
</script>

Vue3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>  
<div>
<input type="text" ref="textInput">
<button @click="focusTextInput">Focus Input</button>
</div>
</template>

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

// 使用 ref 创建响应式引用
const textInput = ref(null);

// 在 onMounted 钩子中获取 DOM 引用
onMounted(() => {
textInput.value.focus();
});

// 方法部分
const focusTextInput = () => {
textInput.value.focus();
};
</script>

vue3 definexpose API 函数

defineExpose 是 Vue 3 中 Composition API 提供的一个函数,用于向外部暴露组件内部的方法和属性。通过 defineExpose,我们可以以一种明确的方式将组件内部的一些函数或属性暴露给外部使用。

computed - 计算属性

主要用于创建计算属性。计算属性的值基于其他响应式数据的变化动态计算,并在依赖的响应式数据改变时自动更新。computed 提供了一种高效的方式来处理复杂的逻辑,而不需要在模板中写复杂的表达式。

计算属性可以被定义为函数,其返回值会被 Vue 所缓存,直到它所依赖的响应式数据发生变化。

  1. 使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div>
{{doubleCount}}
</div>
</template>
<script setup>
import { ref, computed } from 'vue';

const count = ref(1);

const doubleCount = computed(() => {
return count.value * 2;
});

// 使用
console.log(doubleCount.value); // 输出 2
count.value++; // count 的值变为 2
console.log(doubleCount.value); // 输出 4
</script>
  1. 计算属性的特性
  • 缓存:计算属性的结果会根据其依赖进行缓存,只有当依赖的响应式数据发生变化时,计算结果才会被重新计算。这使得计算属性在性能上更优异,尤其是在依赖多个响应式数据源时。
  • 只读计算属性:默认情况下,计算属性是只读的,只能被访问,不能被直接修改。你可以通过提供一个 getter 函数来定义计算值。
  • 计算属性的 setter:如果需要,你可以为计算属性定义一个 setter,这样可以在相关的响应式数据发生变化时更新它。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<template>
<div>
{{fullName}}
</div>
</template>
<script setup>
import { ref, computed } from 'vue';

const firstName = ref('John');
const lastName = ref('Doe');

const fullName = computed({
get: () => {
return `${firstName.value} ${lastName.value}`;
},
set: (newValue) => {
// 逻辑操作
const _names = newValue.split(' ');
firstName.value = _names[0];
lastName.value = _names[1];
}
});

// 使用
console.log(fullName.value); // 输出 'John Doe'
fullName.value = 'Jane Smith'; // 通过 setter 更新值
console.log(firstName.value); // 输出 'Jane'
console.log(lastName.value); // 输出 'Smith'
</script>
  1. 使用场景
  • 数据格式化:当需要对原始数据进行格式化或处理时,计算属性是一个很好的选择。
  • 复杂逻辑:在模板中避免使用复杂的逻辑,可以将其移入计算属性中,使模板更简洁可读。
  • 依赖组合:当某个值依赖于多个响应式变量时,可以利用计算属性将它们组合起来。
  1. 与普通方法的区别

计算属性是基于其依赖的缓存的,只有当依赖发生变化时才会重新计算。而方法每次调用都会执行计算。

1
2
3
const sum = (a, b) => a + b;  
const resultMethod = () => sum(count.value, 10); // 每次调用都会重新计算
const resultComputed = computed(() => sum(count.value, 10)); // 缓存结果

watch - 监听属性

watch 选项允许你监听组件实例上的数据变化,并在数据变化时执行自定义的逻辑。这对于需要在特定数据变化时执行异步操作或者执行复杂逻辑的场景非常有用。

特点

  • watch 是一个更灵活的 API,可以精确地指定要监听的数据和在数据变化时执行的回调函数。
  • 可以监听单个响应式数据、多个响应式数据、甚至是一个 getter 函数返回的值或者一个数组中的多个数据。
  1. 监听单个响应式数据:

我们使用 ref 创建了一个名为 userInfo 的响应式变量,它是一个包含用户信息的对象。然后,我们使用 watch 函数来监听 userInfo 变量的变化。每当 userInfo 对象发生变化时,传入的回调函数就会被触发,可以在回调函数中进行相应的处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup>
import { ref, watch } from 'vue';
// 使用 ref 创建一个响应式变量
const userInfo = ref({
name: 'John Doe',
age: 30,
email: 'john.doe@example.com'
});

// 使用 watch 监听单个响应式数据 userInfo 的变化
watch(userInfo, (newValue, oldValue) => {
console.log('用户信息发生变化:', newValue);
// 可以在这里执行相应的逻辑,比如更新页面内容等
});
</script>
  1. 监听多个响应式数据

使用 ref 创建了两个响应式变量 userNameuserAge,分别表示用户的姓名和年龄。然后,我们使用 watch 函数来同时监听这两个变量的变化。传入的回调函数接收的是一个数组,包含了新旧值的对应项,我们可以根据需要处理这些数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script setup>
import { ref, watch } from 'vue';
// 使用 ref 创建多个响应式变量
const userName = ref('John Doe');
const userAge = ref(30);

// 使用 watch 监听多个响应式数据的变化
watch([userName, userAge], ([newName, newAge], [oldName, oldAge]) => {
console.log('用户信息发生变化:');
console.log(`姓名从 ${oldName} 变为 ${newName}`);
console.log(`年龄从 ${oldAge} 变为 ${newAge}`);
// 可以在这里执行相应的逻辑,比如更新页面内容等
});
</script>
  1. Vue3中watch只能监视以下四种数据:
    • ref定义的数据
    • reactive定义的数据
    • 函数返回的一个值(getter函数)
    • 一个包含了上述内容的数组
  • 监听ref数据
1
2
3
4
5
6
7
8
9
10
11
<script setup>
import { ref, watch } from 'vue';

// 使用 ref 创建响应式数据
const count = ref(0);

// 监听 count 的变化
watch(count, (newValue, oldValue) => {
console.log(`计数器从 ${oldValue} 变为 ${newValue}`);
});
</script>
  • 监听reactuve数据
1
2
3
4
5
6
7
8
9
10
11
12
13
<script setup>
import { reactive, watch } from 'vue';
// 使用 reactive 创建响应式对象
const state = reactive({
message: 'Hello',
count: 0
});

// 监听 state.count 的变化
watch(() => state.count, (newValue, oldValue) => {
console.log(`计数器从 ${oldValue} 变为 ${newValue}`);
});
</script>
  • 监听一个函数返回值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script setup>
import { ref, watch } from 'vue';

// 使用 ref 创建响应式数据
const count = ref(0);

// 监听一个函数返回的值
watch(() => {
// 模拟一个异步操作
return new Promise((resolve) => {
setTimeout(() => {
resolve(count.value);
}, 1000);
});
}, (newValue, oldValue) => {
console.log(`count 的异步值从 ${oldValue} 变为 ${newValue}`);
}
)

</script>
  • 监听数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script setup>
import { ref, watch } from 'vue';

// 使用 ref 创建响应式数据
const count = ref(0);
const message = ref('Hello');

// 监听一个数组,包含 count 和 message 的变化
watch([count, message], ([newCount, newMessage], [oldCount, oldMessage]) => {
console.log(`count 从 ${oldCount} 变为 ${newCount}`);
console.log(`message 从 "${oldMessage}" 变为 "${newMessage}"`);
});

</script>

watchEffect

  • 特点
    • watchEffect 是一个立即执行的函数,并且在其内部访问的任何响应式数据发生变化时都会重新运行。
    • 它不需要显式地指定要监听的数据,而是根据函数内部访问的响应式数据自动进行依赖追踪。
  • 适用场景
    • 当需要立即执行一段代码,并且自动追踪其中使用的响应式数据变化时,适合使用 watchEffect
    • 通常用于处理副作用,比如基于响应式数据进行 DOM 操作、发起网络请求等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
Vue 3 中使用 watchEffect 来监听响应式数据的变化,并执行相应的副作用。这个示例展示了一个简单的计数器应用,每次点击按钮增加计数值,并且自动计算其两倍值。
<template>
<div>
<p>Count: {{ state.count }}</p>
<p>Double: {{ state.double }}</p>
<button @click="increment">Increment Count</button>
</div>
</template>

<script>
import { reactive, watchEffect } from 'vue';

export default {
name: 'WatchEffectExample',
setup() {
// 使用 reactive 创建响应式数据
const state = reactive({
count: 0,
double: 0
});

// 使用 watchEffect 监听 count 的变化,计算 double
watchEffect(() => {
state.double = state.count * 2;
});

// 增加计数的方法
const increment = () => {
state.count++;
};

return {
state,
increment
};
}
};
</script>

watch和watchEffect区别总结

  • watchEffect 适合于那些无需精确控制依赖和副作用的情况,它简化了自动追踪和触发副作用的流程。
  • watch 更适合于需要精细控制的场景,可以精确指定依赖和在数据变化时执行的操作。

Props

props(属性)允许你向一个组件传递数据。props是组件间通信的重要桥梁,它使得组件可以封装和复用,同时保持了组件间数据的独立性和清晰的数据流动方向。通过合理地使用props,可以有效地组织和管理Vue应用程序的各个组件,提高代码的可维护性和可扩展性。

1
2
3
4
<!--父组件-->
<template>
<ChildComponent title="Vue 3 Props Example" :initial-count="5" />
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<!-- 使用setupAPI-->
<!--子组件ChildComponent-->
<template>
<div>
<h2>{{ title }}</h2>
<p>Message: {{ message }}</p>
<button @click="increment">Increment</button>
</div>
</template>

<script>
import { defineComponent, ref } from 'vue';
/*
defineComponent 是一个用来定义组件的工具函数,它允许开发者使用对象形式来声明和配置一个 Vue 组件。主要用途是为了提供类型推断支持,以便 Vue 能够正确地推断组件的 props 类型和方法。
*/
export default defineComponent({
props: {
title: String,
initialCount: {
type: Number,
default: 0
}
},
setup(props) {
// 使用 ref 创建响应式状态
const count = ref(props.initialCount);
const message = ref('Hello from Vue 3!');

// 定义组件方法
const increment = () => {
count.value++;
};

return {
count,
message,
increment
};
}
});
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<!--使用setup语法糖-->
<!--子组件ChildComponent-->
<template>
<div>
<h2>{{ title }}</h2>
<p>Message: {{ message }}</p>
<button @click="increment">Increment</button>
</div>
</template>

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

// props 的声明
const props = defineProps({
title: String,
initialCount: {
type: Number,
default: 0
}
});

// 响应式状态的声明
const count = ref(props.initialCount);
const message = ref('Hello from Vue 3!');

// 组件方法的定义
const increment = () => {
count.value++;
};
</script>

生命周期

在Vue 3中,组件的生命周期钩子函数与Vue 2中的基本相同,但有一些细微的变化和改进。以下是Vue 3中常用的生命周期钩子函数及其详细讲解:

Vue生命周期总共可以分为8个阶段:创建前后, 载入前后,更新前后,销毁前销毁后

生命周期 描述
beforeCreate 组件实例被创建之初
created 组件实例已经完全创建
beforeMount 组件挂载之前
mounted 组件挂载到实例上去之后
beforeUpdate 组件数据发生变化,更新之前
updated 组件数据更新之后
beforeDestroy 更改为beforeunmount 组件实例销毁之前
destroyed 更改为unmount 组件实例销毁之后
activated 更改为activated keep-alive 缓存的组件激活时
deactivated 更改为deactivated keep-alive 缓存的组件停用时调用
errorCaptured 捕获一个来自子孙组件的错误时被调用

image-20240730164037481

  1. onBeforeCreate和onCreated

    • 触发时机:在实例初始化之后,数据观测 (dataprops) 和事件配置之前被调用。

    • 作用:在这个阶段,Vue 实例的初始化工作尚未开始,因此实例上的数据、计算属性等都尚未初始化。

    • 触发时机:实例已经创建完成之后被调用。在这个阶段,实例已完成了数据观测和事件配置,但尚未挂载到DOM上。

    • 作用:通常用于进行数据请求、异步操作、事件订阅等初始化操作。

  2. setup

    setup() 函数与传统的 Options API(如 beforeCreatecreated 生命周期钩子)是互斥的,不能直接在 setup() 函数中调用 beforeCreatecreated

  3. onBeforeMount

    • 触发时机:在挂载开始之前被调用,即在render函数首次调用之前。
    • 作用:在这个阶段,Vue 实例的模板编译已完成,但尚未将生成的 DOM 渲染到页面上。
  4. onMounted

  • 触发时机:在挂载完成之后被调用,此时组件已经被渲染到页面中。
  • 作用:通常用于进行DOM操作、初始化第三方库、添加事件监听器等操作。
  1. onBeforeUpdate

    • 触发时机:数据更新之前被调用,发生在虚拟 DOM 重新渲染和打补丁之前。
    • 作用:可以在这个钩子中对更新之前的状态做一些处理,例如获取更新前的DOM状态。
  2. onUpdated

    • 触发时机:数据更新完成之后被调用,组件的 DOM 已经更新。
    • 作用:通常用于重新计算一些状态,或执行一些需要基于更新后的 DOM 的操作。
  3. onBeforeUnmount

    • 触发时机:在实例解绑之前被调用,通常是在 unmount 方法被调用时。
    • 作用:可以用于清理定时器、取消事件订阅等操作,准备组件实例被销毁。
  4. onUnmounted

    • 触发时机:在实例解绑之后被调用,组件实例被销毁。
    • 作用:用于清理组件实例相关的内容,确保不会造成内存泄漏或其他意外情况。
  5. onActivated

    • 触发时机:当包含该组件的 <keep-alive> 缓存组件被激活时调用。
    • 作用:通常用于执行一些在组件被激活时需要执行的操作,例如重新请求数据、重新初始化状态等。
  6. onDeactivated

    • 触发时机:当包含该组件的 <keep-alive> 缓存组件被停用时调用。
    • 作用:通常用于执行一些在组件被停用时需要执行的操作,例如清理定时器、取消订阅等。
  7. errorCaptured

    • 触发时机:捕获子孙组件生命周期钩子以及事件处理函数中的错误时被调用。
    • 作用:可以用于处理子组件的错误,防止错误影响到父组件和整个应用。

自定义hooks

通过 Composition API,你可以创建自定义的逻辑复用函数,通常被称为自定义 Hooks(自定义钩子)。这些自定义 Hooks 可以帮助你封装和复用逻辑代码,使得组件更加模块化和可维护。

创建自定义 Hook

编写自定义 Hook 函数:自定义 Hook 是一个普通的 JavaScript 函数,它可以使用 Composition API 中的任何功能,如 refreactivewatch 等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// useCounter.js  

import { ref } from 'vue';

// 定义一个名为 useCounter 的自定义 Hook
export function useCounter(initialValue = 0) {
const count = ref(initialValue); // 使用 ref 创建一个响应式数据

// 定义两个操作 count 的函数
function increment() {
count.value++;
}

function decrement() {
count.value--;
}

// 返回响应式数据和操作函数
return {
count,
increment,
decrement
};
}

使用自定义 Hook

在组件中使用自定义 Hook:在需要使用自定义 Hook 的组件中,通过调用自定义 Hook 函数来获取其中定义的响应式状态和方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>  
<div>
<p>Count: {{ counter.count }}</p>
<button @click="counter.increment">Increment</button>
<button @click="counter.decrement">Decrement</button>
</div>
</template>

<script setup>
import { useCounter } from './useCounter'; // 引入自定义 Hook

const counter = useCounter(); // 调用自定义 Hook 获取响应式数据和操作函数
</script>

自定义 Hook 的优势

  • 逻辑复用:将复杂的逻辑抽取到自定义 Hook 中,提高代码的复用性和可维护性。
  • 解耦逻辑:使组件更专注于界面的呈现,而将业务逻辑与状态管理分离。
  • 可测试性:自定义 Hook 是纯函数,易于进行单元测试,增强代码的可靠性和可测试性。

注意事项

  • 命名约定:Vue 社区通常将自定义 Hook 命名为 useSomething,以便于识别和遵循习惯。
  • 响应式数据:确保自定义 Hook 返回的数据是响应式的,以便 Vue 可以正确地追踪和更新视图。
  • 参数传递:自定义 Hook 可以接受参数,从而增加灵活性和通用性。

案例 - 使用自定义hook处理表单逻辑

假设我们有一个表单,需要处理表单的输入状态、验证和提交。我们可以创建一个自定义hook来处理这些逻辑,使得多个组件可以共享相同的表单逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// useForm.js  
import { ref } from 'vue';

// 定义 useForm 自定义hook
export function useForm(initialValues = {}) {
const values = ref(initialValues); // 使用 ref 来创建响应式数据

function handleChange(event) {
values.value[event.target.name] = event.target.value; // 处理输入变化
}

function handleSubmit(callback) {
return function(event) {
event.preventDefault(); // 阻止表单默认提交行为
callback(values.value); // 调用传入的回调函数处理表单数据
};
}

return {
values, // 当前表单数据的引用
handleChange, // 处理输入变化的函数
handleSubmit, // 处理表单提交的函数
};
}

我们创建了一个名为useForm的自定义hook。它返回了一个包含表单数据、处理输入变化的函数和处理表单提交的函数。,这样我们可以在多个组件中使用useFetchData来管理异步数据获取,而不需要重复编写相同的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<template>  
<form @submit="submitForm">
<input type="text" name="name" v-model="form.values.name" />
<input type="email" name="email" v-model="form.values.email" />
<button type="submit">Submit</button>
</form>
</template>

<script>
import { useForm } from './useForm'; // 导入自定义hook

export default {
setup() {
const form = useForm({ // 使用 useForm hook 初始化表单
name: '',
email: '',
});

function submitForm(formData) {
console.log('Form submitted with:', formData); // 处理表单提交逻辑,可以发送到服务器或者其他处理
}

return {
form, // 将 form 对象暴露给模板
submitForm, // 将 submitForm 函数暴露给模板
};
},
};
</script>

案例 - 使用自定义hook管理异步数据获取

使用自定义hook来管理异步数据的获取,例如从API获取数据并处理加载状态和错误处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// useFetchData.js  
import { ref, onMounted } from 'vue';

// 定义 useFetchData 自定义hook
export function useFetchData(url) {
const data = ref(null); // 用于存放获取的数据
const isLoading = ref(false); // 加载状态
const error = ref(null); // 错误状态

async function fetchData() {
isLoading.value = true; // 开始加载,设置isLoading为true
try {
const response = await fetch(url); // 发起请求
const result = await response.json(); // 解析 JSON 响应
data.value = result; // 更新数据
} catch (err) {
error.value = err; // 处理错误
} finally {
isLoading.value = false; // 不论成功或失败,加载结束,设置isLoading为false
}
}

onMounted(() => { // 在组件挂载时执行 fetchData
fetchData();
});

return {
data, // 当前获取的数据
isLoading, // 加载状态
error, // 错误信息
refetchData: fetchData, // 重新获取数据的方法
};
}

我们创建了一个名为 useFetchData 的hook来处理从API获取数据的逻辑,它返回了数据、加载状态、错误状态和重新获取数据的方法。可以在多个组件中使用 useFetchData 来管理异步数据获取,而不需要重复编写相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<template>  
<div>
<div v-if="isLoading">Loading...</div> <!-- 加载中的提示 -->
<div v-else-if="error">Error: {{ error.message }}</div> <!-- 错误信息展示 -->
<ul v-else-if="data">
<li v-for="item in data" :key="item.id">{{ item.name }}</li> <!-- 显示数据列表 -->
</ul>
</div>
</template>

<script>
import { useFetchData } from './useFetchData'; // 导入自定义hook

export default {
setup() {
const { data, isLoading, error, refetchData } = useFetchData('https://api.example.com/data'); // 使用 useFetchData hook 获取数据

return {
data,
isLoading,
error,
refetchData,
};
},
};
</script>

vue3路由

路由理解

它与 Vue.js 核心深度集成,允许开发者构建单页面应用(SPA)中的客户端路由。通过路由,用户可以在不同的URL之间进行导航,同时在不同的视图(组件)之间进行切换,而不需要重新加载页面。

基本概念

  • 路由(Route): 路由是指定义应用程序不同状态下的URL地址。例如,/home/about/products等都可以是应用中的路由。在Vue Router中,每个路由对应一个组件。
  • 路由器(Router): 路由器是 Vue Router 的实例,用于管理应用程序的路由。在创建Vue应用时,需要将路由器安装到应用中。
  • 路由视图(Router View): 路由视图是 Vue Router 中用来显示匹配到的路由组件的地方。通常在应用的主模板中使用 <router-view></router-view> 标签来指定。
  • 路由链接(Router Link): 路由链接用来在不同的路由之间进行导航。它会自动设置 href 属性,并且在当前路由与目标路由匹配时添加一个 router-link-active 类。

安装

1
npm install vue-router@next  
1
yarn add vue-router@4
1
pnpm add vue-router@4

配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { createRouter, createWebHistory } from 'vue-router';  
import Home from './views/Home.vue';

const routes = [
{ path: '/', component: Home }
// 其他路由配置
];

const router = createRouter({
history: createWebHistory(),
routes
});

export default router;

路由工作模式 - Hash 模式和History 模式

Hash 模式

在 Hash 模式下,URL 中的 hash 值(即 # 后面的部分)用来表示当前页面的状态,而不会触发浏览器向服务器发送请求。例如,URL 可能看起来像是这样:

1
http://example.com/#/about  
  • 特点
    • Hash 模式在不同浏览器中具有良好的兼容性。
    • 可以在不需要服务器支持的情况下运行,即使服务器只返回一个静态页面也可以。
  • 工作原理
    • 当 hash 值发生变化时,浏览器会触发 hashchange 事件,框架根据新的 hash 值加载对应的组件或页面内容。
  • 示例
    • Vue Router 中默认使用 Hash 模式,可以通过 createWebHashHistory() 创建基于 Hash 的路由实例。

History 模式

History 模式使用 HTML5 提供的 History API 来管理 URL,通过修改浏览器的历史记录栈来实现 URL 的变化,不再依赖于 hash。例如,URL 可能看起来像是这样:

1
http://example.com/about  
  • 特点
    • URL 更加美观,不会有 # 符号。
    • 可以通过服务器配置来支持刷新页面时的路由。
  • 工作原理
    • 当 URL 发生变化时,浏览器不会重新加载页面,而是会通过 History API 替换当前历史记录,然后由前端路由接管对 URL 的解析和页面展示。
  • 示例
    • Vue Router 可以通过 createWebHistory() 创建基于 History API 的路由实例,但需要服务器支持。

Hash 模式的注意点和问题:

  1. 兼容性和 SEO
    • SEO:搜索引擎对于 Hash 模式的页面索引效果不如 History 模式好,因为它们不会像标准 URL 那样被搜索引擎直接索引。
    • 兼容性:Hash 模式在老旧浏览器中具有很好的兼容性,但可能在某些情况下影响用户体验,例如 URL 中会带有 # 符号。
  2. 路由导航和刷新问题
    • 刷新页面:刷新页面时,Hash 模式可以保持页面状态,因为 hash 值不会发送给服务器。但是,如果用户手动更改了 URL 中的 hash 值,可能会导致路由找不到对应的页面或组件。
    • HashChange 事件:Hash 模式依赖于浏览器的 hashchange 事件来监听 URL 的变化,有时候这个事件的兼容性和触发时机可能会带来一些问题。
  3. URL 的可读性
    • Hash 模式的 URL 看起来不够直观和美观,因为会包含 # 符号和一串 hash 值。

History 模式的注意点和问题:

  1. 服务器配置
    • 服务端重定向:在使用 History 模式时,服务器需要配置以支持单页面应用的路由。例如,当用户刷新页面或直接访问某个 URL 时,服务器需要返回同一个 HTML 页面而不是 404 错误。
  2. 兼容性问题
    • 旧版浏览器:不支持 HTML5 的 History API 的旧版浏览器可能无法完全支持 History 模式,需要进行兼容性处理。
  3. 基础路径问题
    • base 配置:在使用 Vue Router 等前端路由库时,需要配置 base 选项来指定应用的基础 URL,以确保在不同的环境中都能正确解析路由。
  4. 部署时的问题
    • 服务器端配置:在部署到不同的服务器上时,需要确保服务器能正确处理所有的路由请求,否则会导致页面的错误或无法访问。

router-link的两种写法

1
2
3
4
5
6
7
8
9
10
<!--第一种字符串写法-->
<template>
<router-link active-class="active" to="/home">主页</router-link>
</template>


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

命名路由

通过name 属性为路由定义命名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { createRouter, createWebHistory } from 'vue';  
import Home from './components/Home.vue';
import About from './components/About.vue';

const routes = [
{
path: '/',
name: 'home', // 定义命名路由为 'home'
component: Home
},
{
path: '/about',
name: 'about', // 定义命名路由为 'about'
component: About
}
];

const router = createRouter({
history: createWebHistory(),
routes
});

export default router;

在模板中使用命名路由:

1
2
3
4
<template>  
<router-link :to="{ name: 'home' }">Home</router-link>
<router-link :to="{ name: 'about' }">About</router-link>
</template>

在js代码中使用命名路由:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
// 在组件或者页面中使用
import { useRoute } from 'vue-router';

const route = useRoute();

// 获取当前路由的名称
console.log(route.name); // 输出当前路由的名称

// 通过命名路由进行编程式导航
const navigateToHome = () => {
router.push({ name: 'home' });
};
</script>

嵌套路由

嵌套路由(Nested Routes)允许您在一个父路由内部定义子路由,这样可以更好地组织和管理复杂的页面结构。

定义嵌套路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { createRouter, createWebHistory } from 'vue';  

const routes = [
{
path: '/',
component: () => import('./components/Home.vue');
},
{
path: '/about',
component: () => import('./components/About.vue');
},
{
path: '/contact',
component: () => import('./components/Contact.vue');
},
{
path: '/user/:id',
component: () => import('./components/UserProfile.vue'); ,
children: [
{
path: 'posts',
component: () => import('./components/UserPosts.vue');
}
]
}
];

const router = createRouter({
history: createWebHistory(),
routes
});

export default router;

模板语法中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- App.vue -->  
<template>
<div id="app">
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link> |
<router-link to="/contact">Contact</router-link> |
<router-link :to="{ name: 'user', params: { id: 123 }}">User Profile</router-link>
<!-- 显示一级路由 -->
<router-view></router-view>
</div>
</template>

<script>
export default {
name: 'App'
};
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- UserProfile.vue -->  
<template>
<div>
<h2>User Profile</h2>
<router-link :to="{ name: 'userPosts', params: { id: $route.params.id }}">Posts</router-link>
<!--显示二级路由-->
<router-view></router-view>
</div>
</template>

<script>
export default {
name: 'UserProfile'
};
</script>

useRouteruseRoute

  • **useRouter**:用于获取 Vue Router 的全局 router 实例,通过这个实例可以进行路由的各种操作,包括导航、路由信息获取等。
  • **useRoute**:用于在组件中获取当前路由的信息对象 ,包括路径、参数、查询参数等。通常用于在组件内部获取当前路由信息的情况下使用。

query路由传参

定义路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { createRouter, createWebHistory } from 'vue-router';  
import UserProfile from '../views/UserProfile.vue';

const routes = [
{
path: '/profile',
name: 'UserProfile',
component: UserProfile,
props: (route) => ({ userId: route.query.userId }) // 通过 props 传递 userId
}
];

const router = createRouter({
history: createWebHistory(),
routes
});

export default router;

<router-link> 组件的 to 属性中通过对象形式传递 query 参数

1
2
3
4
<template>  
<!--to 对象中的 query 属性可以是一个对象,用来表示你想要传递的 query 参数-->
<router-link :to="{ path: '/profile', query: { userId: 123 }}">User Profile</router-link>
</template>

使用编程式导航 - 使用 useRouter 进行导航操作

1
2
3
4
5
<script setup>
import { useRouter } from 'vue-router';
const router = useRouter();
router.push({ path: '/profile', query: { userId: 123 }});
</script>

获取路由信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<template>  
<div>
<h2>User Profile</h2>
<p>User ID: {{ route.query.userId }}</p>
</div>
</template>

<script setup>
import { useRoute } from 'vue-router';
const route = useRoute();
</script>

<!-- props 接收-->
<template>
<div>
<h2>User Profile</h2>
<p>User ID: {{ userId }}</p>
</div>
</template>

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

const props = defineProps(['userId']); // 定义并接收 userId props

onMounted(() => {
console.log(`User ID is ${props.userId}`);
});
</script>

params路由传参

query 不同,params 是通过路由路径来传递的,通常用于表示资源标识或者唯一标识符等信息。

定义路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { createRouter, createWebHistory } from 'vue';  

const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/user/:userId', //:userId 是一个动态的路由参数占位符,它表示路由的一部分,并且可以用来传递不同的用户 ID。
component: () => import('./components/UserProfile.vue'),
props: ture, // 允许将路由参数作为 props 传递给 UserProfile 组件
}
]
});

export default router;
1
2
3
4
5
6
7
<template>  
<router-link :to="'/user/' + userId">User Profile</router-link>
</template>

<script setup>
const userId = ref(123);
</script>

使用编程式导航 - 使用 useRouter 进行导航操作

1
2
3
4
5
<script setup>
import { useRouter } from 'vue-router';
const router = useRouter();
router.push('/user/' + userId);
</script>

获取和使用 params 参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<template>  
<div>
<h2>User Profile</h2>
<p>User ID: {{ userId }}</p>
</div>
</template>

<script setup>
import { ref, onMounted } from 'vue';
import { route } from 'vue-router';

const userId = ref(route.params.userId);

onMounted(() => {
console.log(`User ID is ${userId.value}`);
});
</script>
<!-- props 接收-->
<template>
<div>
<h2>User Profile</h2>
<p>User ID: {{ userId }}</p>
</div>
</template>

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

const props = defineProps(['userId']); // 定义并接收 userId props

onMounted(() => {
console.log(`User ID is ${props.userId}`);
});
</script>

路由重定向

路由重定向(Redirect)通常是指在某些条件下,自动将用户导航到另一个指定的路由路径。

路由配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { createRouter, createWebHistory } from 'vue-router';  

const routes = [
{ path: '/', redirect: '/home' }, // 默认重定向到 /home
{ path: '/home', component: Home },
{ path: '/about', component: About }
];

const router = createRouter({
history: createWebHistory(),
routes
});

export default router;

编程式导航

1
2
3
4
5
6
7
8
9
10
<script setup>
import { useRouter } from 'vue-router';

const router = useRouter();

// 在某个条件下执行重定向
const redirectToAbout = () => {
router.replace('/about');
};
</script>

使用 <router-view> 和路由导航守卫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { createRouter, createWebHistory } from 'vue-router';  

const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: Home },
{ path: '/about', component: About }
]
});

router.beforeEach((to, from, next) => {
// 某些条件下重定向到其他路由
if (to.path === '/admin' && !isLoggedIn()) {
next('/login'); // 未登录时重定向到登录页
} else {
next(); // 继续正常导航
}
});

export default router;

vue3组件通信方式

Props

  1. 父组件向子组件传递 props
1
2
3
4
5
6
7
8
9
10
<!-- ParentComponent.vue -->  
<template>
<ChildComponent :message="dynamicMessage" />
</template>

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

const dynamicMessage = 'Hello dynamically!';
</script>
  1. 子组件接收 props
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- ChildComponent.vue -->  
<template>
<div>
<p>{{ message }}</p>
<p>Count: {{ count }}</p>
</div>
</template>

<script setup>
props: {
message: {
type: String,
required: true
},
count: {
type: Number,
default: 0
}
};
</script>

自定义事件 - emit

通过自定义事件(Custom Events)实现组件之间的通信

  1. 父组件向子组件传递事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- ParentComponent.vue -->  
<template>
<div>
<ChildComponent @custom-event="handleCustomEvent" />
</div>
</template>

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

const handleCustomEvent = (payload) => {
console.log('Received payload from child:', payload);
};
</script>
  1. 子组件触发父组件自定义事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- ChildComponent.vue -->  
<template>
<button @click="emitCustomEvent">Click me to emit custom event</button>
</template>

<script setup>
import { defineEmits } from 'vue';

const emitCustomEvent = defineEmits(['custom-event']);

const emitCustomEvent = () => {
emit('custom-event', { message: 'Hello from child!' });
};
</script>

mitt库 - 事件总线工具

mitt 库(或类似的事件总线工具)来处理 Vue 3 组件之间的通信时,可以通过引入 mitt 库并在 <script setup> 中使用它来实现自定义事件的传递和响应。

  1. 安装mitt库
1
$ npm install mitt
  1. 父组件向子组件传递事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- ParentComponent.vue -->  
<template>
<div>
<ChildComponent :emitter="emitter" />
</div>
</template>

<script setup>
//通过 mitt 创建一个事件总线 emitter。
import mitt from 'mitt';
const emitter = mitt();

// 使用 emitter.on('event-name', handler) 方法监听来自子组件的事件,并定义处理函数来处理传递的数据 payload。
emitter.on('custom-event', (payload) => {
console.log('Received payload from child:', payload);
});
</script>
  1. 子组件触发自定义事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- ChildComponent.vue -->  
<template>
<button @click="emitCustomEvent">Click me to emit custom event</button>
</template>

<script setup>
// 引入 mitt 并创建一个事件总线 emitter
import mitt from 'mitt';
const emitter = mitt();
// 使用 emitter.emit('event-name', payload) 方法来触发自定义事件,并传递需要的数据。
const emitCustomEvent = () => {
emitter.emit('custom-event', { message: 'Hello from child!' });
};
</script>

v-model 组件通信

v-model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 在input时使用v-mode,vue2,vue3一致
<inpuy v-model="a" />
// 等价于
<input :value="a" @input="(e)=>{a=e.target.value}"/>


// 当v-model 用于自定义组件键上时,vue2跟vue3会有相应区别
// vue2
<son v-model="a"/>
//等价写法
<son :value="a" @input="(a)=>{a=e}"/>
/*
解释:
v-model="a":在自定义组件 son 上使用 v-model,会将父组件的 a 值传递给子组件,并监听子组件的 input 事件更新 a 值。
:value="a":将父组件的 a 值传递给自定义组件的 value prop。
@input="(value) => { a = value }":监听自定义组件 son 的 input 事件,将子组件传递的新值 value 更新到父组件的 a 变量。
*/

// vue3
<son v-model="a"/>
//等价于
<son :modelValue="a" @update:modelValue="(e)=>{a=e}"/>
/*
解释:
v-model="a":在自定义组件 son 上使用 v-model,将父组件的 a 值双向绑定到子组件的 modelValue。
:modelValue="a":将父组件的 a 值传递给自定义组件的 modelValue prop。
@update:modelValue="(value) => { a = value }":监听自定义组件 son 的 update:modelValue 事件,将子组件传递的新值 value 更新到父组件的 a 变量。
*/

v-model 是用于实现双向数据绑定的语法糖,它能够简化父子组件之间数据传递和响应的过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>  
<!-- 父组件 -->
<div>
<h2>Parent Component</h2>
<CustomInput v-model="message" />
<p>Parent message: {{ message }}</p>
</div>
</template>

<script setup>
import { ref } from 'vue';
import CustomInput from './CustomInput.vue';

// 使用 ref 创建响应式变量
const message = ref('');

</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- CustomInput.vue -->  
<template>
<!-- 输入框绑定到父组件传递的 modelValue -->
<input type="text" :value="modelValue" @input="handleInput($event)">
</template>

<script setup>
import { defineProps, defineEmits } from 'vue';

// 定义接收的 props
const props = defineProps({
modelValue: String, // 父组件传递的值
});

// 定义可以触发的事件
const emits = defineEmits(['update:modelValue']); // 更新 modelValue 事件

// 处理输入事件,更新父组件的 modelValue
const handleInput = (event) => {
emits('update:modelValue', event.target.value); // 触发更新事件
};
</script>

透传(Attributes) $attrs - 祖孙组件传值

父组件:直接传属性

1
2
3
4
5
6
7
8
9
<template>
<div>
<ChildComponent name="John Doe" age="25" />
</div>
</template>

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

子组件:使用 $attrs 或 useAttrs() 接收属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div>
<h2>{{ name }}</h2>
<p>{{ $attrs.age }}</p>
<!-- 在模板的表达式中直接用 $attrs 访问到 -->
<p>{{ attrs.age }}</p>
</div>
</template>

<script setup>
// 在js里 使用 useAttrs() API 来访问到
import { useAttrs } from 'vue'

defineProps({
name: String
})

const attrs = useAttrs()
console.log(attrs)
</script>

注意事项

  1. 保留属性与非保留属性

    • Vue 会自动将接收到的非明确声明为 props 的属性(即非保留属性)传递给子组件的 $attrs 对象。这包括了父组件没有显式声明为 props 的所有属性。保留属性例如 classstylekey 不会被包含在 $attrs 中,而是直接应用在子组件根元素上。

    • 注意: 如果子组件没有显式地声明这些属性名,Vue 可能会将其视为非预期行为。确保子组件可以接收和处理所有可能被传递的属性是很重要的。

  2. 属性合并:

    • 如果子组件也声明了相同的 prop 名称,Vue 会优先使用 prop 覆盖 $attrs 中的相同名称的属性。这意味着如果一个属性同时出现在 props 中和 $attrs 中,Vue 将会使用 props 中的定义。
  3. 冲突和重写:

    • 在使用 $attrs 时,要特别注意不要在子组件中声明与 $attrs 中相同名称的 prop,以避免潜在的混淆和冲突。如果确实需要使用相同名称,确保他们的含义和用途是一致的,并在文档中清晰地说明。
  4. 动态绑定和响应性:

    • $attrs 是响应式的,如果父组件的属性值发生变化,子组件也会随之更新。这使得动态绑定和传递属性变得更加方便和灵活

$refs与$parent

$refs 用于: 父 - 子
$parent 用于: 子 - 父

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

$refs

  • 用途
    • $refs 是一个对象,允许你访问在组件中使用 ref 属性标记的子组件或 DOM 元素。
    • 通过 $refs,你可以直接访问子组件的属性和方法,或者操作 DOM 元素,而无需通过 props 和 events 的显式传递。
  • 注意事项
    • 使用 $refs 需要在组件渲染后才能访问,因此在组件的 mounted 生命周期钩子或之后使用 $nextTick 方法访问 $refs 是一个良好的实践。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- 父组件 -->
<template>
<!-- 使用子组件child-component -->
<child-component ref="childRef"></child-component>
<button @click="handleButtonClick1">调用子组件方法</button>
<button @click="handleButtonClick2($refs)">调用所有子组件方法</button>
</template>

<script>
import ChildComponent from './ChildComponent.vue';
import {ref} from 'vue'
// 获取dom节点
const childRef = ref()
// 触发子组件数据++
const handleButtonClick1 = ()=>childRef.count++
// 触发多个子组件数据++
const handleButtonClick2 = (refs)=>{
Object.entries(refs).forEach(([key, value]) => {
refs[key].count += 10
});
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 子组件 -->
<template>
<div>
<span>{{ count }}</span>
</div>
</template>

<script setup>
import {ref} from 'vue'
const count = ref(10)
// 把数据暴露出去
defineExpose({count})
</script>

$parent

  • 用途
    • $parent 是一个属性,允许你访问当前组件的直接父组件实例。
    • 通过 $parent,你可以直接访问父组件的属性和方法,从而在一定程度上实现组件间的数据传递和通信。
  • 注意事项
    • 使用 $parent 可能会使代码耦合度增加,并且在组件层次结构变化时可能导致调试和维护困难。推荐的替代方案是通过 props 和 events 显式传递数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!--父组件-->
<template>
<div>
<div>
{{count}}
</div>
<child-component ref="childRef"></child-component>
</div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';
import {ref} from 'vue'

const count = ref(100)

// 把数据暴露出去
defineExpose({count})
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
<!--子组件-->
<template>
<div>子组件</div>
<button @click="clickPnum($parent)">修改父组件值</button>
</template>

<script>
import {ref} from 'vue'

const clickPnum = (parent)=>{
parent.count += 10
}
</script>

总结

  • $refs 主要用于访问子组件或 DOM 元素,适合直接操作子组件或元素的场景。
  • $parent 用于访问直接父组件实例,以便在必要时进行数据传递和通信。

provide、inject

provideinject 是一对用于在父组件和子组件之间进行依赖注入的 API。它们允许你跨越多层次的组件层级向下传递数据或功能,而无需手动通过 props 一层层传递。

provide

provide 是在父组件中声明的一个方法,用于提供数据或方法,使其可以被子孙组件访问到。它通常结合 refreactive 等 Vue 3 提供的响应式 API 使用,确保数据的响应式传递。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- 父组件 -->
<template>
<div>
{{money}}¥
</div>
</template>
<script setup>
import Child from './Child.vue'
import {ref, reactive, provide} from 'vue'


const money = ref(100)

// 向后代组件提供数据
provide('money', money)
</script>

inject

inject 是在子组件中声明的一个方法,用于接收 provide 提供的数据或方法。

1
2
3
4
5
6
7
8
9
10
11
<!-- 后代组件 -->
<!-- 父组件 -->
<template>
<div>^_^</div>
</template>
<script setup>
import {inject} from 'vue'
// 获取到父代组件使用provide传过来的值
const money = inject('money','default')
</script>

特性和注意事项:

  1. 响应式数据传递:通过 provide 提供的数据,如果使用了 refreactive,在子组件中使用 inject 获取的数据也将是响应式的。
  2. 跨越多层级provideinject 不限于直接父子关系,可以在多层级的组件中进行数据传递。
  3. 非响应式数据:如果提供的数据不是响应式的(比如普通 JavaScript 对象或数组),在子组件中使用 inject 获取的也是普通的数据副本,而非响应式引用。
  4. 类型推断和注入安全:可以通过 TypeScript 或 Flow 等类型系统来确保 inject 获取到的数据类型是正确的,提高代码的安全性和可维护性。
  5. 依赖关系解耦:使用 provideinject 可以帮助解耦组件之间的依赖关系,特别是在跨组件通信和状态管理方面,有利于组件的可复用性和测试性。

pinia/Vuex

pass

slot - 插槽

插槽(slot)是一种非常强大和灵活的机制,用于在父组件中定义可插入内容的位置,以便子组件可以填充这些位置。

默认插槽 - slot

可以通过 <slot> 元素在父组件模板中创建一个插槽,以指定一个或多个位置,子组件可以向这些位置插入内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- ParentComponent.vue -->  
<template>
<div>
<h1>Parent Component</h1>
<Child>
<!-- 在子组件内部的内容将会插入到子组件的<slot>中 -->
<p>Child 插槽</p>
</Child>
</div>
</template>
<script setup->
import Child from './ChildComponent.vue'
</script>
1
2
3
4
5
6
7
8
9
10
<!-- ChildComponent.vue -->  
<template>
<div>
<h2>Child Component</h2>
<slot>
<!-- 不带内容时,显示默认内容 -->
<p>This content will be displayed if no other content is provided.</p>
</slot>
</div>
</template>
具名插槽 - Named Slots

默认插槽的命名为default

可以在父组件调用子组件时,传入多个插槽内容,并按位置渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!-- ParentComponent.vue -->  
<template>
<div>
<ChildComponent>
<!-- 默认插槽内容 -->
<p>This is some default content.</p>

<!-- 具名插槽内容header -->
<template v-slot:header>
<h1>Header Content</h1>
</template>

<!-- 具名插槽内容header(# 缩写) -->
<template #footer>
<footer>Footer Content</footer>
</template>
</ChildComponent>
</div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
components: {
ChildComponent,
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- ChildComponent.vue -->  
<template>
<div>
<!-- 默认插槽 name="default"-->
<slot></slot>

<!-- 具名插槽header -->
<slot name="header"></slot>

<!-- 具名插槽footer -->
<slot name="footer"></slot>
</div>
</template>
作用域插槽 - Scoped Slots

具名插槽数据在父组件
作用域插槽数据在子组件

作用域插槽允许子组件向父组件传递数据,同时保持对数据的作用域控制。作用域插槽可以让子组件将数据传递到父组件中,父组件则可以在其模板中使用这些数据来渲染内容。

作用域插槽的灵活性

  • 动态作用域插槽名:可以根据父组件的数据动态地决定插槽的名称。
  • 传递多个参数:可以通过作用域插槽传递多个参数或对象,以便父组件可以更复杂地处理数据。
  • 默认插槽内容:如果父组件没有提供对应的插槽内容,子组件可以定义默认的内容,确保模板的合理渲染。

定义作用域插槽

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- ChildComponent.vue -->  
<template>
<div>
<!-- 子组件使用v-bing指令给slot元素添加user数据,提供给插槽使用者使用-->
<slot :user="user"></slot>
</div>
</template>

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


const user = reactive({ name: 'John Doe', age: 30 })
</script>

使用作用域插槽

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- ParentComponent.vue -->  
<template>
<div>
<ChildComponent>
<template #default="slotProps">
<p>User name: {{ slotProps.user.name }}</p>
<p>User age: {{ slotProps.user.age }}</p>
</template>
</ChildComponent>
</div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

</script>

shallowRef 和 shallowReactive

shallowRef

shallowRef 是用来创建一个具有单个属性的响应式引用(ref),但只对顶层属性进行响应式处理。这意味着它主要用于创建包装普通 JavaScript 值的响应式引用。当你想要在组件中使用一个单一的可变值时,通常会使用 shallowRef

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div>
<span>{{count}}</span>|<span>{{cat.brand - cat.color}}</span>
</div>
</template>
<script setup>
import { shallowRef } from 'vue';

const count = shallowRef(0);
const cat = shallowRef();
// 在组件中使用,只对顶层属性进行响应式处理,所以只能修改 xxx.value
console.log(count.value); // 输出: 0 能够修改
count.value++; // 自增 count 的值
cat.value.brand = '03' // 不能被修改
cat.value = {brand:'01',color:'02'} // 可以被修改
</script>

shallowReactive

shallowReactive 则是用来创建一个浅层响应式对象。与 reactive 类似,但 shallowReactive 只为对象的第一层属性创建响应式代理,而不会递归地转换内部的所有嵌套对象和数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script setup>
import { shallowReactive } from 'vue';

const state = shallowReactive({
nested: {
count: 0
},
message: 'Hello!'
});

// 在组件中使用: 只能对第一层进行修改
console.log(state.message); // 输出: 'Hello!'
state.message = 'Hi!'; // 修改 message 的值

console.log(state.nested.count); // 输出: 0
state.nested.count++; // 自增 nested.count 的值
</script>

总结

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

readonly 和 shallowReadonly

readonly

readonly 用于创建一个深层次的只读响应式代理对象。这意味着无论是对象的属性还是嵌套对象内部的属性,都是只读的,不能被修改。它适用于那些需要完全保持不可变性的数据结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script setup>
import { reactive, readonly } from 'vue';

const original = reactive({
nested: {
count: 0
},
message: 'Hello!'
});

const readOnlyProxy = readonly(original);

// 试图修改只读代理会报错
readOnlyProxy.message = 'Hi!'; // Error: Cannot assign to read only property 'message' of object '#<Object>'

// 对象的深层次属性也是只读的
readOnlyProxy.nested.count++; // Error: Cannot assign to read only property 'count' of object '#<Object>'

// 修改original时,readOnlyProxy也会跟着改变
origina.message = 'Hi!';
</script>

shallowReadonly

shallowReadonlyreadonly 类似,但是它只创建对象的浅层次只读响应式代理。这意味着只有对象的第一层属性是只读的,而不会递归地将内部的嵌套对象和数组转换为只读代理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script setup>
import { shallowReactive, shallowReadonly } from 'vue';

const original = shallowReactive({
nested: {
count: 0
},
message: 'Hello!'
});

const shallowReadOnlyProxy = shallowReadonly(original);

// 浅层次只读响应式代理,不可以修改第一层属性
shallowReadOnlyProxy.message = 'Hi!'; // Error: Cannot assign to read only property 'message' of object '#<Object>'

// 但是对象的内部嵌套属性可以被修改
shallowReadOnlyProxy.nested.count++; // Allowed
</script>

区别和适用场景

  • readonly 适合需要完全不可变性的数据结构,无论是对象的属性还是嵌套对象内部的属性,都是只读的。
  • shallowReadonly 则适合于只需要对象的第一层属性具有只读特性的场景。内部的嵌套对象和数组仍然可以被修改,这可以提供更灵活的数据管理。

toRaw 和 markRaw

toRaw

toRaw 函数用于获取一个响应式对象的原始未代理的对象。在 Vue 3 中,通过 reactivereadonlyshallowReactiveshallowReadonly 等函数创建的对象都会被 Vue 3 的响应式系统代理。使用 toRaw 可以获取到这些对象的原始版本,即未被 Vue 代理的普通 JavaScript 对象。

1
2
3
4
5
6
7
8
9
10
11
12
<script setup>
import { reactive, toRaw } from 'vue';

const original = { count: 0 };
const state = reactive(original);

console.log(state.count); // 输出: 0

const rawObject = toRaw(state);

console.log(rawObject === original); // 输出: true,原始对象和 toRaw 返回的对象是同一个引用
</script>

markRaw

markRaw 函数用于标记一个对象,使其在不会自动变成响应式的对象。通常情况下,Vue 会自动将传入组件的数据对象转换为响应式对象,但有时我们希望某些对象保持不变,以避免不必要的响应式追踪和性能开销。这时可以使用 markRaw 来标记这些对象,确保它们不会被 Vue 自动转换为响应式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup>
import { reactive, markRaw } from 'vue';

const original = { count: 0 };
const state = reactive({
normal: markRaw(original),
reactive: original
});

console.log(state.normal.count); // 输出: 0,normal 对象被标记为非响应式
console.log(state.reactive.count); // 输出: 0,reactive 对象是响应式的

state.normal.count++; // Error: Cannot assign to read only property 'count' of object '#<Object>'
state.reactive.count++; // 响应式更新,count 值加一
</script>

区别和适用场景

  • toRaw 用于获取已经被 Vue 代理的响应式对象的原始版本,便于在需要时操作原始的非代理对象。
  • markRaw 用于标记一个对象,防止它被 Vue 自动转换为响应式对象。这在一些特定情况下非常有用,比如某些对象本身就不需要响应式特性,或者为了优化性能而避免不必要的响应式追踪。

customRef - 自定义ref

customRef 是一个函数,用于创建一个自定义的 ref。在 Vue 中,ref 是用于包装基本数据类型的响应式引用。通常,ref 可以包装原始值,使其具有响应式特性。而 customRef 则允许开发者定义一个自己的 ref,可以控制其读取和写入时的行为。

使用场景和示例

customRef 可以用于一些高级的响应式数据操作,比如延迟计算、自定义依赖追踪等。下面是一个简单的示例,展示了如何使用 customRef

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<script setup>
import { customRef } from 'vue';
// 自定义useCustomRef 响应式数据
function useCustomRef(initialValue) {
let value = initialValue;
return customRef((track, trigger) => ({
get() {
track(); // 追踪依赖数据,一旦变化进行更新
return value;
},
set(newValue) {
value = newValue;
trigger(); // 触发更新
}
}));
}

// 使用自定义 ref
const customRefValue = useCustomRef(0);

console.log(customRefValue.value); // 输出: 0

customRefValue.value = 10; // 触发更新

console.log(customRefValue.value); // 输出: 10
</script>

参数和回调函数

customRef 接受一个工厂函数作为参数,这个工厂函数会在创建 ref 时调用,并且它接受 tracktrigger 两个函数作为参数:

  • track: 这个函数被调用时,表示依赖于这个 ref 的数据被访问了,Vue 会追踪这些依赖。
  • trigger: 这个函数被调用时,表示 ref 的值发生了变化,需要触发相关的更新。

工厂函数应该返回一个对象,该对象有 getset 方法:

  • get: 当 ref 被读取时调用,用于获取当前值,并且应该调用 track 函数来追踪依赖。
  • set: 当 ref 被写入时调用,用于设置新值,并且应该调用 trigger 函数来触发更新。

自定义 ref 的应用

自定义 ref 可以用于处理一些复杂的响应式数据场景,例如:

  • 延迟初始化:可以在 get 方法中进行懒加载,只有在真正访问时才初始化数据。
  • 自定义依赖追踪:可以根据需要手动管理依赖追踪的精度,优化性能。
  • 特殊数据处理:处理一些不符合 Vue 响应式规则的数据结构。

Teleport - DOM传送

Teleport 是一个非常有用的特性,用于将组件的内容在 DOM 中的任意位置进行传送(即传送到另一个位置),而不需要重新渲染整个组件。这在处理如模态框、弹出菜单等需要在 DOM 中移动位置的情况下非常实用。

用 Teleport 需要使用 <Teleport> 组件,并且需要设置 to 属性来指定目标挂载点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>  
<!--
<Teleport> 将包裹的内容(这里是一个模态框)传送到 #modal-root 所指定的目标挂载点。这样做的好处是,即使 <Teleport> 所在的组件重新渲染,模态框的内容仍然可以在 DOM 中保持在 #modal-root 处,而不需要重新创建或销毁模态框的内容,从而提高了性能和用户体验。
-->
<Teleport to="#modal-root">
<div class="modal">
<h2>Modal Content</h2>
<button @click="closeModal">Close Modal</button>
</div>
</Teleport>
</template>

<script>
import { Teleport } from 'vue';

const closeModal = ()=>{
// 关闭模态框的逻辑
}
</script>

Teleport 的使用场景

Teleport 在以下情况下特别有用:

  • 模态框和弹出框:将模态框的内容传送到 <body> 中或其他顶层节点,确保不受组件树结构影响。
  • 菜单和下拉列表:在使用动态位置或需要显示在特定位置的情况下,可以使用 Teleport 将其传送到需要的位置。
  • 移动端应用:在移动端开发中,处理全屏或浮动组件的布局更加灵活方便。

Teleport 的注意事项

  • 目标挂载点:确保目标挂载点存在于 DOM 中,否则 Teleport 将无法正常工作。
  • 性能优化:Teleport 可以避免大量 DOM 操作和重新渲染,但仍然需要谨慎使用以避免过度复杂的 DOM 结构。

Suspense - 异步组件

用于优化异步组件加载时的用户体验。它允许我们在等待异步组件加载时展示备用内容(例如加载指示器或占位符),从而提高页面的交互感和视觉效果。

通常情况下,异步组件加载需要一定时间,为了优化用户体验,可以使用 Suspense 组件来处理加载过程中的状态。下面是一个简单的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>  
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>

<script setup>
import { Suspense, defineAsyncComponent } from 'vue';

const AsyncComponent = defineAsyncComponent(() =>
import('./AsyncComponent.vue')
);

/*
<Suspense> 组件包裹了一个异步组件 <AsyncComponent>。
#default 插槽用于展示异步组件加载完成后的内容。
#fallback 插槽用于展示在加载过程中的备用内容,例如显示 "Loading..." 的文本或加载动画。
*/
</script>

主要用途

Suspense 的主要用途包括:

  • 优化异步组件加载:在等待异步组件加载时,显示加载状态或占位符,提高用户体验。
  • 数据获取和处理:可以与 fetch 或其他异步数据获取逻辑一起使用,显示数据加载状态。

额外注意事项

  • 多个 Suspense 嵌套:可以在组件中嵌套使用多个 Suspense,以处理不同部分的异步加载状态。
  • 自定义加载指示器:可以根据具体需求自定义加载状态的显示内容和样式。
  • 错误处理:可以使用 errorCaptured 生命周期钩子来捕获异步加载过程中可能出现的错误,以便提供用户友好的错误处理。

性能考虑

使用 Suspense 可以显著减少不必要的渲染和用户界面闪烁,特别是在处理复杂的页面或需要大量异步加载的情况下。它利用了 Vue 3 中新的渲染机制,有效地管理组件的加载状态,从而提高了页面的整体性能和用户体验。

全局属性添加

在 Vue 2 中添加全局属性

在Vue 2中,全局属性包括全局组件、全局指令、全局混入以及全局方法或属性。这些全局属性通过Vue的构造函数和其原型链来实现全局的注册和访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/*
在 Vue 2 中,创建一个 Vue 应用或组件的实例通常是通过 Vue 构造函数进行的
Vue 2 中使用全局的 Vue 构造函数来管理应用实例。
*/
import Vue from 'vue';
import App from './App.vue';

new Vue({
render: h => h(App)
}).$mount('#app');

// 全局注册一个组件,可以在任何Vue组件的模板中使用
Vue.component('global-component', {
// 组件的选项
});


// 全局注册一个指令,可以在任何Vue组件的模板中使用
Vue.directive('global-directive', {
// 指令的定义
});
/*
当我们调用 Vue.component('name', {}) 或 Vue.directive('name', {}) 创建全局组件和自定义指令时,实际上是将组件或指令的定义注册到 Vue.options.components 或 Vue.options.directives 中。
Vue内部会将组件的定义对象存储在 Vue.options.components 或 Vue.options.directives 中。这个 components和directives 属性是一个对象,用于存放所有已注册的全局组件。
*/

// 全局注册一个混入对象,影响所有Vue组件
Vue.mixin({
// 混入对象的选项
});


// 添加一个全局方法或属性 ;全局方法可以在所有组件内部通过 this.$globalMethod() 进行调用
Vue.prototype.$globalMethod = function() {
// 方法的实现
};

在 Vue 3 中添加全局属性

Vue 3在全局属性的处理上与Vue 2有所不同,主要是为了更好地支持现代的ES模块化和Tree-shaking优化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/*
Vue 3 中则通过 createApp() 返回的应用实例对象来管理,这个对象是对整个应用的抽象,包含了应用的配置和方法,可以通过链式调用进行配置和扩展。
这种方式使得注册的组件和指令只在使用到它们的地方被打包,有利于减小项目的打包体积。
*/
import { createApp } from 'vue';
import App from './App.vue';

const app = createApp(App);

// 注册全局组件
app.component('global-component', {
// 组件的选项
});
/*
使用 app.component('name', {}) 方法将组件注册到应用实例上。这种注册方式不再直接操作 Vue 的构造函数,而是通过 createApp 返回的应用实例进行。
在内部,Vue 3 会将组件选项对象存储在应用实例的注册表中,用于后续的组件渲染和实例化。
*/

// 注册全局指令
app.directive('global-directive', {
// 指令的定义
});
/*
使用 app.directive('name', {}) 方法将指令注册到应用实例上。
内部会将指令定义对象存储在应用实例的注册表中,以便在模板中使用全局指令。
*/

// 注册全局混入
app.mixin({
// 混入对象的选项
});

// 添加全局方法或属性
app.config.globalProperties.$globalMethod = function() {
// 方法的实现
};

app.provide('myStore', createStore()); // 使用 provide/inject 提供全局状态

// 将应用实例挂载到DOM元素上
app.mount('#app');

主要区别和注意事项

  1. 全局属性的注册方式
    • Vue 2 中直接通过 Vue 对象来注册全局属性。
    • Vue 3 中通过应用实例 app 的方法来注册全局属性。
  2. 全局方法或属性的挂载
    • Vue 2 中通过 Vue.prototype 来挂载全局方法或属性。
    • Vue 3 中通过 app.config.globalProperties 来挂载全局方法或属性。
  3. 组件、指令和混入的注册方式
    • 在Vue 2和Vue 3中,全局组件、指令和混入的注册方式基本类似,但在Vue 3中更加推荐使用应用实例 app 的方法进行注册,以符合新的组合式API和更好的模块化特性。