Vue3 进阶

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 的命令分别是:

bash 复制代码
node   
npm 
三、测试安装是否成功

安装完成后,注意测试安装是否成功,测试命令:

bash 复制代码
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 等常用的功能,并且在生产环境中也可以更快地构建。

一、创建项目

使用 npmpnpm 命令均可创建 Vite+Vue3 单页应用:

bash 复制代码
npm init vue@latest
或
pnpm create vue@latest

这一命令将会安装并执行 create-vue,它是 Vue 官方的项目脚手架工具。

建议采用 pnpm create vue@latest 进行创建。如果没有安装 pnpm 的话,可以先安装 pnpm,安装命令为:

bash 复制代码
npm install pnpm -g

这里使用 pnpm create vue@latest 命令创建 Vue3 项目的过程中,您将会看到一些如 TypeScript 和测试支持之类的可选功能提示:

可按需要进行选择安装,这里我们选择 TypeScriptVue RouterPinia,其余的功能可以留待以后需要时再进行安装。

plain 复制代码
✔ 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>...

如果我们在上面第一步 Project name 处输入项目名称 RealWorld-frontend 的话,项目创建成功后,会显示如下命令序列,提示您如何进行后续操作。

bash 复制代码
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的配置文件,用于配置开发环境和生产环境的各种选项
二、安装依赖

按命令序列的前两条命令进行操作:

bash 复制代码
cd realworld-frontend	 # 进入项目文件夹
pnpm install  # 安装依赖(推荐)

依赖安装完成后,项目文件夹 RealWorld-frontend 中将多出一个子文件夹 node_modules:

├── node_modules/            // 第三方依赖包目录
├── public/                  
├── src/                     
│   ...

node_modules 文件夹里面就是按照 package.json 的指示下载的各种依赖。

运行单页应用

一、启动项目服务端

项目服务端先启动起来,才能对浏览器端提供网页服务。启动命令为:pnpm devnpm 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 提供了一种新的组件编写方式:组合式 APIsetup() 函数

  • 组合式 API 允许按照逻辑组织代码,而不是按照生命周期函数或选项来组织代码,从而提高了代码的可读性和可维护性。
  • setup() 函数在组件创建和挂载之前运行,用于设置组件的响应式数据、计算属性、方法和监听器等。在 setup() 函数中,可以使用 Vue 3 的组合式 API 如 ref、reactive、computed、watchEffect 等来实现组件的逻辑,从而更好地组织项目代码。

使用 setup 函数

Vue 3 中的 setup() 函数是使用组合式 API 编写组件的入口函数,它会在组件实例创建之前被调用。

setup() 函数内部,可以通过使用 Vue 3 提供的一系列组合式 API 来定义组件的响应式数据、计算属性、方法等等,并将它们返回给组件的模板中使用。setup() 函数返回的对象会被用作组件实例的数据对象,可以在模板中直接访问。

以下是一个简单的例子,演示了如何使用 Vue 3 的 setup() 函数reactive() 来创建一个响应式的计数器:

vue 复制代码
<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>

在上述代码中,

  1. 我们首先引入了 Vue 3 提供的 reactive 函数,用于创建响应式对象。
  2. 在 setup() 函数中,我们使用 reactive 函数创建了一个名为 state 的响应式对象,包含了一个 count 属性。
  3. 接着,我们定义了一个名为 increment 的方法,用于将 state 对象的 count 属性 +1。
  4. 最后,我们将 state 和 increment 作为对象返回,供模板中使用。

由于使用了 reactive 函数,state 对象是响应式的,因此它的值会自动更新到模板中。当state.count 属性更新时,模板中的数据也会更新。

同样的响应式也可以通过 ref() 实现:

vue 复制代码
<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> 的示例组件:

vue 复制代码
<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 的代码:

vue 复制代码
<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 可以改写为:

vue 复制代码
<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 的响应式原理主要包括以下几个步骤:

  1. 创建一个响应式对象,使用 Proxy 对目标对象进行代理,拦截它的 get 和 set 操作,同时使用 Reflect 对操作进行反射,保证原对象的行为不受影响。

  2. 在 get 操作中,收集依赖,即将当前的渲染函数或者副作用函数添加到目标对象的依赖集合中,这样当目标对象发生变化时,就可以通知这些函数进行更新。

  3. 在 set 操作中,触发更新,即遍历目标对象的依赖集合,调用其中的函数,让它们重新执行,从而更新视图或者产生其他效果。

  4. 重复以上步骤,实现数据和视图的动态响应。

Vue 3 的响应式原理相比于 Vue 2 的 Object.defineProperty 方式,有以下优势:

  • 可以拦截更多的操作,如 delete、has、ownKeys 等,提高了灵活性和兼容性。
  • 可以对数组和嵌套对象进行响应式处理,无需额外的处理逻辑。
  • 可以避免原对象被污染,保持了数据的纯净性。
  • 可以提高性能,减少了不必要的依赖收集和更新触发。

Vue 3 的响应式 API

Vue 3 的响应式 api 是一组新的功能,用于创建和管理响应式数据。

它们可以让我们在组件中更灵活地组织和复用逻辑,也可以与其他 Vue 特性(如计算属性、侦听器、生命周期钩子等)配合使用。

