Vue3 进阶
# 任务一 创建 Vite + Vue3 单页应用
Vue 3 是一个流行的 JavaScript 前端框架,用于构建单页应用程序(SPA)。
下面是一些创建 Vue 3 单页应用程序的方式:
- Vue CLI:Vue CLI 是一个命令行界面工具,用于创建和管理 Vue 应用程序。它可以自动生成一个基于 webpack 的项目模板,提供了一些内置的插件和特性,例如 Babel、ESLint、TypeScript 等。使用 Vue CLI 可以方便地创建 Vue 单页应用程序。
- 手动设置:你可以手动设置一个 Vue 3 单页应用程序。这需要你手动创建 webpack 配置文件,并安装和配置必要的插件和库,如 vue-loader、babel-loader 等。这种方式更加灵活,但需要更多的配置和知识。
- 使用 Vite:Vite 是一个现代化的构建工具,用于构建 Vue 应用程序。它使用现代化的技术和原生 ES 模块作为基础,提供了一种快速、轻量级的开发体验。你可以使用 Vite 创建一个 Vue 3 单页应用程序,只需运行几个命令即可。
在本任务中,我们将使用 Vite 来创建 Vue 3 单页应用程序,因为它能够大幅简化开发流程。不过,如果您需要更加灵活的控制和定制,手动设置也是一个不错的选择。
# 单页应用
单页应用(Single Page Application,SPA)是一种 Web 应用程序的架构模式,它使用动态加载的 HTML、CSS 和 JavaScript,以及 AJAX 和 WebSockets 等技术实现无刷新页面的单页应用。
核心思想
单页应用的核心思想是:将所有的页面都加载到一个单一的 HTML 页面中,通过 JavaScript 操作 DOM 实现页面的动态变化。
数据交互过程
当用户与应用程序交互时,JavaScript 会通过 AJAX 或 WebSockets 等技术请求后端 API 获取数据,然后在前端通过 Vue、React 等前端框架对数据进行处理,最终更新视图,从而实现页面的刷新和动态变化。
优点
单页应用的优点是:
- 可以提高页面的加载速度和用户体验,因为只需要加载一次 HTML、CSS 和 JavaScript 等资源,之后就可以在前端通过 JavaScript 动态更新页面内容,而不需要重新加载整个页面。
- 此外,单页应用还可以提高开发效率,因为前端可以采用组件化开发方式,将页面拆分为多个组件,每个组件可以独立开发和测试,最终再将这些组件拼接成完整的页面。
缺点
但是,单页应用也有一些缺点。
- 首先,由于 SPA 的内容是通过 JavaScript 动态加载的,对于 SEO 来说并不友好。
- 其次,SPA 需要处理前后端分离的问题,需要前端开发人员和后端开发人员共同协作完成。此外,单页应用需要处理浏览器历史记录和 URL 路由等问题,需要使用一些第三方库或框架来处理这些问题。
多页应用
相比之下,多页应用(MPA)的优点是:对 SEO 友好,因为每个页面都有自己的 URL 地址和内容,便于搜索引擎进行抓取和索引。此外,多页应用还可以采用传统的服务器渲染方式,在后端渲染页面,以提高页面的加载速度和 SEO 效果。
但是,多页应用的缺点是:页面切换需要重新加载整个页面,页面刷新和交互体验相对较差。此外,多页应用需要在前端和后端进行模板渲染,代码复杂度相对较高,开发效率相对较低。
因此,选择单页应用(SPA)还是多页应用(MPA)取决于项目的具体需求和情况。如果项目注重用户体验和开发效率,且对 SEO 不是特别敏感,可以选择 SPA。
为什么 SPA 对 SEO 不友好?
在这句话中,SEO 指的是搜索引擎优化(Search Engine Optimization),即通过优化网站以提高其在搜索引擎中的排名和可见性。
"对于 SEO 来说并不友好"指的是:单页面应用(SPA)对于传统的搜索引擎优化方法不太友好。
原因:
SPA 是一种通过 JavaScript 动态加载内容的网页应用,它使用 AJAX 和前端路由等技术实现页面的无刷新加载和内容的动态更新。
由于 SPA 的内容大部分是在客户端通过 JavaScript 生成和加载的,而传统的搜索引擎爬虫通常只会抓取和索引【静态 HTML 页面】,因此无法直接获取和理解 SPA 中的内容。
这导致了 SPA 在搜索引擎中的可见性和排名受到一定影响。因为搜索引擎无法像普通的 HTML 页面那样直接抓取和索引【SPA 的内容】,所以对于那些依赖搜索引擎流量的网站来说,SPA 可能无法获得足够的有机流量。
解决方法:
然而,随着搜索引擎的发展,一些搜索引擎(如Google)已经可以执行 JavaScript 并抓取 SPA 的内容。同时,也出现了一些技术和方法来解决 SPA 的 SEO 问题,例如使用预渲染技术、动态生成静态页面、使用服务器端渲染(SSR)等。这些方法可以帮助 SPA 在搜索引擎中获得更好的可见性和排名,提高其对 SEO 的友好程度。
# Vue 3 对单页应用开发的支持
Vue.js 是一个用于构建现代 web 应用的渐进式框架,它对开发单页应用提供了很多支持。我们习惯把 Vue.js 2.x 和 Vue.js 3.x 版本分别简称为 Vue2 和 Vue 3。
以下是一些 Vue 3 的特性,它们可以帮助开发者更容易地创建和维护单页应用:
- 使用了响应式数据绑定和组件化的思想,让开发者可以快速构建高效的用户界面。
- 组合式 API:这是 Vue 3 的一个新特性,它允许开发者使用函数式的方式来组织和复用组件的逻辑,而不是依赖于选项式 API 的 data、methods、computed 等属性。这样可以让组件的代码更加清晰和模块化,也可以避免命名冲突和数据依赖的问题。
- 优化的虚拟 DOM:虚拟 DOM 是 Vue 的核心特性之一,它可以让开发者使用声明式的语法来渲染界面,而不需要直接操作 DOM。Vue 3 对虚拟 DOM 进行了优化,使其更加高效和灵活。例如,Vue 3 引入了静态标记(hoisting)、片段(fragments)、模块化运行时(tree-shaking)等技术,来减少不必要的渲染和内存占用。
- 支持 TypeScript:TypeScript 是一种在 JavaScript 基础上增加了类型检查和其他特性的编程语言,它可以提高代码的可读性和可维护性,也可以避免一些常见的错误。Vue 3 完全支持 TypeScript,不仅在源码层面使用了 TypeScript,还提供了完善的类型声明文件,让开发者可以在编辑器中享受到智能提示和错误检测的功能。
- 更多的内置组件和指令:Vue 3 提供了一些新的内置组件和指令,来增强 SPA 的交互和功能。例如,
<teleport>
组件可以让开发者将子组件渲染到任意位置,<suspense>
组件可以让开发者处理异步组件的加载状态。
总之,Vue3+vite 是一种非常适合开发单页应用的技术栈,它可以让开发者享受到最新的前端技术和最佳的开发体验。
# 安装 Node.js
创建 vite+Vue3 项目需要安装 16.0 或更高版本的 Node.js。您可以在命令行中运行 node -v
命令来检查您是否已安装了 node 环境。如果没有安装或版本不对的话,可以按照下面的步骤进行安装。
# 一、下载 Node.js
下载地址:https://nodejs.org/zh-cn/download/
下载长期维护版:
# 二、安装 Node.js
以 Windows 系统为例,下载 Node.js 安装包后,双击一路安装即可。安装过程,除了 Node.js 外,还会一并安装 npm 包管理器。启动 Node.js 和 npm 的命令分别是:
node
npm
2
# 三、测试安装是否成功
安装完成后,注意测试安装是否成功,测试命令:
node -v
npm -v
2
如果显示出 Node.js 和 npm 的版本号,表明安装成功。否则,需要检查环境变量的配置。
# 创建 Vite+Vue3 单页应用
Vue CLI 和 Vite 都是用于创建 Vue 项目的构建工具。
Vue CLI 是一个基于 Webpack 的构建工具,它提供了一个完整的项目脚手架,包括开发服务器、热重载、代码分割、ESLint 等等。
Vite 是一个基于 ES 模块的构建工具,它使用浏览器原生的 ES 模块加载器来提供快速的开发体验。Vite 提供了零配置的开发环境,无需安装和配置 webpack 等复杂的工具,只需一个 vite.config.js
文件即可启动一个本地服务器,因此它可以更快地启动和重载。Vite 支持 typescript、css 预处理器、jsx 等常用的功能,并且在生产环境中也可以更快地构建。
# 一、创建项目
使用 npm
或 pnpm
命令均可创建 Vite+Vue3 单页应用:
npm init vue@latest
或
pnpm create vue@latest
2
3
这一命令将会安装并执行 create-vue,它是 Vue 官方的项目脚手架工具。
建议采用 pnpm create vue@latest
进行创建。如果没有安装 pnpm
的话,可以先安装 pnpm
,安装命令为:
npm install pnpm -g
这里使用 pnpm create vue@latest
命令创建 Vue3 项目的过程中,您将会看到一些如 TypeScript 和测试支持之类的可选功能提示:
可按需要进行选择安装,这里我们选择 TypeScript
、Vue Router
、Pinia
,其余的功能可以留待以后需要时再进行安装。
✔ Project name: … <your-project-name>
✔ Add TypeScript? … No / Yes -- y
✔ Add JSX Support? … No / Yes
✔ Add Vue Router for Single Page Application development? … No / Yes -- y
✔ Add Pinia for state management? … No / Yes -- y
✔ Add Vitest for Unit Testing? … No / Yes
✔ Add an End-to-End Testing Solution? › No
✔ Add ESLint for code quality? … No / Yes
Scaffolding project in ./<your-project-name>...
2
3
4
5
6
7
8
9
10
如果我们在上面第一步 Project name 处输入项目名称 RealWorld-frontend 的话,项目创建成功后,会显示如下命令序列,提示您如何进行后续操作。
cd realworld-frontend
pnpm install
pnpm dev
2
3
项目结构如下:
├── public/ // 公共资源目录
│ └── favicon.ico // 网站图标
├── src/ // 项目源码目录
│ ├── assets/ // 静态资源目录(如图片、字体等)
│ ├── components/ // 组件目录
│ ├── router/ // 路由目录
│ ├── stores/ // 状态管理目录
│ ├── views/ // 视图目录
│ ├── App.vue // 根组件
│ └── main.ts // 项目入口文件
├── .gitignore // Git 忽略文件列表
├── env.d.ts。 // 为用户自定义环境变量提供 TypeScript 智能提示
├── index.html // 入口 HTML 文件
├── package.json // 项目配置文件
├── README.md // 项目说明文件
├── tsconfig.json // TypeScript 配置文件
├── tsconfig.node.json // 为 Node.js 环境提供单独的TypeScript编译选项
└── vite.config.js // Vite的配置文件,用于配置开发环境和生产环境的各种选项
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 二、安装依赖
按命令序列的前两条命令进行操作:
cd realworld-frontend # 进入项目文件夹
pnpm install # 安装依赖(推荐)
2
依赖安装完成后,项目文件夹 RealWorld-frontend 中将多出一个子文件夹 node_modules:
├── node_modules/ // 第三方依赖包目录
├── public/
├── src/
│ ...
2
3
4
node_modules 文件夹里面就是按照 package.json 的指示下载的各种依赖。
# 运行单页应用
# 一、启动项目服务端
项目服务端先启动起来,才能对浏览器端提供网页服务。启动命令为:pnpm dev
或 npm run dev
启动成功后,屏幕显示:
VITE v4.2.1 ready in 416 ms
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
➜ press h to show help
2
3
4
5
表明项目服务端已经运行在 5173 端口,等待浏览器的访问。
# 二、从浏览器访问应用
按住 Ctrl 键,并用鼠标单击链接 http://localhost:5173/
,将在浏览器中打开应用的首页,内容如下图所示。
# 任务二 了解组合式 API
当使用 Vue 2 的 Options API 进行大型项目开发时,可能会遇到代码复杂度高、难以维护的问题。为了解决这个问题,Vue 3 提供了一种新的组件编写方式:组合式 API
和 setup() 函数
。
组合式 API
允许按照逻辑组织代码,而不是按照生命周期函数或选项来组织代码,从而提高了代码的可读性和可维护性。setup() 函数
在组件创建和挂载之前运行,用于设置组件的响应式数据、计算属性、方法和监听器等。在setup() 函数
中,可以使用 Vue 3 的组合式 API 如 ref、reactive、computed、watchEffect 等来实现组件的逻辑,从而更好地组织项目代码。
# 使用 setup 函数
Vue 3 中的 setup() 函数
是使用组合式 API
编写组件的入口函数,它会在组件实例创建之前被调用。
在 setup() 函数
内部,可以通过使用 Vue 3 提供的一系列组合式 API
来定义组件的响应式数据、计算属性、方法等等,并将它们返回给组件的模板中使用。setup() 函数
返回的对象会被用作组件实例的数据对象,可以在模板中直接访问。
以下是一个简单的例子,演示了如何使用 Vue 3 的 setup() 函数
和 reactive()
来创建一个响应式的计数器:
<template>
<div>
<p>当前计数:{{ state.count }}</p>
<button @click="increment">+ 1</button>
</div>
</template>
<script>
import { reactive } from 'vue'
export default {
setup() {
const state = reactive({
count: 0
})
const increment = () => {
state.count++
}
return {
state,
increment
}
}
}
</script>
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
在上述代码中,
- 我们首先引入了 Vue 3 提供的 reactive 函数,用于创建响应式对象。
- 在 setup() 函数中,我们使用 reactive 函数创建了一个名为 state 的响应式对象,包含了一个 count 属性。
- 接着,我们定义了一个名为 increment 的方法,用于将 state 对象的 count 属性 +1。
- 最后,我们将 state 和 increment 作为对象返回,供模板中使用。
由于使用了 reactive 函数,state 对象是响应式的,因此它的值会自动更新到模板中。当state.count 属性更新时,模板中的数据也会更新。
同样的响应式也可以通过 ref()
实现:
<template>
<div>
<p>当前计数:{{ count }}</p>
<button @click="increment">+1</button>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
const increment = () => {
count.value++
}
return {
count,
increment
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
由于 setup() 函数
的执行时机早于 Vue 2 中的 created 钩子函数,可以在其中进行一些更早的初始化操作,提高组件的性能。
总之,setup() 函数
是 Vue 3 中使用组合式 API
编写组件的重要入口函数,通过它可以更加自由地组织组件的逻辑,使得代码更加清晰易懂。
# 使用 script setup
<script setup>
是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖,用于简化组件的编写和组织。- 当同时使用 SFC 与组合式 API 时该语法是默认推荐。它允许在一个
<script>
标签内使用Composition API
,而不需要定义setup()
函数或返回语句。 - 它还提供了一些额外的功能,如自动导入和导出组件属性、响应式声明、类型推断等。
使用 <script setup>
可以让组件的代码更加清晰和高效。相比于普通的 <script>
语法,它具有更多优势:
- 更少的样板内容,更简洁的代码。
- 能够使用纯 TypeScript 声明 props 和自定义事件。
- 更好的运行时性能 (其模板会被编译成同一作用域内的渲染函数,避免了渲染上下文代理对象)。
- 更好的 IDE 类型推导性能 (减少了语言服务器从代码中抽取类型的工作)。
下面是一个使用 <script setup>
的示例组件:
<template>
<div>
<h1>{{ title }}</h1>
<button @click="increment">Count: {{ count }}</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
// 自动导出为组件属性
const title = 'Hello Vue 3'
// 响应式声明
const count = ref(0)
// 普通函数
function increment() {
count.value++
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
用 <script setup>
语法糖改写目标 1 的代码:
<template>
<div>
<p>当前计数:{{ count }}</p>
<button @click="increment">+1</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
const increment = () => {
count.value++
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
可见,在 <script setup>
中 setup()
函数不用再写了,定义的变量和函数会自动暴露给模板,并且不需要再用 return 语句将它们导出。这样可以更简洁地编写 Vue 组件。
# 引入 TypeScript
在创建 Vue 3 + Vite 项目时,如果已经选择并安装了 “Add TypeScript”,那么就可以采用 TypeScript 编写组件,目标 2 中的代码用 TypeScript 可以改写为:
<template>
<div>
<p>当前计数:{{ count }}</p>
<button @click="increment">+1</button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const count = ref<number>(0)
const increment = () => {
count.value++
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script setup>
中的脚本可以使用 TypeScript 或 JavaScript 编写。这里我们写成<script setup lang="ts">
,其中lang="ts"
是告诉 Vue 3,该组件的脚本使用 TypeScript 编写。使用 TypeScript 可以提供更好的类型检查和代码提示,以及更好的代码可维护性和可读性。根据在 lang 属性中指定的语言类型,Vue 3 会相应地对组件进行编译处理。如果没有指定lang 属性
,则默认使用 JavaScript 编写脚本。- 在这里,
ref<number>(0)
表示将数字类型的值 0 转换为响应式对象。<number>
是 TypeScript 中的类型注解,用于指定 count 的类型为数字。这样在开发过程中,如果不小心给 count 赋予非数字类型的值,TypeScript 编译器就会提示错误。
# 响应式
所谓响应式,就是当我们修改数据后,可以自动做某些事情;对应到组件的渲染,就是修改数据后,能自动触发组件的重新渲染。这其实是一种状态驱动,让用户界面随着数据变化而自动更新的编程范式。
响应式的核心思想是将数据和视图绑定在一起,当数据发生变化时,视图也会相应地变化,而不需要手动操作 DOM 元素。
响应式的优点是可以简化前端开发的复杂度,提高用户体验和性能,避免不必要的重绘和重排。响应式的实现方式有多种,比如使用观察者模式、发布订阅模式、虚拟 DOM 等。响应式的代表框架有 React、Vue.js、Angular 等。
# Vue 3 的响应式原理
Vue 3 的响应式原理是:基于 ES6 的 Proxy 和 Reflect 特性实现的,它可以对对象的各种操作进行拦截和处理,从而实现数据和视图的双向绑定。这个方法的本质是劫持了数据对象的读写。当我们访问数据时,会触发 getter 执行依赖收集;修改数据时,会触发 setter 派发通知。
Vue 3 的响应式原理主要包括以下几个步骤:
创建一个响应式对象,使用 Proxy 对目标对象进行代理,拦截它的 get 和 set 操作,同时使用 Reflect 对操作进行反射,保证原对象的行为不受影响。
在 get 操作中,收集依赖,即将当前的渲染函数或者副作用函数添加到目标对象的依赖集合中,这样当目标对象发生变化时,就可以通知这些函数进行更新。
在 set 操作中,触发更新,即遍历目标对象的依赖集合,调用其中的函数,让它们重新执行,从而更新视图或者产生其他效果。
重复以上步骤,实现数据和视图的动态响应。
Vue 3 的响应式原理相比于 Vue 2 的 Object.defineProperty
方式,有以下优势:
- 可以拦截更多的操作,如 delete、has、ownKeys 等,提高了灵活性和兼容性。
- 可以对数组和嵌套对象进行响应式处理,无需额外的处理逻辑。
- 可以避免原对象被污染,保持了数据的纯净性。
- 可以提高性能,减少了不必要的依赖收集和更新触发。
# Vue 3 的响应式 API
Vue 3 的响应式 api 是一组新的功能,用于创建和管理响应式数据。
它们可以让我们在组件中更灵活地组织和复用逻辑,也可以与其他 Vue 特性(如计算属性、侦听器、生命周期钩子等)配合使用。
Vue 3 的响应式 api 主要包括以下几个部分:
- ref:用于创建一个响应式的值,可以是基本类型或对象类型。ref 返回一个包含
.value
属性的对象,通过这个属性可以访问或修改原始值。ref 也可以用于绑定模板中的 DOM 元素,通过.value
获取元素的引用。 - reactive:用于创建一个响应式的对象,可以是普通对象、数组或类实例。reactive 返回一个代理对象,通过这个对象可以访问或修改原始对象的属性。reactive 不会改变原始对象的结构和身份,只是在访问和修改时触发响应式效果。
- computed:用于创建一个响应式的计算属性,可以根据其他响应式数据(如ref或reactive)动态地计算出一个值。computed 返回一个包含
.value
属性的对象,通过这个属性可以访问计算出的值。computed 也可以接受一个包含 get 和 set 函数的对象,实现可写的计算属性。 - watch:用于监听一个或多个响应式数据(如ref或reactive)的变化,并执行相应的回调函数。watch 可以接受一个源数据或一个返回源数据的函数作为第一个参数,以及一个回调函数作为第二个参数。watch 也可以接受一个包含多个源数据和回调函数的数组作为参数,实现同时监听多个数据。
- watchEffect:用于监听一个副作用函数中使用到的所有响应式数据的变化,并重新执行该函数。watchEffect 接受一个副作用函数作为参数,该函数会在首次调用时立即执行,并在后续任何依赖数据变化时重新执行。
- toRefs:用于将一个响应式对象(如reactive)转换为一个普通对象,该对象的每个属性都是一个 ref,指向原始对象的对应属性。toRefs 可以保持原始对象的响应性,同时避免解构时丢失响应性。
- toRaw:用于将一个响应式对象(如reactive)转换为原始对象,取消其响应性。toRaw 可以在需要直接操作原始对象而不触发响应式效果时使用。
# 任务三 用 reactive 设置响应式数据
# reactive 函数的原理及注意事项
在 Vue 3 中,我们可以使用 Composition API 中的 reactive 函数来构建响应式对象。
这个函数接收一个普通对象作为参数,并返回一个响应式代理对象。这个代理对象包含了原始对象的所有属性,并且这些属性都是响应式的。当我们访问代理对象的属性时,实际上是在访问原始对象的属性,因此会触发 getter 执行依赖收集。当我们修改代理对象的属性时,实际上是在修改原始对象的属性,因此会触发 setter 派发通知。这种方式比 Vue 2.x 中使用的 Object.defineProperty()
实现响应式更加高效和灵活。
在使用 reactive 时,需要注意以下几点:
只有在组件的 setup 函数中才能使用 reactive 函数。
只有通过 reactive 函数创建的对象才是响应式数据对象,直接修改普通 JavaScript 对象的属性不会触发视图更新。
reactive 函数只能处理对象类型数据,如果需要处理其他类型数据,可以使用 ref 函数。
reactive 返回的是一个 Proxy 对象,而不是原始对象。
reactive 返回的 Proxy 对象可以直接访问原始对象的属性和方法。
reactive 返回的 Proxy 对象可以直接修改原始对象的属性和方法。
reactive 返回的 Proxy 对象可以直接监听原始对象的变化。
在模板中使用响应式对象的属性时,需要使用
两个花括号
语法来绑定数据,例如{{ state.list }}
1响应式对象的属性需要在对象创建时就定义好,不能在后面动态添加或删除属性。如果需要动态添加或删除属性,可以使用 reactive 函数创建一个新的响应式对象。
在开发时,建议使用
toRefs
函数将响应式对象转换成普通对象的引用。这可以使代码更加清晰易懂,同时也能避免一些潜在的问题。
# reactive 的使用举例
# 一、reactive 作用于普通的 JavaScript 对象
在 Vue 3 中,reactive 函数可以将一个普通的 JavaScript 对象转换成一个响应式数据对象,使得对象的属性可以被监听到变化并且自动更新视图。下面是 reactive 作用于对象的示例代码及注意事项:
<template>
<button @click="changeName">修改name</button><br>
<button @click="changeAge">修改age</button><br>
<button @click="changeGender">修改gender</button><br>
{{ state.person.name }} - {{ state.person.age }} - {{ state.person.gender }}
</template>
<script setup>
import { reactive } from 'vue'
const state = reactive({
person: {
name: 'Tom',
age: 18,
gender: 'male'
}
})
// 修改响应式对象的name
function changeName() {
state.person.name = "Linda";
console.log(state.person.name) // 输出 Linda
}
// 修改响应式对象的age
function changeAge() {
state.person.age++
console.log(state.person.age) // 输出 19
}
// 修改响应式对象的gender
function changeGender() {
state.person.gender = "female"
console.log(state.person.gender) // 输出 female
}
</script>
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
注意事项:
- 对于一个普通的 JavaScript 对象,通过 reactive 函数可以将其转换成一个响应式数据对象,使得对象的属性可以被监听到变化并且自动更新视图。
- 只有通过 reactive 函数创建的对象才是响应式数据对象,直接修改普通 JavaScript 对象的属性不会触发视图更新。
- 响应式对象的属性需要在对象创建时就定义好,不能在后面动态添加或删除属性。如果需要动态添加或删除属性,可以使用 reactive 函数创建一个新的响应式对象。
- 响应式对象的属性值如果是一个对象,需要使用 reactive 函数将其转换成响应式对象,否则其属性变化不会触发视图更新。
# 二、reactive 作用于数组
在 Vue 3 中,reactive 函数也可以将一个普通的 JavaScript 数组转换成一个响应式数组,使得数组元素的变化可以被监听到并且自动更新视图。下面是一个 reactive 作用于数组的示例代码:
<template>
<button @click="change">修改元素</button><br>
<button @click="add">添加元素</button><br>
<button @click="del">删除元素</button><br>
{{ state.list }}
</template>
<script setup>
import { reactive } from 'vue'
const state = reactive({
list: ['apple', 'banana', 'orange']
});
// 修改响应式数组的元素
function change() {
state.list[0] = 'pear'
console.log(state.list) // 输出 Proxy(Array) {0: 'pear', 1: 'banana', 2: 'orange'}
}
// 向响应式数组中添加元素
function add() {
state.list.push('grape')
console.log(state.list) // 输出 Proxy(Array) {0: 'pear', 1: 'banana', 2: 'orange', 3: 'grape'}
}
// 从响应式数组中删除元素
function del() {
state.list.splice(1, 1)
console.log(state.list) // 输出 Proxy(Array) {0: 'pear', 1: 'orange', 2: 'grape'}
}
</script>
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
注意事项:
- 响应式数组的元素操作需要使用 JavaScript 数组的操作方法,如 push()、pop()、shift()、unshift()、splice() 等。
- 对响应式数组的操作会自动触发视图更新,无需手动调用。
- 如果直接修改响应式数组的长度,例如
state.list.length = 2
,则会导致视图无法更新,需要使用数组的操作方法进行操作。 - 对响应式数组进行操作时,需要保证操作前后引用的是同一个数组对象,否则会导致视图无法更新。例如,不要使用
state.list = []
的方式清空数组,而应该使用state.list.splice(0, state.list.length)
的方式清空数组。
# 任务四 用 ref 设置响应式数据
# ref 的原理及注意事项
在 Vue 3 中,ref 是一个函数,用于创建一个响应式数据对象,它可以将一个普通的 JavaScript 值
转换成一个响应式数据对象,并提供了对该数据对象的访问和修改方法,使得修改该对象的值可以自动触发视图更新。(包裹一个值,这个值可以是对象类型也可以是基本类型,最终都会被转化成一个响应式对象)
ref 是基于 reactive 实现的。具体来说,ref 函数接受一个普通值作为参数,内部会使用 reactive 创建一个响应式对象来包裹这个值。当我们通过 .value
访问 ref 对象时,实际上是访问这个内部的响应式对象的值。
举个例子,以下代码展示了 ref 的实现原理:
import { reactive } from 'vue'
function ref(value) {
const obj = reactive({ value })
return {
get value() {
return obj.value
},
set value(newValue) {
obj.value = newValue
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
上面的代码中,ref 函数接受一个值作为参数,并使用 reactive 函数创建了一个包含该值的响应式对象 obj,然后返回一个包含 get 和 set 方法的对象,这两个方法用来获取和设置 obj.value
属性的值。
当我们访问 ref 对象时,实际上是在访问这个对象的 value 属性,这个属性通过 get
方法返回 obj.value
,当我们修改 ref 对象的值时,实际上是在修改这个对象内部响应式对象的值,这个操作通过 set 方法实现。
因此,可以说 ref 是基于 reactive 实现的,它们都是 Vue 3 中用于创建响应式数据对象的重要函数。
在 Vue 3 中既然有了 reactive,为何还要 ref 呢?
当我们只想让某个变量实现响应式的时候,采用 reactive 就会比较麻烦,因此 Vue 3 提供了 ref 方法进行简单值的监听,但并不是说 ref 只能传入简单值,它的底层是 reactive,所以 reactive 有的,ref 都有。
请牢牢记住:
- ref 本质也是 reactive,
ref(obj)
等价于reactive({value: obj})
。 - 在 Vue 组件的
<script>...</script>
标签中使用 ref 的值,必须通过.value
获取。 - 在 Vue 组件的
<template>...</template>
标签中直接使用 ref 的值,不用也不能通过.value
获取。
# ref 的使用举例
我们将前面用 reactive 实现的代码改为 ref 再实现一次:
<template>
<button @click="changeName">修改name</button><br>
<button @click="changeAge">修改age</button><br>
<button @click="changeGender">修改gender</button><br>
{{ name }} - {{ age }} - {{ gender }}
</template>
<script setup>
import { ref } from 'vue';
// 包裹基本类型
const name = ref('Tom');
const age = ref(18);
const gender = ref('male');
// 修改ref对象的name
function changeName() {
name.value = "Linda";
console.log(name.value); // 输出 Linda
}
// 修改ref对象的age
function changeAge() {
age.value++;
console.log(age.value); // 输出 19
}
// 修改ref对象的gender
function changeGender() {
gender.value = "female";
console.log(gender.value); // 输出 female
}
</script>
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
在上面的代码中,我们使用 ref 分别创建了三个响应式数据 name、age 和 gender,它们的初始值分别为 'Tom'、18 和 'male'。同时,我们也将修改数据的三个函数 changeName、changeAge 和 changeGender 进行了相应的修改,使用 ref 对象的 .value
属性来修改值。
通过这种方式,我们可以将原本使用 reactive 实现的代码转换为使用 ref 实现的代码,这样做可以使代码更加简洁和直观,同时也方便我们进行数据的管理和修改。
前面说过,ref 函数并不只限于定义简单值,它仍然可以作用于对象。现在我们用
ref(obj)
的方式改写刚才的代码:
<template>
<button @click="changeName">修改name</button><br>
<button @click="changeAge">修改age</button><br>
<button @click="changeGender">修改gender</button><br>
{{ person.name }} - {{ person.age }} - {{ person.gender }}
</template>
<script setup>
import { ref } from 'vue'
const person = ref({
name: 'Tom',
age: 18,
gender: 'male'
});
// 修改ref对象的name
function changeName() {
person.value.name = "Linda"
console.log(person.value.name) // 输出 Linda
}
// 修改ref对象的age
function changeAge() {
person.value.age++;
console.log(person.value.age) // 输出 19
}
// 修改ref对象的gender
function changeGender() {
person.value.gender = "female"
console.log(person.value.gender) // 输出 female
}
</script>
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
在修改 person 对象的属性时,需要使用 person.value
来访问内部的对象。
# 任务五 toRef() 与 toRefs()
# toRef() 的使用场景
# 1 问题引入
先来看看下面的代码有什么问题:
<template>
<div>
<p>Name: {{ user.name }}</p>
<p>Age: {{ user.age }}</p>
<p>Gender: {{ user.gender }}</p>
<button @click="changeInfo">Change Info</button>
</div>
</template>
<script setup lang="ts">
interface User {
name: string;
age: number;
gender: string;
}
const user: User = {
name: 'John',
age: 25,
gender: 'male',
}
function changeInfo() {
user.name = 'Mary'
user.age = 30
user.gender = 'female'
}
</script>
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
这段代码存在的问题是:在 Vue3 中使用了非响应式的对象 user。由于 user 对象是普通的 JavaScript 对象,而不是 Vue 的响应式对象,所以当 changeInfo()
函数修改了 user 对象的属性时,界面不会更新。
# 2 解决办法
解决方法是使用 Vue 的响应式对象来代替普通 JavaScript 对象,例如使用 reactive()
函数来创建一个响应式对象。可以将 user 对象改为以下代码:
<template>
<div>
<p>Name: {{ user.name }}</p>
<p>Age: {{ user.age }}</p>
<p>Gender: {{ user.gender }}</p>
<button @click="changeInfo">Change Info</button>
</div>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
interface User {
name: string;
age: number;
gender: string;
}
const user = reactive({
name: 'John',
age: 25,
gender: 'male',
})
function changeInfo() {
user.name = 'Mary'
user.age = 30
user.gender = 'female'
}
</script>
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
这段代码确实实现了页面的同步更新,但模板里反复使用 user.前缀
显得有些冗余,如何减少模板中重复使用 user.前缀
的情况,从而提高了代码的可读性呢?
# 3 继续改进
toRef 是 Vue3 中的一个工具函数,用于将响应式对象上的一个属性转换为一个单独的 ref 对象。我们尝试用它来减少模板中 user. 前缀的重复使用,代码如下:
<template>
<div>
<p>Name: {{ name }}</p> // 少了前缀
<p>Age: {{ age }}</p>
<p>Gender: {{ gender }}</p>
<button @click="changeInfo">Change Info</button>
</div>
</template>
<script setup lang="ts">
import { reactive, toRef } from 'vue'
interface User {
name: string;
age: number;
gender: string;
}
const user: User = reactive({
name: 'John',
age: 25,
gender: 'male',
});
const name = toRef(user, 'name')
const age = toRef(user, 'age')
const gender = toRef(user, 'gender')
function changeInfo() {
name.value = 'Mary'
age.value = 30
gender.value = 'female'
}
</script>
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
当我们使用 toRef()
将一个响应式对象 user 的属性转化为 ref 后,该 ref 会与源对象的属性保持同步。这样创建的 ref 对象与其源属性保持同步:改变源属性的值将更新 ref 的值,反之亦然。
# toRefs() 的使用场景
# 一、问题引入
在上面示例 3 的代码中,如果对象的属性很多,每个属性都要保持响应性,那么势必多次调用 toRef()
函数,有没有什么办法可以一次性将响应式对象上的所有属性都转换为 ref 对象,从而简化代码呢?
# 二、解决办法
使用 Vue3 的 toRefs()
函数可以一次性将响应式对象上的所有属性都转换为 ref 对象,并使用对象解构语法来进一步简化代码,代码如下:
<template>
<div>
<p>Name: {{ name }}</p>
<p>Age: {{ age }}</p>
<p>Gender: {{ gender }}</p>
<button @click="changeInfo">Change Info</button>
</div>
</template>
<script setup lang="ts">
import { reactive, toRefs } from 'vue'
interface User {
name: string;
age: number;
gender: string;
}
const user: User = reactive({
name: 'John',
age: 25,
gender: 'male',
})
const stateRefs = toRefs(user)
const { name, age, gender } = stateRefs
function changeInfo() {
name.value = 'Mary'
age.value = 30
gender.value = 'female'
}
</script>
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
这段代码中使用 toRefs(user)
将响应式对象 user 中的所有属性(即name、age和gender)转换成对应的 ref 对象,并返回一个由这些 ref 对象组成的对象,这些 ref 对象的变化也会影响原响应式对象的属性。
然后使用对象解构将它们分别赋值给 name、age 和 gender,这样,我们在模板中就可以直接访问这三个变量,而不用写成 stateRefs.name、stateRefs.age 和 stateRefs.gender 的形式。在 changeInfo 函数中,直接修改 name.value、age.value 和 gender.value 的值,这些值的变化会直接反映到原响应式对象 user 的属性上,从而触发页面的更新。
# 任务六 computed 与计算属性
# 基本概念
computed 函数
,是一个响应式 API,类似于 Vue2.x 中的 computed 属性
。使用 computed 函数可以让代码更加简洁和高效。
computed 函数的值就是计算属性。computed 函数可以使用响应式依赖来创建计算属性,计算属性会自动追踪响应式依赖,在其任何响应式依赖项更改时自动重新计算。
在模板中绑定计算属性,当计算属性的值发生变化时,会触发组件的重新渲染。在模板中绑定计算属性的另一个好处是,可以在模板中声明性地指定复杂的逻辑,而不必手动更新值或在代码中执行计算。
# 使用计算属性
计算属性的使用场景。
例如,考虑一个简单的 Vue 3 组件,该组件显示一个商品列表和这些商品的总数:
<template>
<div>
<div v-if="isLoading">Loading...</div>
<div v-else>
<ul>
<li v-for="item in goods" :key="item.id">{{ item.name }}</li>
</ul>
<p>商品总数: {{ totalGoods }}</p>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
interface Item {
id: number;
name: string;
price: number;
}
const goods = ref<Item[]>([]);
const isLoading = computed(() => {
return goods.value.length === 0
})
const totalGoods = computed(() => {
return goods.value.length
})
const baseUrl = 'http://localhost:3000'
const fetchGoods = async () => {
try {
const res = await fetch(baseUrl + '/goods')
const data = await res.json()
goods.value = data as Item[]
} catch (e) {
throw new Error('an error happened' + e)
}
}
fetchGoods()
</script>
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
42
43
44
45
46
在这个例子中,totalGoods 计算属性函数负责计算 goods 数组中的商品总数。isLoading
计算属性基于 goods 数组的长度返回一个布尔值,指示组件是否处于加载状态。
请注意,这里使用计算属性 isLoading
来定义应用程序是否正在加载。通常,很多开发人员会定义 isLoading
变量,并在调用 fetchGoods
时将变量 isLoading
设置为 true
,然后在 API 请求完成后再将其设置为 false
。比如:
<script setup lang="ts">
const isLoading = ref(true)
// ...
const fetchGoods = async () => {
try {
isLoading.value = true
// 网络访问的代码
// ...
isLoading.value = false
} catch (e) {
// ...
}
}
fetchGoods()
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
相对于 Goods02.vue,Goods.vue 使用计算属性实现了 Loading 的显示逻辑与商品数据获取逻辑之间的解耦。