Vue 进阶
前言
Vue3 入门文章地址: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
三、测试安装是否成功
安装完成后,注意测试安装是否成功,测试命令:
node -v
npm -v
如果显示出 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
这一命令将会安装并执行 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 / YesScaffolding project in ./<your-project-name>...
如果我们在上面第一步 Project name 处输入项目名称 RealWorld-frontend 的话,项目创建成功后,会显示如下命令序列,提示您如何进行后续操作。
cd realworld-frontend
pnpm install
pnpm dev
项目结构如下:
├── 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的配置文件,用于配置开发环境和生产环境的各种选项
二、安装依赖
按命令序列的前两条命令进行操作:
cd realworld-frontend # 进入项目文件夹
pnpm install # 安装依赖(推荐)
依赖安装完成后,项目文件夹 RealWorld-frontend 中将多出一个子文件夹 node_modules:
├── node_modules/ // 第三方依赖包目录
├── public/
├── src/
│ ...
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
表明项目服务端已经运行在 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>
在上述代码中,
- 我们首先引入了 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>
由于 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>
用 <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>
可见,在 <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>
<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 }}
-
响应式对象的属性需要在对象创建时就定义好,不能在后面动态添加或删除属性。如果需要动态添加或删除属性,可以使用 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>
注意事项:
- 对于一个普通的 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>
注意事项:
- 响应式数组的元素操作需要使用 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}}
}
上面的代码中,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>
在上面的代码中,我们使用 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>
在修改 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 = 30user.gender = 'female'
}</script>
这段代码存在的问题是:在 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 = 30user.gender = 'female'
}</script>
这段代码确实实现了页面的同步更新,但模板里反复使用 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 = 30gender.value = 'female'
}</script>
当我们使用 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 } = stateRefsfunction changeInfo() {name.value = 'Mary'age.value = 30gender.value = 'female'
}</script>
这段代码中使用 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>
在这个例子中,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>
相对于 Goods02.vue,Goods.vue 使用计算属性实现了 Loading 的显示逻辑与商品数据获取逻辑之间的解耦。
th === 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()
```在这个例子中,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>
相对于 Goods02.vue,Goods.vue 使用计算属性实现了 Loading 的显示逻辑与商品数据获取逻辑之间的解耦。