Vue 3 的响应式 api 主要包括以下几个部分:

  1. ref:用于创建一个响应式的值 ,可以是基本类型或对象类型 。ref 返回一个包含 .value 属性的对象,通过这个属性可以访问或修改原始值。ref 也可以用于绑定模板中的 DOM 元素,通过 .value 获取元素的引用。
  2. reactive:用于创建一个响应式的对象,可以是普通对象、数组或类实例。reactive 返回一个代理对象,通过这个对象可以访问或修改原始对象的属性。reactive 不会改变原始对象的结构和身份,只是在访问和修改时触发响应式效果。
  3. computed:用于创建一个响应式的计算属性 ,可以根据其他响应式数据(如ref或reactive)动态地计算出一个值。computed 返回一个包含 .value 属性的对象,通过这个属性可以访问计算出的值。computed 也可以接受一个包含 get 和 set 函数的对象,实现可写的计算属性。
  4. watch:用于监听一个或多个响应式数据(如ref或reactive)的变化,并执行相应的回调函数。watch 可以接受一个源数据或一个返回源数据的函数作为第一个参数,以及一个回调函数作为第二个参数。watch 也可以接受一个包含多个源数据和回调函数的数组作为参数,实现同时监听多个数据。
  5. watchEffect:用于监听一个副作用函数中使用到的所有响应式数据的变化,并重新执行该函数。watchEffect 接受一个副作用函数作为参数,该函数会在首次调用时立即执行,并在后续任何依赖数据变化时重新执行。
  6. toRefs:用于将一个响应式对象(如reactive)转换为一个普通对象,该对象的每个属性都是一个 ref,指向原始对象的对应属性。toRefs 可以保持原始对象的响应性,同时避免解构时丢失响应性。
  7. 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 对象可以直接监听原始对象的变化。

  • 在模板中使用响应式对象的属性时,需要使用 两个花括号 语法来绑定数据,例如

    vue 复制代码
    {{ state.list }}
  • 响应式对象的属性需要在对象创建时就定义好,不能在后面动态添加或删除属性。如果需要动态添加或删除属性,可以使用 reactive 函数创建一个新的响应式对象。

  • 在开发时,建议 使用 toRefs 函数将响应式对象转换成普通对象的引用。这可以使代码更加清晰易懂,同时也能避免一些潜在的问题。

reactive 的使用举例

一、reactive 作用于普通的 JavaScript 对象

在 Vue 3 中,reactive 函数可以将一个普通的 JavaScript 对象转换成一个响应式数据对象,使得对象的属性可以被监听到变化并且自动更新视图。下面是 reactive 作用于对象的示例代码及注意事项:

vue 复制代码
<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 作用于数组的示例代码:

vue 复制代码
<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 的实现原理:

javascript 复制代码
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 再实现一次:

vue 复制代码
<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) 的方式改写刚才的代码:

vue 复制代码
<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 问题引入

先来看看下面的代码有什么问题:

vue 复制代码
<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>

这段代码存在的问题是:在 Vue3 中使用了非响应式的对象 user。由于 user 对象是普通的 JavaScript 对象,而不是 Vue 的响应式对象,所以当 changeInfo() 函数修改了 user 对象的属性时,界面不会更新

2 解决办法

解决方法是使用 Vue 的响应式对象来代替普通 JavaScript 对象,例如使用 reactive() 函数来创建一个响应式对象。可以将 user 对象改为以下代码:

vue 复制代码
<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>

这段代码确实实现了页面的同步更新,但模板里反复使用 user.前缀 显得有些冗余,如何减少模板中重复使用 user.前缀 的情况,从而提高了代码的可读性呢?

3 继续改进

toRef 是 Vue3 中的一个工具函数,用于将响应式对象上的一个属性转换为一个单独的 ref 对象。我们尝试用它来减少模板中 user. 前缀的重复使用,代码如下:

vue 复制代码
<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>

当我们使用 toRef() 将一个响应式对象 user 的属性转化为 ref 后,该 ref 会与源对象的属性保持同步。这样创建的 ref 对象与其源属性保持同步:改变源属性的值将更新 ref 的值,反之亦然。

toRefs() 的使用场景

一、问题引入

在上面示例 3 的代码中,如果对象的属性很多,每个属性都要保持响应性,那么势必多次调用 toRef() 函数,有没有什么办法可以一次性将响应式对象上的所有属性都转换为 ref 对象,从而简化代码呢?

二、解决办法

使用 Vue3 的 toRefs() 函数可以一次性将响应式对象上的所有属性都转换为 ref 对象,并使用对象解构语法来进一步简化代码,代码如下:

vue 复制代码
<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>

这段代码中使用 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 组件,该组件显示一个商品列表和这些商品的总数:

vue 复制代码
<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。比如:

vue 复制代码
<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。比如:

vue 复制代码
<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 的显示逻辑与商品数据获取逻辑之间的解耦

相关推荐
秦jh_3 分钟前
【Linux】多线程(概念,控制)
linux·运维·前端
蜗牛快跑21316 分钟前
面向对象编程 vs 函数式编程
前端·函数式编程·面向对象编程
Dread_lxy17 分钟前
vue 依赖注入(Provide、Inject )和混入(mixins)
前端·javascript·vue.js
涔溪1 小时前
Ecmascript(ES)标准
前端·elasticsearch·ecmascript
榴莲千丞1 小时前
第8章利用CSS制作导航菜单
前端·css
奔跑草-1 小时前
【前端】深入浅出 - TypeScript 的详细讲解
前端·javascript·react.js·typescript
羡与1 小时前
echarts-gl 3D柱状图配置
前端·javascript·echarts
guokanglun1 小时前
CSS样式实现3D效果
前端·css·3d
咔咔库奇2 小时前
ES6进阶知识一
前端·ecmascript·es6
渗透测试老鸟-九青2 小时前
通过投毒Bingbot索引挖掘必应中的存储型XSS
服务器·前端·javascript·安全·web安全·缓存·xss