Vue学习笔记

627 阅读1小时+

参考链接

Vue.js3 前端开发实战

  1. Vue3 开发,或许你需要这样使用请求 API
  2. Vue3.0 中 setup 函数的使用
  3. Vue v-on 指令详解
  4. 验证 Vue Props 类型,你这几种方式你可能还没试用过!
  5. Vue 常用修饰符大全

响应式变量 - ref\reactive\toRef\toRefs

  1. 一文搞懂 Vue3 中 toRef 和 toRefs 函数的使用
  2. 第三章 toRef toRefs toRaw 用法
  3. Vue3: reactive, ref,toRef,toRefs 用法和区别
  4. vue3 reactive 值为什么不推荐解构&&分析响应式结构
  5. 总结 Vue 创建响应式数据对象(reactive、ref、toRef、toRefs)
  6. Vue3 实践指南
  7. 对 Vue3 中 reactive 的深入理解
  8. Vue3 的 reactive 和 ref 原理区别详解
  9. Vue3: reactive, ref,toRef,toRefs 用法和区别
  10. Vue3 中,toRef 与 toRefs,以及使用其他方法 return 数据不可行的原因
  11. isRef()、unRef()、toRef()、toRefs()深度解析,为啥解构会失去响应式?

监听 - watch watchEffect

  1. 终于彻底搞懂 Watch、WatchEffect 了,原来功能如此强大!
  2. Vue3 中你不知道的 watch 和 watchEffect 侦听器
  3. 计算属性 computed 和侦听属性 watch 的区别? 应用场景?
  4. 浅谈 Vue3 中 watchEffect
  5. Vue3 中 watch 的最佳实践

生命周期

  1. vue 生命周期是什么?vue 生命周期详解
  2. Vue3 生命周期钩子(hooks)完整指南
  3. Vue 的完整生命周期源码流程详解
  4. setup 和生命周期
  5. Vue 都使用那么久了,还不了解它的生命周期吗

问题

  • 研究 5_2-3-3.html
  • 研究 6_4-1-2.html
  • 研究 6-4-2.html

定义

一个应用程序中可以定义和使用很多个组件,但是需要配置一个根组件,当应用程序被挂载渲染到页面时,此根组件会作为起点元素进行渲染。

使用 Vue 中的createAPP方法即可创建一个 Vue 应用实例。

我们创建好 Vue 应用实例后,使用mount方法就可以将其绑定到指定的 HTML 文档上

当触发了我们绑定的事件函数时,系统会自动将当前的 Event 对象传递到函数中去

Event 对象中会存储当前事件的很多信息,例如事件类型、鼠标位置、键盘按键情况等。

第一章:从前端基础到 Vue.js 3

前端技术是互联网大技术栈中非常重要的一个分支。前端技术本身也是互联网技术发展的见证,其就像一扇窗户,展现了互联网技术的发展与变迁。

前端技术通常是指通过浏览器将信息展现给用户这一过程中涉及的互联网技术。随着目前前端设备的泛化,并非所有的前端产品都是通过浏览器来呈现的,例如微信小程序、支付宝小程序、移动端应用等被统称为前端应用,相应地,前端技术栈也越来越宽广。

讲到前端技术,虽然目前有各种各样的框架与解决方案,基础的技术依然是前端三剑客:HTML5、CSS3 与 JavaScript。随着 HTML5 与 CSS3 的应用,前端网页的美观程度与交互能力都得到了很大的提升。

本章作为准备章节,向读者简单介绍前端技术的发展过程,以及前端三剑客的基本概念与应用,并简单介绍响应式开发框架的相关概念。本章还将通过一个简单的静态页面来向读者展示如何使用 HTML、CSS 与 JavaScript 代码将网页展示到浏览器界面中。

通过本章,你将学习到:

  • 了解前端技术的发展概况。对 HTML 技术有简单的了解。

  • 对 CSS 技术有简单的了解。

  • 对 JavaScript 技术有简单的了解。

  • 认识渐进式界面开发框架 Vue, 初步体验 Vue 开发框架。

1.1 前端技术演进

说起前端技术的发展历程,还是要从 HTML 说起。1990 年 12 月,计算机学家 Tim Bernrs-Lee 使用 HTML 语言在 NeXT 计算机上部署了第一套由“主机-网站-浏览器”构成的 Web 系统,我们通常认为这是世界上第一套完整的前后端应用,将其作为 Web 技术开发的开端。

1993 年,第一款正式的浏览器 Mosaic 发布,1994 年年底, W3C 组织成立,标志着互联网进入了标准化发展的阶段,互联网技术迎来快速发展的春天。 ​ 1995 年,网景公司推出 JavaScript 语言,赋予了浏览器更强大的页面渲染与交互能力,使之前的静态网页开始真正地向动态化的方向发展,由此后端程序的复杂度大幅度提升, MVC 开发架构诞生,其中前端负责 MVC 架构中的 V(视图层)的开发。 ​ 2004 年, Ajax 技术在 Web 开发中得到应用,使得网页可以灵活地使用 HTTP 异步请求来动态地更新页面,复杂的渲染逻辑由之前的后端处理逐渐更替为前端处理,开启了 Web2.0 时代。由此,类似 jQuery 等非常多流行的前端 DOM 处理框架相继诞生,以其中最流行的 jQuery 框架为例,其几乎成为网站开发的标配。

2008 年, HTMLS 草案发布,2014 年 10 月,W3C 正式发布 HTML5 推荐标准,众多流行的浏览器也都对其进行了支持,前端网页的交互能力大幅度提高。前端网站开始由 Web Site 向 Web App 进化,2010 年开始相继出现了 Angular JS、Vue JS 等开发框架。这些框架的应用开启了互联网网站开发的 SPA 时代,即单页面应用程序时代(Single Page Application),这也是当今互联网 Web 应用开发的主流方向。

总体来说,前端技术的发展经历了静态页面阶段、Ajax阶段、MVC阶段,最终发展到了SPA 阶段。
在静态页面阶段,前端代码只是后端代码中的一部分,浏览器中展示给用户的页面都是静态的,这些页面的所有前端代码和数据都是后端组装完成后发送给浏览器进行展示的,页面响应速度慢,只能处理简单的用户交互,样式也不够美观。
在Ajax阶段,前端与后端实现了部分分离。前端的工作不再仅仅是展示页面,还需要进行数据的管理与用户的交互。当前端发展到Ajax阶段时,后端更多的工作是提供数据,前端代码逐渐变得复杂。
随着前端要完成的功能越来越复杂,代码量也越来越大。应运而生的很多框架都为前端代码工程结构管理提供了帮助,这些框架大多采用MVC或MVVM模式,将前端逻辑中的数据模型、视图展示和业务逻辑区分开来,为更高复杂性的前端工程提供了支持。

前端技术发展到 SPA 阶段则意味着网站不再单单用来展示数据,其是一个完整的应用程序,浏览器只需要加载一次网页,用户即可在其中完整使用多页面交互的复杂应用程序,程序的响应速度快,用户体验也非常好。

1.2 HTML 入门

首先, HTML 是一种编程语言,是一种描述性的网页编程语言。HTML 的全称为 Hyper Text Markup Language, 我们通常也将其称为超文本标记语言,所谓超文本,是指其除了可以用来描述文本信息外,还可以描述超出基础文本范围的图片、音频、视频等信息。

虽然说 HTML 是一种编程语言,但是从编程语言的特性上来看, HTML 并不是一种完整的编程语言,其并没有很强的逻辑处理能力,更确切的说法为 HTML 是一种标记语言,其定义了一套标记标签用来描述和控制网站的渲染。

标签是 HTML 语言中非常重要的一部分,标签是指由尖括号包围的关键词,例如<h1>、<html>等。在 HTML 文档中,大多数标签都是成对出现的,例如<h1></h1>, 在一对标签中,前面的标签是开始标签,后面的标签是结束标签。例如下面就是一个非常简单的 HMTL 文档示例:

<htmI>
<body>
<hI>HeIIoWorId</h1>

<p>HelloWorld 网页</p>

</body>
</htm1>

上面的代码中共有 4 对标签: html、body、h1 和 p,这些标签的排布与嵌套定义了完整的 HTML 文档,最终会由浏览器进行解析渲染。

1.2.1 准备开发工具

HTML 文档本身也是一种文本,我们可以使用任何文本编辑器进行 HTML 文档的编写,只需使用.html 文本后缀名即可。但是使用一个强大的 HTML 编辑器可以极大地提高编写效率,例如很多 HTML 编辑器都会提供代码提示、标签高亮、标签自动闭合等功能,这些功能都可以帮助我们在开发中十分快速地编写代码,并且可以减少因为笔误所产生的错误。 Visual Studio Code (VSCode)是一款非常强大的编辑器,其除了提供语法检查、格式整理、代码高亮等基础编程常用的功能外,还支持对代码进行调试和运行以及版本管理。通过安装扩展,VSCode 几乎可以支持目前所有流行的编程语言。本书示例代码的编写也将采用 VSCode 编辑器完成。你可以在官方网站 (https://code.visualstudio.com) 下载新的 VSCode 编辑器。 目前,VSCode 支持的操作系统有 macOS、Windows 和 Linux, 在网站中下载适合自己操作系统的 VSCode 版本进行安装即可。

下载安装 VSCode 软件后,我们可以尝试使用其创建一个简单的 HTML 文档,新建一个名为 test.html 文件,在其中编写如下测试代码:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>

  <body>
    <h1>Hello World</h1>
  </body>
</html>

相信在输入代码的过程中,你已经能够体验到使用 VSCode 编程带来的畅快体验,并且在编辑器中关键词的高亮显示和自动缩进也使得代码结构看上去更加直观。

在 VSCode 中将代码编写完成后,我们可以直接对其进行运行,对于 HTML 源文件的运行,VSCode 会自动将其以浏览器的方式打开,选择 Run→Run Without Dbugging 选项,之后会弹出环境选择菜单,我们可以选择一款浏览器进行预览,

建议安装 Google Chrome 浏览器,其有很多强大的插件可以帮助我们进行 Web 程序调试。

可安装汉化插件 - chinese,将界面变为中文

1.2.2 HTML 中的基础标签

HTML 中预定义了很多标签,本节通过几个基础标签的应用实例来向读者介绍标签在 HTML 中的简单用法。

1.3 CSS 入门

1.3.1 CSS 选择器入门

1.3.2 CSS 样式入门

1.4 JavaScript 入门

1.4.1 为什么需要 JavaScript

1.4.2 JavaScript 语法简介

1.5 渐进式开发框架 Vue

Vue 的定义为渐进式的 JavaScript 框架,所谓渐进式,是指其被设计为可自底向上逐层应用。我们可以只使用 Vue 框架中提供的某层功能,也可以与其他第三方库整合使用。当然,Vue 本身也提供了完整的工具链,使用其全套功能进行项目的构建也非常简单。

在使用 Vue 之前,需要掌握基础的 HTML、CSS、和 JavaScript 技能,如果你对本章前面介绍的拟人都已掌握,那对于后面 Vue 使用的相关例子会非常容易理解。Vue 的渐进式性质使其使用方式变的非常灵活,在使用时,我们可以使用其完整的框架,也可以只使用部分功能。

一个构建数据驱动的 web 界面的渐进式框架

1.5.1 第一个 Vue 应用

在学习和测试 Vue 功能时,我们可以直接使用 CDN 的方式来进入 Vue 框架。本书将全部采用 Vue 3.0.x 的版本来编写示例。

首先,使用 VS Code 开发工具创建一个名为 Vue1.html 的文件,在其中编写如下代码:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vue3 Demo</title>
    <!--载入vue.js-->
    <script src="https://unpkg.com/vue@next"></script>
  </head>

  <body></body>
</html>

其中,我们在 had 标签中加入了一个 script 标签,采用 CDN 的方式引入了 Vue3 的新版本。以之前编写的计数器应用为例,我们尝试使用 Vue 的方式来实现它。

首先在 body 标签中添加一个标题和按钮,代码如下:

<div style="text-align:center;" id="Application">
  <h1>{{count}}</h1>
  <button v-on:click="clickButton">点击</button>
</div>

上面使用到了一些特殊的语法,例如在 h1 标签内部使用了 Vue 的变量替换功能,{{ count }}是一种特殊的语法,其会将当前 Vue 组件中定义的 count 变量的值替换过来,v-on:click 属性用来进行组件的单击事件绑定,上面的代码将单击事件绑定到了 clickButton 函数上,这个函数也是定义在 Vue 组件中的。定义 Vue 组件非常简单,我们可以中 body 标签下添加一个 script 标签,在其中编写如下代码:

<script>
  const App = {
    data() {
      return {
        count: 0,
      };
    },
    methods: {
      clickButton() {
        this.count = this.count + 1;
      },
    },
  };
  Vue.createApp(App).mount("#Application");
</script>

如以上代码所示,我们定义 Vue 组件时实际上定义了一个 JavaScript 的对象,其中 data 方法用来返回组件所需要的数据,methods 属性用来定义组件所需要的方法函数。

在浏览器中运行上面的代码,当单击页面中的按钮时,计数器会自动增加,可以看到,使用 Vue 实现的计数器应用比使用 JavaScript 直接操作 HTML 元素方便的多,我们不需要获取指定的组件,也不需要修改组件中的文本内容,通过 Vue 这种绑定的编程方式,只需要专注于数据逻辑,当数据本身修改时,绑定这些数据的元素也会同步修改。

实例:

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <title>1-5-1_单击计数器</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <!--载入vue.js-->
    <script src="https://unpkg.com/vue@next"></script>
  </head>

  <body>
    <div style="text-align:center;" id="Application">
      <h1>{{count}}</h1>
      <button v-on:click="clickButton">点击</button>
    </div>

    <script>
      const App = {
        data() {
          return {
            count: 0,
          };
        },
        methods: {
          clickButton() {
            this.count = this.count + 1;
          },
        },
      };
      Vue.createApp(App).mount("#Application");
    </script>
  </body>
</html>

1.5.2 范例:实现一个简单的用户登录界面

  1. 登录页面需要有标题,用来提示用户当前的登录状态
  2. 在未登录时,需要有两个输入框以及登录按钮供用户输入账号、密码和进行登录操作
  3. 在登录完成后,输入框需要隐藏,需要提供按钮让用户登出。

借助 Vue 的单双向绑定和条件渲染功能,完成需要会很简单。

  • v-if 是 Vue 提供的条件渲染功能,其指定的变量如果是 true,则渲染这个元素,否则不渲染
  • v-model 用来进行双向绑定,当输入框中的文字发生变化时,其会将变化同步到绑定的变量上,同样,当我们对变量的值进行修改时,输入框的文本也会对应发生变化

实例

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <title>1-5-1_单击计数器</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <!--载入vue.js-->
    <script src="https://unpkg.com/vue@next"></script>
  </head>

  <body>
    <div style="text-align:center;" id="Application">
      <h1>{{count}}</h1>
      <button v-on:click="clickButton">点击</button>
    </div>

    <script>
      const App = {
        data() {
          return {
            count: 0,
          };
        },
        methods: {
          clickButton() {
            this.count = this.count + 1;
          },
        },
      };
      Vue.createApp(App).mount("#Application");
    </script>
  </body>
</html>

1.5.3 Vue3 的新特性

如果你之前接触过前端开发,相信 Vue 框架对于你来说并不陌生。Vue 3 的发布无疑是 Vue 框架的一次重大改进。一款优秀的前端开发框架的设计一定要遵循一定的设计原理,Vue 3 的设计目标是:

  1. 更小的尺寸和更快的速度
  2. 更加现代化的语法特性,加强 Typescript 的支持
  3. 在 API 设计方面,增强统一性和一致性。
  4. 提高前端工程的可维护性。
  5. 支持更多、更强大的功能,提高开发者的效率。

上面列举了 Vue 3 的核心设计目标,相较于 Vue 2 版本,Vuc 3 有哪些重大的更新点呢?本节我们就来简单介绍一下。

首先,在 Vue 2 时代,最小化被压缩的 Vue 核心代码约为 20KB,目前 Vue 3 的压缩版只有 I0KB,大小足足减少了一半,在前端开发中,依赖模块越小,意味着更少的流量和更快的速度,在这方面,Vue 3 的确表现优异。

在 Vue 3 中,对虚拟 DOM 的设计也进行了优化,使得引学可以更加快速地处理局部的页面元素修改,在一定程度上提升了代码的运行效率。同时,Vue 3 也配套进行了更多编译时的优化,例如将插槽编译为函数等。

在代码语法层面上,相较于 Vue2, Vue 3 有比较大的变化。其基本弃用了“类”风格的 API,而推广采用“函数”风格的 API,以便更好地对 TypeScrp 进行支持。这种编程风格更有利于组件的逻辑复用,例如 Vue 3 组件中心引入的 setup(组合式 API)方法,可以让组件的逻辑更加聚合。

Vue 3 中也添加了一些新的组件, 比如 Teleport 组件(有助于开发者将逻辑关联的组件封装在一起),这些新增的组件提供了更加强大的功能,以便于开发者对逻辑的复用。

总之,在性能方面,Vue 3 无疑完胜 Vue 2,同时打包后的体积也更小。在开发者编程方面,Vue 3 基本是向下兼容的,开发者无须过多的额外学习成本,并且 Vue 3 对功能方面的扩展对于开发者来说也更加友好。

关于 Vue 3 更详细的内容与新特性的使用方法,后面的章节会逐步向读者介绍。

1.5.4 为什么要使用 Vue 框架

在真正开始学习 Vue 之前,还有一个问题至关重要,那就是我们为什么要学习它。

首先,做前端开发,一定要使用一款框架, 这就像生产产品的工厂有一套完整的流水线一样。在学习阶段,我们可以直接使用 HTML、CSS 和 JavaScript 开发出一些简单的静态页面,但是要做大型的商业应用,要完成的代码量会非常大,要编写的功能函数会非常多,而且对于交互复杂的项目来说,如果真的不使用任何框架来开发的话,后期维护和扩展将会非常困难。

既然一定要使用框架,那么我们为什么要选择 Vue 呢?在互联网 Web 时代早期,前后端的界限还比较模糊,有个名为 jQuery 框架非常流行,其内部封装了大量的 JavaScript 函数,可以帮助开发者操作 DOM 且提供了事件处理、动画和网络相关接口。当时的前端页面更多是用来展示,因此使用 jQuery 框架足够应付所需要进行的逻辑交互操作。

后来随着互联网的飞速发展,前端网站的页面越来越复杂,2009 年诞生了一款名为 AngularJS 的前端框架,此框架的核心是响应式与模块化,其使得前端页面的开发方式发生了变革,前端可以自行处理非常复杂的业务逻辑,前后端的职责开始逐渐分离,前端从页面展示向单页面应用发展。

AngularJS 虽然强大,但其缺点也十分明显,总结如下:

  • (1)学习曲线陡峭,入门难度高。
  • (2)灵活性很差,这意味着如果要使用 AngularJS,就必须按所其规定的一套构造方式来开发应用,要完整地使用其一整套的功能。
  • (3)由于框架本身很庞大,使得速度和性能会略差。
  • (4)在代码层面上,某些 API 设计复杂,使用麻烦。

只要 AngularJS 有上述问题,就一定会有新的前端框架来解决这些问题, Vue 和 React 这两个框架就此诞生了。

Vue 和 React 在当下前端项目开发中平分秋色,它们都是非常优秀的现代化前端框架。从设计上,它们有很多相似之处,比如相较于功能齐全的 AngularJS 而言,它们都是“骨架”类的框架,即只包含基础的核心功能,路由、状态管理等功能都是靠分离的插件来支持的。并且在逻辑上,Vue 和 React 都是基于虚拟 DOM 树的,改变页面真实的 DOM 要比虚拟 DOM 的更改性能开销大很多,因此 Vue 和 React 的性能都非常优秀。Vue 和 React 都引导采用组件化的方式进行编程,模块间通过接口进行连接,方便维护与扩展。

当然, Vue 与 React 也有很多不同之处,

Vue 的模板编写采用的是类似 HTML 的模板方式,写起来与标准的 HTML 非常像,只是多了一些数据绑定或事件交互的方法,入手非常简单。

而 React 则是采用 JSX 的方式编写模板,虽然这种编写方式提供的功能更加强大一些,

但是 JavaScript 混合 XML 的语言使得代码看上去非常复杂,阅读起来也比较困难。Vue 与 React 还有一个很大的区别在于组件状态管理, Vue 的状态管理本身非常简单,局部的状态只要在 data 中进行定义,其默认就被赋予了响应性,在需要修改时直接将对应属性更改即可,对于全局的状态也有 Vuex 模块进行支持。在 React 中,状态不能直接修改,需要使用 setState 方法进行更改,从这一点上看, Vue 的状态管理更加简洁一些。

总之,如果你想尽快掌握前端开发的核心技能并上手开发大型商业项目, Vue 一定不会让你失望。

第二章:Vue 模版应用

模版是 Vue 框架中的重要组成部分,Vue 采用了基于 HTML 的模版语法,因此对于大多数开发者来说,上手会非常容易。

在 Vue 的分层设计思想中,模版属于视图层,有了模版功能,开发者可以方便地将项目组件化,也可以方便地封装定制化的通用组件。

在编写组件时,模版的作用是让开发者将重心放在页面布局渲染上,而不需要关心数据逻辑。

同样,在 Vue 组件内部编写数据逻辑代码时,ye 无需关心视图的渲染

  • 基础的模版使用语法
  • 模版中参数的使用
  • Vue 指令相关用法
  • 使用缩写指令
  • 灵活使用条件语句与循环语句

2.1 模版基础

模版有啥用?

模版最直接的用途是帮助我们通过数据来驱动视图的渲染。

对于普通的 HTML 文档,若要在数据变化时对其进行页面更新,则需要通过 JavaScript 的 DOM 操作来获取指定的元素,再对其属性或内部文本进行修改,操作起来十分烦琐切容易出错。

如果使用了 Vue 的模版语法,则事情会变的很简单,我们只需要将要变化的值定义为变量,之后将变量插入 HTML 文档指定的位置即可。

当数据发生变化时,使用此变量的所有组件都会同步刷新,这就使用到了 Vue 模版中的插值技术。

学习模版,我们先从学习插值开始。

2.1.1 模版 插值

创建 html 文件,写入以下代码

<div style="text-align: center;">
  <h1>这里是模版内容:1次点击</h1>
  <button>按钮</button>
</div>

在浏览器中运行上述代码,会渲染一个标题和一个按钮,但点击按钮并没有任何效果。

现在,我们实现这样一个功能:点击按钮,改变数值。

引入 Vue 框架,并通过 Vue 组件来实现这个计数器功能,代码如下:

<!--载入vue.js-->
<script src="https://unpkg.com/vue@next"></script>
<div id="Application" style="text-align: center">
  <h1 v-bind:id="id1">这里是模板内容:{{count}}次点击</h1>
  <button v-on:click="clickButton">按钮</button>
</div>

<script>
  //定义一个Vue组件,名为APP
  const App = {
    //定义组件中的数据
    data() {
      return {
        //目前我们只用到count数据
        count: 0,
      };
    },

    //定义组件中的函数
    methods: {
      //实现单击按钮的方法
      clickButton() {
        this.count = this.count + 1;
      },
    },
  };
  //将Vue组件绑定到页面上id为Application的元素上
  Vue.createApp(App).mount("#Application");
</script>

在浏览器中运行上述代码,单击页面中的按钮,即可看到页面中标题的文本也在不断变化。

如上代码所示,在 HTML 的标签中使用“{{}}”可以进行变量插值,这是 Vue 中基础的模版语法,其可以将当前组件中定义的变量的值插入指定位置,并且这种插值会默认实现绑定效果,即当我们修改了变量的值时,其可以同步地反馈到页面渲染上

某些情况下,某些组件的渲染是由变量控制的,但是我们想让它一旦渲染后就不能够再被修改,这时可以使用模版中的v-once指令实现,被这个指令设置的组件在进行变量插值时只会插值一次,例如:

<h1 v-once>这里是模版的内容:{{count}}次点击</h1>

再次在浏览器中实验,可以发现网页中指定的插值位置被替换成了文本“0”后,无论再怎么单击按钮,标题也不会改变。

注意:如果插值的文本为一段 HTML 代码,则直接使用双括号就不好使了,双括号会将其内的变量解释成纯文本,

使用双括号插值的方式将 HTML 代码插入,最终的效果会将其以文本的方式渲染出来,这种效果不符合预期,

对于 HTML 代码插值,我们需要使用 v-html 指令来完成,示例代码如下

实例:

<!--载入vue.js-->
<script src="https://unpkg.com/vue@next"></script>
<div id="Application" style="text-align: center">
  <h1 v-once>这里是模板的内容:<span v-html="countHTML"></span>次点击</h1>
  <button v-on:click="clickButton">按钮</button>
</div>

<script>
  const App = {
    data() {
      return {
        count: 0,
        countHTML: "<span style='color:red;'>0</span>",
      };
    },

    methods: {
      clickButton() {
        this.count = this.count + 1;
      },
    },
  };
  Vue.createApp(App).mount("#Application");
</script>

V-html 指令可以指定一个 Vue 变量数据,其会通过 HTML 解析的方式将原始 HTML 替换到其指定的标签位置,

前面介绍了如何在标签内部进行内容的插值,我们知道,标签除了其内部的内容外,本身的属性设置也是很重要的,

例如,我们可能需要动态改变标签的 style 属性,从而实现元素渲染样式的修改。

在 Vue 中,我们可以使用属性插值的方式做到标签属性与变量的绑定。

对于标签属性的值,使用 v-bind 指令,示例如下:

<h1 v-bind:id="id1">这里是模版内容:{{count}}次点击</h1>

再添加一个名为 id1 的 Vue 组件属性,示例如下:

data() { return { count:0, countHTML:"
<span style="color:red;">0</span>
", id1:"h1" } }

实例如下:

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <title>VUE学习 - 2.1-模板插值</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <!--载入vue.js-->
    <script src="https://unpkg.com/vue@next"></script>
  </head>

  <body>
    <div id="Application" style="text-align: center">
      <h1 v-bind:id="id1">这里是模板内容:<span>{{count}}</span>次点击</h1>
      <button v-on:click="clickButton">按钮</button>
    </div>

    <script>
      const App = {
        data() {
          return {
            count: 0,
            countHTML: "<span style='color:red;'>0</span>",
            id1: "h1",
          };
        },

        methods: {
          clickButton() {
            this.count = this.count + 1;
          },
        },
      };
      Vue.createApp(App).mount("#Application");
    </script>

    <style>
      #h1 {
        color: red;
      }
    </style>
  </body>
</html>

运行上述代码,我们已将id属性动态地绑定到了指定的标签中,当 Vue 组件中id1属性的值发生变化时,其也会动态地反映到h1标签上,我们通过这种动态绑定的方式灵活地更改标签的样式表。

v-bind指令同样适用于其他 HTML 属性,只需在其中使用冒号加属性名的方式指定即可。

其实,无论是双括号方式的标签内容插值,还是v-bind方式的标签属性插值,除了可以直接将变量插值外,也可以使用基本的JavaScript表达式,例如:

<h1 v-bind:id="id1">这里是模版内容:{{count+10}}次点击</h1>

上面的代码运行后,页面上渲染的数值是 count 属性增加 10 之后的结果。

注意:所有插值的地方若使用表达式,,则只能使用单个表达式,否则会产生异常。

2.1.2 模版指令

本质上,Vue 中的模版指令也是 HTML 标签属性,其通常由前缀”v-“开头,例如前面使用的v-bindv-once等,都是指令。

大部分指令都可以直接设置为JavaScript变量或单个的JavaScript表达式。

实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <h1 v-if="show">标题</h1>
  <button @click="clickShow()">{{show?"不显示":"显示"}}标题</button>
</div>
<script>
  const App = {
    data() {
      return {
        show: true,
      };
    },
    methods: {
      clickShow() {
        this.show = !this.show;
      },
    },
  };
  Vue.createApp(App).mount("#Application");
</script>

点击按钮,可以控制标题是否显示

其中的 v-if 撒一个简单的选择渲染指令,设置为布尔值true时,当前标签元素才会被渲染。

某些特殊的 Vue 指令也可以指定参数,例如v-bindv-on指令,对于可以添加参数的指令,参数和指令使用冒号进行分隔,如下:

v-bind:style
v-on:click

指令的参数本身也可以是动态的,例如我们可以定义区分 id 选择器和类选择器来定义不同的组件样式,之后动态地切换组件的属性,

实例:

<!--载入vue.js-->
<script src="https://unpkg.com/vue@next"></script>

<div id="Application" style="text-align: center">
  <h1 v-bind:[prop]="name" v-if="show">标题</h1>
</div>

<script>
  const App = {
    data() {
      return {
        show: true,
        prop: "class",
        name: "h1",
      };
    },
  };
  Vue.createApp(App).mount("#Application");
</script>

<style>
  #h1 {
    color: red;
  }

  .h1 {
    color: blue;
  }
</style>

运行上面代码,可以看到h1标签被正确地绑定了class属性。

在参数后面还可以为 Vue 中的指令增加修饰符,修饰符会为 Vue 指令增加额外的功能,以一个常见的应用场景为例,在网页中,如果有可以输入信息的输入框,通常不希望用户在首尾输入空格符,通过 Vue 的指令修饰符,可以自动去除首尾空格符的功能,示例:

<input v-model.trim="content">

如上所示,我们使用v-model指令将输入框的文本与content属性进行绑定,当用户在输入框中输入的文本首尾有空格符时,以及输入框失去焦点时,Vue 会自动帮我们去掉这些首尾空格。

在 VUE 应用开发中,v-bindv-on两个指令使用的很频繁,我们可以使用更加高效的缩写方式,对于 v-bind 可直接使用冒号加属性名的方式进行绑定,例如:v-bind:id="id",可以缩写为以下模样:

:id="id"

对于v-on类的事件绑定指令,例如v-on:click="myFunc"指令可以缩写成如下模样:

@click="myFunc"

这两个缩写功能,将大大提高 VUE 应用的编写效率。

2.2 条件渲染

条件渲染是 Vue 控制 HTML 页面渲染的方式之一。很多时候,我们都需要通过条件渲染的方式来控制 HTML 元素的显示和隐藏。在 Vue 中,要实现条件渲染,可以使用v-if相关的指令,也可以使用v-show相关指令。

2.2.1 使用 v-if 指令进行条件渲染

v-if指令在之前的测试代码中也简单用过,简单来讲,其可以有条件地选择是否渲染一个 HTML 元素。

v-if指令可以设置为一个 JavaScript 变量或表达式,当变量或表达式为真值时,其指定的元素才会被渲染。

简单的条件渲染示例如下:

<h1 v-if="show">标题</h1>

在上面的代码中,只有当 show 变量的值为真时,当前标题元素才会被渲染,Vue 模版中的条件渲染指令 v-if 类似于 JavaScript 编程语言中的 if 语句。

我们都知道中 JavaScript 中,if 关键字可以和else关键字结合使用组成if-else块,在 Vue 模版中也可以使用类似的条件渲染逻辑,v-if指令可以和v-else指令结合使用,示例如下:

<h1 v-if="show">标题</h1>
<p v-else>
    如果不显示标题就显示段落
</p>

实例:

<!--载入vue.js-->
<script src="https://unpkg.com/vue@next"></script>

<div id="Application">
  <h1 v-if="show">标题</h1>
  <p v-else>如果不显示标题就显示段落</p>
  <button @click="btn">
    切换按钮,切换show属性为:{{show ? "false" : "true"}}
  </button>
</div>

<script>
//定义一个组件,名为App
const App = {
  //组件的data方法,提供应用所需全局数据
  data() {
    //返回对象
    return {
      show: true,
    };
  },

  //配置组件使用的方法
  methods: {
    btn() {
      this.show = !this.show;
    },
  },
};

Vue.createApp(App).mount("#Application"); //将Vue组件绑定到页面上id为Application的元素上。
</script>

运行代码可以看到,标题元素与段落元素是互斥出现的,如果根据条件渲染出了标题元素,则不会再渲染出段落元素,如果没有渲染出标题元素,则会渲染出段落元素。

需要注意,在将v-ifv-else结合使用时,设置了 v-else 指令的元素必须紧跟在v-ifv-else-if指令的元素后面,否则其不会被识别到。

错误示例:

<h1 v-if="show">标题</h1>
<h1>
    Hello
</h1>
<p v-else>
    如果不显示标题就显示段落
</p>

在 v-if 与 v-else 之间,我们还可以插入任意个 v-else-if 来实现多分支渲染逻辑。在实际应用中,多分支渲染逻辑也很常用,例如根据学生的分数来将成绩进行分档,就可以使用多分支逻辑。示例代码如下:

<h1 v-if="mark == 100">
    满分
</h1>
<h1 v-else-if="mark > 60">
    及格
</h1>
<h1 v-else>
    不及格
</h1>

实例:

<!--载入vue.js-->
<script src="https://unpkg.com/vue@next"></script>

<div id="Application">
  <h1 v-if="mark == 100">满分</h1>
  <h1 v-else-if="mark > 80">优秀</h1>
  <h1 v-else-if="mark > 60">及格</h1>
  <h1 v-else>不及格</h1>
  <button @click="btn">生成分数:{{ mark }}</button>
</div>

<script>
//定义一个组件,名为App
const App = {
  //组件的data方法,提供应用所需全局数据
  data() {
    //返回对象
    return {
      mark: "100",
    };
  },

  //配置组件使用的方法
  methods: {
    //随机生成一个mark值
    btn() {
      this.mark = Math.ceil(Math.random() * 100);
    },
  },
};

Vue.createApp(App).mount("#Application"); //将Vue组件绑定到页面上id为Application的元素上。
</script>

v-if 指令的使用必须添加到一个 HTML 元素上,如果我们需要使用条件同时控制多个标签元素的渲染,有两种方式可以实现。

(1)使用div标签对要进行控制的元素进行包装,示例如下:

<div v-if="show">
    <p>内容</p>
    <p>内容</p>
    <p>内容</p>
</div>

(2)使用template标签对元素进行分组,示例如下:

<template v-if="show">
  <p>内容</p>
  <p>内容</p>
  <p>内容</p>
</template>

通常,更推荐使用template分组的方式来控制一组元素的条件逻辑渲染,因为在 HTML 渲染元素时,使用div包装组件后,div元素本身会被渲染出来,而使用template进行分组的组件渲染后并不会渲染template标签本身。

我们可以通过 Chrome 浏览器来验证这种特性。

在开发者工具窗口的Elements栏目中可以看到使用div和使用template标签对元素组合包装进行条件渲染的异同。

2.2.2 使用 v-show 指令进行条件渲染

v-show指令的基本用法与 v-if 类似,其也是通过设置条件的值的真假来决定元素的渲染情况的,示例如下:

<h1 v-show="show">v-show标题在这里</h1>

与 v-if 不同的是,v-show并不支持template模版,同样也不可以和 v-else 结合使用。

虽然,v-ifv-show的用法非常相似,但是它们的渲染逻辑是天差地别的。

从元素本身的存在性来说,v-if采石真正意义上的条件渲染,其在条件变换的过程中,组件内部是事件监听器都会正常地执行,子组件也会正常地被销毁或重建。同事,v-if采取的撒懒加载的方式进行渲染,热感初始条件为假,则关于这个组件的任何渲染工作都不会进行,至到其绑定的条件为真时,才会真正开始渲染此元素。

v-show指令的渲染逻辑只是一种视觉上的条件渲染,实际上无论v-show指令设置的条件是真是假,当前元素都会被渲染,v-show指令只是简单地通过切换元素 CSS 样式中的display属性来实现展示效果。

我们可以通过 Chrome 浏览器的开发者工具来观察v-ifv-show指令的渲染逻辑。

<h1 v-if="show">v-if标题在这里</h1>
<h1 v-show="show">v-show标题在这里</h1>

实例:

<!--载入vue.js-->
<script src="https://unpkg.com/vue@next"></script>

<div id="Application">
  <h1 v-if="show">v-if标题在这里</h1>
  <h1 v-show="show">v-show标题在这里</h1>
  <button @click="btn">
    切换按钮,切换show属性为:{{show ? "false" : "true"}}
  </button>
</div>

<script>
//定义一个组件,名为App
const App = {
  //组件的data方法,提供应用所需全局数据
  data() {
    //返回对象
    return {
      show: true,
    };
  },

  //配置组件使用的方法
  methods: {
    btn() {
      this.show = !this.show;
    },
  },
};

Vue.createApp(App).mount("#Application"); //将Vue组件绑定到页面上id为Application的元素上。
</script>

当条件为假时,可以看到,v-if指定的元素不会出现在 HTML 文档 DOM 结构中,而v-show指定的元素依然会存在。

由于v-ifv-show这两种指令的渲染原理不同,通常v-if指令有更高的切换性能,而v-show指令有更高的初始渲染性能消耗。

在实际的开发中,

如果组件的渲染条件会比较繁琐地切换,则建议使用v-show指令来控制

如果组件的渲染条件中初始指定后就很少变化,则建议使用 v-if 指令控制

2.3 循环渲染

在网页中,列表是非常常见的一种组件。在列表中,每一行元素都有相似的 UI,只是其填充的数据有所不同,使用 Vue 中的循环渲染指令,我们可以轻松地构建出列表视图。

2.3.1 v-for 指令的使用方法

在 Vue 中,v-for指令可以将一个数组中的数据渲染为列表视图。v-for指令需要设置为一种特殊的语法,其格式如下:

item in list

在上面的格式中,in为语法关键字,其也可以替换为of

v-for指令中,

item是一个临时变量,其为列表中被迭代出的元素名,

list是列表变量本身。

实例:

<!--载入vue.js-->
<script src="https://unpkg.com/vue@next"></script>

<div id="Application">
  <div v-for="item in list">{{ item }}</div>
</div>

<script>
  //定义一个组件,名为App
  const App = {
    //组件的data方法,提供应用所需全局数据
    data() {
      //返回对象
      return {
        list: [1, 2, 3, 4, 5],
      };
    },
  };

  Vue.createApp(App).mount("#Application"); //将Vue组件绑定到页面上id为Application的元素上。
</script>

更懂的时候,我们需要渲染的数据都是对象数据,使用对象来对列表元素进行填充。

实例:

<!--载入vue.js-->
<script src="https://unpkg.com/vue@next"></script>

<div id="Application">
  <ul>
    <li v-for="item in list">
      <div>{{ item.name }}</div>
      <div>{{ item.num }}</div>
    </li>
  </ul>
</div>

<script>
  //定义一个组件,名为App
  const App = {
    //组件的data方法,提供应用所需全局数据
    data() {
      //返回对象
      return {
        list: [
          {
            name: "辉少",
            num: "asdfsadf",
          },
          {
            name: "Jack",
            num: "5234234",
          },
          {
            name: "Tim",
            num: "41234",
          },
          {
            name: "Lucy",
            num: "241234",
          },
          {
            name: "Baobo",
            num: "234234",
          },
        ],
      };
    },
  };

  Vue.createApp(App).mount("#Application"); //将Vue组件绑定到页面上id为Application的元素上。
</script>

在 v-for 指令中,我们也可以获取到当前遍历项的索引,示例如下:

<ul>
    <li v-for="(item,index) in list">
    <div>{{ index + "." + item.name}}</div>
    <div>{{ item.num  }}</div>
    </li>
</ul>

注意,index 索引的取值是从 0 开始的。

在上面的示例代码中,v-for指令遍历的为列表,实际上我们也可以对一个 JavaScript 对象进行v-for遍历。在 JavaScript 中,列表本身也是一种特殊的对象,我们使用v-for对对象进行遍历时,

v-for="(value,key,index) in preson"

  • 指令中的第 1 个参数为遍历对象中的属性的值 - value(辉少)

  • 第 2 个参数为遍历的对象中的属性的名字 - key(name)

  • 第 3 个参数为遍历的索引 - index(0,1,2,3)

实例:

<!--载入vue.js-->
<script src="https://unpkg.com/vue@next"></script>

<div id="Application">
  <ol>
    <li v-for="(value,key,index) in preson ">{{key}}:{{value}}</li>
  </ol>
</div>

<script>
//定义一个组件,名为App
const App = {
  //组件的data方法,提供应用所需全局数据
  data() {
    //返回对象
    return {
      preson: {
        name: "辉少",
        age: "00",
        num: "asdfsadf",
        email: "xxx@xx.cn",
      },
    };
  },
};

Vue.createApp(App).mount("#Application"); //将Vue组件绑定到页面上id为Application的元素上。
</script>

注意,在使用v-for指令进行循环渲染时,为了更好地对列表项进行重用,我们可以将其key属性绑定为一个唯一的值,代码如下:

<ol>
    <li v-for="(value, key, index) in person" :key="index">
    {{ key }}:{{value}}
    </li>
</ol>

通过设置一个惟一的键属性,它可以确保组件以您期望的方式工作。

如果我们不使用key,Vue 将尝试使 DOM 尽可能高效,这可能意味着 v-for 元素可能会出现乱序或其他不可预测的行为。

如果我们对每个元素都有唯一的键引用,那么我们就可以更好地准确地预测 DOM 将如何操作。

2.3.2 v-for 指令的高级用法

当我们使用v-for指令进行循环渲染时,实际上就实现了对这个数据对象的绑定。当我们调用下面这些函数对列表数据对象进行更新时,视图也是对应地进行更新:

函数效果
push()向列表尾部追加一个元素,向数组的末尾添加一个或多个元素,并返回新的长度
pop()删除列表尾部的一个元素,最后一位元素删除并返回数组的最后一个元素
shift()删除列表头部的一个元素,把数组的第一个元素从其中删除,并返回第一个元素的值
unshift()向列表头部插入一个元素,向数组的开头添加一个或更多元素,并返回新的长度
splice()对列表进行分割操作,从数组中添加/删除项目,然后返回被删除的项目
sort()对列表进行排序操作,对原列表进行排序,如果指定参数,则使用比较函数指定的比较函数·
reverse()对列表进行逆序

splice(index,howmany,item1, …, itemX) 方法向/从数组中添加/删除项目,然后返回被删除的项目 第一个参数:表示从哪个索引位置(index)添加/删除元素 第二个参数:要删除的项目数量。如果设置为 0,则不会删除项目。 第三个参数:可选。向数组添加的新项目。

逆序实例 - 2_3-2-1.html:


  <!--载入vue.js-->
  <script src="https://unpkg.com/vue@next"></script>

  <div id="Application">

    <button @click="click_push">向列表尾部追加一个元素</button>
    <button @click="click_pop">删除列表尾部的一个元素</button>
    <button @click="click_unshift">向列表头部插入一个元素</button>
    <button @click="click_shift">删除列表头部的一个元素</button>
    <button @click="click_splice">对列表进行分割操作</button>
    <button @click="click_sort">对列表进行排序操作</button>
    <button @click="click_reverse">对列表进行逆序</button>
    <h1>排序</h1>
    <div v-for="(item, key, index) in list" :key="index">{{key}}->{{item}}</div>
  </div>

  <script>
    const App = {
      data() {
        return {
          list: [1, 2, 3, 4, "b", "a", "c",],
          msg: "简简单单的生活",
        };
      },



      methods: {


        //向列表尾部追加一个元素
        click_push() {
          this.list.push(this.msg);
        },

        //删除列表尾部的一个元素
        click_pop() {
          this.list.pop();
        },

        //把数组的第一个元素从其中删除,并返回第一个元素的值
        click_shift() {

          this.list.shift();
        },

        //向数组的开头添加一个或更多元素,并返回新的长度。
        click_unshift() {
          this.list.unshift(this.msg);
        },

        //对列表进行分割操作
        click_splice() {
          this.list.splice(1, 2, "a", "b");
          //从索引位置(index:1)删除,删除2个元素,并添加2个新元素来替代被删除的元素
        },

        //对列表进行排序操作
        click_sort() {
          this.list.sort();//按字母顺序排序
        },

        //对列表进行逆序
        click_reverse() {
          this.list.reverse();
        },
      },



    };

    Vue.createApp(App).mount("#Application");
  </script>
  <style>
    button {
      display: block;
    }
  </style>

若需添加动画,可参考第 8.3.4 节 - 列表过度动画

运行代码,可以看到当单击页面上的按钮时,列表元素的渲染顺序会进行正逆交换。

当我们需要对整个列表都进行替换时,直接对列表变量重新赋值即可。

在实际开发中,原始的列表数据往往并不适合直接渲染到页面,v-for指令支持在渲染前对数据进行额外的处理。

<ul>
    <li v-for="(item, index ) in handle(list)" :key="index" >
        <div>
            {{ index + "." + item.name}}
        </div>
        <div>
            {{ item.num }}
        </div>
    </li>
</ul>

上面的代码中,handle为定义的处理函数,在进行渲染前,通过这个函数来对列表数据进行处理。例如,我们可以使用过滤器来进行列表数据的过滤渲染,实现handle函数如下:

handle(1) {
    return 1.filter(obj => obj.name != "辉少")
}

实例:

<h1>数据渲染前预处理</h1>
<!--载入vue.js-->
<script src="https://unpkg.com/vue@next"></script>

<div id="Application">
  <ul>
    <li v-for="( item, index ) in handle( list )" :key="index">
      <div>{{ index + "." + item.name }}</div>
      <div>{{ item.num }}</div>
    </li>
  </ul>
</div>

<script>
  const App = {
    data() {
      return {
        list: [
          {
            name: "辉少",
            num: "asdfsadf",
          },
          {
            name: "Jack",
            num: "5234234",
          },
          {
            name: "Tim",
            num: "41234",
          },
          {
            name: "Lucy",
            num: "241234",
          },
          {
            name: "Baobo",
            num: "234234",
          },
        ],
      };
    },

    methods: {
      handle(l) {
        return l.filter((obj) => obj.name != "辉少");
        //filter:它创建一个新数组,新数组中的元素是通过检查指定数组中符合条件的所有元素。
      },
    },
  };

  Vue.createApp(App).mount("#Application");
</script>

当需要同时循环多个元素时,与v-if指令类似,常用的方式是使用template标签进行包装,如下

<template v-for="(item, index) in list" :key="item">
  <div>{{ index + "." + item.name }}</div>
  <div>{{ item.num }}</div>
</template>

2.4 范例:实现待办任务列表应用

<!--载入vue.js-->
<script src="https://unpkg.com/vue@next"></script>

<div id="Application">
  <!--输入框元素,用来新建待办任务-->
  <form @submit.prevent="addTask">
    <!--.prevent修改提交属性,使其提交不刷新页面-->
    <span>新建任务</span>
    <input v-model="taskText" placeholder="请输入任务..." />
    <button>添加</button>
  </form>

  <!--有序列表,使用v-for来构建-->
  <ol>
    <li v-for="item in todos" :key="keys">
      {{item}}
      <button @click="remove(keys)">删除任务</button>
      <hr />
    </li>
  </ol>
</div>

<script>
const App = {
  data() {
    return {
      //待办任务列表数据
      todos: [],
      //当前输入的待办任务
      taskText: "",
    };
  },
  methods: {
    //添加一条待办任务
    addTask() {
      //判断输入框是否为空
      if (this.taskText.length == 0) {
        alert("请输入任务");
        return;
      }
      this.todos.push(this.taskText); //给数组添加新元素
      this.taskText = "";
    },
    //删除一条待办任务
    remove(keys) {
      this.todos.splice(keys, 1);
    },
  },
};

Vue.createApp(App).mount("#Application");
</script>

2.5:补充

2.5.1:使用v-for时,不建议将index作为key

使用 Vue SFC Playground 来演示这个过程

实例

 <!--载入vue.js-->
    <script src="https://unpkg.com/vue@next"></script>
    <div id="Application" style="text-align: center">
        <h2>index</h2>
        <ul>
            <li v-for="(item, index) in list" :key="index">
                item: {{item}}
                <button @click="handleRemove(index)">
                    删除
                </button>
            </li>
        </ul>
        <h2>item</h2>
        <ul>
            <li v-for="(item, index) in list" :key="item">
                item: {{item}}
                <button @click="handleRemove(index)">
                    删除
                </button>
            </li>
        </ul>
        <button @click="handleAddd">
            新增
        </button>
    </div>

    <script>
        const App = Vue.createApp({
            setup() {
                const list = Vue.ref(['111', '222', '333', '444'])

                const handleAddd = () => {
                    list.value.unshift('test' + Math.random().toString(16).slice(2))
                }

                const handleRemove = index => {
                    list.value.splice(index, 1)
                }
                return {
                    list,
                    handleAddd,
                    handleRemove
                }
            }
        })
        //将Vue组件绑定到页面上id为Application的元素上
        App.mount("#Application");
    </script>

如上代码所示,我们使用index作为key时,此时我们在数组 list 的头部添加一个元素,会导致其他li进行不必要的更新。

因list子项的key发生了变化导致更新

删除也是如此,由于li的key发生变化,会导致不必要的更新

发生变化的子项key会导致dom更新

此时我们将 key 绑定为 item 时,将只更新需要更新的dom

应确保绑定的 keylist 中能唯一,不与其他项相同

key绑定为唯一确定该项的值时将减少不必要的dom更新

第三章:Vue 组件的属性和方法

在定义 Vue 组件时,属性和方法是很重要的两部分。我们创建组件时,实现了其内部的 data 方法,这个方法会返回一个对象,此对象中定义的数据会存储在组件实例中,并提供响应式的更新原理来影响页面渲染。

方法定义在 Vue 组件的methods选项中,其与属性一样,可以在组件中访问到。本章将介绍有关 Vue 组件中属性与方法的相关基础知识。以及计算属性和侦听器的应用。

  • 属性的基础知识
  • 方法的基础知识
  • 计算属性的应用
  • 侦听器的应用
  • 如何进行函数的限流
  • 表单的数据绑定技术
  • 使用 Vue 进行样式绑定

3.1 属性与方法基础

前面编写 Vue 组件时,组件的数据都放在了data选项中,Vue 组件的data选项是一个函数,组件在被创建时会调用此函数来构建响应的数据系统。

3.1.1 属性基础

在 Vue 组件中定义的属性数据应该怎么调用呢?

可以直接使用组件来调用组件中的属性数据

在 Vue 组件中定义的属性数据,我们可以直接使用组件来调用,这是因为 Vue 在组织数据时,任何定义的属性都会暴露在组件中。实际上,这些属性数据是存储在组件的$data对象中的,示例如下:

//定义组件
const App = {
  data() {
    return {
      count: 0,
    };
  },
};

//创建组件并获取组件实例
let instance = Vue.createApp(App).mount("#Application");

//可以获取到组件中的data数据
console.log(instance.count);

//可以获取到组件中的data数据
console.log(instance.$data.count);

实例:

<!--载入vue.js-->
<script src="https://unpkg.com/vue@next"></script>

<div id="Application">{{count}}</div>
<script>
//定义组件
const App = {
  data() {
    return {
      count: "5",
    };
  },
};

//创建组件并获取组件实例
let instance = Vue.createApp(App).mount("#Application");

//可以获取到组件中的data数据
console.log(instance.count);
//

//可以获取到组件中的data数据
console.log(instance.$data.count);
</script>

运行上面的代码,通过控制台的打印可以看出,使用组件实例直接获取属性与使用$data的方式获取属性的结构是一样的,本质上访问的数据也是同一块数据,无论使用那种方式对数据进行修改,两种方式获取到的值都会改变,示例如下:

//修改属性
instance.count = 5;

//下面获取到的count的值为5
console.log(instance.cont);
console.log(instance.$data.count);

需要注意,在实际开发中,我们也可以动态地向组件实例中添加属性,但是这种方式添加的属性不能被响应式系统跟踪,其变化无法同步到页面元素。

3.1.2 方法基础

组件的方法被定义在methods选项中,我们在实现组件的方法是,可以放心地中其中使用this关键字,Vue 自动将其绑定到当前组件本身。如下:

methods: {
    add() {
        this.count ++
    }
}

我们可以将其绑定到 HTML 元素上,也可以直接使用组件实例来调用此方法,示例如下:

// 0
console.log(instance.count);
instance.add();

// 1
console.log(instance.count);

实例:

<!--载入vue.js-->
<script src="https://unpkg.com/vue@next"></script>
<h1>方法的基础</h1>
<div id="Application">{{count}}</div>
<script>
//定义组件
const App = {
  data() {
    return {
      count: "5",
    };
  },
  methods: {
    add() {
      this.count++;
    },
  },
};

//创建组件并获取组件实例
let instance = Vue.createApp(App).mount("#Application");

// 0
console.log(instance.count);
instance.add();

// 1
console.log(instance.count);
</script>

3.2 计算属性和侦听器

大多数情况下,我们到可以将 Vue 组件中定义的属性数据直接渲染到 HTML 元素上,但是有些场景下,属性中的数据并不适合直接渲染,需要处理后再进行渲染。在 Vue 中,我们通常使用计算属性或侦听器来实现这种逻辑。

3.2.1 计算属性

在前面的章节中,我们定义的属性都是存储属性。存储属性的值是我们直接定义好的,当前属性只是起到了存储这些值的作用。在 Vue 中,与之相对的还有计算属性,计算属性并不是用来存储数据的,而是通过一些计算逻辑来实时地维护当前属性的值。

以 3.1 节的代码为例,假设我们需要中组件中定义一个type属性,当组件的count属性不大于 10 时,type属性的值为“小”,否则 type 属性的值为“大”。示

实例:

<!--载入vue.js-->
<script src="https://unpkg.com/vue@next"></script>
<h1>计算属性</h1>
<div id="Application">{{count}}</div>
<script>
//定义组件
const App = {
  data() {
    return {
      count: 0,
    };
  },
  // computed选项定义计算属性
  computed: {
    type() {
      return this.count > 10 ? "大" : "小";
    },
  },
  methods: {
    add() {
      this.count++;
    },
  },
};
//创建组件并获取组件实例
let instance = Vue.createApp(App).mount("#Application");

//像访问普通属性一样访问计算属性
console.log(instance.type);
</script>

如上代码所示,计算属性定义在 Vue 组件的computed选项中,在使用时,我们可以像访问普通属性那样访问它。通常计算属性最终的值都是由存储属性通过逻辑运算得来的。

计算属性强大的地方在于,当会影响其值的存储属性发生变化时,计算属性也会同步进行更新,如果有元素绑定了计算属性,其也会同步进行更新。示例如下:

<div id="Application">
    <div>
        {{ type }}
    </div>
    <button @click="add">
        Add
    </button>
</div>

实例:

<!--载入vue.js-->
<script src="https://unpkg.com/vue@next"></script>
<h1>计算属性</h1>
<div id="Application">{{count}}</div>
<script>
//定义组件
const App = {
  data() {
    return {
      count: 8,
    };
  },
  // computed选项定义计算属性
  computed: {
    type() {
      return this.count > 10 ? "大" : "小";
    },
  },
  methods: {
    add() {
      this.count++;
    },
  },
};
//创建组件并获取组件实例
let instance = Vue.createApp(App).mount("#Application");

//像访问普通属性一样访问计算属性
console.log(instance.type);
</script>

运行代码,单击页面上的按钮,当组件count的值超过 10 时,页面上对应的文案会更新成”大“。

3.2.2 使用计算属性还是函数

对于 3.2.1 节示例的场景,我们也可以使用函数来实现,实例如下:

<!--载入vue.js-->
<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <div id="Application">
    <div>{{ typeFunc() }}</div>
    <button @click="add">Add</button>
  </div>
</div>
<script>
  //定义组件
  const App = {
    data() {
      return {
        count: 8,
      };
    },
    // computed选项定义计算属性
    computed: {
      type() {
        return this.count > 10 ? "大" : "小";
      },
    },
    methods: {
      add() {
        this.count++;
      },
      typeFunc() {
        return this.count > 10 ? "大" : "小";
      },
    },
  };
  //创建组件并获取组件实例
  let instance = Vue.createApp(App).mount("#Application");

  //像访问普通属性一样访问计算属性
  console.log(instance.type);
</script>

从代码的运行行为上看,使用函数与使用计算属性的结果完全一致。

然而,事实上,

计算属性是基于其所依赖的存储属性的值的变化而重新计算的。计算完成后,其结构会被缓存,下次访问计算属性时,只要其所依赖的属性没有变化,其内的逻辑代码就不会被重复执行。

而函数则不同,每次访问器都会重新执行函数内的逻辑代码得到的结果。

因此,在实际应用中,我们可以根据是否需要缓存这一标准来选择使用计算属性或函数

需缓存 → 计算属性

无需缓存 → 函数

3.2.3 计算属性的赋值

存储属性主要用于数据的存取,我们可以使用赋值运算来修改属性值。通常,计算属性只用来取值,不会用来存值,因此计算属性默认提供的是取值的方法,通常称为get方法。但是这并不代表计算属性不支持赋值,计算属性也可以通过赋值进行存数据操作,存数据的方法我们需要手动实现,通常称之为set方法。

例如,修改上一节编写的代码中type计算属性。

            computed: {
                type: {
                    //实现计算属性的get方法,用来取值
                    get() {
                        return this.count > 11 ? "大" : "小"
                    },

                    //实现计算属性的set方法,用来设置值
                    set(newValue) {
                        if (newValue == "大") {
                            this.count = 11
                        } else {
                            this.count = 0
                        }
                    },
                }
            },

可以直接使用组件实例计算属性type的赋值,赋值时会调用我们定义的set方法,从而实现对存储属性counnt的修改,示例如下:

//创建组件并获取组件实例
let instance = Vue.createApp(App).mount("#Application");
//像访问普通属性一样访问计算属性
//初始值为0
console.log(instance.count);
//初始状态为小
console.log(instance.type);

//对计算属性进行修改
instance.type = "大";

//打印结果为11
console.log(instance.count);

实例:

<!--载入vue.js-->
<script src="https://unpkg.com/vue@next"></script>

<div id="Application">
  <h1>{{count}}</h1>
  <h1>{{type}}</h1>
  <button @click="add">Add</button>
</div>

<script>
const App = {
  data() {
    return {
      count: 0,
    };
  },
  computed: {
    type: {
      //实现计算属性的get方法,用来取值
      get() {
        return this.count > 11 ? "大" : "小";
      },

      //实现计算属性的set方法,用来设置值
      set(newValue) {
        if (newValue == "大") {
          this.count = 11;
        } else {
          this.count = 0;
        }
      },
    },
  },

  methods: {
    add() {
      this.count++;
    },
  },
};

//创建组件并获取组件实例
let instance = Vue.createApp(App).mount("#Application");
//像访问普通属性一样访问计算属性
//初始值为0
console.log(instance.count);
//初始状态为小
console.log(instance.type);

//对计算属性进行修改
instance.type = "大";

//打印结果为11
console.log(instance.count);
</script>

如上代码所示,在实际使用中,计算属性对使用方是透明的,我们无需关心某个属性是不是计算属性,按照普通属性的方式对其进行使用即可。

但需要额外注意的,如果一个计算属性只实现了get方法而没有实现set方法,则在使用时只能进行取值操作,而不能进行赋值操作。

在 Vue 中,这类只实现了get方法的计算属性也被称为只读属性,如果对一个只读属性进行赋值操作,则会产生异常,响应的控制台会输出如下异常信息:

[Vue warn]:Write operation failed: computed property "type" is readonly.

3.2.4 属性侦听器

属性侦听是 Vue 非常强大的功能之一。使用属性侦听器可以方便地监听某个属性的变化,以完成复杂的业务逻辑。相信大部分使用互联网的人都使用过搜索引擎,以百度搜索引擎为例,当我们向搜索框中写入关键字后,网页上会自动关联一些推荐词供用户选择,这种场景就非常适合使用监听器来实现。

在定义 Vue 组件时,可以通过watch选项来定义属性侦听器。

实例:

<!--载入vue.js-->
<script src="https://unpkg.com/vue@next"></script>

<div id="Application">
  <input v-model="searchText" />
</div>

<script>
//定义组件
const App = {
  //组件的data方法
  data() {
    //返回对象
    return {
      searchText: "",
    };
  },
  //计算属性
  computed: {},
  //属性侦听
  watch: {
    searchText(oldValue, newValue) {
      if (newValue.length > 10) {
        alert("文本太长了");
      }
    },
  },

  //组件的方法
  methods: {},
};

Vue.createApp(App).mount("#Application");
</script>

运行上面的代码,尝试在页面的输入框中输入一些字符,可以看到当输入框中的字符超过 10 个时,就会有警告框弹出,提示输入的文本过长。

从一些特性上来看,属性侦听器和计算属性有类似的应用场景,使用计算属性的set方法也可以实现与上面实例代码类似的功能。

3.3 进行函数限流

在工程开发中,限流是一个非常重要的概念。我们在实际开发中也经常会遇到需要进行限流的场景,例如网页上某个按钮当用户单击后会从后端服务器进行数据的请求,在数据请求回来之前,用户额外的单机是无效的且消耗性能的。

或者,网页中命某个按钮会导致页面的更新,我们需要限制用户对其频繁地进行操作。这时就可以使用限流函数,场景的限流方案是根据时间间隔进行限流,即在指定的时间间隔内不允许重复执行同一函数。

本节将讨论如何在前端开发中使用限流函数。

3.3.1 手动实现一个简易的限流函数

先尝试手动实现一个基于时间间隔的限流函数,要实现这样一个功能:页面中有一个按钮,单机按钮后通过打印方法在控制台输出当前时间,要求这个按钮的两次事件触发间隔不能小于 2 秒。

分析我们需要实现的功能,直接的思路是使用一个变量来控制按钮事件是否可触发,在触发按钮事件时对此变量进行修改,并使用setTimeout函数来控制 2 秒后将变量的值还原。使用这个思路来实现限流函数非常简单,

实例:

<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<!--载入vue.js-->
<script src="https://unpkg.com/vue@next"></script>

<div id="Application">
  点击一次,两秒后可获取时间
  <button @click="click">按钮</button>
</div>

<script>
const App = {
  //组件的data方法
  data() {
    //返回对象
    return {
      throttle: false,
    };
  },

  //组件的方法
  methods: {
    //限流函数原理
    click() {
      if (!this.throttle) {
        console.log(Date());
      } else {
        return;
      }
      this.throttle = true;
      setTimeout(() => {
        this.throttle = false;
      }, 2000); //每隔2秒执行一次函数
    },
  },
};

Vue.createApp(App).mount("#Application");
</script>

运行上面的代码,快速单击页面上的按钮,从浏览器控制台中可以看到,无论按钮被单击了多少次,打印方法都按照每 2 秒最多执行 1 次的频率进行限流。

其实,在上述代码中,限流本身是一种通用逻辑,打印时间才是业务逻辑,因此我们可以将限流的逻辑封装成单独的工具方法,修改核心 JavaScript 代码如下:

<!--载入vue.js-->
<script src="https://unpkg.com/vue@next"></script>

<div id="Application">
  点击一次,两秒后可获取时间
  <button @click="n_click">按钮</button>
</div>

<script>
//定义组件
var throttle = false;
function throttleTool(callback, timeout) {
  if (!throttle) {
    callback();
  } else {
    return;
  }
  throttle = true;
  setTimeout(() => {
    throttle = false;
  }, timeout);
}

const App = {
  //组件的方法
  methods: {
    //自做限流函数
    n_click() {
      throttleTool(() => {
        console.log(Date());
      }, 2000);
    },
  },
};

Vue.createApp(App).mount("#Application");
</script>

再次运行代码,程序依然可以正确地运行。我们现在已经有了一个限流工具,可以为任意函数增加限流功能,并且可以任意地设置限流的时间间隔。

3.3.2 使用 Lodash 库进行函数限流

目前我们已经了解了限流函数的实现逻辑,在 3.3.1 节中也手动实现了一个简单的限流工具,尽管其能够满足当前的需求,细细分析,还有许多需要优化的地方。在实际开发中,每个业务函数需要的限流间隔都不同,而且需要各自独立地进行限流,我们自己编写的限流工具就无法满足了,但是得益于 JavaScript 生态的繁荣,有许多第三方工具库都提供了函数限流功能,它们强大且易用,Lodash 库就是其中之一。

Lodash 是一款高性能的 JavaScript 实用工具库,其提供了大量的数组、对象、字符串等边界的操作方法,使开发者可以更加简单地使用 JavaScript 来编程。

Lodash 库中提供了debounce函数来进行方法的调用限流,要使用它,首先需要引入 Lodash 库,代码如下:

<script src="https://unpkg.com/lodash@4.17.20/lodash.min.js"></script>

以 3.3.1 节编写的代码为例,修改代码如下:

实例:

<script src="https://unpkg.com/vue@next"></script>
<script src="https://unpkg.com/lodash@4.17.20/lodash.min.js"></script>

<div id="Application">
  点击一次,两秒后可获取时间
  <button @click="p_click">按钮</button>
</div>

<script>
const App = {
  //组件的方法
  methods: {
    //用Lodash函数
    p_click: _.debounce(function () {
      console.log(Date());
    }, 2000),
  },
};

Vue.createApp(App).mount("#Application");
</script>

运行代码,体验一下 Lodash 限流函数的功能。

3.4 表单数据的双向绑定

双向绑定是 Vue 中处理用户交互的一种方式,文本输入框、多行文本输入框、单选框与多选框等都可以进行数据的双向绑定。

3.4.1 文本输入框

文本输入框的数据绑定之前有使用过,使用 Vue 的v-model指令直接设置即可。

实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <input v-model="textField" />
  <p>文本输入框内容:{{textField}}</p>
</div>
<script>
const App = {
  data() {
    return {
      textField: "",
    };
  },
};
Vue.createApp(App).mount("#Application");
</script>

运行代码,当输入框中输入的文本发生变化时,我们可以看到段落中的文本也会同步产生变化。

3.4.2 多行文本输入区域

多行文本可以使用textarea标签来实现,textarea可以方便地定义一块区域用来显示和输入多行文本,文本支持换行,并且可以设置最多可以输入多少文本。

textarea的数据绑定方式与input一样,

实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <textarea v-model="textField"></textarea>

  <p style="white-space: pre-line">多行文本内容:{{textField}}</p>
</div>
<script>
const App = {
  data() {
    return {
      textField: "",
    };
  },
};
Vue.createApp(App).mount("#Application");
</script>

在上面的代码中,为p标签设置white-space样式是为了使其可以正常地展示多行文本中的换行,

注意,textarea元素只能通过v-model指令的方式来设置内容,不能直接在标签内插入文本,例如下面的代码是错误的:

<textarea v-model="textarea">{{text}}</textarea>"

3.4.3 复选框与单选框

复选框为网页提供多项选择的功能,当将 HTML 中的input标签的类型设置为checkbox时,其就会以复选框的样式进行渲染。

复选框通常成组出现,每个选项的状态只有两种,选中或未选中,如果只有一个复选框,在使用v-model指令进行数据绑定时,可以直接将其绑定为布尔值,

实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <input type="checkbox" v-model="checkbox" />
  <h2>布尔值:{{checkbox}}</h2>
</div>
<script>
const App = {
  data() {
    return {
      checkbox: false,
    };
  },
};
Vue.createApp(App).mount("#Application");
</script>

运行上面的代码,当复选框的选中状态发生变化时,对应的属性chexkbox的值也会切换。

更多时候复选框都是成组出现的,这时我们可以为每一个复选框元素设置一个特殊的值。通过数组属性的绑定来获取每个复选框是否被选中,如果被选中,则数组中会存在其所关联的值;如果没有被选中,则数组中其关联的值会被删除掉。

实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <input type="checkbox" value="足球" v-model="checkList" />足球
  <input type="checkbox" value="篮球" v-model="checkList" />篮球
  <input type="checkbox" value="排球" v-model="checkList" />排球
  <h2>您选中的是:{{checkList}}</h2>
</div>
<script>
const App = {
  data() {
    return {
      checkList: [],
    };
  },
};
Vue.createApp(App).mount("#Application");
</script>

单选框的数据绑定逻辑与复选框类似,对每一个单选框元素都可以设置一个特殊的值,并将同一组单选框绑定到同一个属性中即可,同一组中的某个单选框被选中时,对应的其绑定的变量的值也会替换为当前选中的单选框的值。

实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <h1>单选框绑定</h1>
  <input type="radio" value="男" v-model="sex" />男
  <input type="radio" value="女" v-model="sex" />女
  <p>您选中的是:{{sex}}</p>
</div>
<script>
const App = {
  data() {
    return {
      sex: "男",
    };
  },
};
Vue.createApp(App).mount("#Application");
</script>

3.4.4 选择列表

选择列表能够给用户一组选项进行选择,其可以支持单选,也可以支持多选。HTML 中使用select标签来定义选择列表。如果是单选的选择列表,可以将其直接绑定到 Vue 组件的一个属性上,如果是支持多选的选择列表,则可以将其绑定到数组属性上。

单选的选择列表,

实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <h1>单选-选择列表</h1>
  <select v-model="select">
    <option>男</option>
    <option>女</option>
  </select>
  <p>您单选的是:{{select}}</p>
</div>
<script>
const App = {
  data() {
    return {
      select: "女",
    };
  },
};
Vue.createApp(App).mount("#Application");
</script>

select标签内部,option标签用来定义一个选项,若要使选择列表支持多选操作,则只需要为其添加上multiple属性即可。

实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <h1>单选-多选列表</h1>
  按住command(control)按键即可进行多选
  <hr />
  <select v-model="selectList" multiple>
    <option>足球</option>
    <option>排球</option>
    <option>篮球</option>
  </select>
  <p>您多选的是:{{selectList}}</p>
</div>
<script>
const App = {
  data() {
    return {
      selectList: [],
    };
  },
};
Vue.createApp(App).mount("#Application");
</script>

在页面中进行选择时,按住command(control)按键即可进行多选

3.4.5 两个常用的修饰符

在对表单进行数据绑定时,我们可以使用修饰符来控制绑定指令的一些行为。比较常用的修饰符有lazytrim

lazy修饰符的作用有些类似于属性的懒加载,当我们使用v-model指令对文本输入框进行绑定时,每当输入框中的文本发生变换,其都会同步修改对应属性的值。

在某些业务场景下,我们并不需要实时关注输入框中文案的变化,只需要当用户输入完成后再进行数据逻辑的处理,这时就可以使用 lazy 修饰符,

实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <h2>输入框失去焦点再传入数据</h2>
  <input v-model.lazy="textField" />
  <p>文本输入框内容:{{textField}}</p>
</div>
<script>
const App = {
  data() {
    return {
      textField: "",
    };
  },
};
Vue.createApp(App).mount("#Application");
</script>

运行上面的代码,只有当用户完成输入,即输入框失去焦点后,段落中才会同步到输入框中最终的文本数据。

trim修饰符的作用是将绑定的文本数据的首尾空格去掉,在很多应用场景中,用户输入的文案都是要提交到服务端进行处理的,trim修饰符处理首尾空格的特性可以为开发者提供很大的方便。

实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <h2>去除文本首尾的空格</h2>
  <input v-model.trim="textField" />
  <p>文本输入框内容:{{textField}}</p>
</div>
<script>
const App = {
  data() {
    return {
      textField: "",
    };
  },
};
Vue.createApp(App).mount("#Application");
</script>

3.5 样式绑定

我们可以通过 HTML 元素的class属性,id属性或直接使用标签名来进行 CSS 样式的绑定,其中常用的撒使用class的方式进行样式绑定。在 Vue 中,对class属性的数据绑定做了特殊的增强,我们可以方便地通过布尔变量控制其设置的样式是否被选用

3.5.1 为 HTML 标签绑定 Class 属性

v-bind指令虽然可以直接对class属性进行数据绑定,但如果将绑定的值设置为一个对象,其就会产生一种新的语法规则,设置的对象中可以指定对应的class样式是否被选用。

实例:

<script src="https://unpkg.com/vue@next"></script>

<style>
  .red {
    color: red;
    font-size: 2em;
  }

  .blue {
    color: blue;
  }
</style>

<div id="app">
  <h1>内联对象</h1>
  <div :class="{blue:isBlue,red:isRed}">示例文案</div>
  <button @click="btn">改变样式</button>
</div>

<script>
  const App = {
    data() {
      return {
        //内联对象
        isBlue: true,
        isRed: false,
        //作为数据对象
      };
    },
    methods: {
      btn() {
        (this.isBlue = !this.isBlue), (this.isRed = !this.isRed);
      },
    },
  };
  Vue.createApp(App).mount("#app");
</script>

如以上代码所示,其中div元素的class属性的值会根据isBlueisRed属性的值而改变,当只有isBlue属性的值为true时,div元素的class属性为blue;当只有isRed属性的值为true时,div元素的class属性为red

注意,class属性可绑定的值并不会冲突,如果设置的对象中有多个属性的值都是true,则都会被添加到class属性中。

在实际开发中,并不一定要用内联的方式为class绑定控制对象,我们也可以直接将其设置为一个 Vue 组件中的数组对象。

实例:

<script src="https://unpkg.com/vue@next"></script>

<style>
    .red {
        color: red;
        font-size: 2em;
    }

    .blue {
        color: blue;
    }
</style>

<div id="app">
    <h1>数据对象</h1>
    <div :class="style">示例文案</div>
    <button @click="btn">改变样式{{ style.blue ? "为红" : "为蓝" }}</button>
</div>

<script>
    const App = {
        data() {
            return {
                //作为数据对象
                style: {
                    blue: true,
                    red: false,
                },
            };
        },
        methods: {
            btn() {
                (this.style.blue = !this.style.blue),
                    (this.style.red = !this.style.red);
            },
        },
    };
    Vue.createApp(App).mount("#app");
</script>

修改后的代码运行效果与之前完全一样,更多时候我们可以将样式对象作为计算属性返回,使用这种方式进行组件样式的控制非常有高效。

Vue 还支持使用数组对象来控制class属性,

实例:

<script src="https://unpkg.com/vue@next"></script>

<style>
    .red {
        color: red;
        font-size: 2em;
    }

    .font {
        font-weight: bold;
    }
</style>

<div id="app">
    <h1>数组对象</h1>
    <div :class="[redClass,fontClass]">
        示例文案
    </div>

</div>

<script>
    const App = {
        data() {
            return {
                //数组
                redClass: "red",
                fontClass: "font",
            };
        },
    };
    Vue.createApp(App).mount("#app");
</script>

3.5.2 绑定内联样式

内联样式是指直接通过 HTML 元素的style属性来设置样式,style 属性可以直接通过 JavaScript 对象来设置样式,我们可以直接中其内部使用 Vue 属性,

实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="app">
    <h1>绑定内联样式</h1>
    <div :style="{color:textColor,fontSize:textFont}">
        简单的一句话
    </div>
</div>
<script>
    const App = {
        data() {
            return {
                //绑定内联样式
                textColor: "green",
                textFont: "50px",

            }
        }
    }
    Vue.createApp(App).mount("#app")
</script>

需要注意,内联设置的 CSS 与外部定义的 CSS 有一点区别,

外部定义的 CSS 属性在命名时,多采用“-”符号进行连接(font-size),

内联的 CSS 属性的命名采用的是驼峰命名法(fontSize)

内联style同样支持直接绑定对象属性,直接绑定对象属性在实际开发中更加常用,使用计算属性来承载样式对象可以十分方便地进行动态样式更新。

实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="app">
    <h1>绑定内联对象</h1>
    <div :style="styleNpc">
        简单的一句话
    </div>

    <h1>绑定内联数组对象</h1>
    <div :style="[styleRed,styleFont]">
        简简单单一句话
    </div>
</div>

<script>
    const App = {
        data() {
            return {
                //对象
                styleNpc: {
                    color: "red",
                    fontSize: "100px",
                },
                //数组
                styleRed: {
                    color: "red"
                },
                styleFont: {
                    fontSize: "100px"
                }
            }
        }
    }
    Vue.createApp(App).mount("#app")
</script>

3.6 范例:实现一个功能完整的用户注册页面

<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <title>VUE_3_6 - 功能完整的用户注册页面</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <script src="https://unpkg.com/vue@next"></script>
</head>



<div class="container" id="Application">
    <div class="container">
        <div class="subTitle">加入我们,一起创造美好世界</div>
        <h1 class="title">创建你的账号</h1>
        <div v-for="(item, index) in fields" class="inputContainer">
            <div class="field">{{item.title}}
                <span v-if="item.required" style="color: red;">*</span>
            </div>

            <input v-model="item.model" class="input" :type="item.type" />
            <div class="tip" v-if="index == 2">请确认密码程度需要大于6位</div>
        </div>

        <div class="subContainer">
            <div class="setting">偏好设置</div>
            <input v-model="receiveMsg" class="checkbox" type="checkbox" />
            <label class="label">接受更新邮件</label>
        </div>


    </div>
    <button @click="createAccount" class="btn">创建账号</button>



</div>




<script>
    const App = {
        data() {
            return {
                fields: [
                    {
                        title: "用户名",
                        required: true,
                        type: "text",
                        model: "",
                    },

                    {
                        title: "邮箱地址",
                        required: false,
                        type: "text",
                        model: "",
                    },

                    {
                        title: "密码",
                        required: true,
                        type: "password",
                        model: "",
                    },

                ],
                receiveMsg: false,
            }
        },

        computed: {
            name: {
                get() {
                    return this.fields[0].model
                },

                set(value) {
                    this.fields[0].model = value//???
                }
            },

            email: {
                get() {
                    return this.fields[1].model
                },

                set(value) {
                    this.fields[1].model = value
                }
            },

            password: {
                get() {
                    return this.fields[2].model
                },

                set(value) {
                    this.fields[2].model = value
                }
            },
        },

        methods: {
            emailCheck() {
                var verify = /^[a-zA-Z0-9]+([-_.][A-Za-zd]+)*@([a-zA-Z0-9]+[-.])+[A-Za-zd]{2,5}$/
                if (!verify.test(this.email)) {//???
                    return false
                } else {
                    return true
                }
            },

            createAccount() {
                if (this.name.length == 0) {
                    alert("请输入用户名")
                    return
                } else if (this.password.length <= 6) {
                    alert("密码设置需大于六位字符")
                    return
                } else if (this.email.length > 0 && !this.emailCheck(this.email)) {
                    alert("请输入正确的邮箱")
                    return
                }

                alert("注册成功")
                console.log(`name:${this.name}\npassword:${this.password}\nemail:${this.email}\nreceiveMsg:${this.receiveMsg}`)
            }
        }
    }
    Vue.createApp(App).mount("#Application")

</script>




<style>
    .container {
        margin: 0 auto;
        margin-top: 70px;
        text-align: center;
        width: 300px;
    }

    .subTitle {
        color: gray;
        font-size: 14px;
    }

    .title {
        font-size: 45px;
    }

    .input {
        width: 90%;
    }

    .inputContainer {
        text-align: left;
        margin-bottom: 20px;
    }

    .subContainer {
        text-align: left;
    }

    .field {
        font-size: 14px;
    }

    .input {
        border-radius: 6px;
        height: 25px;
        margin-top: 10px;
        border-color: silver;
        border-style: solid;
        background-color: cornsilk;
    }

    .tip {
        margin-top: 5px;
        font-size: 12px;
        color: gray;
    }

    .setting {
        font-size: 9px;
        color: blank;
    }

    .label {
        font-size: 12px;
        margin-left: 5px;
        height: 20px;
        vertical-align: middle;
        /*该属性定义行内元素的基线相对于该元素所在行的基线的垂直对齐。*/
        /*???*/
    }

    .checkbox {
        height: 20px;
        vertical-align: middle;
    }

    .btn {
        border-radius: 10px;
        height: 40px;
        width: 300px;
        margin-top: 30px;
        background-color: deepskyblue;
        border-color: blue;
        color: white;
    }
</style>



</body>

</html>

在注册页面中,元素的 UI 效果预示了其部分功能,例如在输入框上方有些标了红星,表示此项是必填项,即如果用户不填写,将无法完成注册操作。

对于密码输入框,我们将其类型设置为password,当用户在输入文本时,此项会被自动加密。

在用户单击注册按钮时,我们需要获取用户输入的用户名、密码

邮箱和偏好设置,其中用户名和密码是必填项,并且密码的长度需要大于 6 位,对于用户输入的邮箱,也可以使用正则来对其进行校验,只有格式正确的邮箱才允许被注册。

由于页面中的 3 个文本框是通过循环动态渲染的,因此在对其进行绑定时,我们也需要采用动态的方式进行绑定。

通过配置输入框field对象来实现动态数据绑定,为了方便值的操作,我们使用计算属性对几个常用的输入框数据实现了便捷的存取方法,这些技巧都是本章介绍的核心内容。

当用户单击“创建账号”时,createAccount方法会进行一些有效性校验,我们对每个字段需要满足的条件依次进行校验即可,实例代码使用了正则表达式对邮箱地址的有效性进行了检查。

现在运行代码,在浏览器中尝试进行用户注册的操作。截止目前,我们完成了一个较为完善的客户端注册页面,在实际运用中,最终的注册操作还需要与后端进行交互。

修饰符补充

2.0 表单修饰符

trim,lazy

number

如果你想让用户输入自动转换为数字,你可以在 v-model 后添加 .number 修饰符来管理输入:

<input v-model.number="age" type="number" />

2.1 事件修饰符

事件修饰符是对事件捕获以及目标进行了处理,有如下修饰符:

  • stop
  • prevent
  • self
  • once
  • capture
  • passive
  • native
1. stop

阻止了事件冒泡,相当于调用了event.stopPropagation方法,单击事件将停止传递

<div @click="shout(2)">
  <button @click.stop="shout(1)">ok</button>
</div>
//只输出1 复制代码
2. prevent

阻止了事件的默认行为,相当于调用了event.preventDefault方法,提交事件将不再重新加载页面

<form @submit.prevent="onSubmit"></form>
复制代码
3. self

仅当 event.target 是元素本身时才会触发事件处理器,例如:事件处理器不来自子元素

<div v-on:click.self="doThat">...</div>
复制代码

使用修饰符时需要注意调用顺序,因为相关代码是以相同的顺序生成的。因此使用 @click.prevent.self 会阻止元素及其子元素的所有点击事件的默认行为@click.self.prevent 则只会阻止对元素本身的点击事件的默认行为。

4. once

绑定了事件以后只能触发一次,第二次就不会触发

<button @click.once="shout(1)">ok</button> 复制代码
5. capture

添加事件监听器时,使用 capture 捕获模式,例如:指向内部元素的事件,在被内部元素处理前,先被外部处理。使事件触发从包含这个元素的顶层开始往下触发

<div @click.capture="shout(1)">
  obj1
  <div @click.capture="shout(2)">
    obj2
    <div @click="shout(3)">
      obj3
      <div @click="shout(4)">obj4</div>
    </div>
  </div>
</div>
// 输出结构: 1 2 4 3 复制代码
6. passive

在移动端,当我们在监听元素滚动事件的时候,会一直触发onscroll事件会让我们的网页变卡,因此我们使用这个修饰符的时候,相当于给onscroll事件整了一个.lazy修饰符。

滚动事件的默认行为 (scrolling) 将立即发生而非等待 onScroll 完成,以防其中包含 event.preventDefault()

<!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 -->
<!-- 而不会等待 `onScroll` 完成  -->
<!-- 这其中包含 `event.preventDefault()` 的情况 -->
<div v-on:scroll.passive="onScroll">...</div>
复制代码

.passive 修饰符一般用于触摸事件的监听器,可以用来改善移动端设备的滚屏性能

请勿同时使用 .passive.prevent,因为 .passive 已经向浏览器表明了你不想阻止事件的默认行为。如果你这么做了,则 .prevent 会被忽略,并且浏览器会抛出警告。

7. native

让组件变成像html内置标签那样监听根元素的原生事件,否则组件上使用 v-on 只会监听自定义事件

<my-component v-on:click.native="doSomething"></my-component> 复制代码

使用.native 修饰符来操作普通 HTML 标签是会令事件失效的

2.3 鼠标按钮修饰符

鼠标按钮修饰符针对的就是左键、右键、中键点击,有如下:

  • left 左键点击
  • right 右键点击
  • middle 中键点击
<button @click.left="shout(1)">ok</button>
<button @click.right="shout(1)">ok</button>
<button @click.middle="shout(1)">ok</button>
复制代码

2.4 键盘修饰符

键盘修饰符是用来修饰键盘事件(onkeyuponkeydown)的,有如下:

keyCode存在很多,但vue为我们提供了别名,分为以下两种:

  • 普通键(enter、tab、delete、space、esc、up...)
  • 系统修饰键(ctrl、alt、meta、shift...)
// 只有按键为keyCode的时候才触发
<input type="text" @keyup.keyCode="shout()" />
复制代码

2.5 v-bind 修饰符

v-bind 修饰符主要是为属性进行操作,用来分别有如下:

  • async
  • prop
  • camel
1. async

能对props进行一个双向绑定

//父组件
<comp :myMessage.sync="bar"></comp>
//子组件 this.$emit('update:myMessage',params); 复制代码

以上这种方法相当于以下的简写

//父亲组件
<comp :myMessage="bar" @update:myMessage="func"></comp>
func(e){ this.bar = e; } //子组件js func2(){
this.$emit('update:myMessage',params); } 复制代码

使用async需要注意以下两点:

  • 使用sync的时候,子组件传递的事件名格式必须为update:value,其中value必须与子组件中props中声明的名称完全一致
  • 注意带有 .sync 修饰符的 v-bind 不能和表达式一起使用
  • v-bind.sync 用在一个字面量的对象上,例如 v-bind.sync=”{ title: doc.title }”,是无法正常工作的
2. props

设置自定义标签属性,避免暴露数据,防止污染 HTML 结构

<input id="uid" title="title1" value="1" :index.prop="index" /> 复制代码
3. camel

将命名变为驼峰命名法,如将 view-Box属性名转换为 viewBox

<svg :viewBox="viewBox"></svg> 复制代码
三、应用场景

根据每一个修饰符的功能,我们可以得到以下修饰符的应用场景:

  • .stop:阻止事件冒泡
  • .native:绑定原生事件
  • .once:事件只执行一次
  • .self :将事件绑定在自身身上,相当于阻止事件冒泡
  • .prevent:阻止默认事件
  • .caption:用于事件捕获
  • .once:只触发一次
  • .keyCode:监听特定键盘按下
  • .right:右键

第四章:处理用户交互

处理用户交互实际上就是对用户操作事件的监听和处理。

在 Vue 中,使用v-on指令来进行事件的监听和处理,可用”@“代替v-on指令。

对于网页应用,事件监听主要分键盘按键事件和鼠标操作事件。

本章将系统的介绍中 Vue 中监听和处理事件的方法。

本章学习:

  • 事件监听和处理方法
  • Vue 中的多事件处理功能的使用
  • Vue 中的事件修饰符的使用
  • 键盘事件与鼠标事件的处理

4.1 事件的监听与处理

v-on指令(通常使用@符号代替)用来为 DOM 事件绑定监听,

其可以设置为一个简单的 JavaScript 语句,也可以设置为一个 JavaScript 函数。

4.1.1 事件监听示例

DOM 事件的绑定

实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <div>点击次数:{{count }}</div>
  <button @click="click">点击</button>
</div>
<script>
  const App = {
    data() {
      return {
        count: 0,
      };
    },
    methods: {
      click() {
        this.count += 1;
      },
    },
  };
  Vue.createApp(App).mount("#Application");
</script>

运行代码,单击按钮时会执行click函数,从而改变count属性的值,并能在页面上实时看到变化效果。

使用@click直接绑定单击事件方法,是一种基础的用户交互处理方式。

我们也可以直接将小执行的逻辑代码放入@click赋值的地方,示例如下

<button @click="this.count +=1">点击</button>

修改后代码可正常运行。

通常情况下,事件的处理方法都不是单行 JavaScript 可以搞定的,更多时候会采用绑定方法函数的方式来处理事件。

在上面的代码中,定义的click函数并没有参数,实际上当触发了我们绑定的事件函数时,系统会自动将当前的 Event 对象传递到函数中去。

如果需要使用此Event对象,定义的处理函数往往是下面这样的:

示例

click(event) {
  console.log(event)
  this.count += 1;
},

Event 对象中会存储当前事件的很多信息,例如事件类型、鼠标位置、键盘按键情况等。

如果 DOM 元素绑定执行事件的函数需要传自定义的参数怎么办?

以上述代码为例,若计数器的步长可设置,例如通过函数的参数来进行控制,修改click的方法如下:

示例:

click(step) {
  this.count += step;
},

在进行事件绑定时,可以采用内联处理的方式设置函数参数,

示例:

<button @click="click(2)">点击</button>

实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <div>点击次数:{{count }}</div>
  <button @click="click(2)">点击</button>
</div>
<script>
  const App = {
    data() {
      return {
        count: 0,
      };
    },
    methods: {
      click(step) {
        this.count += step;
      },
    },
  };
  Vue.createApp(App).mount("#Application");
</script>

运行上述代码,可看到计数器将以 2 为步长进行增加,

如果在自定义传参的基础上,我们需要使用系统的 Event 对象参数,可以使用$event来传递此参数,例如,修改click函数如下:

示例:

click(step,event) {
  console.log(event)
  this.count += step;
},

使用如下方式绑定事件:

<button @click="click(2,$event)">点击</button>

4.1.2 多事件处理

多事件处理是指对于同一个用户交互事件,需要调用多个方法进行处理。

当然,一种简单的方式是编写一个聚合函数作为事件的处理函数,但在 Vue 中,绑定事件时支持使用逗号对多个函数进行调用绑定。

以 4.1.1 节中的代码为例,click函数实际上完成了两个功能:计数和打印 Log。我们将功能拆分,改写如下:

示例:

    methods:{
        click(step) {
            this.count +=step
        },
        log(event){
            console.log(event)
        }
    }

使用:

<button @click="click(2),log($event)">点击</button>

注意:如果要进行多事件处理,在绑定事件时都要采用内联调用的方式绑定。

实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <div>点击次数:{{count }}</div>
  <button @click="click(2),log($event)">点击</button>
</div>
<script>
  const App = {
    data() {
      return {
        count: 0,
      };
    },
    methods: {
      click(step) {
        this.count += step;
      },
      log(event) {
        console.log(event);
      },
    },
  };
  Vue.createApp(App).mount("#Application");
</script>

4.1.3 事件修饰符

我们先回顾下 DOM 事件的传递原理。

当我们在页面上触发了一个单击事件时,事件首先会从父组件开始依次传递到子组件,这一过程通常被形象地称为事件捕获

当事件传递到最上层的子组件时,其还会逆向地再进行一轮传递,从子组件依次向下传递,这一过程称为事件冒泡

我们在 Vue 中使用@click的方式绑定事件时,默认监听的是 DOM 事件的冒泡阶段,即从子组件传递到父组件的这一过程。

实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="app">
  <div @click="click1" style="border: solid red">
    外层
    <div @click="click2" style="border: solid red">
      中层
      <div @click="click3" style="border: solid red">点击</div>
    </div>
  </div>
</div>

<script>
  //定义一个Vue组件,名为App
  const App = {
    //定义组件中的数据
    data() {
      return {
        count: 0,
      };
    },

    //定义计算属性
    computed: {},

    //定义组件中的函数
    methods: {
      click(step) {
        this.count += step;
      },
      log(event) {
        console.log(event);
      },
      click1() {
        console.log("外层");
      },
      click2() {
        console.log("中层");
      },
      click3() {
        console.log("内层");
      },
    },
  };
  Vue.createApp(App).mount("#app");
</script>

运行上述代码,单击页面最内层元素,控制台打印:

内层;
中层;
外层;

如果监听捕获阶段的事件,我们需要使用事件修饰符,事件修饰符 capture 可以将监听事件的实际设置为捕获阶段,

示例:

<div @click.capture="click1" style="border: solid red">
  外层
  <div @click.capture="click2" style="border: solid red">
    中层
    <div @click.capture="click3" style="border: solid red">点击</div>
  </div>
</div>

运行上述代码,单击最内层元素,控制台打印:

外层 中层 内层

捕获事件触发的顺序刚好与冒泡事件相反。在实际运用中,需要根据具体需求来选择使用。

理解事件的传递对处理页面用户交互来说至关重要,但有的场景也会不希望事件进行传递,在上面的例子中,

当用户单击内层的组件时,我们只想让其触发内层组件绑定的方法,

当用户单击外层的组件时,只触发外层组件绑定方法,这就需要使用 Vue 中另外一个重要是修饰符:stop

stop修饰符可以阻止事件的传递:

<div @click.stop="click1" style="border: solid red">
  外层
  <div @click.stop="click2" style="border: solid red">
    中层
    <div @click.stop="click3" style="border: solid red">点击</div>
  </div>
</div>

此时单击,只有被单击的当前组件绑定的方法会被调用。

除了capturestop时间修饰符外,还有一些常用的修饰符,列表如下:

事件修饰符作用
stop阻止事件传递
capture监听捕获场景的事件
once只触发一次事件
self当事件对象的 target 属性是当前组件时才触发事件
Prevent禁止默认事件
passive不禁止默认事件

注意:事件修饰符可以串联使用,如下例子,既能阻止事件传递,又能控制只能触发一次事件

<div @click.stop.once="click3" style="border: solid red">点击</div>

对于键盘事件来说,Vue 中定义了一组按钮别名事件修饰符,其用法稍后会介绍。

4.2 Vue 中的事件类型

事件本身是有类别之分的,例如使用@click绑定的就是元素的单机事件,如果需要通过用户鼠标操作行为来实现更加复杂的交互逻辑,则需要监听更复杂的鼠标事件。

当使用 Vue 中的v-on指令进行普通HTML元素的事件绑定时,其支持所有的原生DOM事件,更进一步,如果使用v-on指令对自定义的 Vue 组件进行事件绑定,则其也可以支持自定义事件。

4.2.1 常用事件类型

click 事件是页面开发中常用的交互事件。当 HTML 元素被单击时会触发此事件,常用的交互事件列举如下:

事件意义可用元素
click单击事件,当组件被单击时触发大部分 HTML 元素
dblclick双击事件,当组件被双击时触发
focus获取焦点事件,例如输入框开启编辑模式时触发Input、select、textarea 等
blur失去焦点事件,例如输入框结束编辑时触发
change元素内容改变事件,输入框结束输入后,若内容有变即触发此事件
select元素内容选中事件,输入框中的文本被选中时会触发此事件
mousedown鼠标按键按下事件
mouseup鼠标按键抬起事件
mousemove鼠标在组件内移动事件
mouseout鼠标移出组件时触发
mouseover鼠标移入组件时触发
Keydown键盘按键被按下
keyup键盘按键被抬起HTML 中所有元素

对于上面列举的事件类型,可编写代码来理解其触发时机。

实例:

<!--载入vue.js-->
<script src="https://unpkg.com/vue@next"></script>

<div id="Application">
  <div @click="click">单击事件</div>
  <div @dblclick="dblclick">双击事件</div>
  <input @focus="focus" @blur="blur" @change="change" @select="select" />
  <div @mousedown="mousedown">鼠标按下</div>
  <div @mouseup="mouseup">鼠标抬起</div>
  <div @mousemove="mousemove">鼠标移动</div>
  <div @mouseout="mouseout" @mouseover="mouseover">鼠标移入移出</div>
  <input @keydown="keydown" @keyup="keyup" />

  <h1>按键修饰符</h1>
</div>

<script>
  //定义组件
  const App = {
    //组件的data方法
    data() {
      //返回对象
      return {};
    },
    //计算属性
    computed: {},

    //定义组件的函数
    methods: {
      click() {
        console.log("单击事件");
      },

      dblclick() {
        console.log("双击事件");
      },

      focus() {
        console.log("获取焦点");
      },

      blur() {
        console.log("失去焦点");
      },

      change() {
        console.log("内容改变");
      },

      select() {
        console.log("文本选中");
      },

      mousedown() {
        console.log("鼠标按键按下");
      },

      mouseup() {
        console.log("鼠标按键抬起");
      },

      mousemove() {
        console.log("鼠标移动");
      },

      mouseout() {
        console.log("鼠标移出");
      },

      mouseover() {
        console.log("鼠标移入");
      },

      keydown() {
        console.log("键盘按键按下");
      },
    },
  };

  Vue.createApp(App).mount("#Application");
</script>

对于每一种类型的事件,我们都可以通过Event对象来获取事件的具体信息,例如在鼠标单击事件中,可以获取到用户单击的撒左键还是右键。

4.2.2 按键修饰符

当需要对键盘按键进行监听时,我们通常使用keyup参数,如果仅仅要对某个按键进行监听,可以通过Event对象来判断,

例如,监听用户敲击了回车键,可以这么写

示例

keyup(event){
    console.log("键盘按键抬起")
    if (event.key == 'Enter') {
        console.log("回车键被点击")
    }
}

在 Vue 中,还有一种更加简单的方式可以实现对某个具体的按键进行监听,即使用按键修饰符,

在绑定监听方法时,我们可以设置要监听的具体案件,例如:

示例

<input @keyup.enter="keyup"></input>

实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
    <h3>方法一:</h3>
  <input @keyup="keyup" />
  <h3>方法二:</h3>
  <input @keyup.enter="keyup"></input>
</div>
<script>
  const App = {
    data() {
      //返回对象
      return {};
    },

    methods: {
      keyup(event) {
        console.log("键盘按键抬起");
        if (event.key == "Enter") {
          console.log("回车键被点击");
        }
      },
    },
  };
  Vue.createApp(App).mount("#Application");
</script>

注意:修饰符的命名规则与Event对象中属性key值的命名略有不同,Event对象中的属性采用的是大写字母驼峰法,如EnterPageDown,在使用按键修饰符时需要将格式转换为中画线驼峰法,如enterpage-down

Vue 中还提供了一些特殊的系统按键修饰符,这些修饰符是配合其他键盘按键或鼠标按键进行使用的,主要有以下四种:

  1. ctrl
  2. alt
  3. shift
  4. meta

这些系统修饰符的使用意义是,只有当用户按下这些键时,对应键盘或鼠标事件才能触发,在处理组合键指令时经常会用到,例如:

示例:

<div @mousedown.ctrl="mousedown">鼠标按下</div>

上面代码的作用是,用户按下 Ctrl 键的同时,再按下鼠标按键才会触发绑定的事件函数

<input @keyup.alt.enter="keyup"></input>

上面代码的作用是,用户按下 Alt 键的同时,再按回车键才会触发绑定的事件函数。

还有一个细节,上面示例的系统修饰符只要满足条件就会触发,以鼠标按下事件为例,只要满足用户按下了 Ctrl 键的时候按下了鼠标按键,就会触发事件,即时用户同时按下其他了其他案件也不会受影响,例如用户使用 Shit+Ctrl 鼠标左键的组合键。

如果想要精准地进行按键修饰,可以使用 exact 修饰符,使用这个修饰符后,只有精准地满足按键的条件才会被触发。

<div @mousedown.ctrl.exact="mousedown">鼠标按下</div>

上面修饰后的代码在使用 Shift+Ctrl+鼠标左键的组合方式进行操作时不会再触发事件函数。

实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">

    <div @mousedown.ctrl="mousedown">用户按下Ctrl键的同时,再按下鼠标按键才会触发绑定的事件函数 </div>
    <input placeholder="用户按下Alt键的同时,再按回车键才会触发绑定的事件函数" size="60" @keyup.alt.enter="keyup"></input>

  <div @mousedown.ctrl.exact="mousedown">只有用户按下Ctrl键的同时,再按下鼠标按键才会触发绑定的事件函数</div>
</div>
<script>
  const App = {
    data() {
      //返回对象
      return {};
    },

    methods: {
        mousedown() {
        console.log("鼠标按下啦");
      },
      keyup(){
        console.log("keyup触发啦")
      }
    },
  };
  Vue.createApp(App).mount("#Application");
</script>

注意:meat 系统修饰键:在 Mac 键盘上表示 Command 键,在 Windows 系统上对应 Windows 徽标键。

上面介绍了键盘按键相关修饰符,Vue 中还有 3 个常用修饰符。

在进行网页应用开发时,通常左键用来选择,右键用来进行配置。

通过下面这些修饰符可以设置当用户按了鼠标指定按键后才会触发事件函数。

  • left
  • right
  • middle

如下代码,只有按了鼠标左键才会触发事件。

<div @click.left="click">单击事件</div>

实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <div @click.left="click">单击事件</div>
</div>
<script>
  const App = {
    data() {
      //返回对象
      return {};
    },

    methods: {
      click() {
        console.log("左键单击啦");
      },
    },
  };
  Vue.createApp(App).mount("#Application");
</script>

4.3 范例:随鼠标移动的小球

4.4 范例:弹球游戏

4.5 小结与练习

第五章:组件基础

通过组件,开发者可以封装出复用性强、扩展性性强的 HTML 元素,并且通过组件的组合可以将复杂的页面元素拆分成多个独立的内部组件,方便代码的逻辑分离与管理。

组件的系统的核心是将大型应用拆分成多个可以独立使用且可复用的小组件,然后通过组件树的方式将小组件构建成完整的应用程序。

通过本章,可以学到:

  • Vue 应用程序的基础概念
  • 如何定义组件和使用组件
  • Vue 应用与组件的相关配置
  • 组件中的数据传递技术
  • 组件事件的传递与响应
  • 组件插槽的相关知识
  • 动态组件的应用

5.1 关于 Vue 应用与组件

Vue 框架将常规的网页页面开发以面向对象的方式进行了抽象,一个网页甚至一个网站在 Vue 中被抽象为一个应用程序。

一个应用程序中可以定义和使用很多个组件,但是需要配置一个根组件,当应用程序被挂载渲染到页面时,此根组件会作为起点元素进行渲染。

5.1.1 Vue 应用的数据配置选项

使用 Vue 中的createAPP方法即可创建一个 Vue 应用实例。

const App = Vue.createApp({});

createApp 方法会返回一个 Vue 应用实例,在创建应用实例时,我们可以传入一个 JavaScript 对象来提供应用创建时数据相关的配置项。

例如经常使用的 data 选项和methosd选项。

data 选项本身需要配置为一个 JavaScript 函数,此函数需要提供应用所需的全局数据。例如:

const appData = {
  count: 0,
};
const App = Vue.createApp({
  data() {
    return appData;
  },
});

props选项用于接收父组件传递的数据,后续会介绍

computed选项用于配置组件的计算属性,可在其中实现 getter 和 setter 方法,例如:

computed:{
  countString:{
    get(){
      return this.count + "次"
    }
  }
}

methods 选项用来配置组件中需要使用到的方法,注意,不要使用箭头函数来定义 methods 中的方法,会影响其 this 关键字的指向,示例:

methods:{
  click(){
    this.count += 1
  }
}

watch可以对组件属性的变化添加监听函数 ,如下:

watch:{
  count(value,oldValue){
    console.log(value,oldValue)
  }
}

注意,当要监听的组件属性发生变化时,监听函数中会将变化后与变化前的值作为参数传递进来。

如果要使用的监听函数本身定义在组件的methods选项中,也可以直接使用字符串的方式来指定要执行的监听方法。

methods:{
  click(){
    this.count += 1
  },
    countChange(value,oldValue) {
      console.log(value,oldValue)
    }
},
  watch:{
    count:"countChange"
  }

watch的更多功能,后续章节会有介绍。

5.1.2 定义组件

我们创建好 Vue 应用实例后,使用mount方法就可以将其绑定到指定的 HTML 文档上。

应用实例可以使用component方法来定义组件,定义好的组件可直接在 HTML 文档中进行使用(根元素内)。

实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <my-alert></my-alert>
  <my-alert></my-alert>
</div>
<script>
  const App = Vue.createApp({});
  const alertComponent = {
    data() {
      return {
        msg: "警告标识",
        count: 0,
      };
    },
    methods: {
      click() {
        alert(this.msg + this.count++);
      },
    },
    template: `<div><button @click="click">按钮</button></div>`,
  };
  App.component("my-alert", alertComponent);
  App.mount("#Application");
</script>

在 Vue 应用中定义组件时使用component方法,

这个方法的第一个参数用来设置组件名,

第二个参数进行组件配置,组件的配置选项与应用的配置选项基本一致。

上面的配置中,data选项配置了组件必要的数据

methods选项为组件提供了所需方法,

注意:定义组件是最重要的template选项,这个选项设置组件的 HTML 模版。

当需要使用自定义组件时,只需要使用组件名标签即可。

<div id="Application">
  <my-alert></my-alert>
  <my-alert></my-alert>
</div>

注意:代码中的my-alert组件是定义在Application应用实例中的,在组织 HTML 框架结构时,my-alert组件也只能在Application挂载的标签内使用,在外部是无法正常工作的,下面这种写法就无法正常渲染组件:

<!--不会正常渲染-->
<div id="Application"></div>
<my-alert></my-alert>

使用 Vue 中的组件可以使得HTML代码的复用性大大增强,在日常开发中,我们也可以将一些通用的页面元素封装成可定制化的组件,在开发新网站应用时,可以使用日常积累的组件进行快速搭建。

组件在定义时的配置选项与 Vue 应用实例中创建时的配置选项是一致的。都有data,methods,watchcomputed等配置选项。

这是因为,我们在创建应用时传入的参数实际上就是根组件

当组件复用时,每个标签实际上都是一个独立的组件实例,其内部数据是独立维护的,

例如上面的代码中,my-alert组件内部维护了一个名为count的属性,单击按钮后其会计数,不同的按钮将会分别进行计数。

5.2 组件中的数据与事件传递

由于组件具有复用性,因此要使得组件能够在不同的应用中得到最大程度的复用与最少的内部改动,就需要组件具有一定的灵活度,即可配置性。

可配置性归根结底是通过数据的传递来实现的,在使用组件时,通过传递不同的数据来使组件的交互行为,渲染样式有略微的差异。

本节将探讨如何通过数据与事件的传递使得我们编写的 Vue 组件更具灵活性。

5.2.1 为组件添加外部属性

与使用原生的 HTML 标签元素类似,我们可以通过属性来控制元素的一些渲染行为。

自定义组件也可通过属性来控制其内部行为。

以 5.1 节的代码为例,my-alert组件渲染的按钮元素,“按钮”文本文案是写死在template模版字符串中的,使用组件时无法修改按钮文本。

如果需要在使用此组件时灵活地设置其按钮的显示,就需要使用组件中的props配置。

propsproperties的缩写,意思是属性,props定义的属性是提供给外部进行设置使用的,也可以将其称为外部属性。

修改my-alert组件的定义如下

实例:

<script src="https://unpkg.com/vue@next"></script>

<div id="Application">
  <my-alert title="按钮1"></my-alert>
  <my-alert title="按钮2"></my-alert>
</div>

<script>
  const App = Vue.createApp({}); //创建一个VUE应用
  const alertComponent = {
    //创建一个名为alertComponent的组件
    data() {
      return {
        msg: "警告框提示",
        count: 0,
      };
    },
    methods: {
      click() {
        alert(this.msg + this.count++);
      },
    },
    props: ["title"],
    template: `<div><button @click="click">{{title}}</button></div>`,
  };
  App.component("my-alert", alertComponent); //component定义组件,组件名/配置
  App.mount("#Application"); //mount方法绑定到指定实例
</script>

prop 选项用来定义自定义组件内的外部属性,组件可以定义任意多个外部属性,

template模版中,可以用访问内部data属性一样的方式来访问定义的外部属性。

在使用my-alert组件时,可以直接设置title属性来设置按钮的标题。

<my-alert title="按钮1"></my-alert>
<my-alert title="按钮2"></my-alert>

props也可以进行许多复杂的配置,例如类型检查、默认值等,后面章节会有更加详细的介绍。

5.2.2 处理组件事件

在开发自定义组件时,少不了事件传递,前面开发的my-alert组件,在不同的项目中使用,可能需要弹出不同类型的系统警告框,例如确认,警告等。

不同项目使用的警告框风格不一样,逻辑也不一样,目前这个组件的复用性差,不能满足各种定制化需求。

my-alert组件进行改造,将其中按钮单机时间传递给父组件处理,即传递给使用此组件的业务方处理。

在 Vue 中,可以使用内建的$emit方法来传递事件,实例如下:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <my-alert @myclick="appfunc" title="按钮1"></my-alert>
  <my-alert title="按钮2"></my-alert>
</div>

<script>
  const App = Vue.createApp({
    methods: {
      appfunc() {
        console.log("点击了自定义组件-");
      },
    },
  }); //创建一个VUE应用
  const alertComponent = {
    //创建一个名为alertComponent的组件
    props: ["title"],
    template: `<div><button @click="$emit('myclick')">{{title}}</button></div>`,
  };
  App.component("my-alert", alertComponent); //component定义组件,组件名/配置
  App.mount("#Application"); //mount方法绑定到指定实例
</script>

修改后的代码将my-alert组件中按钮的单机事件定义为myclick事件进行传递,在使用此组件时,可直接使用myclick这个事件名进行监听。

$emit方法在传递事件时也可以传递一些参数,很多自定义组件都有状态,这时我们就可以将状态作为参数传递,实例如下:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <my-alert @myclick="appfunc" title="按钮1"></my-alert>
  <my-alert @myclick="appfunc" title="按钮2"></my-alert>
</div>

<script>
  const App = Vue.createApp({
    methods: {
      appfunc(param) {
        console.log("点击了自定义组件-" + param);
      },
    },
  }); //创建一个VUE应用
  const alertComponent = {
    //创建一个名为alertComponent的组件
    props: ["title"],
    template: `<div><button @click="$emit('myclick',title)">{{title}}</button></div>`,
  };
  App.component("my-alert", alertComponent); //component定义组件,组件名/配置
  App.mount("#Application"); //mount方法绑定到指定实例
</script>

运行上述代码,单击按钮时,控制台会打印当前按钮标题,这个标题就是子组件传递事件时带给父组件的事件参数。

如果在传递事件之前,子组件还有一些内部的逻辑需要处理,也可以在子组件中包装一个方法,在方法内调用$emit进行事件传递,实例如下:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <my-alert @myclick="appfunc" title="按钮1"></my-alert>
  <my-alert @myclick="appfunc" title="按钮2"></my-alert>
</div>

<script>
  const App = Vue.createApp({
    methods: {
      appfunc(param) {
        console.log("点击了自定义组件-" + param);
      },
    },
  }); //创建一个VUE应用
  const alertComponent = {
    //创建一个名为alertComponent的组件
    props: ["title"],
    methods: {
      click() {
        console.log("组件内部的逻辑");
        this.$emit("myclick", this.title);
      },
    },
    template: `<div><button @click="click">{{title}}</button></div>`,
  };
  App.component("my-alert", alertComponent); //component定义组件,组件名/配置
  App.mount("#Application"); //mount方法绑定到指定实例
</script>

现在,你可以灵活地通过事件的传递来使自定义组件的功能更加纯粹,好的开发模式是将组件内部的逻辑在组件内部处理掉,而需要调用犯法处理的业务逻辑属于组件外部的逻辑,将其传递到调用方处理。

5.2.3 在组件上使用 v-model 指令

v-model指令通常被形象地称为 Vue 中的双向绑定指令,

对于可交互用户输入的相关元素来说,使用这个指令可以将数据的变化同步到元素上,同样对元素输入的信息变化时,也会同步到对应的数据属性上来

v-model指令的基本使用

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <div>
    <input v-model="inputText" />
    <div>{{inputText}}</div>
    <button @click="this.inputText = ''">清空</button>
  </div>
</div>
<script>
  const App = Vue.createApp({
    data() {
      return {
        inputText: "",
      };
    },
  });
  App.mount("#Application");
</script>

运行代码,在框中输入文本,在对应的div标签中的文案也会改变,点击“清空”按钮,输入框和对应div标签中的文本内容也会清空,这就是v-model双向绑定指令提供的基础功能。

不用v-model指令,也能实现类似的功能,

实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <div>
    <input :value="inputText" @input="action" />
    <div>{{inputText}}</div>
    <button @click="this.inputText = ''">清空</button>
  </div>
</div>
<script>
  const App = Vue.createApp({
    data() {
      return {
        inputText: "",
      };
    },
    methods: {
      action(event) {
        this.inputText = event.target.value;
      },
    },
  });
  App.mount("#Application");
</script>

代码中先使用v-bind指令来控制输入框的内容,即当属性的inpuText改变后,v-bind指令会将其同步更新到输入框中,之后使用v-on:input指令来监听输入框的输入事件,当输入框内容变化时,手动通过 action 函数来更新inpuText属性,从而实现双向绑定效果。

这也是v-bind指令的基本工作原理。

为自定义组件增加v-model支持就很简单,

实例:

<script src="https://unpkg.com/vue@next"></script>

<div id="Application">
  <my-input v-model="inputText"></my-input>
  <div>{{inputText}}</div>
  <button @click="this.inputText = ''">清空</button>
</div>
<script>
  const App = Vue.createApp({
    data() {
      return {
        inputText: "",
      };
    },
  });
  const inputComponent = {
    props: ["modelValue"],
    methods: {
      action(event) {
        this.$emit("update:modelValue", event.target.value);
      },
    },

    template: `<div><span>输入框:</span> <input  :value="modelValue" @input="action" /></div>`,
  };
  App.component("my-input", inputComponent);
  App.mount("#Application");
</script>

上面的代码可正常工作。

所有支持v-model指令的组件中,默认都会提供一个名为modelValue的属性,而组件内部的内容变化后向外传递的事件为update:modelValue,并且在事件传递时会将组件内容作为参数进行传递。

我们要让自定义组件能使用v-model指令,只需要按照正确的规范来定义组件即可。

5.3 自定义组件插槽

插槽是指 HTML 起始标签与结束标签中间的部分,通常在使用 div 标签时,其内部的插槽位置既可以放置要显示的文案,又可以嵌套放置其他标签。

示例:

<div>文案部分</div>
<div>
  <button>按钮</button>
 </div>

嵌套的核心作用是将组件内部的元素抽离到外部进行实现,在进行自定义组件的设计时,良好的插槽逻辑可以使组件的使用更加的灵活。

对于开发容器类型的自定义组件来说,插槽更加重要,在定义容器类的组件时,开发者只需要将容器本身编写好,内部的内容都可以通过插槽来实现。

5.3.1 组件插槽的基本用法

实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <my-container>组件内部</my-container>
</div>
<script>
  const App = Vue.createApp({});
  const containerComponent = {
    template: `<div style="border-style:solid; border-color:red; border-width:10px;">
                <slot></slot>
                </div>`,
  };

  App.component("my-container", containerComponent);

  App.mount("#Application");
</script>

我们定义了一个 my-container 的容器组件,

若需自定义组件支持插槽,需要使用slot标签来指定插槽位置。

插槽中也支持任意的标签内容或其他组件。

对于支持插槽的组件来说,我们也可以为插槽添加默认的内容,当组件在使用时如果没有设置默认内容,则会自动渲染默认的内容。

实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <my-container>自定义插槽内容</my-container>
</div>
<script>
  const App = Vue.createApp({});
  const containerComponent = {
    template: `<div style="border-style:solid; border-color:red; border-width:10px;">
                <slot>插槽默认内容</slot>
                </div>`,
  };

  App.component("my-container", containerComponent);

  App.mount("#Application");
</script>

注意:一旦组件在使用时设置了插槽的内容,默认内容就不会显示。

5.3.2 多具名插槽的用法

具名插槽是指为插槽设置一个具体名称,在使用组件时,可通过插槽的名称来设置插槽内容。

由于具名插槽可以非常明确地指定插槽内容的位置,因此,当一个组件要支持多个插槽时,就需要用到具名插槽。

我们编写一个容器组件,组件由头部元素、主元素和尾部元素组成,就需要用到 3 个插槽。

实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <my-container2>
    <template v-slot:header>
      <h1>这里是头部元素</h1>
    </template>
    <template v-slot:main>
      <p>内容部份</p>
      <p>内容部份</p>
    </template>
    <template v-slot:footer>
      <p>这里是尾部元素</p>
    </template>
  </my-container2>
</div>
<script>
  const App = Vue.createApp({});
  const container2Component = {
    template: `<div>
                <slot name="header"></slot>
                <hr>
                <slot name="main"></slot>
                <hr>
                <slot name="footer"></slot
                </div>`,
  };
  App.component("my-container2", container2Component);
  App.mount("#Application");
</script>

如上代码所示,在组件内部定义slot插槽时,可以使用name属性来为其设置具体名称,

注意:在使用此组件时,要使用template标签来包装插槽内容,对于template标签,通过v-slot来指定与其对应的插槽位置。

在 Vue 中,很多指令都有缩写形式,具名插槽的缩写,可以使用符号“#”来代替“v-slot”,对上面的代码进行如下改造,可正常运行。

实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <my-container2>
    <template #header>
      <h1>这里是头部元素</h1>
    </template>
    <template #main>
      <p>内容部份</p>
      <p>内容部份</p>
    </template>
    <template #footer>
      <p>这里是尾部元素</p>
    </template>
  </my-container2>
</div>
<script>
  const App = Vue.createApp({});
  const container2Component = {
    template: `<div>
                <slot name="header"></slot>
                <hr>
                <slot name="main"></slot>
                <hr>
                <slot name="footer"></slot
                </div>`,
  };
  App.component("my-container2", container2Component);
  App.mount("#Application");
</script>

5.4 动态组件的简单应用

动态组件是 Vue 开发中经常会使用到的一种高级功能,有时页面中某个位置要渲染的组件并不是固定的,可能会根据用户的操作二渲染不同的组件,这是就需要用到动态组件了。

新建一个radio选项,用户选择不同的选项,展示不同的内容。

实例:

<script src="https://unpkg.com/vue@next"></script>

<div id="Application">
  <input type="radio" value="page1" v-model="page" />页面1
  <input type="radio" value="page2" v-model="page" />页面2
  <div>{{page}}</div>
</div>

<script>
  const App = Vue.createApp({
    data() {
      return {
        page: "page1",
      };
    },
  });

  App.mount("#Application");
</script>

运行上面的代码,会在页面中渲染一组单选框,用户切换选项后,其 div 标签中渲染的文案就会进行对应的修改。

实际情况中,更多的会采用更换组件的方式进行内容切换。

实例:

<script src="https://unpkg.com/vue@next"></script>

<div id="Application">
  <input type="radio" value="page1" v-model="page" />页面1
  <input type="radio" value="page2" v-model="page" />页面2
  <!--<div>{{page}}</div>-->
  <component :is="page"></component>
</div>

<script>
  const App = Vue.createApp({
    data() {
      return {
        page: "page1",
      };
    },
  });

  const page1 = {
    template: `<div style="color:red">页面组件1</div>`,
  };

  const page2 = {
    template: `<div style="color:blue">页面组件2</div>`,
  };

  App.component("page1", page1);
  App.component("page2", page2);
  App.mount("#Application");
</script>

component是一个特殊的标签,其通过 is 属性来指定要渲染的组件名称,如上代码所示,随着 Vue 应用中的page属性的变化,component所渲染的组件也是动态化的。

截止目前,我们使用component方法定义的都是全局组件,对小型项目来说,这种开发方式比较方便。

但对大型项目来说,缺点很明显。

全局定义的模版名不能重复,大型项目中可能会使用非常多的组件,维护困难

在定义全局组件时,组件内容是通过字符串格式的HTML模版定义的,在编写时对开发者不友好

全局版本定义中不支持使用内部的CSS样式。

这些问题都可以通过单文件组件技术解决,第六种中会讲到。

5.5 范例:开发一款小巧的开关按钮组件

5.6:补充

字符串模版

HTML 特性是不区分大小写的。所以,当使用的不是字符串模板时,camelCase (驼峰式命名) 的 prop 需要转换为相对应的 kebab-case,如果你使用字符串模板,则没有这些限制。

  • 字符串模板:指的是在组件选项里用 template:"" 指定的模板,换句话说,写在 js 中的 template:"" 中的就是字符串模板。比如下面这个:
  • 非字符串模板:在单文件里用 指定的模板,换句话说,写在 html 中的就是非字符串模板。
// 在 JavaScript 中使用 camelCase
      props: ['myMessage'],
      template: '<span>{{ myMessage }}</span>'
<!-- 在 HTML 中使用 kebab-case -->
<child my-message="hello!"></child>

字符串模板就是写在vue中的template中定义的模板,如.vue的单文件组件模板和定义组件时template属性值的模板。字符串模板不会在页面初始化参与页面的渲染,会被vue进行解析编译之后再被浏览器渲染,所以不受限于html结构和标签的命名。

dom模板(或者称为Html模板)就是写在html文件中,一打开就会被浏览器进行解析渲染的,所以要遵循html结构和标签的命名,否则浏览器不解析也就不能获取内容了。

因为html对大小写不敏感,所以在DOM模板中使用组件必须使用kebab-case命名法(短横线命名)。 因此,对于组件名称的命名,可参考如下实现:

/*-- 在单文件组件、JSX和字符串模板中 --*/
<MyComponent/>
/*-- 在 DOM 模板中 --*/
<my-component></my-component>
或者
/*-- 在所有地方 --*/
<my-component></my-component>

第六章:组件进阶

  • 组件的生命周期
  • 应用的全局配置
  • 组件属性的高级用法
  • 自定义指令的应用
  • Vue3 的 Teleport 新特性应用

6.1 组件的生命周期与高级配置

组件的创建与销毁都会经过一系列过程,这一过程称为生命周期。

在 Vue 中,组件的生命周期节点会被定义为一系列方法,称之为生命周期钩子。

有了生命周期钩子,就可以在合适的时机来完成合适的工作。

  • 在组件挂载前准备组件所需数据
  • 组件销毁时清除某些残留数据

Vue 提供了许多对组件进行配置的高级 API 接口,包括对应用或组件进行全局配置的 API 功能接口以及组件内部相关的高级配置选项。

6.1.1 生命周期方法

通过下面这个列子来感受组件生命周期方法的调用时机

<script src="https://unpkg.com/vue@next"></script>

<div id="Application"></div>

<script>
  const root = {
    beforeCreate() {
      console.log("组件即将创建前");
    },

    created() {
      console.log("组件创建完成");
    },

    beforeMount() {
      console.log("组件即将挂载前");
    },

    updated() {
      console.log("组件挂载完成");
    },

    beforeUpdate() {
      console.log("组件即将更新前");
    },

    updated() {
      console.log("组件更新完成");
    },

    activated() {
      console.log("被缓存的组件激活时调用");
    },

    deactivated() {
      console.log("被缓存的组件停用时调用");
    },

    beforeMount() {
      console.log("组件即将被卸载前调用");
    },

    unmounted() {
      console.log("组件被卸载后调用");
    },

    errorCaptured(erro, instance, info) {
      console.log("捕获到来自子组件的异常时调用");
    },

    renderTracked(event) {
      console.log("虚拟DOM重建渲染时调用");
    },

    renderTriggered(event) {
      console.log("虚拟DOM被触发渲染时调用");
    },
  };

  const App = Vue.createApp(root);

  App.mount("#Application"); //将Vue组件绑定到页面上id为Application的元素上。
</script>

每个方法的log标明了调用时机,运行代码会输出以下内容

组件即将创建前;
组件创建完成;
组件即将被卸载前调用;

页面渲染过程中只执行了组件的创建和挂载过程,没有卸载过程。

如果某个组件通过 v-if 指令来控制渲染,则当期渲染状态切换时,组件会进行交替的挂载和卸载动作。

实例

<script src="https://unpkg.com/vue@next"></script>

<div id="Application">
  <sub-com v-if="show"> </sub-com>
  <button @click="changeShow">测试</button>
</div>

<script>
  const sub = {
    beforeCreate() {
      console.log("组件即将创建前");
    },

    created() {
      console.log("组件创建完成");
    },

    beforeMount() {
      console.log("组件即将挂载前");
    },

    updated() {
      console.log("组件挂载完成");
    },

    beforeMount() {
      console.log("组件即将被卸载前调用");
    },

    unmounted() {
      console.log("组件被卸载后调用");
    },
  };

  const App = Vue.createApp({
    data() {
      return {
        show: false,
      };
    },
    methods: {
      changeShow() {
        this.show = !this.show;
      },
    },
  });
  App.component("sub-com", sub);

  App.mount("#Application"); //将Vue组件绑定到页面上id为Application的元素上。
</script>

以下四个方法最为常用:

  1. renderTriggered
  2. renderTracked
  3. beforeUpdate
  4. updated

当组件中的 HTML 元素发生渲染或更新时,会调用这些方法。

实例

<script src="https://unpkg.com/vue@next"></script>

<div id="Application">
  <sub-com> {{content}} </sub-com>
  <button @click="change">测试</button>
</div>

<script>
  const sub = {
    beforeCreate() {
      console.log("组件即将创建前");
    },

    created() {
      console.log("组件创建完成");
    },

    renderTracked(event) {
      console.log("虚拟DOM重建渲染时调用");
    },

    renderTriggered(event) {
      console.log("虚拟DOM被触发渲染时调用");
    },
    template: `<div><slot></slot></div>`,
  };

  const App = Vue.createApp({
    data() {
      return {
        content: 0,
      };
    },
    methods: {
      change() {
        this.content += 1;
      },
    },
  });
  App.component("sub-com", sub);

  App.mount("#Application"); //将Vue组件绑定到页面上id为Application的元素上。
</script>

控制台打印输出

组件即将创建前;
组件创建完成;
虚拟DOM重建渲染时调用;
虚拟DOM被触发渲染时调用;

现在,我们对 Vue 组件的生命周期钩子有了直观的认识,对各个生命周期函数的调用时机与顺序也有了初步的了解,

生命周期钩子可以帮助我们在开发中更有效地组织和管理数据。

名称用途
beforeCreate组件即将创建前
created组件创建完成
beforeMount组件即将挂载前
updated组件挂载完成
beforeUpdate组件即将更新前
updated组件更新完成
activated被缓存的组件激活时调用
deactivated被缓存的组件停用时调用
beforeMount组件即将被卸载前调用
unmounted组件被卸载后调用
errorCaptured(erro, instance, info)捕获到来自子组件的异常时调用
renderTracked(event)虚拟 DOM 重建渲染时调用
renderTriggered(event)虚拟 DOM 被触发渲染时调用

6.1.2 应用的全局配置选项

在调用Vue.createApp方法后,会创建一个 Vue 应用实例,其内部封装了config对象,我们可以通过这个对象的一些全局选项来对其进行配置。

常用的配置项有异常与警告捕获配置和全局属性配置。

示例

const App = Vue.createApp({});

App.config.errorHandler = (err, vm, info) => {
  //捕获运行中产生的异常
  //err参数是错误对象,info为具体的错误信息
};
App.config.warnHandler = (msg, vm, trace) => {
  //捕获运行中产生的警告
  //msg是警告信息,trace是组件的关系回溯
};

在实际开发中,有些数据是全局的,例如应用名称、应用版本等,为了方便的在任意组件中使用哲轩全局数据,可通过globalProperties全局属性对象进行配置。

//配置全局数据
App.config.globalProperties = {
  version: "1.0.1",
};
//访问
console.log(this.version);

实例:

<script src="https://unpkg.com/vue@next"></script>
  <div style="text-align: center" id="Application">
    <h1>{{count}}</h1>
    <button v-on:click="clickButton">点击</button>
  </div>

  <script>
    const App = Vue.createApp({
      data() {
        return {
          count: 0,
        };
      },
      methods: {
        clickButton() {
          this.count = this.count + 1;
          console.log(this.version);
        },
      },
    });
    App.config.globalProperties = {
      version: "1.0.1",
    };
    App.mount("#Application");
  </script>
</body>

6.1.3 组件的注册方式

组件注册分全局注册与局部注册两种。

直接使用应用实例的component方法注册的组件都是全局组件,即可以在应用内的任何地方使用这些组件,包括在其他组件内部。

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <comp1></comp1>
</div>
<script>
  const App = Vue.createApp({});
  const comp1 = {
    template: `<div>组件1<comp2></comp2></div>`,
  };
  const comp2 = {
    template: `<div>组件2</div>`,
  };
  App.component("comp1", comp1);
  App.component("comp2", comp2);
  App.mount("#Application");
</script>

如上所示,在 comp2 组件中可直接使用 comp1 组件,

全局注册组件虽然使用起来很方便,但很多时候,并不是最佳编程方式。

一个复杂的组件内部可能由许多子组件组成,这些子组件本身并不暴露到父组件外面,这是使用全局注册组件,会污染 JavaScript 代码,更理想的方式是使用局部注册的方式注册组件。

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <comp1></comp1>
</div>
<script>
  const App = Vue.createApp({});
  const comp2 = {
    template: `<div>组件2</div>`,
  };
  const comp1 = {
    components: {
      comp2: comp2,
    },
    template: `<div>组件1<comp2></comp2></div>`,
  };
  App.component("comp1", comp1);
  App.mount("#Application");
</script>

如上所示,comp2 组件只能在 comp1 中使用。

6.2 组件 Props 属性的高级用法

使用 Props 可以方便地向组件传递数据。

从功能上说,Props 也可以称为组件的外部属性,通过 Props 的不同传参,组件可以有很强的灵活性和扩展性。

6.2.1 对 Prop 属性进行验证

JavaScript 是一种灵活且自由的编程语言,在 JavaScript 中定义函数时无需指定参数的类型,这对开发者来说,这种编程风格虽然十分方便,但并不是特别安全。

以 Vue 组件为例,某个自定义组件需要使用 Props 进行外部传值,如果其要接受的参数为一个数值,但最终调用方法传递了一个字符串类型数据,则组件内部难免会出错。

Vue 在定义组件的 Props 时,可以通过添加约束的方式来对其类型、默认值、是否选填等进行配置。

实例

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <comp1 :count="7"></comp1>
</div>
<script>
  const App = Vue.createApp({});
  const comp1 = {
    props: ["count"],
    data() {
      return {
        thisCount: 0,
      };
    },
    methods: {
      click() {
        this.thisCount += 1;
      },
    },
    computed: {
      innerCount: {
        get() {
          return this.count + this.thisCount;
        },
      },
    },
    template: `
        <button @click="click">点击</button>
        <div>计数:{{innerCount}}</div>
        `,
  };
  App.component("comp1", comp1);
  App.mount("#Application");
</script>

上面的代码中,我们定义了一个名为count的外部属性,这个属性实际上的作用是控制组件技术的初始值。

**注意:**在外部传递数值类型的数据到组件内部时,必须使用 v-bind 指令的方式进行传递,直接使用 HTML 属性设置的方式会将传递的数据作为字符串传递(而不是 JavaScript 表达式)。

如下组件使用方式,最终页面渲染的技术结果将不是预期值:

<comp1 count="7"></comp1>

虽然count属性的作用是作为组件内部计数的初始值,但是调用方不一定理解组件内部的逻辑,

在调用此组件时极有可能会传递非数值类型的数据,比如:

<comp1 :count="{}"></comp1>
//输出
//计数:[object Object]0

这个结果明显不符合预期的。

在 Vue 中,我们可以对定义的 Props 进行约束来显式地指定其类型。

将组件的props配置项配置为列表时,其表示当前定义的属性没有任何约束控制,

如果将其配置为对象,则可以进行更多约束设置。

示例:

 props: {
      count: {
        //定义此属性的类型为数值类型
        type: Number,
        //设置此属性是否必传
        required: false,
        //设置默认值
        default: 10
       }
    }

再当调用此组件时,设置的count属性不符合要求,就会报错。

在实际开发过程中,建议所有的props都采用对象的方式定义,显式地设置其类型、默认值等,

这样既可以在使组件调用时更加安全,也能为开发者提供组件的参数使用文档。

若只需指定属性类型,而不需要指定更加复杂的性质,可使用如下定义:

 props: {
      //数值类型
      count: Number,

      //字符串类型
      count2:String,

      //布尔值类型
      count3:Boolean,

      //数组类型
      count4:Array,

      //对象类型
      count5:Object,

      //函数类型
      count6:Function

      //Symbol的值是唯一的、独一无二的,不会重复
      count7:Sybom
 }

如果一个属性可能是多种类型,可如下定义:

props: {
  //指定属性类型为字符串或数值
  param: [String, Number];
}

在对属性的默认值进行配置时,如果默认值的获取方式比较复杂,可将其定义为函数,函数执行结果会被当作当前属性的默认值,如下

props:{
  count: {
    default:function(){
      return 10
    }
  }
}

Vue 中 props 的定义也支持自定义验证,以上述代码为例,假设组件内需要接收的count属性的值必须大于数值10,可通过自定义验证函数实现,如下:

props:{
  count:{
        //自定义验证,count值大于10
        validator: function (value) {
          if (typeof value != "number" || value <= 10) {
            return false;
          }
          return true;
        }
  }
}

当组件的 count 属性被赋值时,会自动调用验证函数进行验证,

若验证函数返回true,表明赋值有效,若验证函数返回false,则控制台会输出异常信息。

深入 Props 验证

vue 要求任何传递给组件的数据,都要声明为 props。此外,它还提供了一个强大的内置机制来验证这些数据。这就像组件和消费者之间的契约一样,确保组件按预期使用。

复杂类型
export default {
  props: {
    // 默认值的对象
    propE: {
      type: Object,
      // 对象或数组的默认值必须从
      // 一个工厂函数返回。该函数接收原始
      // 元素作为参数。
      default(rawProps) {
        return { message: "hello" };
      },
    },
    // 数组默认值
    propF: {
      type: Array,
      default() {
        return [];
      },
    },
    // 函数默认值
    propG: {
      type: Function,
      // 不像对象或数组的默认值。
      // 这不是一个工厂函数
      // - 这是一个作为默认值的函数
      default() {
        return "Default function";
      },
    },
  },
};
instanceof 进行检查

此外,type 也可以是一个自定义的类或构造函数,然后使用 instanceof 进行检查。例如,给定下面的类:

class Person {
  constructor(firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
  }
}
复制代码

我们可以把 Person 作为一个类型传递给 prop 类型:

export default {
  props: {
    author: Person;
  }
}
validator 方法

Props 支持使用一个 validator 函数。这个函数接受 prop 原始值,并且必须返回一个布尔值来确定这个 prop 是否有效。

   prop: {
      validator(value) {
        // The value must match one of these strings
        return ['success', 'warning', 'danger'].includes(value)
      }
    }
复制代码
使用枚举

有时我们想把值缩小到一个特定的集合,这可以通过枚举来实现:

export const Position = Object.freeze({
  TOP: "top",
  RIGHT: "right",
  BOTTOM: "bottom",
  LEFT: "left"
});
复制代码

它可以导入 validator 中使用,也可以作为默认值:

<template>
  <span :class="`arrow-position--${position}`">
    {{ position }}
  </span>
</template>

<script>
import { Position } from "./types";
export default {
  props: {
    position: {
      validator(value) {
        return Object.values(Position).includes(value);
      },
      default: Position.BOTTOM,
    },
  },
};
</script>
复制代码

最后,父级组件也可以导入并使用这个枚举,它消除了我们应用程序中对魔法字符串的使用:

<template>
  <DropDownComponent :position="Position.BOTTOM" />
</template>

<script>
import DropDownComponent from "./components/DropDownComponent.vue";
import { Position } from "./components/types";
export default {
  components: {
    DropDownComponent,
  },
  data() {
    return {
      Position,
    };
  },
};
</script>
复制代码
布尔映射

布尔类有独特的行为。属性的存在或不存在可以决定 prop 的值。

<!-- 等价于 :disabled="true" -->
<MyComponent disabled />

<!-- 价于 :disabled="false" -->
<MyComponent />
复制代码
TypeScript

将 Vue 的内置 prop 验证与 TypeScript 相结合,可以让我们对这一机制有更多的控制,因为 TypeScript 原生支持接口和枚举。

Interface

我们可以使用一个接口和 PropType 来注解复杂的 prop 类型。这确保了传递的对象将有一个特定的结构。

<script lang="ts">
import Vue, { PropType } from 'vue'
interface Book {
  title: string
  author: string
  year: number
}
const Component = Vue.extend({
  props: {
    book: {
      type: Object as PropType<Book>,
      required: true,
      validator (book: Book) {
        return !!book.title;
      }
    }
  }
})
</script>
复制代码
枚举

我们已经探讨了如何在 JS 中伪造一个枚举。这对于 TypeScript 来说是不需要的,它本来就支持了:

<script lang="ts">
import Vue, { PropType } from 'vue'
enum Position {
  TOP = 'top',
  RIGHT = 'right',
  BOTTOM = 'bottom',
  LEFT = 'left',
}
export default {
  props: {
    position: {
      type: String as PropType<Position>,
      default: Position.BOTTOM,
    },
  },
};
</script>
Vue 3

上述所有内容在使用 Vue 3 与 选项 API 或 组合 API 时都有效。区别在于使用 <script setup>时。props 必须使用 defineProps() 宏来声明,如下所示:

<script setup>
const props = defineProps(['foo'])
console.log(props.foo)
</script>


<script setup>
defineProps({
  title: String,
  likes: Number
})
</script>

或者在使用 TypeScript 的 <script setup> 时,可以使用纯类型注解来声明 prop:

<script setup lang="ts">
defineProps<{
  title?: string
  likes?: number
}>()
</script>

或者使用一个接口:

<script setup lang="ts">
interface Props {
  foo: string
  bar?: number
}
const props = defineProps<Props>()
</script>

最后,在使用基于类型的声明时,声明默认值。

<script setup lang="ts">
interface Props {
  foo: string
  bar?: number
}


const { foo, bar = 100 } = defineProps<Props>()
</script>

6.2.2 Props 的只读性质

对组件内部来说,Props 是只读的,我们不能在组件内部修改 Props 属性的值

Props 的这种只读性能是 Vue 单向数据流特性的一种体现。

所有的外部属性 Props 都只允许父组件的数据流动到子组件中,子组件的数据则不允许流动到父组件。

在组件内部修改 Props 的值是无效的。

以计数器页面为例,若定义 Props 只是为了设置组件某些属性的初始值,完全可以使用计算属性来进行桥接,也可以将外部属性的初始值映射到组件内部属性上,

示例:

props:{
  count:{
        //自定义验证,count值大于10
        validator: function (value) {
          if (typeof value != "number" || value <= 10) {
            return false;
          }
          return true;
        }
  }
},
  data() {
    return {
      thisCount:this.count
    }
  }

实例:暂无

6.2.3 组件数据注入

数据注入是一种便捷的组件间数据传递方式。

一般情况下,当父组件需要将数据传递到子组件时,我们会使用 Props,当组件嵌套层级很多,子组件需要使用多层之外的父组件的数据时,就非常麻烦了,数据需要一层一层的传递。

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <my-list :count="5"></my-list>
</div>
<script>
  const App = Vue.createApp({});
  const listCom = {
    props: {
      count: Number,
    },
    template: `
        <div style="border: red solid 10px;">
            <my-item v-for="i in this.count" :list-count="this.count" :index="i"></my-item>
            </div>
            `,
  };
  const itemCom = {
    props: {
      listCount: Number,
      index: Number,
    },
    template: `
        <div style="border: blue solid 10px;">
            <my-label :list-count="this.listCount" :index="this.index">
                </my-label>
            </div>
        `,
  };
  const labelCom = {
    props: {
      listCount: Number,
      index: Number,
    },
    template: `
        <div>{{index}}/{{this.listCount}}</div>
        `,
  };
  App.component("my-list", listCom);
  App.component("my-item", itemCom);
  App.component("my-label", labelCom);
  App.mount("#Application");
</script>

在上面的代码中,我们创建了 3 个自定义组件,

my-list 组件用来创建一个列表视图,其中每一行的元素为 my-item 组件,

my-item 组件中又使用了 my-label 组件进行文本显示,列表中每一行会渲染出当前的行数以及总行数。

代码运行没有问题,繁琐的地方在于 my-label 组件中需要使用 my-list 组件中的 count 属性,要通过 my-item 组件的数据才能顺利进行传递。

随着组件的嵌套层级数增多,数据的传递将越来越复杂。

在这种场景下,可以使用数据注入的方式来跨层级进行数据传递。

所谓的数据注入,是指父组件可以向其所有子组件提供数据,不论在层级结构上此组件的层级有多深。

在上面的代码中,my-label 组件可以跳过 my-item 组件直接使用 my-list 组件中提供的数据。

实现数据注入需要使用组件的 provide 与 inject 两个配置项,

提供数据的父组件需要设置 provide 配置项来提供数据,

子组件需要设置 inject 配置项来获取数据。

将上面的代码修改如下:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <my-list :count="5"></my-list>
</div>
<script>
  const App = Vue.createApp({});
  const listCom = {
    props: {
      count: Number,
    },
    provide() {
      return {
        listCount: this.count,
      };
    },
    template: `
        <div style="border: red solid 10px;">
            <my-item v-for="i in this.count" :index="i"></my-item>
            </div>
            `,
  };
  const itemCom = {
    props: {
      index: Number,
    },
    template: `
        <div style="border: blue solid 10px;">
            <my-label  :index="this.index">
                </my-label>
            </div>
        `,
  };
  const labelCom = {
    props: {
      index: Number,
    },
    inject: ["listCount"],
    template: `
        <div>{{index}}/{{this.listCount}}</div>
        `,
  };
  App.component("my-list", listCom);
  App.component("my-item", itemCom);
  App.component("my-label", labelCom);
  App.mount("#Application");
</script>

以上代码也可正常运行。

使用数据注入的方式传递数据时,父组件不需要了解哪些子组件要使用这些数据,同样,子组件也无需关心所使用的数据来自哪里。

一定程度上,这使代码的可控性降低了,因此在实际开发中,我们要根据场景来决定使用怎样的方式来传递数据,而不是滥用注入技术。

6.3 组件 Mixin 技术

使用组件开发的一大优势在于可以提高代码的复用性。通过 Mixin 技术,组件的复用性可以得到进一步的提高。

6.3.1 使用 Mixin 来定义组件

在开发大型项目时,会有非常多的组件,这些组件中的部分功能是通用的,对于这部分通用功能,集中起来维护,会更方便。

我们编写 3 个简单的示例组件,

实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <my-com1 title="组件1"></my-com1>
  <my-com2 title="组件2"></my-com2>
  <my-com3 title="组件3"></my-com3>
</div>

<script>
  const App = Vue.createApp({});

  const com1 = {
    props: ["title"],
    template: `
        <div style="border:red solid 2px;">
            {{title}}
            </div>
        `,
  };
  const com2 = {
    props: ["title"],
    template: `
        <div style="border:blue solid 2px;">
            {{title}}
            </div>
        `,
  };
  const com3 = {
    props: ["title"],
    template: `
        <div style="border:green solid 2px;">
            {{title}}
            </div>
        `,
  };
  App.component("my-com1", com1);
  App.component("my-com2", com2);
  App.component("my-com3", com3);
  App.mount("#Application");
</script>

在上面的三个组件中,每个组件都定义了一个名为 title 的外部属性,这部分代码可抽离出来作为独立的“功能模块”,需要此功能的组件只需要“混入”此功能模块即可。

实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <my-com1 title="组件1"></my-com1>
  <my-com2 title="组件2"></my-com2>
  <my-com3 title="组件3"></my-com3>
</div>

<script>
  const App = Vue.createApp({});
  const myMixin = {
    props: ["title"],
  };
  const com1 = {
    mixins: [myMixin],
    template: `
        <div style="border:red solid 2px;">
            {{title}}
            </div>
        `,
  };
  const com2 = {
    mixins: [myMixin],
    template: `
        <div style="border:blue solid 2px;">
            {{title}}
            </div>
        `,
  };
  const com3 = {
    mixins: [myMixin],
    template: `
        <div style="border:green solid 2px;">
            {{title}}
            </div>
        `,
  };
  App.component("my-com1", com1);
  App.component("my-com2", com2);
  App.component("my-com3", com3);
  App.mount("#Application");
</script>

如上代码所示,我们可以定义一个混入对象,混入对象中可以包含任意的组件定义选项,当此对象杯混入组件时,组件会将混入对象中提供的选项引入当前组件内部。

有些类似于编程中的“继承”概念。

6.3.2 Mixin 选项的合并

当混入对象于组件中定义了相同的选项时,Vue 可以智能地对这些选项进行合并。

不冲突的配置将完整合并,冲突的配置会以组件中自己的配置为准,

实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <my-com></my-com>
</div>
<script>
  //定义一个组件,名为App
  const App = Vue.createApp({});
  const myMixin = {
    data() {
      return {
        a: "a",
        b: "b",
        c: "c",
      };
    },
  };
  const com = {
    mixins: [myMixin],
    data() {
      return {
        d: "d",
      };
    },
    template: `<div>{{a}}{{d}}</div>`,
    //组件杯创建后会调用,用来测试混入的数据情况
    created() {
      //a,b,c,d都存在
      console.log(this.$data);
    },
  };
  App.component("my-com", com);
  App.mount("#Application"); //将Vue组件绑定到页面上id为Application的元素上。
</script>

在上面的代码中,混入对象中定义了组件的属性数据,包含 a、b 和 c 共 3 个属性,组件本身定义了 d 属性,最终使用时,其内部包含了 a,b,c,d。

如果属性的定义有冲突,则以组件内部定义为准,如下

实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <my-com></my-com>
</div>
<script>
  //定义一个组件,名为App
  const App = Vue.createApp({});
  const myMixin = {
    props: ["title"],
    data() {
      return {
        a: "a",
        b: "b",
        c: "c",
      };
    },
  };
  const com = {
    mixins: [myMixin],
    data() {
      return {
        c: "CCC",
      };
    },
    template: `<div>{{a}}{{c}}</div>`,
    //组件杯创建后会调用,用来测试混入的数据情况
    created() {
      //属性c的值为CCC
      console.log(this.$data);
    },
  };
  App.component("my-com", com);
  App.mount("#Application"); //将Vue组件绑定到页面上id为Application的元素上。
</script>

生命周期函数的这些配置项混入与属性类的配置项混入略有不同,不重名的生命周期函数会被完整混入组件,重名的生命周期函数被混入组件时,在函数触发时,会先触发 Mixin 对象中的视线,再触发组件内部的实现,这类似于面向对象编程中,子类对父类的覆写,如下

实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <my-com></my-com>
</div>
<script>
  //定义一个组件,名为App
  const App = Vue.createApp({});
  const myMixin = {
    mounted() {
      console.log("Mixin对象mounted");
    },
  };
  const com = {
    mixins: [myMixin],
    template: `<div>组件内容</div>`,
    mounted() {
      console.log("组件本身mounted");
    },
  };
  App.component("my-com", com);
  App.mount("#Application"); //将Vue组件绑定到页面上id为Application的元素上。
</script>

运行上面的代码,当com组件被挂载时,控制台会先打印“Mixin 对象mounted”,之后再打印“组件本身 mounted”

6.3.3 进行全局 Mixin

Vue 也支持对应用进行全局 Mixin 混入。直接对应实例进行 Mixin 设置即可,

实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application"></div>
<script>
  //定义一个组件,名为App
  const App = Vue.createApp({});
  App.mixin({
    mounted() {
      console.log("Mixin对象mounted");
    },
  });
  App.mount("#Application"); //将Vue组件绑定到页面上id为Application的元素上。
</script>

注意:全局 Mixin 的使用,会使之后所有注册的组件都默认被混入这些选项,当程序出现问题时,会增加排查问题的难度

全局 Mixin 技术很适合开发插件,如开发组件挂载的记录工具等。

6.4 使用自定义指令

在 Vue 中,指令的使用无处不在,v-bindv-modelv-on等都是指令。

Vue 中提供了自定义指令的能力,对于某些定制化的需求,配和自定义指令来封装组件可以使得开发过程变的很容易。

6.4.1 认识自定义指令

有时候,我们仍需要直接操作 DOM 元素来实现业务功能,这时就可以使用自定义指令。

功能需求:页面提供一个input输入框,页面加载后,输入框处于焦点状态,用户可直接对输入框进行输入。

实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <input v-getfocus />
</div>
<script>
  const App = Vue.createApp({});
  App.directive("getfocus", {
    //当绑定此指令的元素被挂载时调用
    mounted(element) {
      console.log("组件获得了焦点");
      element.focus();
    },
  });
  App.mount("#Application");
</script>

如上所示,调用应用示例的directive方法可以注册全局的自定义指令。

在上面的代码中,getfocus是指令名称,在使用时需要加上“v-”前缀,

运行上面的代码,可看到页面加载完成时,输入框默认处于焦点状态,可直接进行输入。

在自定义指令时,通常需要在组件的某些生命周期节点进行操作,自定义指令中除了支持 mounted 生命周期方法外,也支持

  • beforeMount
  • beforeUpdate
  • updated
  • beforeUnmlount
  • unmounted

我们可以选择合适的时机来实现自定义指令的逻辑

上面的代码中采用全局注册的方式来自定义指令,因此,所有组件都可以使用,如果只想让发自定义指令在指令的组件上可用,也可以在定义组件时(局部注册),在组件内部进行directives配置来定义自定义指令。

示例:

const sub = {
  directive: {
    //组件内部的自定义指令
    getfocus: {
      mounted(el) {
        el.focus();
      },
    },
  },
  mounted() {
    //组件挂载
    console.log(this.version);
  },
  template: `<input v-getfocus />`,
};

6.4.2 自定义指令的参数

Vue 内置的指令是可以设置值和参数的,对于v-on指令,可以设置值为函数来响应交互事件,也可以通过设置参数来控制要监听的事件类型。

自定义指令也可以设置值和参数,这些设置数据会通过一个param对象传递到指令中实现生命周期方法中。

实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <input v-getfocus:custom="1" />
</div>
<script>
  const App = Vue.createApp({});
  App.directive("getfocus", {
    //当绑定此指令的元素被挂载时调用
    mounted(element, param) {
      if (param.value == "1") {
        element.focus();
      }
      //将打印参数:custom
      console.log("参数:" + param.arg);
    },
  });
  App.mount("#Application");
</script>

指令设置的值 1 被绑定到 param 对象的 value 属性上,指令设置的 custom 参数被绑定到 param 对象的 arg 属性上。

有了参数,Vue 自定义指令的使用可以非常灵活,通过不同的参数进行区分,我们可以很方便地处理复杂的组件渲染逻辑。

对于指令设置的值,其也允许直接设置为 JavaScript 对象,例如下面的设置也是合法的。

<input v-getfocus:custom="{a:1, b:2}" />

6.5 使用组件的 Teleport 功能开发全局弹窗

Teleport 可以简单翻译为”传递“,”传送“,是 Vue 3.0 新提供的功能,

有了Teleport,开发者可以将相关行为的逻辑和 UI 封装到同一个组件中,以提高代码的聚合性。

需求:开发一个全局弹窗组件,单击触发按钮弹出弹窗

实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <my-alert></my-alert>
</div>
<script>
  const App = Vue.createApp({});
  App.component("my-alert", {
    template: `
        <div>
            <button @click="show = true">弹出弹窗</button>
            </div>
            <div v-if="show" style="text-align:center;
            padding:20px;
            position:absolute;
            top:45%;
            left:30%;
            width:40%;
            border:black solid 2px;
            background-color:white">
            <h3>弹窗</h3>
            <button @click="show = false">隐藏弹窗</button>
            </div>
        `,
    data() {
      return {
        show: false,
      };
    },
  });
  App.mount("#Application");
</script>

代码可成功运行,但此组件的可用性不好,若将代码放到其他组件内部,全局弹窗可能无法达到预期,如:放到下面代码中使用:

<div id="Application">
  <div style="position: absolute; width: 50px">
    <my-alert></my-alert>
  </div>
</div>

为了避免这种由于组件树结构的改变而影响组件内元素的布局问题,

一种是将触发事件按钮与全局他弹窗分成两个组件,保证全局弹窗组件挂载在 body 标签下,这样会使相关逻辑被分散在不同地方,不利于后期维护。

另一种方式就是使用Teleport

在定义组件时,如果组件模版中的某些元素只能挂载在指定标签下,可使用Teleport来指定,可以形象地理解Teleport的功能是将此部分元素”传送“到指令的标签下,

以上面的代码为例,可指定全局弹窗只挂载在body元素下。

实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <div style="position: absolute; width: 50px; border: red solid 2px">
    <my-alert></my-alert>
  </div>
</div>
<script>
  const App = Vue.createApp({});
  App.component("my-alert", {
    template: `
          <div>
              <button @click="show = true">弹出弹窗</button>
              </div>
              <teleport to="body">
              <div v-if="show" style="text-align:center;
              padding:20px;
              position:absolute;
              top:45%;
              left:30%;
              width:40%;
              border:black solid 2px;
              background-color:white">
              <h3>弹窗</h3>
              <button @click="show = false">隐藏弹窗</button>
              </div>
              </telport>
          `,
    data() {
      return {
        show: false,
      };
    },
  });
  App.mount("#Application");
</script>

第七章:Vue 响应式编程

响应式是 Vue 框架的重要特点,常通过数据绑定的方式将变量的值渲染到页面中,变量变化时,页面对应的元素也会同步更新。

  • Vue 的响应式底层原理
  • 在 Vue 中使用响应式对象与数据
  • 组合式 API 的应用

7.1:响应式编程的原理及在 Vue 中的应用

响应式的本质是对变量的监听,监听到变量变化时,可做一些预定义的逻辑。 对于数据绑定来说,要做的就是在变量发生改变时及时对页面元素进行刷新。

7.1.1 手动追踪变量的变化

<script>
  let a = 1; let b = 2; let sum = a + b; console.log(sum); a = 3; b = 4;
  console.log(sum);
</script>

逻辑上讲,sum 的值是变量 a 和变量 b 的值,但在两侧打印中,解构均为 3,sum 值不会响应式改变

在 JavaScript 中,可以使用 Proxy 对原对象进行包装,从而实现对对象属性设置和获取的监听。

<script>
    //定义对象数据
    let a = {
        value: 1,
    };
    let b = {
        value: 2,
    };

    //定义处理器
    handleA = {
        //get是对象的属性值返回的方法
        //get语法将对象属性绑定到查询该属性时将被调用的函数。
        get(target, prop) {
            //对象a,属性
            console.log(`${target[prop]}获取A:${prop}的值`);
            return target[prop];
        },
        //set是属性值修改的方法
        //当尝试设置属性时,set语法将对象属性绑定到要调用的函数。
        set(target, key, value) {
            //对象a,属性, 设置的值
            console.log(`设置A:${key}的值为${value}`);
        },
    };
    handleB = {
        get(target, prop) {
            console.log(`获取B:${prop}的值`);
            return target[prop];
        },
        set(target, key, value) {
            console.log(`设置B:${key}的值为${value}`);
        },
    };

    //let d=new Proxy(target,handle);
    //new Proxy()表示生成一个Proxy实例,(ES6)
    //target参数表示所要拦截的目标对象,
    //handler参数也是一个对象,用来定制拦截行为
    //Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,
    //因此提供了一种机制,可以对外界的访问进行过滤和改写
    //作为构造函数,Proxy接受两个参数:
    //第一个参数是所要代理的目标对象,即如果没有Proxy的介入,操作原来要访问的就是这个对象;
    //第二个参数是一个配置对象,对于每一个被代理的操作,需要提供一个对应的处理函数,该函数将拦截对应的操作。

    let pa = new Proxy(a, handleA);
    let pb = new Proxy(b, handleB);
    let sum = pa.value + pb.value; //第一次执行
    console.log("第一次打印,sum的值是:", sum);

    pa.value = 3;
    console.log(`简简单单的测试:`, a.value);
    pb.value = 4;
    console.log("第二次打印,sum的值是:", sum);

    //打印对象
    console.log(JSON.stringify(a));
    //打印:
    //1获取A:value的值
    //获取B:value的值
    //第一次打印,sum的值是: 3
    //设置A:value的值为3
    //简简单单的测试: 1
    //设置B:value的值为4
    //第二次打印,sum的值是: 3
    //{"value":1}
</script>

效果:每次获取对象 value 属性的值时,都会调用我们定义的 get 方法,对 value 赋值时,也会先调用 set 方法。

Proxy 对象在初始化时需要传入一个要包装的对象和对应的处理器,处理器中可定义 get 方法和 set 方法 在用法上,创建的新代理对象的用法与原对象一致,只是在对其内部属性进行获取或设置时,都会被处理器中定义的 get 方法和 set 方法拦截。

  • get 关键字将对象属性与函数进行绑定,当属性被访问时,对应函数被执行。
  • set 关键字将对象属性与函数进行绑定,当改变属性值时,对应函数被执行。

现在,可以让 sum 变量具备响应式了。

<script>
    //数据对象
    let a = {
        value: 1
    };
    let b = {
        value: 2
    };

    //定义触发器,用来刷新数据
    let trigger = null;

    //数据变量处理器,当数据变化时,调用触发器刷新
    handleA = {
        set(target, key, value) {
            target[key] = value
            if (trigger) {
                trigger()
            }
        }
    }
    handleB = {
        set(target, key, value) {
            target[key] = value
            if (trigger) {
                trigger()
            }
        }
    }

    //进行对象的代理包装
    let pa = new Proxy(a, handleA)
    let pb = new Proxy(b, handleB)
    let sum = 0;

    //实现触发器逻辑
    //箭头函数
    trigger = () => {
        sum = pa.value + pb.value;
    };
    trigger();
    console.log("开始");
    console.log(sum);
    pa.value = 3;
    pb.value = 4;
    console.log(sum);
</script>

运行代码,此时,只要数据对象的 value 属性值发生变化,sum 变量的值就会实时更新。

7.1.2 Vue 中的响应式对象(reactive)

在 Vue 中,按照 Vue 组件模版编写组件元素,data 方法中返回的数据默认都是响应式的。

在 Vue3.0 中引入了组合式 API 特性,这允许我们在 setup 方法中定义组件需要的数据和函数。

setup 方法可以在组件被创建前定义组件需要的数据和函数

实例 1:

  <script src="https://unpkg.com/vue@next"></script>
  <div id="Application"></div>
  <script>
    const App = Vue.createApp({
      //进行组件的数据的初始化
      setup() {
        //数据
        let myData = {
          value: 0,
        };
        //按钮的单击方法
        function click() {
          myData.value += 1;
          console.log(myData.value);
        }
        //将数据返回
        return {
          myData,
          click,
        };
      },
      //模板中可以直接使用setup方法中定义的数据和函数
      template: `
            <h1>测试数据:{{myData.value}}</h1>
            <button @click="click">点击</button>
            `,
    });
    App.mount("#Application");
    </script>

单击按钮,从控制台可看出,myData 对象的 value 属性已发生变化,但页面数据并没有刷新, 这是因为 myData 对象是自定义的普通 JavaScript 对象,其本身没有响应式。故对其修改也不会同步刷新到页面上了。

这与我们常规使用组件的 data 方法返回数据不同,data 方法返回的数据会被默认保存成 Proxy 对象,从而获得响应式。

Vue 3.0 提供了 reactive 方法,使用这个方法对自定义的 JavaScript 对象进行包装,即可方便地为其添加响应式。

修改上述代码中的 setup 方法如下:

//数据
let myData = Vue.reactive({
  value: 0,
});

实例 2:

  <script src="https://unpkg.com/vue@next"></script>
  <div id="Application"></div>
  <script>
    const App = Vue.createApp({
      //进行组件的数据的初始化
      setup() {
        //数据
        let myData = Vue.reactive({
          value: 0,
        });
        //按钮的单击方法
        function click() {
          myData.value += 1;
          console.log(myData.value);
        }
        //将数据返回
        return {
          myData,
          click,
        };
      },
      //模板中可以直接使用setup方法中定义的数据和函数
      template: `
            <h1>测试数据:{{myData.value}}</h1>
            <button @click="click">点击</button>
            `,
    });
    App.mount("#Application");
    </script>

运行代码,现在,当 myData 中的 value 属性发生变化时,已经可以同步进行页面元素的刷新了。

注意,对于异步请求到的数组来说,有两种方式接受数据,因为 reactive 是由 proxy 代理的对象,不能直接赋值,否则会覆盖 proxy 对象,破坏响应式

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>7_1_3-3-Reactive补充.html</title>
</head>

<body>


    <script src="https://unpkg.com/vue@next"></script>

    <div id="Application">

        <h1>测试数据1:{{arr1}}</h1>
        <button @click="add1">点击</button>
        <h1>测试数据2:{{arr2}}</h1>
        <button @click="add2">点击</button>
        <h1>测试数据3:{{arr3}}</h1>
        <button @click="add3">点击</button>
    </div>
    <script>
        const App = Vue.createApp({
            setup() {

                //对于异步请求到的数组来说,有两种方式接受数据,因为reactive是由proxy代理的对象,不能直接赋值,否则会覆盖proxy对象,破坏响应式
                //方法一:可以使用push加解构
                const arr1 = Vue.reactive([])
                //const arr = Vue.reactive < string[] > ([])
                const add1 = () => {
                    setTimeout(() => {
                        let res = ['1', '2', '3']
                        arr1.push(...res)
                    }, 500)
                }
                //方法二:可以将reactive变成一个对象,在对象里添加一个数组来进行赋值
                const arr2 = Vue.reactive({ arr: [] });
                //const arr1 = reactive < { arr: string[] } > ({ arr: [] });
                const add2 = () => {
                    setTimeout(() => {
                        let res = ['1', '2', '3']
                        arr2.arr = res
                    }, 500)

                }
                //方法三
                const arr3 = Vue.reactive([])
                const add3 = () => {
                    //赋值
                    arr3.length = 0
                    let newData = ['1', '2', '3']
                    arr3.push(...newData)
                    //要赋值的新数组
                }


                return {
                    arr1,
                    add1,
                    arr2,
                    add2,
                    arr3,
                    add3
                };
            },

        });
        App.mount("#Application");
    </script>
</body>

</html>

7.1.3 独立的响应式值 Ref 的应用(ref toRefs)

很多时候,我们需要的只是一个独立的原始值,在 7.1.2 的列子中,我们要的只是一个数值。不需要手动将其包装为对象的属性。

ref

这种情况,可以直接使用 Vue 提供的ref 方法来定义响应式独立值ref 方法会帮我们完成对象的包装、

例子: let myObject = Vue.ref(0); myObject 会自动包装对象,其中定义 value 属性为原始值。

实例:

  <script src="https://unpkg.com/vue@next"></script>
  <div id="Application"></div>
  <script>
    const App = Vue.createApp({
      setup() {
        //定义响应式独立值
        let myObject = Vue.ref(10);
        //需要注意,myObjiect会自动包装对象,其中定义value属性为原始值
        function click() {
          myObject.value += 1;
          console.log(myObject.value);
        }
        //返回的数据myObject在模板中使用时已经是独立值
        return {
          myObject,
          click,
        };
      },
      template: `
            <h1>测试数据:{{myObject}}</h1>
            <button @click="click">点击</button>
            `,
    });
    App.mount("#Application");
  </script>

运行效果与之前使用 reactive 并无二致。

注意

使用 ref 方法创建的响应式对象后,在 setup 方法内修改数据,则需要对 myObjectvalue 属性值进行修改

value 属性值是 Vue 内部生成的,但对于 setup 方法导出的数据来说,我们在模版中使用的数据已经是最终的独立值了,可直接使用。

在模版中使用 setup 返回使用 ref 定义的数据时,数据对象会被自动展开

  • 定义: let myObject = Vue.ref(10);
  • 修改: myObject.value += 1;
  • 使用:<h1>测试数据:{{myObject}}</h1>
toRefs

Vue 提供了一个 toRefs 的方法来支持响应式对象的解构赋值。

解构赋值是 JavaScript 对象中的一种语法,可直接将 JavaScript 对象中的属性进行结构,从而赋值给变量使用

实例 1:

let myObject = Vue.reactive({
  value: 0,
});
let { value } = myObject;
function click() {
  value += 1;
  console.log(value);
}

实例 1(完整):

<script src="https://unpkg.com/vue@next"></script>

<div id="Application"></div>
<script>
  const App = Vue.createApp({
    setup() {
      let myObject = Vue.reactive({
        value: 0,
      });
      let { value } = myObject;
      function click() {
        value += 1;
        console.log(value);
      }
      return {
        value,
        click,
      };
    },
    template: `
            <h1>测试数据:{{value}}</h1>
                <button @click="click">点击</button>
            `,
  });
  App.mount("#Application");
</script>

改造后,代码可正常运行,可正确获取到 value 变量的值,但此时,value 变量已失去响应式,对其进行的修改也无法同步刷新到页面。

我们可以使用 Vue 中提供的 toRefs 方法来进行对象结构,其会自动将解构出的变量转换为 ref 变量,从而获得响应式。 实例 2:

let myObject = Vue.reactive({
  value: 0,
});
//对myObject对象进行解构赋值,将value属性单独提取出来
//解构赋值时,value会直接被转成ref变量
let { value } = Vue.toRefs(myObject);
function click() {
  value.value += 1;
  console.log(value.value);
}

实例 2(完整):

<script src="https://unpkg.com/vue@next"></script>

<div id="Application"></div>
<script>
  const App = Vue.createApp({
    setup() {
      let myObject = Vue.reactive({
        value: 0,
      });
      //对myObject对象进行解构赋值,将value属性单独提取出来
      //解构赋值时,value会直接被转成ref变量
      let { value } = Vue.toRefs(myObject);
      function click() {
        value.value += 1;
        console.log(value.value);
      }
      return {
        value,
        click,
      };
    },
    template: `
            <h1>测试数据:{{value}}</h1>
                <button @click="click">点击</button>
            `,
  });
  App.mount("#Application");
</script>

总结

基本类型使用 ref, 引用类型使用 reactive(对象,数组) ref 是使用 reactive 实现的。

  • ref 模块是用来声明简单数据类型的,例如,string , number ,boolean
  • reactive 模块是用来声明复杂数据类型的,例如,数组,对象等

Vue3 的 reactiveref 正是借助了 Proxy 来实现。

7.2:响应式的计算与监听

<script>
  let a = 1; let b = 2; let sum = a + b; console.log(sum); a = 3; b = 4;
  console.log(sum);
</script>

这段代码本身与页面元素没有绑定关系,变量 a 和变量 b 的值仅仅会影响变量 sum 的值。

对于这种场景,sum 变量更像是一种计算变量,在 Vue 中提供了 computed 方法来定义计算变量。

7.2.1 关于计算变量 - compted

有时我们定义变量的值依赖于其他变量的状态。

在组件中,可以使用 computed 选项来==定义属性==。 Vue setup 中可使用同名的 computed 创建==计算变量==。 实例 1:

<script src="https://unpkg.com/vue@next"></script>

<div id="Application"></div>
<script>
  const App = Vue.createApp({
    setup() {
      let a = 1;
      let b = 2;
      let sum = a + b;
      function click() {
        a += 1;
        b += 2;
        console.log(a);
        console.log(b);
      }
      return {
        sum,
        click,
      };
    },
    template: `
            <h1>测试数据:{{sum}}</h1>
            <button @click="click">点击</button>
            `,
  });
  App.mount("#Application");
</script>

实例 1-1:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application"></div>
<script>
  const App = Vue.createApp({
    setup() {
      let sum = Vue.ref(0);
      let a = Vue.ref(1);
      let b = Vue.ref(2);
      sum.value = a.value + b.value;
      function click() {
        a.value += 1;
        b.value += 2;
        console.log(a.value);
        console.log(b.value);
      }
      return {
        sum,
        a,
        b,
        click,
      };
    },
    template: `
            <h1>测试数据:sum:{{sum}},a:{{a}},b:{{b}}</h1>
            <button @click="click">点击</button>
            `,
  });
  App.mount("#Application");
</script>

运行上面的代码,单击按钮时,页面上渲染的数值并不会改变,

使用计算变量的方法定义sum变量如下:

let a = Vue.ref(1);
let b = Vue.ref(2);
let sum = Vue.computed(() => {
  return a.value + b.value;
});
function click() {
  a.value += 1;
  b.value += 2;
}

变量 a 或变量 b 的值一旦变化,就会同步改变 sum 的值,并且响应式地进行页面元素刷新

实例 2:

<script src="https://unpkg.com/vue@next"></script>

<div id="Application"></div>
<script>
  const App = Vue.createApp({
    setup() {
      let a = Vue.ref(1);
      let b = Vue.ref(2);
      let sum = Vue.computed(() => {
        return a.value + b.value;
      });
      function click() {
        a.value += 1;
        b.value += 2;
        console.log(a.value);
        console.log(b.value);
      }
      return {
        sum,
        click,
      };
    },
    template: `
            <h1>测试数据:{{sum}}</h1>
            <button @click="click">点击</button>
            `,
  });
  App.mount("#Application");
</script>

与计算属性类似,计算变量也支持被赋值

实例 3:

<script src="https://unpkg.com/vue@next"></script>

<div id="Application"></div>
<script>
  const App = Vue.createApp({
    setup() {
      let a = Vue.ref(1);
      let b = Vue.ref(2);
      let sum = Vue.computed({
        //拿值
        get() {
          return a.value + b.value;
        },
        //改变值
        set(value) {
          a.value = value;
          b.value = value;
        },
      });
      function click() {
        a.value += 1;
        b.value += 2;
        console.log(a.value);
        console.log(b.value);
        if (sum.value > 10) {
          sum.value = 0;
        }
      }
      return {
        sum,
        click,
        a,
        b,
      };
    },
    template: `
            <h1>测试数据:{{sum}}</h1>a:{{a}},b:{{b}}
            <button @click="click">点击</button>
            `,
  });
  App.mount("#Application");
</script>

7.2.2 监听响应式变量(watch,watchEffect)

现在,我们已经能使用 Vue 中提供的refreactivecomputed等方法来创建拥有响应式特性的变量。

有时候,我们需要在响应式变量发生改变时监听其变化行为。

watchEffect

在 Vue3 中,watchEffect 方法可以自动对其内部用到的响应式变量进行变化监听

原理就是在组件初始化时对所有依赖进行收集,因此,在使用时无需手动指定要监听的变量。

实例:

<script src="https://unpkg.com/vue@next"></script>

<div id="Application">{{a}}</div>
<script>
  const App = Vue.createApp({
    setup() {
      let a = Vue.ref(1);
      Vue.watchEffect(() => {
        //当变量a变化时,即执行当前函数
        console.log("变量a变化了", a.value);
      });

      a.value = 2;

      return {
        a,
      };
    },
  });
  App.mount("#Application");
  //变量a变化了 1
  //变量a变化了 2
</script>

在调用 watchEffect 方法时,其会立即执行传入的函数参数,并会追踪其内部的响应式变量,在其变更时再次调用此函数。

watchEffectsetup 方法中被调用后,其会和当前组件的生命周期绑定在一起,组件卸载时会自动停止监听。

若需手动停止监听,方法如下:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">{{a}}</div>
<script>
  const App = Vue.createApp({
    setup() {
      let a = Vue.ref(1);
      //暂存watchEffect的操作句柄
      let stop = Vue.watchEffect(() => {
        //当变量a变化时,即执行当前函数
        console.log("变量a变化了", a.value);
      });
      a.value = 2;
      //手动停止监听
      stop();
      a.value = 3;
      return {
        a,
      };
    },
  });
  App.mount("#Application");
</script>
watch

watch 是一个与 watchEffect 类似的方法,与 watchEffect 方法相比,watch 方法能够更精准地监听响应式数据的变化,

实例 1:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">a:{{a.data}},b:{{b}}</div>
<script>
  const App = Vue.createApp({
    setup() {
      let = a = Vue.reactive({
        data: 0,
      });
      let b = Vue.ref(0);
      Vue.watch(
        () => {
          //监听a对象的data属性变化
          return a.data;
        },

        (value, old) => {
          //新值和旧值都可以获取到
          console.log(value, old);
        }
      );
      a.data = 1;

      //可以直接监听ref对象
      Vue.watch(b, (value, old) => {
        //新值和旧值都能获取到
        console.log(value, old);
      });
      b.value = 3;
      return { a, b };
    },
  });
  App.mount("#Application");
  //打印
  //1 0
  //3 0
</script>


watch 方法比 watchEffect 方法强大的地方在于,其可以分别获取到变化前后的值,很方便做某些需要与值比较相关的业务逻辑。

监听多个数据源

watch 也支持同时监听多个数据源。

实例 2:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">a:{{a.data}},b:{{b}}</div>
<script>
  const App = Vue.createApp({
    setup() {
      let a = Vue.reactive({
        data: 0,
      });
      let b = Vue.ref(0);

      Vue.watch(
        [
          () => {
            //监听a对象的data属性变化
            return a.data;
          },
          b,
        ],
        ([valueA, valueB], [oldA, oldB]) => {
          //新值和旧值都可以获取到
          console.log(valueA, oldA);
          console.log(valueB, oldB);
        }
      );
      a.data = 1;
      b.value = 3;
      return { a, b };
    },
  });
  App.mount("#Application");
    //打印
    //1 0
    //3 0
</script>
副作用

函数副作用是指当调用函数时,除了返回函数值之外,还对主调用函数产生附加的影响。 副作用的函数不仅仅只是返回了一个值,而且还做了其他的事情,比如:

  1. 修改了一个变量
  2. 直接修改数据结构
  3. 设置一个对象的成员
  4. 抛出一个异常或以一个错误终止
  5. 打印到终端或读取用户输入
  6. 读取或写入一个文件
  7. 在屏幕上画图

函数副作用会给程序设计带来不必要的麻烦,给程序带来十分难以查找的错误,并且降低程序的可读性

例如:在计算得出结果后,给显示的结果添加一个放大缩小动画

7.3:组合式 API 的应用

组合式 API 的使用,能够帮助我们更好地梳理复杂组件的逻辑分布,能够从代码层面上将分离的相关逻辑点进行聚合,更适合进行复杂模块组件的开发

一般是这样用的:

<script src="https://unpkg.com/vue@next"></script>

<div id="Application">
  <input type="text" v-model="sub" />
  您输入的是:{{sub}}
  <br />
  <button @click="test()">点击打印控制台信息</button>
  <br />
  当前数组: {{state.arr}} <br />
  删除按钮:<button @click="splice()">删除第二个数据</button>
</div>
<script>
  const App = Vue.createApp({
    setup() {
      let sub = Vue.ref();
      const test = () => {
        console.log("打印咯");
        alert("您单击了按钮");
      };

      const state = Vue.reactive({ arr: [] });
      state.arr = [1, 2, 3, 4, 5, 6, 7, 8];
      const splice = () => {
        state.arr.splice(0, 1); //删除数组第一个
      };

      return { sub, test, state, splice };
    },
  });
  App.mount("#Application");
</script>

7.3.1 关于 setup 方法

  1. setup 方法是 Vue3 中新增的方法,是组合式 API 的核心方法
  2. 如果使用组合式 API 模式进行组件开发,则逻辑代码都要编写在 setup 方法中。
  3. setup 方法会在组件创建之前被执行,对应组件生命周期方法 beforeCreate 方法调用之前被执行。
  4. 由于 setup 方法特殊的执行时机,除了可以访问组件的传参外部属性 props 之外,在其内部我们不能使用 this 来引用组件的其他属性。
  5. 在 setup 方法的最后,我们可以将定义的组件所需的数据、函数等内容暴露给组件的其他选项(比如生命周期函数、业务方法、计算属性等)

setp 方法可以接收两个参数:props 和 context

  • props 是组件使用时设置的外部参数,其是响应式的;
  • context 则是一个 JavaScript 对象,其中可用的属性有 attrs、slots 和 emit
实例
    <script src="https://unpkg.com/vue@next"></script>

    <div id="Application">
      <com name="组件名"></com>
    </div>
    <script>
      const App = Vue.createApp({});
      App.component("com", {
        setup(props, context) {
          console.log(props.name);
          //属性
          console.log("属性:", context.attrs);
          //插槽
          console.log("插槽:", context.slots);
          //触发事件
          console.log("触发事件:", context.emit);
        },
        props: {
          name: String,
        },
      });

      App.mount("#Application");
    </script>

在 setup 方法的最后可以返回一个 JavaScript 对象,此对象包装的数据可以在组件的其他选项中使用,也可以直接用于 HTML 模版中。

实例
    <script src="https://unpkg.com/vue@next"></script>

    <div id="Application">
      <com name="组件名"></com>
    </div>
    <script>
      const App = Vue.createApp({});
      App.component("com", {
        setup(props, context) {
          let data = "setup数据";
          return { data };
        },
        props: {
          name: String,
        },
        template: `<div>{{data}}</div>`,
      });

      App.mount("#Application");
    </script>

如果不在组件中定义 template 模版,也可以直接使用 setup 方法来返回一个渲染函数,当组件将要被展示时,会使用此渲染函数进行渲染。

实例
    <script src="https://unpkg.com/vue@next"></script>

    <div id="Application">
      <com name="组件名"></com>
    </div>
    <script>
      const App = Vue.createApp({});
      App.component("com", {
        setup(props, context) {
          let data = "setup数据";
          return () => Vue.h("div", [data]);
        },
        props: {
          name: String,
        },
        //template: `<div>{{data}}</div>`,
      });

      App.mount("#Application");
    </script>

7.3.2 在 setup 方法中定义生命周期行为

setup 方法本身也可以定义组件的生命周期方法,方便将相关逻辑组合在一起。 (在组件的原生命周期方法前加 on 即可)

效果
beforeCreate->setup()直接写
created->setup()直接写
beforeMount->onBeforeMount组件挂载前
mounted->onMounted组件挂载后
beforeUpdate->onBeforeUpdate数据更新前
updated->onUpdated数据更新后
beforeDestroy->onBeforeUnmount元素销毁前
destroyed->onUnmounted元素销毁后

从逻辑上说,setup 方法的执行时机与beforeCreatecreated两个生命周期方法执行时机基本一致的,在 setup 方法中直接编写相关逻辑即可。

实例

    <script src="https://unpkg.com/vue@next"></script>

    <div id="Application">
      <com name="组件名"></com>
    </div>
    <script>
      const App = Vue.createApp({});
      App.component("com", {
        setup(props, context) {
          let data = "setup数据";
          //设置的函数参数的调用时机与mounted一样
          Vue.onMounted(() => {
            console.log("setup定义的mounted-1");
          });
          console.log("简简单单-2");
          return () => Vue.h("div", [data]);
        },
        props: {
          name: String,
        },
        mounted() {
          console.log("组件内部定义的monted-3");
        },
      });

      App.mount("#Application");
    </script>

如果组件和 setup 方法中都定义了同样的生命周期方法,它们之间不会冲突。

在实际调用时,会先调用 setup 方法中定义的,再调用组件内部定义的。

7.4:范例:实现支持搜索和筛选的用户列表

实例:

筛选有问题,但书上是这么写的,

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>7_4_2-组合式API重命名</title>
  </head>

  <body>
    <script src="https://unpkg.com/vue@next"></script>

    <div id="Application"></div>
    <script>
      let mock = [
        {
          name: "小王",
          sex: 0,
        },

        {
          name: "小红",
          sex: 1,
        },

        {
          name: "小李",
          sex: 1,
        },

        {
          name: "小张",
          sex: 0,
        },

        {
          name: "小玉",
          sex: 1,
        },
      ];
      const App = Vue.createApp({
        setup() {
          //先处理用户列表相关逻辑

          //const 声明一个只读的常量,一旦声明,常量的值就不能改变。
          const showDatas = Vue.ref([]);

          //获取数据方法
          const queryAllData = () => {
            //模拟请求过程
            //setTimeout(要执行的代码, 等待的毫秒数)
            //setTimeout(JavaScript 函数, 等待的毫秒数)
            //https://www.runoob.com/w3cnote/javascript-settimeout-usage.html
            setTimeout(() => {
              showDatas.value = mock;
            }, 3);
          };
          //组件挂载时获取数据
          Vue.onMounted(queryAllData);

          //处理筛选与检索逻辑

          //let 声明的变量只在 let 命令所在的代码块内有效。
          //-1:所有人,0:男性,1:女性

          //查性别
          let sexFilter = Vue.ref(-1);
          //查名字
          let searchKey = Vue.ref("");

          //需要的,重组后的数组
          let fliterData = () => {
            searchKey.value = "";
            if (sexFilter.value == -1) {
              //输出所有人
              showDatas.value == mock;
            } else {
              //https://www.runoob.com/jsref/jsref-filter.html
              //filter() 方法创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素。

              //(单一参数) => {函数声明}
              showDatas.value = mock.filter((data) => {
                //如果sexFilter.value是1,就返回sex的值为1的数组
                //如果sexFilter.value是0,就返回sex的值为0的数组
                return data.sex == sexFilter.value;
              });
            }
          };

          //查名字
          searchData = () => {
            sexFilter.value = -1;
            if (searchKey.value.length == 0) {
              //如果名字为空,就输出所有人
              showDatas.value = mock;
            } else {
              showDatas.value = mock.filter((data) => {
                //search() 方法用于检索字符串中指定的子字符串,或检索与正则表达式相匹配的子字符串。
                //如果没有找到任何匹配的子串,则返回 -1。
                //!= 在表达式两边的数据类型不一致时,会隐式转换为相同数据类型,然后对值进行比较.
                return data.name.search(searchKey.value) != -1;
              });
            }
          };
          //添加侦听
          //查性别,查性别后的数组
          Vue.watch(sexFilter, fliterData);

          //查名字,查完名字后的数组
          Vue.watch(searchKey, searchData);
          //将模板中需要使用的数据返回
          return {
            //所有人
            showDatas,

            //查名字
            searchKey,

            //查性别
            sexFilter,
          };
        },
        template: `
            <div class="container">
            <div class="content">
                <input type="radio" :value="-1" v-model="sexFilter" />全部
                <input type="radio" :value="0" v-model="sexFilter" />男
                <input type="radio" :value="1" v-model="sexFilter" />女
            </div>
            <div class="content">搜索:<input type="text" v-model="searchKey" /></div>
            <div class="content">
                <table border="1" width="300px">
                    <tr>
                        <th>姓名</th>
                        <th>性别</th>
                    </tr>
                    <tr v-for="(data,index) in showDatas">
                        <td>{{data.name}}</td>
                        <td>{{data.sex==0?'男':'女'}}</td>
                    </tr>
                </table>
            </div>
        </div>
            `,
      });
      App.mount("#Application");
    </script>

    <style>
      .container {
        margin: 50px;
      }

      .content {
        margin: 20px;
      }
    </style>

    <script></script>
  </body>
</html>

7.5:详解响应式基础

7.5.1:vue3 响应式的实现

针对Object.defineProperty的弊病, 在 ES6 中引入了一个新的对象——Proxy(对象代理)

Proxy 对象,用于创建一个对象的代理,主要用于改变对象的某些默认行为, Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截, 因此提供了一种机制,可以对外界的访问进行过滤和改写。

基本语法如下:

/*
 * target: 目标对象
 * handler: 配置对象,用来定义拦截的行为
 * proxy: Proxy构造器的实例
 */
var proxy = new Proxy(target, handler);

拦截 get,取值操作

var proxy = new Proxy(
  {},
  {
    get: function (target, propKey) {
      return 35;
    },
  }
);

proxy.time; // 35
proxy.name; // 35
proxy.title; // 35

可以拦截的操作有:

函数操作
get读取一个值
set写入一个值
hasin操作符
deletePropertyObject.getPrototypeOf()
getPrototypeOfObject.getPrototypeOf()
setPrototypeOfObject.setPrototypeOf()
isExtensibleObject.isExtensible()
preventExtensionsObject.preventExtensions()
getOwnPropertyDescriptorObject.getOwnPropertyDescriptor()
definePropertyObject.defineProperty
ownKeysObject.keys()Object.getOwnPropertyNames()和 Object.getOwnPropertySymbols()
apply调用一个函数
constructnew一个函数
那么使用 Proxy 可以解决 Vue2 中的哪些问题,
  • Proxy 是对整个对象的代理,而 Object.defineProperty 只能代理某个属性。
  • 对象上新增属性,Proxy 可以监听到,Object.defineProperty 不能。
  • 数组新增修改,Proxy 可以监听到,Object.defineProperty 不能。
  • 若对象内部属性要全部递归代理,Proxy 可以只在调用的时候递归,而 Object.definePropery 需要一次完成所有递归,Proxy 相对更灵活,提高性能。
实现总结
  • reactive 将引用类型值变为响应式,使用Proxy实现
  • ref 可将基本类型和引用类型都变成响应式,通过监听类的value属性的getset实现,但是当传入的值为引用类型时实际上内部还是使用reactive方法进行的处理
  • 推荐基本类型使用ref,引用类型使用 reactive

实现原理

7.5.2:reactive

reactive 方法 根据传入的对象 ,创建返回一个深度响应式对象。

响应式对象看起来和传入的对象一样,但是,响应式对象属性值改动,不管层级有多深,都会触发响应式。新增和删除属性也会触发响应式。

实现:

会对传入对象进行包裹,创建一个该对象的 Proxy 代理对象。 它是源对象的响应式副本,不等于原始对象。 它“==深层==”转换了源对象的所有嵌套 property(属性),解包并维持其中的任何 ref 引用关系。

reactive 定义的响应式数据是“深层次的”, 响应式对象属性值改动,不管层级有多深,都会触发响应式。

// 响应式对象
const state = Vue.reactive({
  name: "太凉",
  age: 18,
  hobby: ["游泳", "爬山"],
  address: {
    provoince: "北京",
    city: "北京直辖市",
    street: "东城区长安街",
  },
});

如果给 reactive 传递了其他对象,默认情况下修改对象,界面不会自动更新。如果想更新,可以通过重新赋值的方式.

作用:

创建原始对象的响应式副本,即将「引用类型」数据转换为「响应式」数据 语法:

const 代理对象= reactive(源对象)接收一个对象(或数组),返回一个代理对象(Proxy 的实例对象,简称 Proxy 对象)

参数:

reactive 参数必须是对象或数组,reactive 只能 给对象添加响应式,对于值类型,比如StringNumberBooleanSymbol无能为力。

  • reactive 主要是用来操作对象或数组,定义为响应式数据
  • 新增和删除属性也会触发响应式。
  • 内部基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据进行操作。
实例:

看一个例子,通过 reactive 创建了一个响应式,然后 3 秒之后,做以下三种方式操作,都会触发响应式

  • 改变 name 属性
  • 深度改变 address 属性
  • 新增 school 属性
  • 删除 age 属性
<script src="https://unpkg.com/vue@next"></>
<div id="Application">
  <div class="demo">
    <div>姓名:{{state.name}}</div>
    <div v-if="state.age>0">年龄:{{state.age}}</div>
    <div>
      爱好:
      <span v-for="items in state.hobby">{{items}},</span>
    </div>
    <div>
      地址:{{state.address.provoince}} - {{state.address.city}} -
      {{state.address.street}}
    </div>
  </div>

  <div class="demo">
    <div>学校:{{state.school||'自学成才'}}</div>
  </div>
</div>
<script>
  const App = Vue.createApp({
    setup() {
      // 响应式对象
      const state = Vue.reactive({
        name: "太凉",
        age: 18,
        hobby: ["游泳", "爬山"],
        address: {
          provoince: "北京",
          city: "北京直辖市",
          street: "东城区长安街",
        },
      });

      // 过3秒后改变
      setTimeout(() => {
        // update1:改变name属性
        state.name = "冰箱太凉";

        // update2:深度改变 address属性
        state.address.provoince = "山东省";
        state.address.city = "临沂市";

        // update3:新增 school属性
        state.school = "清华北大";

        // update4:删除 年龄属性
        delete state.age;

        // update5:数组 添加一项
        state.hobby.push("打豆豆");
      }, 3000);

      return {
        //注意这里不能通过 ...state 方式结构,这样会丢失响应式
        state,
      };
    },
  });
  App.mount("#Application");
</script>

使用 reactive 函数声明数组如何正确赋值

需求:将接口请求到的列表数据赋值给响应数据 array

const arr = reactive([]);
const load = () => {
  const res = [2, 3, 4, 5]; //假设请求接口返回的数据
  // 方法1 失败,直接赋值丢失了响应性
  // arr = res;
  // 方法2 这样也是失败
  // arr.concat(res);
  // 方法3 可以,但是很麻烦
  res.forEach((e) => {
    arr.push(e);
  });
};

问题原因:

这是因为 arr = newArr这行代码让arr失去了响应式。 vue3 使用proxy,对于对象和数组都不能直接整个赋值。

具体原因:

reactive声明的响应式对象被 arr 代理, 操作代理对象需要有代理对象的前缀,直接覆盖会丢失响应式。

方法 2 为什么不行? 只有 push 或者根据索引遍历赋值才可以保留 reactive 数组的响应性?

如何方便的将整个数组拼接到响应式数据上? 下面我们看下解决方案:

// 这几种办法都可以触发响应性,推荐第一种
// 方案1:创建一个响应式对象,对象的属性是数组
const state = reactive({
  arr: [],
});
state.arr = [1, 2, 3];

// 方案2: 使用ref函数
const state = ref([]);
state.value = [1, 2, 3];

// 方案3: 使用数组的push方法
const arr = reactive([]);
arr.push(...[1, 2, 3]);
异步请求的数据赋值

对于异步请求到的数组来说,有两种方式接受数据,因为reactive是由proxy代理的对象,不能直接赋值,否则会覆盖proxy对象,破坏响应式

 <script src="https://unpkg.com/vue@next"></script>

    <div id="Application">

        <h1>测试数据1:{{arr1}}</h1>
        <button @click="add1">点击</button>
        <h1>测试数据2:{{arr2}}</h1>
        <button @click="add2">点击</button>
        <h1>测试数据3:{{arr3}}</h1>
        <button @click="add3">点击</button>
    </div>
    <script>
        const App = Vue.createApp({
            setup() {

                //对于异步请求到的数组来说,有两种方式接受数据,因为reactive是由proxy代理的对象,不能直接赋值,否则会覆盖proxy对象,破坏响应式
                //方法一:可以使用push加解构
                const arr1 = Vue.reactive([])
                //const arr = Vue.reactive < string[] > ([])
                const add1 = () => {
                    setTimeout(() => {
                        let res = ['1', '2', '3']
                        arr1.push(...res)
                    }, 500)
                }
                //方法二:可以将reactive变成一个对象,在对象里添加一个数组来进行赋值
                const arr2 = Vue.reactive({ arr: [] });
                //const arr1 = reactive < { arr: string[] } > ({ arr: [] });
                const add2 = () => {
                    setTimeout(() => {
                        let res = ['1', '2', '3']
                        arr2.arr = res
                    }, 500)

                }
                //方法三
                const arr3 = Vue.reactive([])
                const add3 = () => {
                    //赋值
                    arr3.length = 0
                    let newData = ['1', '2', '3']
                    arr3.push(...newData)
                    //要赋值的新数组
                }

                return {
                    arr1,
                    add1,
                    arr2,
                    add2,
                    arr3,
                    add3
                };
            },

        });
        App.mount("#Application");
    </script>

7.5.3:ref

对于基本数据类型如何实现响应式呢?

vue 的解决方法是把基本数据类型变成一个对象:这个对象只有一个 value 属性,

value 属性的值就等于这个基本数据类型的值。

然后,就可以用 reative 方法将这个对象,变成响应式的 Proxy 对象。 实际上就是: ref(0) --> reactive( { value:0 })

实现:

调用 ref 函数时会 new 一个类,这个类监听了 value 属性的 getset ,实现了在 get 中收集依赖,在 set 中触发依赖,

而如果需要对传入参数深层监听的话,就会调用我们上面提到的 reactive 方法。 即:

ref(0); // 通过监听对象(类)的value属性实现响应式
ref({ a: 6 }); // 调用reactive方法对对象进行深度监听

ref 函数用来将一项数据包装成一个响应式 ref 对象。 它接收任意数据类型的参数,作为这个 ref 对象 内部的 value property 的值。

生成值类型数据(StrngNumberBooleanSymbol)的响应式对象

symbol 是 ES6 引入了一种新的基本数据类型(原始数据类型) Symbol ,表示独一无二的值

生成对象和数组类型的响应式对象 (对象和数组一般不选用 ref 方式,而选用 reactive 方式,比较便捷)

作用:
  • 把基本类型的数据变为响应式数据。
  • 生成对象和数组类型的响应式对象 (对象和数组一般会选用 reactive 方式,比较便捷)
参数:
  1. 基本数据类型
  2. 引用类型
  3. DOM 的 ref 属性值
访问:

可以用 ref 对象.value 访问或更改这个值。 ref 方法包装的数据,需要使用.value 来访问,但在模板中不需要,Vue 解析时会自动添加。

实例:
<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <div>
    <div>countRef:{{countRef}}</div>
    <div>objCountRef:{{objCountRef.count}}</div>
    <div>爱好:{{hobbyRef.join('---')}}</div>
  </div>
</div>
<script>
  const App = Vue.createApp({
    setup() {
      // 值类型
      const countRef = Vue.ref(1);
      console.log(countRef);

      // 对象
      const objCountRef = Vue.ref({ count: 1 });

      // 数组
      const hobbyRef = Vue.ref(["爬山", "游泳"]);

      setTimeout(() => {
        // 通过value改变值
        countRef.value = 2;
        objCountRef.value.count = 3;
        hobbyRef.value.push("吃饭");
      }, 4000);

      return {
        countRef,
        objCountRef,
        hobbyRef,
      };
    },
  });
  App.mount("#Application");
</script>

ref 属性:

虽然在 Vue 中不提倡我们直接操作 DOM,毕竟 Vue 的理念是以数据驱动视图。但是在实际情况中,我们有很多需求都是需要直接操作 DOM 节点的,这个时候 Vue 提供了一种方式让我们可以获取 DOM 节点:ref 属性

只要想要在 Vue 中直接操作 DOM 元素,就必须用 ref 属性进行注册

在使用组合式 API 时,响应式引用和模板引用的概念是统一的。

  1. 被用来给元素或子组件注册引用信息(id 的替代者)
  2. 应用在 html 标签上获取的是真实 DOM 元素,应用在组件标签上是组件实例对象(vc)
三种用法:
  1. ref 加在普通元素上,可以获取到 dom 元素。
  2. ref 加在子组件上,可以获取到组件实例,可以使用子组件的所有方法。
  3. 利用 v-for 和 ref 组合获取一组 dom 节点
注意:
  1. 获取 ref 要确保在 dom 已经渲染完成,比如可以在 vue 生命周期的 onMounted() {}钩子函数中调用,或者可以在 this.$nextTick(() => {})中调用。
  2. 在页面初始渲染的时候是不能访问 ref 的,因为此时 ref 还不存在, $ref 也不是响应式的,不能在模板中做数据绑定。
  3. 如果 ref 是循环出来的,有多个重名,那么 ref 的值会是一个数组 ,此时要拿到单个的 ref 只需要循环就可以了。
  4. 在 vue 中用 ref 来获取 dom 的时候,可能会出现获取的值 为 undefined 的情况

(1)场景一:在 created()中使用 在这个生命周期中进行数据观测、属性和方法的运算,watch 事件回调,但是此时 dom 还没有渲染完成,是不能通过 ref 调用 dom 的。 **解决:**在 onMounted 中调用或者使用 nextTick。

(2)场景二:父元素或者当前元素使用了 v-if 或者 v-show 因为 ref 不是响应式的,只在组件渲染完之后才会生效,在初始渲染的时候是不存在的 因为是非响应式的,所有动态加载的模板更新它都无法相应的变化 **解决:**通过 setTimeout(() => {…}, 0)来实现

用法一:获取 DOM 元素

使用方式:打标识:<h1 ref="xxx"></h1>或<School ref="xxx"> </School>

ref 拿到的是 整个标签元素 或 整个组件元素

获取:

const hello = Vue.ref(null); //dom加载后再获取 console.log("onMound后获取:",
hello.value);

总结 定义 const a = ref(xxx) return a 在 标签里 ref = ”a“ 即可绑定成功 使用直接 a.value

静态引用 - 本页面获取 dom 元素

此方法常用于代替 id,获取 DOM 元素,我们在使用 Echarts 中时会用到这个,以便在指定位置展示图表内容

实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <div ref="hello">简简单单的晚饭</div>
</div>
<script>
  const App = Vue.createApp({
    setup() {
      const hello = Vue.ref(null);
      console.log("第一次获取:", hello.value);
      Vue.onMounted(() => {
        //dom加载后再获取
        console.log("onMound后获取:", hello.value); // <div>简简单单的晚饭</div>
        console.log("onMound后获取:", hello.value.textContent); // 简简单单的晚饭
      });
      return {
        hello,
      };
    },
  });
  App.mount("#Application");
</script>

上段代码中我们同样给 div 元素添加了 ref 属性,为了获取到这个元素,我们声明了一个与 ref 属性名称相同的变量 hello,然后我们通过 hello.value 的形式便获取到了该 div 元素。

注意点:

  • 变量名称必须要与 ref 命名的属性名称一致。
  • 通过 hello.value 的形式获取 DOM 元素。
  • 必须要在 DOM 渲染完成后(onMounted)才可以获取 hello.value,否则就是 null。

用法二:动态引用 - v-for - 获取循环的 DOM 节点

使用 ref 的场景有多种,一种是单独绑定在某一个元素节点上,另一种便是绑定在 v-for 循环出来的元素上了。这是一种非常常见的需求

  • 当 v-for 用于元素或组件时,引用信息将是包含 DOM 节点或组件实例的数组
  • 所有组件都有一个属性:el,用于获取组件中的元素
  • el 绑定 app 以后,就在#app 的 div 里作为根组建 - el 你可以随意改 这看上去有点像是把回调的值赋给 data[i]

V-for 中的 ref 绑定常量

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <ul>
    <li v-for="item in 10" ref="itemRefs">{{item}}</li>
  </ul>
</div>
<script>
  const App = Vue.createApp({
    setup() {
      const itemRefs = Vue.ref([]);
      Vue.onMounted(() => {
        console.log("DOM加载完毕后打印:", itemRefs.value);
      });
      return {
        itemRefs,
      };
    },
  });
  App.mount("#Application");
</script>

上段代码中尽管是 v-for 循环,但是我们似乎使用 ref 的形式与静态引用中的方式没有任何变化,我们同样使用变量的形式拿到了每一个 li 标签元素。

但是这里我们需要注意一下:我们似乎没办法区分哪个 li 标签哪个 ref,初次之外,我们的 itemRefs 数组不能够保证与原数组顺序相同,即与 list 原数组中的元素一一对应。

v-for - ref 绑定函数

前面我们在组件上定义 ref 时,都是以一个字符串的形式作为 ref 的名字,其实我们的 ref 属性还可以接收一个函数作为属性值,这个时候我们需要在 ref 前面加上:

  • 如果通过 v-for 遍历想加不同的 ref 时记得加 :号,即 :ref =某变量 ;

这点和其他属性一样,如果是固定值就不需要加 :号,如果是变量记得加 :号。(加冒号的,说明后面的是一个变量或者表达式;没加冒号的后面就是对应的字符串常量(String)

实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <div :ref="setHelloRef">简简单单的晚饭</div>
</div>
<script>
  const App = Vue.createApp({
    setup() {
      const setHelloRef = (el) => {
        console.log(el); // <div>简简单单的晚饭</div>
      };

      return {
        setHelloRef,
      };
    },
  });
  App.mount("#Application");
</script>

上段代码中 ref 属性接收的是一个 setHelloRef 函数,该函数会默认接收一个 el 参数,这个参数就是我们需要获取的 div 元素。假如需求中我们采用这种方式的话,那么完全可以把 el 保存到一个变量中去,供后面使用。

那么,我们在 v-for 中是否也能采用这种方式呢?

答案是可以的!

实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <ul>
    <li v-for="item in 10" :ref="(el)=>{setItemRefs(el,item)}">{{item}}</li>
  </ul>
</div>
<script>
  const App = Vue.createApp({
    setup() {
      let itemRefs = Vue.ref([]);
      const setItemRefs = (el, item) => {
        if (el) {
          itemRefs.value.push({
            id: item,
            el,
          });
        }
      };
      Vue.onMounted(() => {
        console.log("打印itemRefs:", itemRefs.value);
      });

      return {
        setItemRefs,
      };
    },
  });
  App.mount("#Application");
</script>

在 v-for 中使用函数的形式传入 ref 与不使用 v-for 时的形式差不多,不过这里我们做了一点变通,为了区别出哪个 ref 是哪一个 li 标签,我们决定将 item 传入函数,也就是(el) => setItemRefs(el, item)的写法。

这种形式的好处既让我们的操作性变得更大,还解决了 v-for 循环是 ref 数组与原数组顺序不对应的问题。

这是另外一个例子:

<script src="https://unpkg.com/vue@next"></script>

<div id="Application">
  <div v-for="(item, i) in list" :ref="el=>{divs[i]=el}">{{i}}-{{item}}</div>
</div>
<script>
  const App = Vue.createApp({
    setup() {
      const list = Vue.reactive([1, 2, 3]);
      const divs = Vue.ref([]);
      console.log("打印下看看有啥", divs.value);
      //数据更新前
      Vue.onBeforeUpdate(() => {
        //确保在每次更新之前重置参考。
        divs.value = [];
      });
      Vue.onMounted(() => {
        console.log("DOM加载完毕后打印:", divs.value);
      });

      return {
        list,
        divs,
      };
    },
  });
  App.mount("#Application");
</script>
用法三:组件上使用 ref

我们也可以将 ref 绑定在组件上,便可以获取到该组件里面的所有数据和方法

实例:
<script>
  const App = Vue.createApp({
    setup() {
      const childRef = Vue.ref(null);

      Vue.onMounted(() => {
        //dom加载后再获取
        console.log("onMound后获取组件实例:", childRef.value); // child组件实例
        console.log("onMound后获取组件的值:", childRef.value.message); // 组件的值
        childRef.value.onChange(); //执行子组件的方法
      });
      return {
        childRef,
      };
    },
  });

  //子组件
  const childBlack = {
    setup() {
      //一个信息
      const message = Vue.ref("我是子组件");
      //一个方法
      const onChange = () => {
        alert("我是子组件方法");
      };
      return {
        message,
        onChange,
      };
    },
    template: "<h1>{{message}}</h1>",
  };
  App.component("child", childBlack);

  //挂载
  App.mount("#Application");
</script>

上段代码中我们新增了一个子组件,然后再子组件上面绑定了 ref,其用法基本上和 ref 直接绑定在 DOM 元素上一致。

在 Vue3 中,使用 ref 获取子组件时,如果想要获取子组件的数据或者方法,子组件可以通过

defineExpose 方法暴露数据。

获取子组件的数据
    <script src="https://unpkg.com/vue@next"></script>
    <div id="Application">
        <child ref="hello"></child>
        <button type="button" @click="getHello">获取子组件中的值</button>
    </div>
    <script>
        const App = Vue.createApp({
            setup() {
                const hello = Vue.ref()//获取子组件
                const getHello = () => {
                    console.log("拿到了子组件的信息:", hello.value.msg)
                }
                return {
                    hello,
                    getHello
                }
            },
        });

        //子组件
        const childBlock = {
            setup() {
                const msg = Vue.ref("Hello Worlld")
                return {
                    msg
                }

            },

            template: '<h1>{{msg}}</h1>'
        }
        App.component("child", childBlock);

        //挂载
        App.mount("#Application");
    </script>
获取子组件的方法
<script src="https://unpkg.com/vue@next"></script>
    <div id="Application">
        <child ref="hello"></child>
        <button type="button" @click="getHello">拿到子组件中的方法</button>
    </div>
    <script>
        const App = Vue.createApp({
            setup() {
                const hello = Vue.ref()//获取子组件
                const getHello = () => {
                    hello.value.open();
                    //拿到子组件的方法
                }
                return {
                    hello,
                    getHello
                }
            },
        });

        //子组件
        const childBlock = {
            setup() {
                const open = () => {
                    console.log("调用到了")
                }
                return {
                    open
                }

            },

            template: '<h1>我是子组件哦</h1>'
        }
        App.component("child", childBlock);

        //挂载
        App.mount("#Application");
    </script>
ref子组件调用父组件方法
 <script src="https://unpkg.com/vue@next"></script>
    <div id="Application">
        <child ref="hello" @refreshdata="getData"></child>
        <button type="button" @click="getHello">拿到子组件中的方法</button>
    </div>
    <script>
        const App = Vue.createApp({
            setup() {
                const hello = Vue.ref()//获取子组件
                const getHello = () => {
                    hello.value.open();
                    //拿到子组件的方法
                }
                const getData = () => {
                    console.log("11111")
                    //拿到子组件的方法
                }
                return {
                    hello,
                    getHello,
                    getData
                }
            },
        });

        //子组件
        const childBlock = {
            setup(props, ctx) {
                const open = () => {
                    console.log("调用到了");
                    //子组件调用父组件方法
                    ctx.emit("refreshdata");
                }
                return {
                    open
                }

            },

            template: '<h1>我是子组件哦</h1>'
        }
        App.component("child", childBlock);

        //挂载
        App.mount("#Application");
    </script>
return 数据时, 为什么用 ref,看到数据仍然可以变化,但是却不能直接用 ref?
setup() {
    let person = reactive({
      name: '张珍',
      age: 18,
      job: {
        j1: {
          salary: 20
        }
      }
    })
    return {
      person,
      name: ref(person.name),
      age: ref(person.age),
      salary: ref(person.job.j1.salary)
    }
  }

ref(person.name) 读取出来的字符串'张珍',是打包成了一个新的 ref,页面上读取的是新的 ref 的数据,但是实际上源数据 person 是没有变化的。

即: name: ref(person.name), // 虽然显示的是张珍~~ person, // 但是源数据personname的值还是张珍,数据丝毫不变。

7.5.4:toRef

针对一个响应式对象(reactive 封装)的 prop(属性)创建一个 ref,且保持响应式。 创建的 ref 与其源属性保持同步:改变源属性的值将更新 ref 的值,反之亦然。 两者保持引用关系

作用:

创建一个 ref 对象,其 value 值指向另一个对象中的某个属性。

语法:

const 属性名= toRef(对象,'属性名') const newName = toRef(person,'name')

详解:

toRef(person, 'name') 是个 ref 对象,里面有 value 值,将这个 value 值利用 getter 指向了 person 源数据,所以 toRef 是引用,ref 是复制。

操作后可以发现 person 中的源数据也是在同时变的

实现:

基于响应式对象上的一个属性,创建一个对应的 ref,实际上内部做的就是创建一个对象,这个对象的 value 属性被监听了 getset,将对这个对象的 value 值的访问代理到了响应式对象的一个属性上。

应用:

要将响应式对象中的某个属性单独提供给外部使用时。

演示:
const state = reactive({
  foo: 1,
  bar: 2,
});

const fooRef = toRef(state, "foo");

// 更改该 ref 会更新源属性
fooRef.value++;
console.log(state.foo); // 2

// 更改源属性也会更新该 ref
state.foo++;
console.log(fooRef.value); // 3

实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <div>姓名--state.name:{{state.name}}</div>
  <div>姓名2--nameRef:{{nameRef}}</div>
  <div>年龄:{{state.age}}</div>
</div>
<script>
  const App = Vue.createApp({
    setup() {
      // 响应式对象
      const state = Vue.reactive({
        name: "太凉",
        age: 18,
      });

      // 通过toRef创建一个Ref响应式
      const nameRef = Vue.toRef(state, "name");

      // 过3秒后改变 两者 保持引用关系
      setTimeout(() => {
        // update1:改变name属性
        state.name = "冰箱太凉";
      }, 3000);

      // 过6秒后改变 两者 保持引用关系
      setTimeout(() => {
        // update1:改变name属性
        nameRef.value = "我就是冰箱太凉";
      }, 6000);

      return {
        nameRef,
        state,
      };
    },
  });
  App.mount("#Application");
</script>

把一个 prop 的 ref 传递给一个组合式函数时会很有用
const props = defineProps(/* ... */);

// 将 `props.foo` 转换为 ref,然后传入
// 一个组合式函数
useSomeFeature(toRef(props, "foo"));

相当于;

const fooRef = toRef(props, "foo"); // 你可以简单粗暴理解为 fooRef.value = props.foo
useSomeFeature(fooRef); // 操作 fooRef.value 时就会访问原 props.foo 所以关于禁止对 props 做出更改的限制依然有效

当  toRef  与组件 props 结合使用时,关于禁止对 props 做出更改的限制依然有效。

尝试将新的值传递给 ref 等效于尝试直接更改 props,这是不允许的。(参考 ref 的实现)

在这种场景下,你可能可以考虑使用带有  get  和  set  的  computed  替代。

详情请见在组件上使用  v-model  指南

如果要更改 fooRef.value,最好使用计算属性,不要直接更改 fooRef.value

即使源属性当前不存在,toRef()  也会返回一个可用的 ref

这让它在处理可选 props 的时候格外实用,相比之下  toRefs  就不会为可选 props 创建对应的 refs

即使源属性当前不存在,toRef()  也会返回一个可用的 ref,你可以传入一个默认值在触发 get 时返回,因为 fooRef.value 操作的是原响应式对象,相当于 fooRef.value = {},所以如果没有这个属性的话你取值就是 undefined,赋值就直接添加属性了,

return 数据时不用 toRef(),可以吗?
setup() {
    let person = reactive({
      name: '张珍',
      age: 18,
      job: {
        j1: {
          salary: 20
        }
      }
    })
    return {
       name: person.name,
       age: person.age,
       salary: person.job.j1.salary,
    }

name 是自己新定义的一个变量:let name = p.name, 这种方式就相当于直接写了 let name = '张珍'

p.name 就相当于一个字符串,然后给了 name 变量,是没有引用关系的。

就相当于: js let obj = { a: 100 } let x = obj.a x = 200 变量 x 确实会变成 200, 但是 obj 中的 a 是不变的

所以: name: person.nameperson.name 就是个字符串 相当于 name: '张珍'

retrun {
name: person.name
}

return 的内容就是返回了一个普通的对象!无法实现双向绑定

7.5.5:toRefs

toRefs 是一种用于破坏响应式对象,并将其所有属性转换为 ref 的实用方法

  • 将响应式对象(reactive 封装)转成普通对象
  • 对象的每个属性(Prop)都是对应的 ref
  • 两者保持引用关系
语法:

const 属性名= toRefs(对象,'属性名') toRefs(person)

详解:

toRefs 适合操作有很多数据的情况;如果很多数据用 toRef,则要一个个去操作。

toRefs() 返回值是个对象,由于不能在一个对象中直接再放一个对象,所以需要用解构的方法将 toRefs() 返回的对象,全部摊开变成 key-value

实现

将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref。 每个单独的 ref 都是使用 toRef() 创建的。

  • 实际上就是生成一个新对象,新对象的每个属性对应一个 Ref 对象,
  • 原响应式属性的操作被代理到了新对象的 Ref 上,
  • 可以解构响应式对象而不会失去响应式。
注意:

toRefstoRef 功能一致,但可以批量创建多个 ref 对象,

toRefs 在调用时只会为源对象上可以枚举的属性创建 ref。如果要为可能还不存在的属性创建 ref,请改用 toRef

reactive 封装的响应式对象,不要直接通过解构的方式 return,这是不具有响应式的。

可以通过 toRefs 处理,然后再解构返回,这样才具有响应式

演示:
const state = reactive({
  foo: 1,
  bar: 2,
});

const stateAsRefs = toRefs(state);
/*
stateAsRefs 的类型:{
  foo: Ref<number>,
  bar: Ref<number>
}
*/

// 这个 ref 和源属性已经“链接上了”
state.foo++;
console.log(stateAsRefs.foo.value); // 2

stateAsRefs.foo.value++;
console.log(state.foo); // 3

实例 1:

const state = reactive({
        age: 20,
        name: 'zhangsan'});
//return {...state}; // 错误的方式,会丢失响应式
//最佳方式
return ...toRefs(state)//将对象的各个属性的ref解构到对象根下面。
//第二种方法(HTML页面写法)
const stateAsRefs = Vue.toRefs(state);
      return {
        ...stateAsRefs,
      };

实例 1-1:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <div>姓名:{{name}}</div>
  <div>年龄:{{age}}</div>
</div>
<script>
  const App = Vue.createApp({
    setup() {
      const state = Vue.reactive({
        age: 20,
        name: "太凉",
      });

      const stateAsRefs = Vue.toRefs(state);
      return {
        ...stateAsRefs,
      };
    },
  });
  App.mount("#Application");
</script>

实战:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <div class="demo">
    <div>姓名:{{name}}</div>
    <div v-if="age>0">年龄:{{age}}</div>
    <div>
      爱好:
      <span v-for="items in hobby">{{items}},</span>
    </div>
    <div>
      地址:{{address.provoince}} - {{address.city}} - {{address.street}}
    </div>
  </div>

  <div class="demo">
    <div>学校:{{school||'自学成才'}}</div>
  </div>
</div>
<script>
  const App = Vue.createApp({
    setup() {
      // 响应式对象
      const state = Vue.reactive({
        name: "太凉",
        age: 18,
        hobby: ["游泳", "爬山"],
        school: "",
        address: {
          provoince: "北京",
          city: "北京直辖市",
          street: "东城区长安街",
        },
      });

      // 过3秒后改变
      setTimeout(() => {
        // update1:改变name属性
        state.name = "冰箱太凉";

        // update2:深度改变 address属性
        state.address.provoince = "山东省";
        state.address.city = "临沂市";

        // update3:新增 school属性
        state.school = "清华北大";

        // update4:删除 年龄属性
        delete state.age;

        // update5:数组 添加一项
        state.hobby.push("打豆豆");
      }, 3000);

      return {
        //注意这里不能通过 ...state 方式结构,这样会丢失响应式
        ...Vue.toRefs(state),
      };
    },
  });
  App.mount("#Application");
</script>

当从组合式函数中返回响应式对象时,toRefs 相当有用。

使用它,消费者组件可以解构/展开返回的对象而不会失去响应性

function useFeatureX() {
  const state = reactive({
    foo: 1,
    bar: 2,
  });

  // ...基于状态的操作逻辑

  // 在返回时都转为 ref
  return toRefs(state);
}

// 可以解构而不会失去响应性,因为解构后的 foo 是个 Ref 对象,如果直接解构state的话解构完是一个值 ‘1’
const { foo, bar } = useFeatureX();

toRefs 在调用时只会为源对象上可以枚举的属性创建 ref。 如果要为可能还不存在的属性创建 ref,请改用 toRef

使用 toRefs(state)方式返回

注意 reactive 封装的响应式对象,不要通过解构的方式 return,这是不具有响应式的。 可以通过 toRefs 处理,然后再解构返回,这样才具有响应式

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <div>姓名:{{name}}</div>
  <div>年龄:{{age}}</div>
</div>
<script>
  const App = Vue.createApp({
    setup() {
      const state = Vue.reactive({
        age: 20,
        name: "太凉",
      });

      const stateAsRefs = Vue.toRefs(state);
      return {
        ...stateAsRefs,
      };
    },
  });
  App.mount("#Application");
</script>

7.5.6:watch

当我们需要在数据变化时执行一些“副作用”:如更改 DOM、执行异步操作,我们可以使用 watch 函数。

watch 需要侦听特定的数据源,并在单独的回调函数中执行副作用。默认情况下,它也是惰性的——即回调仅在侦听源发生变化时被调用。

基础用法:
    <script src="https://unpkg.com/vue@next"></script>
    <div id="Application">
    </div>
    <script>
        const App = Vue.createApp({
            setup() {
                const count = Vue.ref(0);
                Vue.watch(count, (val, oldVal) => {
                    console.log(val, oldVal);
                });

                setTimeout(() => {
                    count.value += 1; // 当值改变时执行
                }, 1000);
                return {

                }
            }
        })
        App.mount("#Application")
    </script>

watch() 一共可以接受三个参数,侦听数据源、回调函数和配置选项。

侦听数据源(1)

watch 的第一个参数可以是不同形式的“数据源”,它可以是:

  • 一个 ref
  • 一个计算属性
  • 一个 getter 函数(有返回值的函数)
  • 一个响应式对象
  • 以上类型的值组成的数组
<script src="https://unpkg.com/vue@next"></script>

<div id="Application"></div>
<script>
  const App = Vue.createApp({
    setup() {
      const x = Vue.ref(1);
      const y = Vue.ref(1);
      const doubleX = Vue.computed(() => x.value * 2);
      const obj = Vue.reactive({ count: 0 });

      // 单个 ref
      Vue.watch(x, (newValue) => {
        console.log(`x is ${newValue}`);
      });

      // 计算属性
      Vue.watch(doubleX, (newValue) => {
        console.log(`doubleX is ${newValue}`);
      });

      // getter 函数
      Vue.watch(
        () => x.value + y.value,
        (sum) => {
          console.log(`sum of x + y is: ${sum}`);
        }
      );

      // 响应式对象
      Vue.watch(obj, (newValue, oldValue) => {
        // 在嵌套的属性变更时触发
        // 注意:`newValue` 此处和 `oldValue` 是相等的
        // 因为它们是同一个对象!
      });

      // 以上类型的值组成的数组
      Vue.watch([x, () => y.value], ([newX, newY]) => {
        console.log(`x is ${newX} and y is ${newY}`);
      });

      Vue.watch(
        () => obj.count,
        (count) => {
          console.log(`count is: ${count}`);
        }
      );

      const vt = () => {
        x.value = 2;
        y.value = 2;
        doubleX.value = Vue.computed(() => x.value * 3);
        obj.count = 1;
      };

      setTimeout(() => {
        vt();
      }, 2000);
    },
  });
  App.mount("#Application");
</script>

注意,你不能直接侦听响应式对象的属性值,例如:

const obj = reactive({ count: 0 });

// 错误,因为 watch() 得到的参数是一个 number
watch(obj.count, (count) => {
  console.log(`count is: ${count}`);
});

这里需要用一个返回该属性的 getter 函数:

// 提供一个 getter 函数
watch(
  () => obj.count,
  (count) => {
    console.log(`count is: ${count}`);
  }
);
回调函数(2)

watch 的第二个参数是数据发生变化时执行的回调函数。

这个回调函数接受三个参数:新值、旧值,以及一个用于清理副作用的回调函数。

该回调函数会在副作用下一次执行前调用,可以用来清除无效的副作用,如等待中的异步请求:

const id = ref(1);
const data = ref(null);

watch(id, async (newValue, oldValue, onCleanup) => {
  const { response, cancel } = doAsyncWork(id.value);
  // `cancel` 会在 `id` 更改时调用
  // 以便取消之前未完成的请求
  onCleanup(cancel);
  data.value = await response.json();
});

watch 的返回值是一个用来停止该副作用的函数:

const unwatch = watch(() => {});

// ...当该侦听器不再需要时
unwatch();

注意:使用同步语句创建的侦听器,会自动绑定到宿主组件实例上,并且会在宿主组件卸载时自动停止。使

用异步回调创建一个侦听器,则不会绑定到当前组件上,你必须手动停止它,以防内存泄漏。如下面这个例子:

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

// 组件卸载会自动停止
watchEffect(() => {})

// 组件卸载不会停止!
setTimeout(() => {
  watchEffect(() => {})
}, 100)
</script>
配置选项(3)

watch 的第三个参数是一个可选的对象,支持以下选项:

  • immediate:在侦听器创建时立即触发回调(默认情况下 watch 是惰性的)。
  • deep:深度遍历,以便在深层级变更时触发回调。
  • flush:回调函数的触发时机。pre:默认,dom 更新前调用,post: dom 更新后调用,sync 同步调用。
  • onTrack / onTrigger:用于调试的钩子。在依赖收集和回调函数触发时被调用。

flush 选项可以更好地控制回调的时间。

  • 默认值是 pre,指定的回调应该在渲染前被调用。

  • post 值是可以用来将回调推迟到渲染之后的。如果回调需要通过 $refs 访问更新的 DOM 或子组件,那么则使用该值。

  • 如果 flush 被设置为 sync,一旦值发生了变化,回调将被同步调用(少用,影响性能)。

深层侦听器

直接给 watch() 传入一个响应式对象,会默认创建一个深层侦听器 —— 所有嵌套的属性变更时都会被触发:

const obj = reactive({ count: 0 })

watch(obj, (newValue, oldValue) => {
  // 在嵌套的属性变更时触发
  // 注意:`newValue` 此处和 `oldValue` 是相等的
  // 因为它们是同一个对象!
})

obj.count++

实例:

<script src="https://unpkg.com/vue@next"></script>

<div id="Application"></div>
<script>
  const App = Vue.createApp({
    setup() {
      const obj = Vue.reactive({ count: 0 });

      Vue.watch(obj, (newValue, oldValue) => {
        // 在嵌套的属性变更时触发
        // 注意:`newValue` 此处和 `oldValue` 是相等的
        // 因为它们是同一个对象!
        console.log("变化了哦");
      });

      const vt = () => {
        obj.count++;
      };

      setTimeout(() => {
        vt();
      }, 2000);
    },
  });
  App.mount("#Application");
</script>

相比之下,一个返回响应式对象的 getter 函数,只有在对象被替换时才会触发:

const obj = reactive({
  someString: 'hello',
  someObject: { count: 0 }
})

watch(
  () => obj.someObject,
  () => {
    // 仅当 obj.someObject 被替换时触发
  }
)

实例

<script src="https://unpkg.com/vue@next"></script>

<div id="Application"></div>
<script>
  const App = Vue.createApp({
    setup() {
      const obj = Vue.reactive({
        someString: "hello",
        someObject: { count: 0 },
      });

      Vue.watch(
        () => obj.someObject.count,
        () => {
          // 仅当 obj.someObject 的count值被修改啦
          console.log("obj.someObject 的值被修改啦");
        }
      );

      const vt = () => {
        obj.someObject.count++;
      };

      setTimeout(() => {
        vt();
      }, 1000);
    },
  });
  App.mount("#Application");
</script>

当然,你也可以显式地加上 deep 选项,强制转成深层侦听器:

watch(
  () => obj.someObject,
  (newValue, oldValue) => {
    // `newValue` 此处和 `oldValue` 是相等的
    // 除非 obj.someObject 被整个替换了
    console.log("deep", newValue.count, oldValue.count);
  },
  { deep: true }
);

obj.someObject.count++; // deep 1 1
复制代码;

深层侦听一个响应式对象或数组,新值和旧值是相等的。为了解决这个问题,我们可以对值进行深拷贝。

watch(
  () => _.cloneDeep(obj.someObject),
  (newValue, oldValue) => {
    // 此时 `newValue` 此处和 `oldValue` 是不相等的
    console.log("deep", newValue.count, oldValue.count);
  },
  { deep: true }
);

obj.someObject.count++; // deep 1 0
复制代码;

注意:深层侦听需要遍历所有嵌套的属性,当数据结构庞大时,开销很大。所以我们要谨慎使用,并且留意性能。

immediate 选项

watch() 是懒执行的:当数据源发生变化时,才会执行回调。

但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。当然使用 immediate 选项也能实现:

const url = ref('https://...')
const data = ref(null)

async function fetchData() {
  const response = await fetch(url.value)
  data.value = await response.json()
}

// 立即执行一次,再侦听 url 变化
watch(url, fetchData, { immediate: true })
复制代码

可以看到 watch 用到了三个参数,我们可以用 watchEffect 来简化上面的代码。

watchEffect 会立即执行一遍回调函数,如果这时函数产生了副作用,Vue 会自动追踪副作用的依赖关系,自动分析出侦听数据源。

上面的例子可以重写为:

const url = ref("https://...");
const data = ref(null);

// 一个参数就可以搞定
watchEffect(async () => {
  const response = await fetch(url.value);
  data.value = await response.json();
});
1.监听 ref 定义的一个响应式数据
 import { watch, ref } from 'vue'
 setup() {
      let mes = ref('会好的')
      //第一种情况 监听ref定义的一个响应式数据
      watch(mes, (qian, hou) => {
        console.log('变化----', qian, hou)
      }, {immediate: true})
 }
2.监听 ref 定义的多个响应式数据
 setup() {
      let mes = ref('会好的')
      let mess = ref('我是谁')
      //第二种情况 监听ref定义的多个响应式数据
      watch([mes,mess], (qian, hou) => {
        console.log('变化----', qian, hou)
      },{immediate:true})
 }
3.监听 reactive 定义的属性,会将 deep:true 强制开启
setup() {
       let rea=reactive({
        name:'我是谁',
        obj:{
          salary:20
        }
      })
      //第三种情况 监听reactive定义的属性
       watch(rea,(qian,hou)=>{
        console.log('rea变化了',qian,hou)
      })
 }

4.监听 reactive 定义的属性(基本数据)
setup() {
       let rea=reactive({
        name:'我是谁',
        obj:{
          salary:20
        }
      })
      //第四种情况 监听reactive定义的属性(基本数据)
      watch(()=>rea.name,(qian,hou)=>{
        console.log('名称变化===',qian,hou)
      })
 }
5.监听 reactive 定义的 引用数据 (需要自己开启 deep:true 深度监听)
setup() {
       let rea=reactive({
        name:'我是谁',
        obj:{
          salary:20
        }
      })
      //第五种情况 监听reactive定义的 (引用数据)
       watch([()=>rea.name,()=>rea.obj],(qian,hou)=>{
        console.log('监听reactive定义的某一些属性---',qian,hou)
      },{deep:true})
 }
实例一:侦听单个数据源及停止侦听
<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  姓名:{{person.name}}
  <div>年龄:{{ageRef}}</div>
  <button @click="changeAge()">修改年龄</button>
</div>
<script>
  const App = Vue.createApp({
    setup() {
      // 侦听一个 getter
      const person = Vue.reactive({ name: "小松菜奈" });
      Vue.watch(
        () => person.name,
        (value, oldValue) => {
          console.log(value, oldValue);
        },
        { immediate: true } //watch会在初始化时立即执行回调函数。
      );
      person.name = "有村架纯";

      // 直接侦听ref
      const ageRef = Vue.ref(16);
      const stopAgeWatcher = Vue.watch(ageRef, (value, oldValue) => {
        console.log(value, oldValue);
        if (value > 18) {
          stopAgeWatcher(); // 当ageRef大于18,停止侦听
        }
      });

      //年龄加一方法
      const changeAge = () => {
        ageRef.value += 1;
      };

      return { person, ageRef, changeAge };
    },
  });
  App.mount("#Application");
</script>

//小松菜奈 undefined
//有村架纯 小松菜奈
//17 16
//18 17

**现象:**配置了immediate:truewatch,在初始化时触发了一次watch的回调。我们连续点击增加年龄,当年龄的当前值大于 18 时,watch 停止了侦听。

结论:侦听器数据源可以是返回值的 getter 函数,也可以直接是 ref我们可以利用给watch函数取名字,然后通过执行名字()函数来停止侦听

实例二:监听多个数据源
<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <div>姓名:{{name}}</div>
  <div>年龄:{{age}}</div>
  <button @click="change1()">同一函数里改变</button>
  <button @click="change2()">nextTick等待</button>
</div>
<script>
  const App = Vue.createApp({
    setup() {
      const name = Vue.ref("小松菜奈");
      const age = Vue.ref(25);

      Vue.watch([name, age], ([name, age], [prevName, prevAge]) => {
        console.log("newName", name, "oldName", prevName);
        console.log("newAge", age, "oldAge", prevAge);
      });

      // 如果你在同一个函数里同时改变这些被侦听的来源,侦听器只会执行一次
      const change1 = () => {
        name.value = "有村架纯";
        age.value += 2;
      };

      // 用 nextTick 等待侦听器在下一步改变之前运行,侦听器执行了两次
      const change2 = async () => {
        name.value = "新垣结衣";
        await nextTick();
        age.value += 2;
      };

      return { name, age, change1, change2 };
    },
  });
  App.mount("#Application");
</script>

现象:以上,当我们在同一个函数里同时改变nameage两个侦听源,watch的回调函数只触发了一次;当我们在nameage的改变之间增加了一个nextTickwatch回调函数触发了两次。

结论:我们可以通过 watch 侦听多个数据源的变化。如果在同一个函数里同时改变这些被侦听的来源,侦听器只会执行一次。若要使侦听器执行多次,我们可以利用 nextTick ,等待侦听器在下一步改变之前运行。

实例三:侦听引用对象(数组 Array 或对象 Object)
<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <div>
    <div>ref定义数组:{{arrayRef}}</div>
    <div>reactive定义数组:{{arrayReactive}}</div>
  </div>
  <div>
    <button @click="changeArrayRef">改变ref定义数组第一项</button>
    <button @click="changeArrayReactive">改变reactive定义数组第一项</button>
  </div>
</div>
<script>
  const App = Vue.createApp({
    setup() {
      const arrayRef = Vue.ref([1, 2, 3, 4]);
      const arrayReactive = Vue.reactive([1, 2, 3, 4]);

      // ref not deep, 不能深度侦听
      const arrayRefWatch = Vue.watch(arrayRef, (newValue, oldValue) => {
        console.log(
          "壹\n",
          "newArrayRefWatch",
          newValue,
          "oldArrayRefWatch",
          oldValue
        );
      });

      // ref deep, 深度侦听,新旧值一样
      const arrayRefDeepWatch = Vue.watch(
        arrayRef,
        (newValue, oldValue) => {
          console.log(
            "贰\n",
            "newArrayRefDeepWatch",
            newValue,
            "oldArrayRefDeepWatch",
            oldValue
          );
        },
        { deep: true }
      );

      // ref deep, getter形式 , 新旧值不一样
      const arrayRefDeepGetterWatch = Vue.watch(
        () => [...arrayRef.value],
        (newValue, oldValue) => {
          console.log(
            "叁\n",
            "newArrayRefDeepGetterWatch",
            newValue,
            "oldArrayRefDeepGetterWatch",
            oldValue
          );
        }
      );

      // reactive,默认深度监听,可以不设置deep:true, 新旧值一样
      const arrayReactiveWatch = Vue.watch(
        arrayReactive,
        (newValue, oldValue) => {
          console.log(
            "肆\n",
            "newArrayReactiveWatch",
            newValue,
            "oldArrayReactiveWatch",
            oldValue
          );
        }
      );

      // reactive,getter形式 , 新旧值不一样
      const arrayReactiveGetterWatch = Vue.watch(
        () => [...arrayReactive],
        (newValue, oldValue) => {
          console.log(
            "伍\n",
            "newArrayReactiveFuncWatch",
            newValue,
            "oldArrayReactiveFuncWatch",
            oldValue
          );
        }
      );

      const changeArrayRef = () => {
        arrayRef.value[0] = 3;
      };
      const changeArrayReactive = () => {
        arrayReactive[0] = 6;
      };

      return {
        arrayRef,
        arrayReactive,
        changeArrayRef,
        changeArrayReactive,
      };
    },
  });
  App.mount("#Application");
</script>
//点击按钮1
贰
 newArrayRefDeepWatch Proxy {0: 3, 1: 2, 2: 3, 3: 4} oldArrayRefDeepWatch Proxy {0: 3, 1: 2, 2: 3, 3: 4}
叁
 newArrayRefDeepGetterWatch (4) [3, 2, 3, 4] oldArrayRefDeepGetterWatch (4) [1, 2, 3, 4]

//点击按钮2
肆
 newArrayReactiveWatch Proxy {0: 6, 1: 2, 2: 3, 3: 4} oldArrayReactiveWatch Proxy {0: 6, 1: 2, 2: 3, 3: 4}
伍
 newArrayReactiveFuncWatch (4) [6, 2, 3, 4] oldArrayReactiveFuncWatch (4) [1, 2, 3, 4]

现象:

当将引用对象采用ref形式定义时,如果不加上deep:truewatch侦听不到值的变化的;

而加上deep:truewatch可以侦听到数据的变化,但是当前值和先前值一样,即不能获取先前值。

当将引用对象采用reactive形式定义时,不作任何处理,watch可以侦听到数据的变化,但是当前值和先前值一样。

两种定义下,把watch的数据源写成 getter 函数的形式并进行深拷贝返回,可以在watch回调中同时获得当前值和先前值。

总结

当我们使用watch侦听引用对象时

  • 若使用ref定义的引用对象:
    • 只要获取当前值,watch第一个参数直接写成数据源,另外需要加上deep:true选项
    • 若要获取当前值和先前值,需要把数据源写成getter函数的形式,并且需对数据源进行深拷贝
  • 若使用reactive定义的引用对象:
    • 只要获取当前值,watch第一个参数直接写成数据源,可以不加deep:true选项
    • 若要获取当前值和先前值,需要把数据源写成getter函数的形式,并且需对数据源进行深拷贝
watch 存在的一些问题

1.监听 reactive 定义的响应式数据,会强制开启深度监听(deep:true),无法获取正确的 oldvalue(变化前的值)。

2.监听 reactive 定义的响应式数据中的某个属性(对象形式)时,不会强制开启深度监听,需要自己手动设置(deep:true)才会有效果。

7.5.7:watchEffect

watchEffect,它立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。

换句话说:watchEffect 相当于将 watch  的依赖源和回调函数合并,当任何你有用到的响应式依赖更新时,该回调函数便会重新执行。

不同于  watchwatchEffect  的回调函数会被立即执行(即  { immediate: true }

如果watchEffect涉及到dom或者ref的操作,需要在onMounted生命周期里调用

基础用法:
    <script src="https://unpkg.com/vue@next"></script>


    <div id="Application">

    </div>
    <script>
        const App = Vue.createApp({
            setup() {
                const count = Vue.ref(0);
                Vue.watchEffect(() => {
                    console.log(count.value); // 初始化时会执行一次
                });

                setTimeout(() => {
                    count.value += 1; // 值变化时又执行了一次
                }, 1000);
                return {

                }
            }
        })
        App.mount("#Application")
    </script>

watchEffect 接受两个参数,第一个参数是数据发生变化时执行的回调函数,用法和 watch 一样。

第二个参数是一个可选的对象,支持 flushonTrack / onTrigger 选项,功能和 watch 相同。

注意:watchEffect 仅会在其同步执行期间,才追踪依赖。使用异步回调时,只有在第一个 await 之前访问到的依赖才会被追踪。

// 例子来源于(https://v3.vuejs.org/api/computed-watch-api.html#watcheffect)

import { watchEffect, ref } from 'vue'
setup () {
    const userID = ref(0)
    watchEffect(() => console.log(userID))
    setTimeout(() => {
      userID.value = 1
    }, 1000)

    /*
      * LOG
      * 0
      * 1
    */

    return {
      userID
    }
 }
1.停止监听

watchEffect 在组件的 setup() 函数或生命周期钩子被调用时,侦听器会被链接到该组件的生命周期,并在组件卸载时自动停止。

在一些情况下,也可以显式调用返回值以停止侦听:

const stop = watchEffect(() => {
  /* ... */
})

// later
stop()
2.清除副作用(onInvalidate)

有时副作用函数会执行一些异步的副作用,这些响应需要在其失效时清除 (即完成之前状态已改变了) 。

所以侦听副作用传入的函数可以接收一个 onInvalidate 函数作入参,用来注册清理失效时的回调。当以下情况发生时,这个失效回调会被触发:

  • 副作用即将重新执行时
  • 侦听器被停止 (如果在 setup() 或生命周期钩子函数中使用了 watchEffect,则在组件卸载时)
watchEffect(onInvalidate => {
  const token = performAsyncOperation(id.value)
  onInvalidate(() => {
    // id已更改或观察程序已停止。
    // 使先前挂起的异步操作无效
    token.cancel()
  })
})

我们之所以是通过传入一个函数去注册失效回调,而不是从回调返回它,是因为返回值对于异步错误处理很重要。

详解副作用

在执行数据请求时,副作用函数往往是一个异步函数

const data = ref(null)
watchEffect(async onInvalidate => {
  onInvalidate(() => {
    /* ... */
  }) // 我们在Promise解析之前注册清除函数
  data.value = await fetchData(props.id)
})

我们知道异步函数都会隐式地返回一个 Promise,但是清理函数必须要在 Promise 被 resolve 之前被注册。

另外,Vue 依赖这个返回的 Promise 来自动处理 Promise 链上的潜在错误。

watchEffect的回调函数就是一个副作用函数,因为我们使用watchEffect就是侦听到依赖的变化后执行某些操作。

当执行副作用函数时,它势必会对系统带来一些影响,如在副作用函数里执行了一个定时器setInterval,因此我们必须处理副作用。 Vue3watchEffect侦听副作用传入的函数可以接收一个 onInvalidate 函数作为入参,用来注册清理失效时的回调。当以下情况发生时,这个失效回调会被触发:

  • 副作用即将重新执行时(即依赖的值改变)
  • 侦听器被停止 (通过显示调用返回值停止侦听,或组件被卸载时隐式调用了停止侦听)
实例 1:
<script src="https://unpkg.com/vue@next"></script>
<div id="Application"></div>
<script>
  const App = Vue.createApp({
    setup() {
      const count = Vue.ref(0);
      Vue.watchEffect((onInvalidate) => {
        console.log(count.value);
        onInvalidate(() => {
          console.log("执行了onInvalidate");
        });
      });

      setTimeout(() => {
        count.value++;
      }, 1000);
    },
  });
  App.mount("#Application");
</script>

上述代码打印的顺序为: 0 -> 执行了onInvalidate,最后执行 -> 1

分析:初始化时先打印count的值0, 然后由于定时器把count的值更新为1, 此时副作用即将重新执行,因此onInvalidate的回调函数会被触发,打印执行了onInvalidate,然后执行了副作用函数,打印count的值1

实例 2
<script src="https://unpkg.com/vue@next"></script>
<div id="Application"></div>
<script>
  const App = Vue.createApp({
    setup() {
      const count = Vue.ref(0);
      const stop = Vue.watchEffect((onInvalidate) => {
        console.log(count.value);
        onInvalidate(() => {
          console.log("执行了onInvalidate");
        });
      });

      setTimeout(() => {
        stop();
      }, 1000);
    },
  });
  App.mount("#Application");
</script>

上述代码:当我们显示执行stop函数停止侦听,此时也会触发onInvalidate的回调函数。

同样,watchEffect所在的组件被卸载时会隐式调用stop函数停止侦听,故也能触发onInvalidate的回调函数。

副作用例子:

const count = ref(0);

const stop = watchEffect(onInvalidate => {
  const token = asyncOperate(count.value);
  onInvalidate(() => {  // 消除上次的副作用
    token.cancel();
  })
}, {
  flush: "sync",     // 副作用的调度机制 sync, pre, post
  onTrack: e => {    // 调试用: 追踪的信息
    console.log(e);
  },
  onTrigger: e => {  // 调试用:响应式信息
    console.log(e);
  },
})

// 如副作用中设计到DOM的操作和ref的获取,需要放到mounted周期中执行
onMounted(() => {
  watchEffect(() => { // .. 操作dom或ref等 })
})

watchEffect 的应用

利用watchEffect的非惰性执行,以及传入的onInvalidate 函数,我们可以做什么事情了?

定时器和事件监听

场景一:平时我们定义一个定时器,或者监听某个事件,我们需要在mounted生命周期钩子函数内定义或者注册,然后组件销毁之前在beforeUnmount钩子函数里清除定时器或取消监听。这样做我们的逻辑被分散在两个生命周期,不利于维护和阅读。

如果我利用watchEffect创造和销毁逻辑放在了一起,此时代码更加优雅易读~

// 定时器注册和销毁
watchEffect((onInvalidate) => {
  const timer = setInterval(() => {
    // ...
  }, 1000);
  onInvalidate(() => clearInterval(timer));
});

const handleClick = () => {
  // ...
};
// dom的监听和取消监听
onMounted(() => {
  watchEffect((onInvalidate) => {
    document
      .querySelector(".btn")
      .addEventListener("click", handleClick, false);
    onInvalidate(() =>
      document.querySelector(".btn").removeEventListener("click", handleClick)
    );
  });
});
防抖节流

场景二:利用 watchEffect 作一个防抖节流(如取消请求)

const id = ref(13);
watchEffect((onInvalidate) => {
  // 异步请求
  const token = performAsyncOperation(id.value);
  // 如果id频繁改变,会触发失效函数,取消之前的接口请求
  onInvalidate(() => {
    // id has changed or watcher is stopped.
    // invalidate previously pending async operation
    token.cancel();
  });
});

当然watchEffect还能做很多事情,比如打开一个修改的modal弹窗,如果检测到id变化,我们可以在onInvalidate函数内,重置初始参数...

监听 a 修改 b

当 a 变化时,修改 b 的值

<script src="https://unpkg.com/vue@next"></script>

<div id="Application">
  <button @click="click">单击改变a的值</button>
</div>
<script>
  const App = Vue.createApp({
    setup() {
      let a = Vue.ref(1);
      let b = Vue.ref(10);
      const click = () => {
        a.value += 1;
      };
      Vue.watchEffect(() => {
        //当a变化时,修改b的值
        a;
        b.value += 1;
        console.log(a.value, b.value);
      });

      return {
        a,
        click,
      };
    },
  });
  App.mount("#Application");
</script>
注意点

watchEffect 会在 Vue3 开发中大量使用,这里有几个注意点:

1.如果有多个负效应,不要粘合在一起,建议写多个 watchEffect

watchEffect(() => {
  setTimeout(() => console.log(a.val + 1), 1000);
  setTimeout(() => console.log(b.val + 1), 1000);
});

这两个 setTimeout 是两个不相关的效应,不需要同时监听 a 和 b,分开写:

watchEffect(() => {
  setTimeout(() => console.log(a.val + 1), 1000);
});

watchEffect(() => {
  setTimeout(() => console.log(b.val + 1), 1000);
});

2.watchEffect 也可以放在其他生命周期函数内

比如你的副作用函数在首次执行时就要调用 DOM,你可以把他放在 onMounted 钩子里:

onMounted(() => {
  watchEffect(() => {
    // 访问DOM或模板引用
  });
}

7.5.8:其他(对比和详解)

reactive 对比 ref

从定义数据角度对比:

  • ref 用来定义:任意数据类型
  • reactive 用来定义:对象(或数组)类型数据

如何选择 ref 和 reactive?

  • 基础类型值(StringNumberBooleanSymbol) 或单值对象(类似{ count: 1 }这样只有一个属性值的对象) 使用 ref
  • 引用类型值(ObjectArray)使用 reactive

从原理角度对比: 调用 ref 函数时会 new 一个类,这个类监听了 value 属性的 getset ,实现了在 get 中收集依赖,在 set 中触发依赖,

而如果需要对传入参数深层监听的话,就会调用我们上面提到的 reactive 方法

  • ref(0); // 通过监听对象(类)的 value 属性实现响应式
  • ref({a: 6}); // 调用 reactive 方法对对象进行深度监听

reactive 通过使用Proxy来实现响应式(数据劫持),并通过 Reflect 操作源对象内部的数据

有了 reactive 函数为啥还要 ref 函数呢?

当我们只想让某个变量实现响应式的时候,采用 reactive 就会比较麻烦,因此 vue3 提供了 ref 方法进行简单值的监听,

但并不是说 ref 只能传入简单值,他的底层是 reactive,所以 reactive 有的,它都有。

记住:

ref本质也是reactiveref(obj)等价于reactive({value: obj})

watch 对比 watchEffect

watchwatchEffect 的主要功能是相同的,都能响应式地执行回调函数。 它们的区别是追踪响应式依赖的方式不同:

watch 只追踪明确定义的数据源,不会追踪在回调中访问到的东西; 默认情况下,只有在数据源发生改变时才会触发回调;

watchEffect 会初始化执行一次,在副作用发生期间追踪依赖,自动分析出侦听数据源;

简单一句话,watch 功能更加强大,而 watchEffect 在某些场景下更加简洁。

watch 和 watchEffect 的区别
  1. watch 可以访问新值和旧值,watchEffect 不能访问。
  2. watchEffect 有副作用,DOM 挂载或者更新之前就会触发,需要我们自己去清除副作用。
  3. watch 是惰性执行,也就是只有监听的值发生变化的时候才会执行,但是 watchEffect 不同,每次代码加载 watchEffect 都会执行。
  4. watch 需要指明监听的对象,也需要指明监听的回调。watchEffect 不用指明监视哪一个属性,监视的回调函数中用到哪个属性,就监视哪个属性。
  • watchEffect会自动的收集依赖,而watch是明确的指定监听某个变量
  • watch可以获取到新值和旧值,watchEffect则只能取到最新的
  • watchEffect会在初始化的时候执行一次,类似computed
watchEffect与compute有什么区别?

实例:

    <script src="https://unpkg.com/vue@next"></script>
    <div id="Application">
        {{sayHello}}
    </div>
    <script>
        const App = Vue.createApp({
            setup() {
                const age = Vue.ref(0);
                Vue.watchEffect(() => {
                    //当变量a变化时,即执行当前函数
                    console.log("变量age变化了")
                    console.log(age.value)
                })
                setTimeout(() => { age.value = 20 }, 1000)

                const name = Vue.ref('小王');
                const sayHello = Vue.computed(() => {
                    console.log('年龄:', age.value);
                    return "我是" + name.value + "," + age.value + "岁"
                })
                return {
                    sayHello,
                    name,
                    age
                }
            }
        })
        App.mount("#Application")
    </script>

computed和watchEffect相同的地方是会自动收集依赖,在值更新时会触发回调,会初始化调用一次。 但是在触发初始化的时机是不一样的,如果computed的值没有被使用,是不会触发回调的,只有在该值被使用的时候才会触发回调,

但watchEffect是在setup的时候就会初始化。 根据他们的差异,我们可以在不同的业务场景下选择合适的处理方法。

为啥解构会失去响应式?

这个其实与 JS 基本类型和引用类型在内存中的存储方式有关:

基本数据类型是指存放在栈中的简单数据段,数据大小确定,内存空间大小可以分配, 它们是直接按值存放的,所以可以直接按值访问。 基本数据类型的值是没有办法添加属性和方法的****

引用类型是存放在堆内存中的对象,引用类型数据在栈内存中保存的实际上是对象在堆内存中的引用地址。通过这个引用地址可以快速查找到保存中堆内存中的对象,即按引用访问。

const state = reactive({
  foo: 1,
  bar: 2,
});

// state.foo 本来是一个响应式对象,被重新赋值后就不是响应式对象了,只是一个普通基本类型值

const { foo, bar } = state; // 此时 foo 就等于 1 了,1 是一个值,不是一个响应式对象
foo = 6; // 更改在视图中并不会生效,因为解构时 foo 被重新赋值了,即 const foo = state.foo; const foo = 1;

所以,解构基本类型会失去响应式,

那如果是引用类型呢?

// 对于子属性值是引用类型的,reactive()方法会递归添加响应式
const state = reactive({
    foo: [1, 2, 3],
    bar: {a: 1, b: 2},
})

const {foo, bar} = state;

相当于

const foo = state.foo; // state.foo指向 [1, 2, 3]的代理对象的地址,即 new Proxy([1,2,3], ...)
foo[1] = 6; // 此时更新是生效的,因为 foo 指向的还是响应式对象

// [1, 2, 3]的代理对象
     Proxy {0: 1, 1: 2, 2: 3}
     [[Handler]]: Object
     [[Target]]: Array(3)
     [[IsRevoked]]: false

所以,实际上对于引用类型的解构是不会失去响应式的,尤大可能为了不增加大家的学习负担,所以也没区分基本类型和引用类型。

将属性传给一个函数时失去响应式也是一样的道理,如果你传的是值就会失去响应式,如果传的是引用地址则不会失去响应式

setup 语法糖中 reactive + toRefs+解构

比如下面这样,我定义了一个 reactive() 声明的对象,想在模板上响应式的使用其值,如果不使用 setup 语法糖,就可以使用 toRefs 然后配合解构 return 出去。使用 setup 语法糖的话,就可以这样:

let starData = reactive({
  total: 0,
  stars: Array<Star>(),
})
const { total, stars } = toRefs(starData)

7.5.9:扩展

isRef()

检查某个值是否为 ref。

Ts

function isRef<T>(r: Ref<T> | unknown): r is Ref<T>;

请注意,返回值是一个类型判定 (type predicate),这意味着 isRef 可以被用作类型守卫:

ts

let foo: unknown;
if (isRef(foo)) {
  // foo 的类型被收窄为了 Ref<unknown>
  foo.value;
}

作用

判断某个值是否为 Ref 对象,如果是 Ref 对象的话,它就是响应式的,且需要通过 .value 取值或赋值。

另外可以做类型保护

上面的 is 是一个 ts 中的类型谓词,用来帮助 ts 编译器 收窄 变量的类型。例如:

function isString(test: any): test is string {
  return typeof test === "string";
}

isString 函数返回一个类型判定,意思是当 test 的类型是 string 时,那么该函数的返回值类型就是 string

你可以放心地把它当做 string 类型使用。

function example(foo: unknown) {
  if (isString(foo)) {
    //foo被收窄为string类型,string类型有slice方法,所以不会报错
    console.log(foo.slice(0, 2));
  }
  //编译会报错,对象的类型为“unknown”
  console.log(foo.slice(0, 2));
}

isRef(foo)用作类型保护时,它能确保你的 .value 操作不会出现任何问题。

你还可以使用 typeof、instanceof、in 操作符来进行类型收窄,但是都有其弊端,而 is 是更全面的方法,

unref()

如果参数是 ref,则返回内部值,否则返回参数本身。这是 val = isRef(val) ? val.value : val 计算的一个语法糖。

function useFoo(x: number | Ref<number>) {
  const unwrapped = unref(x);
  // unwrapped 现在保证为 number 类型
}

作用

让你更方便快捷地获取 Ref 对象的 value 值,不用自己重复写判断代码。因为 Ref 对象需要 .value 取值,所以才有了这个函数。

Ref 在模板中的自动解包源码实现就是用的这个方法。

7.6:详解setup

setup是一个组件选项,在组件被创建之前,props 被解析之后执行。它是组合式 API 的入口。

setup() 函数中返回的对象会暴露给模板和组件实例。其它的选项也可以通过组件实例来获取 setup() 暴露的属性

在模板中访问从 setup 返回的 ref 时,它会自动浅层解包,因此你无须再在模板中为它写 .value

7.6.1:setup

简单实例:
    <script src="https://unpkg.com/vue@next"></script>
    <div id="Application">
        <button @click="click">单击我+1</button>
        按钮展示内容{{ins}}
    </div>
    <script>

        const App = Vue.createApp({
            setup() {
                let ins = Vue.ref(0)
                function click() {
                    ins.value = ins.value + 1
                }
                return { ins, click }
            }
        })

        App.mount("#Application")
    </script>
setup 函数的注意点:

1、由于在执行 setup 函数的时候,还没有执行 Created 生命周期方法,所以在 setup 函数中,无法使用 data 和 methods 的变量和方法

2、由于我们不能在 setup 函数中使用 data 和 methods,所以 Vue 为了避免我们错误的使用,直接将 setup 函数中的 this 修改成了 undefined

3、setup 函数只能是同步的不能是异步的

4、setup 函数是 Composition API(组合 API)的入口

5、在 setup 函数中定义的变量和方法最后都是需要 return 出去的 不然无法再模板中使用

综合例子:
 <script src="https://unpkg.com/vue@next"></script>

    <div id="Application">
        <child otherTitle="别人的标题" :my-title="msg" @son-click="sonClick"></child>
    </div>
    <script>
        const App = Vue.createApp({
            setup() {
                const msg = Vue.ref({ title: '父组件给子组件的数据' })
                //打印子组件传的值
                const sonClick = (msss) => {
                    console.log("打印下拿到的值:", msss)
                }

                return {
                    msg,
                    sonClick
                }
            },
        });

        //子组件
        const childBlock = {
            props: {
                myTitle: {
                    type: Object,

                },

            },
            setup(props, { attrs, slots, emit }) {
                //输出{title:父组件传递的值}
                console.log('props==>', props.myTitle)
                // 输出别人的标题【使用context获取值,不需要使用props去接收】
                console.log('context==> ', attrs.othertitle);

                // 输出undefined,因为context不需要使用props去接受。
                console.log('contextmytitle==> ', attrs.mytitle);
                const sonHander = () => {
                    emit('sonClick', '子组件传递给父组件')
                }
                return {
                    sonHander,
                    attrs
                }

            },

            template: '<button type="button" @click="sonHander">点击我,我是子组件中的数据</button> <h2>使用了props声明接收==>{{ myTitle  }}</h2> <h2>使用参数attrs获取==>{{ attrs.othertitle  }}</h2> '

        }
        App.component("child", childBlock);

        //挂载
        App.mount("#Application");
    </script>

7.6.2:props

使用props将数据从父组件传递到子组件,

setup 函数的第一个参数是组件的 props。和标准的组件一致,一个 setup 函数的 props 是响应式的,并且会在传入新的 props 时同步更新。

请注意如果你解构了 props 对象,解构出的变量将会丢失响应性。因此我们推荐通过 props.xxx 的形式来使用其中的 props。

如果你确实需要解构 props 对象,或者需要将某个 prop 传到一个外部函数中并保持响应性,那么你可以使用 toRefs()toRef() 这两个工具函数。

基本使用实例:

    <script src="https://unpkg.com/vue@next"></script>
    <div id="Application">
        <child number="100"></child>
    </div>
    <script>
        const App = Vue.createApp({
            setup() {
                return {
                }
            },
        });

        //子组件
        const childBlock = {
            props: {
                number: {
                    type: Number
                }
            },
            setup(props) {
                console.log(props.value);//100
                const msg = Vue.ref("我是子组件")
                return {
                    msg
                }

            },

            template: '{{number}}{{msg}}'
        }
        App.component("child", childBlock);

        //挂载
        App.mount("#Application");
    </script>

7.6.3:上下文(context)

传入 setup 函数的第二个参数是一个 Setup 上下文对象。上下文对象暴露了其他一些在 setup 中可能会用到的值:

setup(props, context) {
    // 透传 Attributes(非响应式的对象,等价于 $attrs)
    console.log(context.attrs)

    // 插槽(非响应式的对象,等价于 $slots)
    console.log(context.slots)

    // 触发事件(函数,等价于 $emit)
    console.log(context.emit)

    // 暴露公共属性(函数)
    console.log(context.expose)
  }

该上下文对象是非响应式的,可以安全地解构:

setup(props, { attrs, slots, emit, expose }) {
    ...
  }

attrsslots 都是有状态的对象,它们总是会随着组件自身的更新而更新。这意味着你应当避免解构它们,并始终通过 attrs.xslots.x 的形式使用其中的属性。此外还需注意,和 props 不同,attrsslots 的属性都不是响应式的。如果你想要基于 attrsslots 的改变来执行副作用,那么你应该在 onBeforeUpdate 生命周期钩子中编写相关逻辑。

一:attrs
二:slots
三:emit

需要一个子组件将数据传给它的父组件

使用 emit,我们可以触发事件并将数据传递到组件的层次结构中。

使用情况

这对下面几种情况很有用,如:

  • 从 input 中发出数据
  • 从 modal 本身内部关闭 modal
  • 父组件响应子组件

Vue Emit是如何工作的?

当我们 emit 一个事件时,我们用一个或多个参数调用一个方法:

  • eventName: string - 事件的名称
  • values: any - 通过事件传递的参数

正确的事件命令

在 vue3 中,与组件和 prop 一样,事件名提供了自动的大小写转换。如果在子组件中触发一个以 camelCase (驼峰式命名) 命名的事件,你将可以在父组件中添加一个 kebab-case (短横线分隔命名) 的监听器。

总结

  1. emits是记录了当前组件的事件列表
  2. 类型: Array | Object
  3. 若为Object增加emit事件的校验(返回值应是boolean),
  4. 可以解决自定义事件名与原生事件相同导致事件多次执行问题(上述)

emits无论是数组或者对象用法最终都会将事件给传递出去,数组或对象的使用只是为了记录实例中的emit事件,或者是验证事件中的参数

基础例子:
    <script src="https://unpkg.com/vue@next"></script>
    <div id="Application">
        <child v-model:number="n"></child>
    </div>
    <script>
        const App = Vue.createApp({
            setup() {
                const n = Vue.ref(99)
                return {
                    n
                }
            },
        });

        //子组件
        const childBlock = {
            props: {
                number: {
                    type: Number
                }
            },
            setup(props, { emit }) {
                //模拟请求接口
                setTimeout(() => {
                    emit('update:number', props.number + 100)
                }, 1000)
                const msg = Vue.ref("我是子组件")
                return {
                    msg
                }

            },

            template: '{{number}}{{msg}}'
        }
        App.component("child", childBlock);

        //挂载
        App.mount("#Application");
    </script>
实例一:随机数
 <script src="https://unpkg.com/vue@next"></script>
    <div id="Application">
        <child @add="add"></child>
        <p> Count:{{count}}</p>
    </div>
    <script>
        const App = Vue.createApp({
            setup() {
                const count = Vue.ref(0)
                const add = (i) => {
                    count.value += i
                    //接收传来的值
                }


                return {
                    count,
                    add
                }
            },
        });

        //子组件
        const childBlock = {
            setup(props, ctx) {
                const clickBtn = () => {
                    ctx.emit("add", Math.random())
                    //自定义触发事件,传出值
                }
                return {
                    clickBtn
                }

            },

            template: ' <button type="button" @click="clickBtn">生成一个随机数</button>'
        }
        App.component("child", childBlock);

        //挂载
        App.mount("#Application");
    </script>

每次我们点击按钮,子组件都会 emit 一个 add 事件,并带有一个0到1之间的随机值。 然后,父组件捕捉到这个事件,并将这个值添加到计数中。

可以传递任意多的参数,监听器也会收到所有的参数:

  • 子组件 - $emit('add', Math.random(), 44, 50)
  • 父组件:@add="(i, j, k) => count += i + j + k"

现在,我们知道如何在我们的模板中 emit 内联事件,但在更复杂的例子中,如果我们从SFC的script 中 emit 一个事件会更好。特别是当我们想在 emit 事件之前执行一些逻辑时,这很有用。

实例二:传递值

在setup()使用emit的功能,需要借助setup(props, context)中的context;

在这里我们需要使用context.emit;如下的🌰

 <script src="https://unpkg.com/vue@next"></script>
    <div id="Application">
        <child @on-change="emitFn"></child>
    </div>
    <script>
        const App = Vue.createApp({
            setup() {
                const emitFn = v => {
                    console.log(v)
                }

                return {
                    emitFn
                }
            },
        });

        //子组件
        const childBlock = {
            setup(props, ctx) {
                const clickBtn = () => {
                    ctx.emit("on-change", "我在子组件里,需要传递出去")
                    //自定义触发事件
                }
                return {
                    clickBtn
                }

            },

            template: ' <button type="button" @click="clickBtn">hi~</button>'
        }
        App.component("child", childBlock);

        //挂载
        App.mount("#Application");
    </script>

如果触发的事件名与原生事件名同名(如 click)会出现问题: 自定义事件与原生事件都会触发

点击子组件按钮,触发事件

实例三:小写转大写
<script src="https://unpkg.com/vue@next"></script>
    <div id="Application">
        <child @custom-change="add"></child>
        <p> 您输入的字符大写:{{count}}</p>

    </div>
    <script>
        const App = Vue.createApp({
            setup() {
                const count = Vue.ref("")
                const add = (i) => {
                    count.value = i
                    //接收传来的值
                }
                return {
                    count,
                    add
                }
            },
        });

        //子组件
        const childBlock = {
            setup(props, { emit }) {
                const handleChange = (event) => {
                    emit("customChange", event.target.value.toUpperCase())
                    //自定义触发事件,传出值
                }
                return {
                    handleChange
                }

            },

            template: ' <input type="text" placeholder="请输入字符" @input="handleChange" />'
        }
        App.component("child", childBlock);

        //挂载
        App.mount("#Application");
    </script>
在setup语法糖中使用

defineEmits:

  • 指定组件要 emit 事件
  • 为每个事件添加验证信息
  • 可以访问与context.emit相同的值

在最简单的情况下,defineEmits是一个字符串数组,每个字符串是一个事件的名称。

四:expose

7.6.4:生命周期

setup()函数是在 created 生命周期函数被完全初始化之前执行的函数。所以不能使用 this

通过 ref()和 reactive( )函数的使用,可以完全替换掉以前的 data{}语法形式。 使用组合式 API 没有 beforeCreate 和 created 这两个生命周期

setup 返回的是一个对象,这个对象的属性会与组件中 data 函数返回的对象进行合并,返回的方法和 methods 合并,合并之后直接可以在模板中使用,如果有重名的情况,会使用 setup 返回的属性和方法,

methods 和 data 能够拿到 setup 中的方法应该进行了合并,反之 setup 不能拿到它们的属性和方法,因为这个时候 this = undefined

效果
beforeCreate->setup()
created->setup()
beforeMount->onBeforeMount组件挂载前
mounted->onMounted组件挂载后
beforeUpdate->onBeforeUpdate数据更新前
updated->onUpdated数据更新后
beforeDestroy->onBeforeUnmount元素销毁前
destroyed->onUnmounted元素销毁后
----
errorCaptured->onErrorCaptured
renderTracked->onRenderTracked
renderTriggered->onRenderTriggered
activated->onActivated
deactivated->onDeactivated

在 Composition API 的生命周期钩子中,beforeCreate 和 created 被 setup()方法代替。这意味着你应该将对应的代码写在 setup 方法中。

import { ref } from "vue";

export default {
  setup() {
    const val = ref("hello");
    console.log("Value of val is: " + val.value);
    return {
      val,
    };
  },
};

疑难解答

关于 setup 中没有 this 的问题

vue 官方文档是这么解释的: 在 setup() 内部,this 不会是该活跃实例的引用,因为 setup() 是在解析其它组件选项之前被调用的,所以 setup() 内部的 this 的行为与其它选项中的 this 完全不同。

这在和其它选项式 API 一起使用 setup() 时可能会导致混淆。 这意味着,除了 props 之外,你将无法访问组件中声明的任何属性 —— 本地状态,计算属性/方法。

但是从源码实现你会发现其实组件实例创建在前,函数之所以访问不到 this,是因为它在执行 setup 函数的时候,就没有把组件实例 instance 传给 setup。也没有把 this 指向实例 instance。

setup 的执行时机

因此执行顺序其实是:组件实例创建在 setup 函数执行之前,但是 setup 执行的时候,组件还没有 mounted,而晚于 beforeCreate 钩子,早于 create 钩子。

组合式 API(Composition API) 与选项式 API(Options API)怎么选?

Options API 约定:

  • 我们需要在 props 里面设置接收参数
  • 我们需要在 data 里面设置变量
  • 我们需要在 computed 里面设置计算属性
  • 我们需要在 watch 里面设置监听属性
  • 我们需要在 methods 里面设置事件方法

你会发现 Options APi 都约定了我们该在哪个位置做什么事,这反倒在一定程度上也强制我们进行了代码分割。

现在用 Composition API,不再这么约定了,于是乎,代码组织非常灵活,我们的控制代码写在 setup 里面即可。

如何区分场景使用 Options API or Composition API 主要看业务逻辑的复杂程序, 例如一些简单的 toast/button 等基础组件,用 options API 形式会更加清晰和简洁。 而相对复杂的业务逻辑,可以用 Composition API,可以把单独一块逻辑抽离到一个模块,通过 hook 函数的方式去解决。

优异点

我们没有了 this 上下文,没有了 Options API 的强制代码分离。Composition API 给了我们更加广阔的天地,那么我们更加需要慎重自约起来。

对于复杂的逻辑代码,我们要更加重视起 Composition API 的初心,不要吝啬使用 Composition API 来分离代码,用来切割成各种模块导出。 就算 setup 内容代码量越来越大,但是始终围绕着大而不乱,代码结构清晰的路子前进。

两者只能选一个?

Vue3 并没有废弃 Options API,甚至还会全力支持兼容 Vue2 语法的工作

Composition API(响应式编程) 出现的背景主要是为了解决逻辑抽象和和复用的问题,但不意味着它成为了 Vue3 的标准

两者混用会对性能产生影响吗?

不会, 不应该被 option api 限制思维,而更多关注逻辑内聚问题。

7.7:详解生命周期

Vue 生命周期是指 vue 实例对象从创建之初到销毁的过程。

vue 所有功能的实现都是围绕其生命周期进行的,在生命周期的不同阶段调用对应的钩子函数可以实现组件数据管理和 DOM 渲染两大重要功能。

每个主要的(main)Vue 生命周期事件会对应 2 个钩子函数,分别在事件开始前和事件结束后调用。 在 Vue app 中你可以应用 4 个主要事件(8 个主要钩子(main hooks)函数)

生命周期的使用

假设我们想访问 mounted()和 updated()生命周期钩子,可以这么写:

// 选项 API
export default {
  mounted() {
    console.log("mounted!");
  },
  updated() {
    console.log("updated!");
  },
};

在组合式 API 中,我们需要将生命周期钩子导入到项目中,才能使用,这有助于保持项目的轻量性。

// 组合 API
import { onMounted } from "vue";

export default {
  setup() {
    onMounted(() => {
      console.log("mounted in the composition api!");
    });
  },
};

在HTML文件中,我们可以这样写


    <script src="https://unpkg.com/vue@next"></script>


    <div id="Application">

    </div>
    <script>
        const App = Vue.createApp({
            setup() {

                Vue.onMounted(() => {
                    //xxx
                })
                return {
                }
            }
        })
        App.mount("#Application")
    </script>

四事件

  • 初始化阶段:为 Vue 实例初始化一些事件、属性和响应式数据等
  • 模板编译阶段:把我们写的 <template></template> 模板编译成渲染函数 render
  • 挂载阶段:把模板渲染到真实的 DOM 节点上,以及数据变更时执行更新操作
  • 销毁阶段:把组件实例从父组件中删除,并取消依赖监听和事件监听
名称单词解释
创建Creation在组件创建时执行
挂载MountingDOM 被挂载时执行
更新Updates当响应数据被修改时执行
销毁Destruction在元素被销毁之前立即运行

生命周期图

八阶段

vue 生命周期可以分为八个阶段,分别是:

  1. beforeCreate(创建前)、
  2. created(创建后)、
  3. beforeMount(载入前)、
  4. mounted(载入后)、
  5. beforeUpdate(更新前)、
  6. updated(更新后)、
  7. beforeDestroy(销毁前)、
  8. destroyed(销毁后)

生命周期的每个阶段适合做什么

下面我们来讲讲,在不同的阶段我们可以做些什么:

created: 在 Vue 实例创建完毕状态,我们可以去访问 data、computed、watch、methods 上的方法和数据,但现在还没有将虚拟 Dom 挂载到真实 Dom 上,所以我们在此时访问不到我们的 Dom 元素(el 属性,ref 属性此时都为空)。

我们在此时可以进行一些简单的 Ajax,并可以对页面进行初始化之类的操作

beforeMount: 它是在挂载之前被调用的,会在此时去找到虚拟 Dom,并将其编译成 Render

mounted: 虚拟 Dom 已经被挂载到真实 Dom 上,此时我们可以获取 Dom 节点,$ref 在此时也是可以访问的。

我们在此时可以去获取节点信息,做 Ajax 请求,对节点做一些操作

beforeupdate: 响应式数据更新的时候会被调用,beforeupdate 的阶段虚拟 Dom 还没更新,所以在此时依旧可以访问现有的 Dom。

我们可以在此时访问现有的 Dom,手动移除一些添加的监听事件

updated: 此时补丁已经打完了,Dom 已经更新完毕,可以执行一些依赖新 Dom 的操作。

但还是不建议在此时进行数据操作,避免进入死循环(这个坑我曾经踩过)

beforeDestroy: 在 Vue 实例销毁之前被调用,在此时我们的实例还未被销毁。

在此时可以做一些操作,比如销毁定时器,解绑全局事件,销毁插件对象等

下面分别看看 vue 生命周期的这八个阶段:


详解钩子

Creation 钩子——VueJS 生命周期的起始

Creation 钩子是你启动项目时触发的第一个事件。

创建前(beforeCreate)

对应的钩子函数为 beforeCreate。

介绍

此阶段为实例初始化之后,此时的数据观察和事件机制都未形成,不能获得 DOM 节点。

因为 created 钩子是用来初始化响应数据和事件的,所以你不能在 beforeCreate 钩子里访问任何组件的响应式数据和事件。

用途

beforeCreate 对那些不需要分配数据(data)的逻辑和 API 调用来说十分有用。 如果我们此时对数据(data)赋值,那么这些值将在状态初始化后丢失。

实例
export default {
  data() {
    return {
      val: "hello",
    };
  },
  beforeCreate() {
    console.log("Value of val is: " + this.val); //
  },
};

因为此时数据还未初始化,所以 val 的值是 undefined,并且你也不能在此函数中调用组件中的其他方法。

2、创建后(created)

对应的钩子函数为 created。

介绍

在这个阶段 vue 实例已经创建,仍然不能获取 DOM 元素。 此时,我们已经可以访问组件的数据和事件

用途

当需要处理响应式数据读/写(reading/writing)时 created 方法非常有用。举个例子,如果你需要完成一个 API 调用并存储它的值,那么你应该将它写在这里。

这将比在 mounted 中处理来的更好,因为它在 Vue 的初始化进程中更早触发,并且你也能读写所有的数据。

实例
export default {
  data() {
    return {
      val: "hello",
    };
  },
  created() {
    console.log("Value of val is: " + this.val);
  },
};

因为此时已经初始化,所以上例将输出 Value of val is: hello

Mounting 钩子——访问 DOM

mounting 钩子处理组件的挂载和渲染。这是我们在项目和应用程序中最常用的一组钩子。

载入前(beforeMount)

对应的钩子函数是 beforemount,

介绍

在这一阶段,我们虽然依然得不到具体的 DOM 元素,但 vue 挂载的根节点已经创建,下面 vue 对 DOM 的操作将围绕这个根元素继续进行;

在组件 DOM 实际渲染和挂载前触发。在此阶段,根元素(root element)还未存在。

使用

beforeMount 这个阶段是过渡性的,一般一个项目只能用到一两次。

实例

可以通过this.$el访问

export default {
  beforeMount() {
    console.log(this.$el);
  },
};
载入后(mounted)

对应的钩子函数是 mounted。

介绍

mounted 是平时我们使用最多的函数了,一般我们的异步请求都写在这里。 在这个阶段,数据和 DOM 都已被渲染出来。

使用

在组件第一次渲染时调用。此时,组件已经可以访问 DOM。

同样,在 Options API 中,我们使用 this.$el 访问我们的 DOM

实例
Update 钩子——VueJS 生命周期中的响应

无论何时响应数据被修改,updated 生命周期事件都将触发,并且触发渲染更新。

更新前(beforeUpdate)

对应的钩子函数是 beforeUpdate。

介绍

在这一阶段,vue 遵循数据驱动 DOM 的原则; beforeUpdate 函数在数据更新后虽然没立即更新数据,但是 DOM 中的数据会改变,这是 Vue 双向数据绑定的作用。

在数据修改且组件重渲染之前执行。这是任何更改还未发生前,手动修改 DOM 的好地方。

使用

beforeUpdate 对追踪组件的编辑次数时非常有用,甚至可以通过追踪对应的操作来创建一个“撤销”功能。

实例
更新后(updated)

对应的钩子函数是 updated。 在这一阶段 DOM 会和更改过的内容同步。 updated 方法在 DOM 更新后调用一次

这些方法很有用,但多数情况我们可能会通过监听器(watchers)去检测对应数据的改变。因为监听器可以很好的提供数据更改时的旧值和新值。

另一种方式是通过计算属性来改变元素的状态。

Destruction 钩子——清理

destruction 钩子在组件被移除并需要清理一些待释放的功能时使用。这是删除事件监听并且防止内存溢出的好地方。

销毁前(beforeDestroy)

对应的钩子函数是 beforeDestroy。 在上一阶段 vue 已经成功的通过数据驱动 DOM 更新,当我们不在需要 vue 操纵 DOM 时,就需要销毁 Vue,也就是清除 vue 实例与 DOM 的关联,调用 destroy 方法可以销毁当前组件。在销毁前,会触发 beforeDestroy 钩子函数。

触发在组件开始销毁之前,在此会进行绝大多数的清理工作。在此阶段,你的组件仍然拥有所有的功能,任何东西都还未被销毁。

销毁后(destroyed)

对应的钩子函数是 destroyed。

在销毁后,会触发 destroyed 钩子函数。

vue 的生命周期的思想贯穿在组件开发的始终,通过熟悉其生命周期调用不同的钩子函数,我们可以准确地控制数据流和其对 DOM 的影响;

vue 生命周期的思想是 Vnode 和 MVVM 的生动体现和继承。

详解setup后的钩子

onBeforeMount

在挂载开始之前被调用:相关的 render 函数首次被调用。

onMounted

组件挂载时调用

onBeforeUpdate

数据更新时调用,发生在虚拟 DOM 打补丁之前。这里适合在更新之前访问现有的 DOM,比如手动移除已添加的事件监听器。

onUpdated

由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子

onBeforeUnmount

在卸载组件实例之前调用。在这个阶段,实例仍然是完全正常的。

onUnmounted

卸载组件实例后调用。调用此钩子时,组件实例的所有指令都被解除绑定,所有事件侦听器都被移除,所有子组件实例被卸载。

onErrorCaptured

Vue 实例有一个 onErrorCaptured 钩子,每当事件处理程序或生命周期钩子抛出错误时,Vue 会调用该钩子。

return false 错误不会冒泡到父级组件 return true 错误不会冒泡到父级组件 不消费事件 控制台抛出异常 ★ 只捕获嵌套组件中的错误,同级 view 不会捕获

<template>
  <span id="count">{{ count }}</span>
  同组件不会经过<button @click="notAMethod">Throw</button> 子组件方式会经过
  <test></test>
</template>
<script setup>
import { defineComponent, onErrorCaptured, ref } from "vue";
// Vue 不会调这个钩子,因为错误发生在这个 Vue 实例中,而不是子组件。
onErrorCaptured((err) => {
  console.log("Caught error", err.message);
  return false;
});
</script>
onRenderTracked

注册一个调试钩子,当组件的渲染效果跟踪到响应性依赖项时,调用。 此挂钩仅在开发模式下运行,在服务器端渲染期间不调用。 vue.js 文件从 3.2.0 开始改变了只会在页面渲染的时候执行一次,局部更新不执行;

onRenderTriggered

当虚拟 DOM 重新渲染被触发时调用。数据更新视图更新时触发

onActivated

被 keep-alive 缓存的组件激活时调用。

onDeactivated

被 keep-alive 缓存的组件停用时调用

第八章:动画

在前端网页开发中,动画是一种非常重要的技术。合理运用动画可以极大地提高用户的使用体验。VUE 中提供了一些与过渡和动画相关的抽象概念,它们可以帮助我们方便快速地定义和使用动画。本章将从原生的 CSS 动画开始介绍,逐步深入 VUE 中动画 API 的相关应用

  • 纯粹的 CSS3 动画的使用
  • 使用 JavaScript 的方式实现动画效果
  • VUE 中过渡组件的使用
  • 为列表的变化添加动画过渡

8.1 使用 CSS 创建动画

CSS3 本身支持非常丰富的动画效果。组件的过渡、渐变、移动、翻转等都可以添加动画效果。CSS3 动画的核心是定义 keyframes 或 transition。keyframes 定义了动画的行为,比如对于颜色渐变的动画,需要定义起始颜色和终止颜色,浏览器会自动帮助我们计算期间的所有中间态来执行动画。

transition 的使用更加简单,当组件的 CSS 属性发生变化时,使用 transition 来定义其过渡动画的属性即可。

8.1.1 transition 过渡动画

实例:

<script src="https://unpkg.com/vue@next"></script>
<style>
    .demo {
        width: 100px;
        height: 100px;
        background-color: red;


    }

    .demo-ani {
        width: 200px;
        height: 200px;
        background-color: blue;
        transition: width 2s, height 2s, background-color 2s;
    }
</style>
<div id="Application">
    <div :class="cls" @click="run"></div>
</div>
<script>
    const App = Vue.createApp({
        data() {
            return {
                cls: "demo"
            }
        },
        methods: {
            run() {
                if (this.cls == "demo") {
                    this.cls = "demo-ani"
                } else {
                    this.cls = "demo"
                }
            }
        }
    })
    App.mount("#Application")
</script>

如上代码所示,CSS 中定义的 demo-ani 类中指定了 transition 属性,这个属性中可以设置要过渡的属性以及动画时间。运行上述代码,单击页面中的色块,可以看到色块变大的过程会附带动画效果,颜色变换的过程也附带动画效果。上面的示例代码实际上使用了简写方式,我们也可以逐条属性对动画效果进行设置。

width: 100px;
height: 100px;
background-color: red;

/*动画属性*/
transition-property: width, height, background-color;

/*动画时长*/
transition-duration: 1s;

/*动画执行方式 - 线性方式*/
transition-timing-function: linear;

/*多久后执行动画*/
transition-delay: 0.5s;

其中,

名称效果
transition-property设置动画属性
transition-duration设置动画执行时常
transition-timing-function设置动画执行方式
linear以线性方式执行
transition-delay进行延时设置,即多久后开始执行动画

8.1.2 keyframes 动画

transition 动画使用用来创建简单的过渡效果。CSS3 中支持使用 animation 属性来配置更加复杂的动画效果。animation 属性根据 keyframes 配置来执行基于关键帧的动画效果。

实例:

<script src="https://unpkg.com/vue@next"></script>
<style>
    /*定义动画名称和每个关键帧的状态*/
    @keyframes animation1 {

        /*起始*/
        0% {
            background-color: red;
            width: 100px;
            height: 100px;
        }

        /*执行到1/4状态*/
        25% {
            background-color: orchid;
            width: 200px;
            height: 200px;
        }

        75% {
            background-color: green;
            width: 150px;
            height: 150px;
        }

        100% {
            background-color: blue;
            width: 200px;
            height: 200px;
        }

    }



    .demo {
        width: 100px;
        height: 100px;
        background-color: red;
    }

    .demo-ani {
        animation: animation1 2s linear;
        width: 200px;
        height: 200px;
        background-color: blue;
    }
</style>

<div id="Application">
    <div :class="cls" @click="run"></div>
</div>

<script>
    const App = Vue.createApp({
        data() {
            return {
                cls: "demo"
            }
        },
        methods: {
            run() {
                if (this.cls == "demo") {
                    this.cls = "demo-ani"
                } else {
                    this.cls = "demo"
                }
            }
        }
    })
    App.mount("#Application")
</script>

在上面的 CSS 代码中,keyframes 用来定义动画的名称和每个关键帧的状态,0%表示动画起始时的状态,25%表示动画执行到 1/4 时的状态,同理,100%表示动画的终止状态。

对于每个状态,我们将其定义为一个关键帧,在关键帧中,我们可以定义元素的各种渲染属性,比如宽和高、位置、颜色等。

在定义 keyframes 时,如果只关心起始状态与终止状态,也可以这样定义:

/*若只关注起始与结束状态*/
@keyframes animation2 {
  from {
    background-color: red;
    width: 100px;
    height: 100px;
  }

  to {
    background-color: blue;
    width: 200px;
    height: 200px;
  }
}

定义好了 keyframes 关键帧,在编写 CSS 样式代码时可以使用 animation 属性为其指定动画效果,如以上代码设置要执行的动画为名 animation1 的关键帧动画,执行时长为 2 秒,执行方式为线性。

animation 的这些配置项也可以分别进行配置,示例如下:

/*分别配置animation*/
.demo-ani2 {
  /*设置关键帧名称*/
  animation-name: animation3;

  /*设置动画时长*/
  animation-duration: 2s;

  /*设置动画播放方式 - 淡入淡出*/
  animation-timing-function: ease-in-out;

  /*设置动画播放方向*/
  animation-direction: alternate;

  /*设置动画播放的次数*/
  animation-iteration-count: infinite;

  /*设置动画的播放状态*/
  animation-play-state: running;

  /*设置播放动画的延迟时间*/
  animation-delay: 1s;

  /*设置动画播放结束应用的元素样式*/
  animation-fill-mode: forwards;
  width: 200px;
  height: 200px;
  background-color: blue;
}

通过上面的范例,我们已经基本了解如何使用原生的 CSS,有了这些基础,再使用 VUE 中提供的动画相关 API 时会非常容易。

8.2 使用 JavaScript 的方式实现动画效果

动画的本质是将元素的变化以渐进的方式完成,即将大的状态变化拆分成非常多个小的状态变化,通过不断执行这些变化来达到动画的效果。使用 JavaScript 代码来启用定时器,按照一定频率进行组件状态的变化也可以实现动画效果。

JavaScript 动画实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <div
    :style="{backgroundColor:'blue',width:width+'px',height:height+'px'}"
    @click="run"
  ></div>
</div>
<script>
  const App = Vue.createApp({
    data() {
      return {
        width: 100,
        height: 100,
        timer: null,
      };
    },
    methods: {
      run() {
        //https://www.runoob.com/js/js-timing.html
        //setInterval() - 间隔指定的毫秒数不停地执行指定的代码。
        this.timer = setInterval(this.animation, 10);
      },

      //回调函数
      animation() {
        if (this.width == 200) {
          //https://www.runoob.com/js/js-timing.html
          //clearInterval() 方法用于停止 setInterval() 方法执行的函数代码。
          clearInterval(this.timer);
          return;
        } else {
          this.width += 1;
          this.height += 1;
        }
      },
    },
  });
  App.mount("#Application");
</script>

setInterval 方法用来开启一个定时器,上面代码中设置每 10 毫秒执行一次回调函数,在回调函数中,我们逐像素地将色块的尺寸放大,最终产生了动画效果。使用 JavaScript 可以更加灵活地控制动画的效果,在实际开发中,结合 Canvas 的使用,JavaScript 可以实现非常强大的自定义动画效果。还有一点需要注意,当动画结束后,要使用 clearInterval 方法将对应的定时器停止。

8.3 Vue 过渡动画

Vue 组件在页面中被插入、移除或者更新的时候都可以附带转场效果,即可以展示过渡动画。例如,当我们使用 v-if 和 v-show 指令控制组件的显示和隐藏时,就可以将其过程以动画的方式进行展现。

8.3.1 定义过渡动画

Vue 过渡动画的核心原理依然是采用 CSS 类来实现的,只是 Vue 帮助我们在组件的不同生命周期自动切换不同的 CSS 类。

Vue 中默认提供了一个名为 transition 的内置组件,可以用其来包装要展示过渡动画的组件。transition 组件的 name 属性用来设置要执行的动画名称,Vue 中约定了一系列 CSS 类名规则来定义各个过渡过程中的组件状态。我们可以通过一个简单的示例来体会 Vue 的这一功能。

实例

<script src="https://unpkg.com/vue@next"></script>
<style>
  .ani-enter-from {
    width: 0px;
    height: 0px;
    background-color: red;
  }

  .ani-enter-active {
    transition: width 2s, height 2s, background-color 2s;
  }

  .ani-enter-to {
    width: 100px;
    height: 100px;
    background-color: blue;
  }

  .ani-leave-from {
    width: 100px;
    height: 100px;
    background-color: blue;
  }

  .ani-leave-active {
    transition: width 2s, height 2s, background-color 3s;
  }

  .ani-leave-to {
    width: 0px;
    height: 0px;
    background-color: red;
  }
</style>
<div id="Application">
  <button @click="click">显示/隐藏</button>
  <transition name="ani">
    <div v-if="show"></div>
  </transition>
</div>
<script>
  const App = Vue.createApp({
    data() {
      return {
        show: false,
      };
    },
    methods: {
      click() {
        this.show = !this.show;
      },
    },
  });
  App.mount("#Application");
</script>

运行代码,尝试单击页面上的功能按钮,可以看到组件在显示/隐藏过程中表现出的过渡动画效果。上面代码的核心是定义的 6 个特殊的 CSS 类,这 6 个 CSS 类没有显式地使用,但是其在组件执行动画的过程中起到了不可替代的作用。当我们为 transition 组件的 name 属性设置动画名称之后,当组件被插入页面或被移除时,其会自动寻找以此动画名称开头的 CSS 类,格式如下:

x - enter - from;
x - enter - active;
x - enter - to;
x - leave - from;
x - leave - active;
x - leave - to;

其中,x 表示定义的过渡动画名称。上面 6 种特殊的 CSS 类,前 3 种用来定义组件被插入页面的动画效果,后 3 种用来定义组件被移出页面的动画效果。

x-enter-from 类在组件即将被插入页面时被添加到组件上,可以理解为组件的初始状态,元素被插入页面后此类会马上被移除。

x-enter-to 类在组件被插入页面后立即被添加,此时 x-enter-from 类会被移除,可以理解为组件过渡的最终状态。

x-enter-active 类中组件的整个插入过渡动画中都会被添加,直到组件的过渡动画结束后才会被移除。可以在这个类中定义组件过渡动画的时长、方式、延迟等。

x-leave-from 与 x-enter-from 相对应,在组件即将被移除时此类会被添加。用来定义移除组件时过渡动画的起始状态。

x-leave-to 则对应地用来设置移除组件动画的终止状态。

x-leave-active 类中组件的整个移除过渡动画中都会被添加,直到组件的过渡动画结束后才会被移除。可以在这个类中定义组件过渡动画的时长、方式、延迟等。

你可能也发现了,上面提到的 6 种特殊的 CSS 类虽然被添加的时机不同,但是最终都会被移除,因此当动画执行完成后,组件的样式并不会保留,更常见的做法是中组件本身绑定一个最终状态的样式类,示例如下:

<transition name="ani">
        <div v-if="show" class="demo"></div>
    </transition>
.demo {
  width: 100px;
  height: 100px;
  background-color: rgb(207, 106, 11);
}

这样,组件的显示或隐藏过程就变得非常流程。上面的示例代码使用 CSS 中的 transition 来实现动画,其实使用 animation 的关键帧方式定义动画效果也是一样的,CSS 代码如下:

@keyframes keyframes-in {
  from {
    width: 0px;
    height: 0px;
    background-color: red;
  }

  to {
    width: 100px;
    height: 100px;
    background-color: blue;
  }
}

@keyframes keyframes-out {
  from {
    width: 100px;
    height: 100px;
    background-color: blue;
  }

  to {
    width: 0px;
    height: 0px;
    background-color: red;
  }
}

/*默认样式*/
.demo {
  width: 100px;
  height: 100px;
  background-color: rgb(207, 106, 11);
}

.ani-enter-from {
  width: 0px;
  height: 0px;
  background-color: red;
}

.ani-enter-active {
  animation: keyframes-in 3s;
}

.ani-enter-to {
  width: 100px;
  height: 100px;
  background-color: blue;
}

.ani-leave-from {
  width: 100px;
  height: 100px;
  background-color: blue;
}

.ani-leave-active {
  animation: keyframes-out 3s;
}

.ani-leave-to {
  width: 0px;
  height: 0px;
  background-color: red;
}

实例:

<script src="https://unpkg.com/vue@next"></script>
<style>
    @keyframes keyframes-in {
        from {
            width: 0px;
            height: 0px;
            background-color: red;
        }

        to {
            width: 100px;
            height: 100px;
            background-color: blue;
        }
    }

    @keyframes keyframes-out {
        from {
            width: 100px;
            height: 100px;
            background-color: blue;
        }

        to {
            width: 0px;
            height: 0px;
            background-color: red;
        }
    }

    /*默认样式*/
    .demo {
        width: 100px;
        height: 100px;
        background-color: rgb(207, 106, 11);
    }

    .ani-enter-from {
        width: 0px;
        height: 0px;
        background-color: red;
    }

    .ani-enter-active {
        animation: keyframes-in 3s;
    }

    .ani-enter-to {
        width: 100px;
        height: 100px;
        background-color: blue;
    }

    .ani-leave-from {
        width: 100px;
        height: 100px;
        background-color: blue;
    }

    .ani-leave-active {
        animation: keyframes-out 3s;
    }

    .ani-leave-to {
        width: 0px;
        height: 0px;
        background-color: red;
    }
</style>
<div id="Application">
    <button @click="click">显示/隐藏</button>
    <transition name="ani">
        <div v-if="show" class="demo"></div>
    </transition>
</div>
<script>
    const App = Vue.createApp({
        data() {
            return {
                show: false
            }
        },
        methods: {
            click() {
                this.show = !this.show
            }
        }
    })
    App.mount("#Application")
</script>

8.3.2 设置动画过程中的监听回调

我们知道,对于组件的加载或卸载过程,有一系列的生命周期函数会被调用。对于 Vue 中的转场动画来说,也可以注册一系列的函数来对其过程进行监听。示例如下:

<transition
  name="ani"
  @before-enter="beforeEnter"
  @enter="enter"
  @after-enter="afterEnter"
  @enter-cancelled="enterCancelled"
  @before-leave="beforeLeave"
  @leave="leave"
  @after-leave="afterLeave"
  @leave-cancelled="leaveCancelled"
>
        <div v-if="show" class="demo"></div>
    </transition>

上面注册的回调方法需要在组件的 methods 选项中实现:

methods: { //组件插入过度开始前 beforeEnter(el) { console.log("beforeEnter") },
//组件插入过渡开始 enter(el, done) { console.log("enter") }, //组件插入过渡后
afterEnter(el) { console.log("afterEnter") }, //组件插入过渡取消
enterCancelled(el) { console.log("enterCancelled") }, //组件移除过渡开始前
beforeLeave(el) { console.log("beforeLeave") }, //组件移除过渡开始 leave(el,
done) { console.log("leave") }, //组件移除过渡后 affterLeave(el) {
console.log("afterLeave") }, //组件移除过渡取消 leaveCancelled(el) {
console.log("leveCancelled") } }

有了这些回调函数,可以在组件过渡动画过程中实现复杂的业务逻辑,也可以通过 JavaScript 来自定义过渡动画,当我们需要自定义过渡动画时,需要将 transition 组件的 CSS 属性关掉,代码如下:

<div id="Application">
    <button @click="click">显示/隐藏</button>
    <transition name="ani" :css="false">
        <div v-if="show" class="demo"></div>
    </transition>
</div>

还有一点需要注意,上面列举的回调函数中,有两个函数比较特殊:enter 和 leave。这两个函数除了会将当前元素作为参数外,还有一个函数类型的 done 参数,如果我们将 transition 组件的 CSS 属性关闭,决定使用 JavaScript 来实现自定义的过渡动画,这两个方法中的 done 函数最后必须被手动调用,否则过渡动画会立即完成。

实例:

<script src="https://unpkg.com/vue@next"></script>
<div id="Application">
  <button @click="click">显示/隐藏</button>
  <transition
    name="ani"
    @before-enter="beforeEnter"
    @enter="enter"
    @after-enter="afterEnter"
    @enter-cancelled="enterCancelled"
    @before-leave="beforeLeave"
    @leave="leave"
    @after-leave="afterLeave"
    @leave-cancelled="leaveCancelled"
  >
    <div v-if="show" class="demo"></div>
  </transition>
</div>
<script>
  const App = Vue.createApp({
    data() {
      return {
        show: false,
      };
    },
    methods: {
      click() {
        this.show = !this.show;
      },
      //组件插入过度开始前
      beforeEnter(el) {
        console.log("beforeEnter");
      },

      //组件插入过渡开始
      enter(el, done) {
        console.log("enter");
      },

      //组件插入过渡后
      afterEnter(el) {
        console.log("afterEnter");
      },

      //组件插入过渡取消
      enterCancelled(el) {
        console.log("enterCancelled");
      },

      //组件移除过渡开始前
      beforeLeave(el) {
        console.log("beforeLeave");
      },

      //组件移除过渡开始
      leave(el, done) {
        console.log("leave");
      },

      //组件移除过渡后
      affterLeave(el) {
        console.log("afterLeave");
      },

      //组件移除过渡取消
      leaveCancelled(el) {
        console.log("leveCancelled");
      },
    },
  });
  App.mount("#Application");
</script>

<style>
  @keyframes keyframes-in {
    from {
      width: 0px;
      height: 0px;
      background-color: red;
    }

    to {
      width: 100px;
      height: 100px;
      background-color: blue;
    }
  }

  @keyframes keyframes-out {
    from {
      width: 100px;
      height: 100px;
      background-color: blue;
    }

    to {
      width: 0px;
      height: 0px;
      background-color: red;
    }
  }

  /*默认样式*/
  .demo {
    width: 100px;
    height: 100px;
    background-color: rgb(207, 106, 11);
  }

  .ani-enter-from {
    width: 0px;
    height: 0px;
    background-color: red;
  }

  .ani-enter-active {
    animation: keyframes-in 3s;
  }

  .ani-enter-to {
    width: 100px;
    height: 100px;
    background-color: blue;
  }

  .ani-leave-from {
    width: 100px;
    height: 100px;
    background-color: blue;
  }

  .ani-leave-active {
    animation: keyframes-out 3s;
  }

  .ani-leave-to {
    width: 0px;
    height: 0px;
    background-color: red;
  }
</style>

8.3.3 多个组件的过度动画

Vue 中的 transition 组件支持同时包装多个互斥的子组件元素,从而实现多组件的过渡效果。在实际开发中,有很多这类常见的场景,例如 A 元素消失的同时元素 B 展示。核心实例代码如下:

<script src="https://unpkg.com/vue@next"></script>
<style>
  .demo {
    width: 100px;
    height: 100px;
    background-color: blue;
  }

  .demo2 {
    width: 100px;
    height: 100px;
    background-color: blue;
  }

  .ani-enter-from {
    width: 0px;
    height: 0px;
    background-color: red;
  }

  .ani-enter-active {
    transition: width 2s, height 2s, background-color 3s;
  }

  .ani-enter-to {
    width: 100px;
    height: 100px;
    background-color: blue;
  }

  .ani-leave-from {
    width: 100px;
    height: 100px;
    background-color: blue;
  }

  .ani-leave-active {
    transition: width 2s, height 2s, background-color 3s;
  }

  .ani-leave-to {
    width: 0px;
    height: 0px;
    background-color: red;
  }
</style>

<div id="Application">
  <button @click="click">显示/隐藏</button>
  <h3>未设置mode="in-out"</h3>
  <transition name="ani">
    <div v-if="show" class="demo"></div>
    <div v-else class="demo2"></div>
  </transition>

  <h3>设置了mode="in-out"</h3>
  <transition name="ani" mode="in-out">
    <div v-if="show" class="demo"></div>
    <div v-else class="demo2"></div>
  </transition>

  <h3>设置了mode="out-in"</h3>
  <transition name="ani" mode="out-in">
    <div v-if="show" class="demo"></div>
    <div v-else class="demo2"></div>
  </transition>
</div>

<script>
  const App = Vue.createApp({
    data() {
      return {
        show: false,
      };
    },
    methods: {
      click() {
        this.show = !this.show;
      },
    },
  });
  App.mount("#Application");
</script>

运行代码,单击页面上的按钮,可以看到两个色块会以过渡动画的方式交替出现。默认情况下,两个元素的插入和移除动画会同步进行,有些时候这并不能满足我们的需求,大多数时候需要移除动画执行完成后,再执行插入动画。要实现这一功能很简单,只需要对 transition 组件的 mode 属性进行设置即可,当我们将其设置为 out-in 时,就会先执行移除动画,再执行插入动画;

将其设置为 in-out 时,则会先执行插入动画,再执行移除动画。

<h3>设置了mode="in-out"</h3>
<transition name="ani" mode="in-out">
  <div v-if="show" class="demo"></div>
  <div v-else class="demo2"></div>
</transition>

<h3>设置了mode="out-in"</h3>
<transition name="ani" mode="out-in">
  <div v-if="show" class="demo"></div>
  <div v-else class="demo2"></div>
</transition>

8.3.4 列表过度动画

在实际开发中,列表是一种非常流行的页面设计方式。在 Vue 中,通常使用 v-for 指令来动态构建列表视图。在动态构建列表视图的过程中,其中的元素经常会有增删、重排等操作,在 Vue 中使用 transition-group 组件可以非常方便地实现列表元素变动的动画效果。

实例:

<script src="https://unpkg.com/vue@next"></script>
<style>
  /*添加动画的中间与结束动画的中间*/
  .list-enter-active,
  .list-leave-active {
    transition: all 1s ease;
  }

  /*添加动画的开始与移除动画的结束*/
  .list-enter-from,
  .list-leave-to {
    opacity: 0;
  }

  /*排序过程动画*/
  .list-move {
    transition: transform 1s ease;
  }
</style>
<div id="Application">
  <button @click="click">添加元素</button>
  <button @click="dele">删除元素</button>
  <button @click="ro">逆序元素</button>
  <transition-group name="list">
    <div v-for="item in items" :key="item">元素:{{item}}</div>
  </transition-group>
</div>
<script>
  const App = Vue.createApp({
    data() {
      return {
        items: [1, 2, 3, 4, 5],
      };
    },
    methods: {
      //添加元素
      click() {
        this.items.push(this.items[this.items.length - 1] + 1);
      },

      //删除元素
      dele() {
        if (this.items.length > 0) {
          this.items.pop();
        }
      },

      //逆序元素
      ro() {
        this.items.reverse();
      },
    },
  });
  App.mount("#Application");
</script>

运行上面的代码,单击页面中的“添加元素”按钮后,可以看到列表的元素在增加,并且是以渐进式动画的方式插入的。

在使用 transition-group 组件实现列表动画时,与 transition 类似,首先需要定义动画所需的 CSS 类,上面的实例代码中,我们只定义了透明度变化的动画。有一点需要注意,如果要使用列表动画,列表中的每一个元素都需要有一个唯一的 key 值。如果为上面的列表再添加一个删除元素的功能,它依然会很好地展示动画效果。删除元素示例:

dele() { if (this.items.length > 0) { this.items.pop() } },

除了对列表中的元素进行插入和删除可以添加动画外,对列表元素的排序过程也可以采用动画来进行过渡,只需要额外定义一个 v-move 类型的特殊动画类即可,例如为上面的代码增加如下的 CSS 类:

/*排序过程动画*/
.list-move {
  transition: transform 1s ease;
}

之后可以尝试对列表中的元素进行逆序,Vue 会以动画的方式将其中的元素移动到正确的位置。

8.4 范例:优化用户列表页面

第九章:构建工具 Vite 的使用

Vue 本身是一个渐进式的前端 Web 开发框架,其允许我们只中项目中的部分页面中使用 Vue 进行开发,也允许我们仅仅使用 Vue 中的部分功能来进行项目开发,但是如果你的目标是完成一个风格统一、可扩展性强的现代化 Web 单页面应用,那么使用 Vue 提供的一整套完整的流程进行开发是非常合适的。

Vue CLL 就是这样一个基于 Vue 进行快速项目开发的工具。

9.1 Vite 工具入门

这是一个帮助开发者快速创建和开发 Vue 项目的工具,对于开发者来说,使用其开发和调试 Vue 应用都很方便。

9.1.1 Vite 安装

安装 Vite 的前提是安装了 Node.js 环境

进入 Node.js 官网,网址如下:

nodejs.org

选择当前新的稳定版本进行下载安装即可。

安装 Node.js 环境后,即可在终端使用 npm 相关指令来安装软件包。在终端输入以下命令可以检查 Node.js 环境是否正确安装完成

node -v

执行以上命令后,只要终端输出了版本号信息,就表明 Node.js 已经安装成功了。

v18.12.1

接下来使用 npm 安装 Vite 工具,在终端输入以下命令

npm create vite@latest
//或
npm init @vitejs/app

由于有很多依赖包需要下载,因此安装过程可能会持续一段时间,耐心等待即可。(可根据后续教程,替换为国内源,加快速度)需要注意,如果在安装过程中终端输出了如下异常错误信息:

Unhandled rejection Error:EACCES:permission denied

这是因为当前操作系统登录的用户权限不足所致,使用一下命令重新安装即可。

sudo npm create vite@latest
  • 以管理员身份打开 win 端的 CMD
  • 在 MAC Os 端使用 sudo 命令,输入密码(输入的密码没有显示)进行安装

在命令前添加 sudo 标识使用超级管理员权限进行命令的执行,执行命令前终端会要求输入设备的启动密码。

若您选择npm init @vitejs/app

之后需要一步步渐进式地来选择一些配置项。首先需要输入工程名和包名,例如 viteDemo,然后选择使用的框架,Vite 不止支持构建 Vue 项目,有支持构建基于 React 等框架的项目,这里选择 Vue 即可。

项目创建完成后,可以看到生成的工程目录结构。

执行

在工程目录下执行npm run dev(第一次执行前需要先执行npm install指令安装依赖)即可开启开发服务器,执行npm run build即可进行打包操作。

9.1.2 快速创建项目

本节将演示使用 Vite 创建一个完整的 Vue 项目工程的过程。在终端执行如下命令来创建 Vue 项目工程

npm create vite@latest my-vue-app -- --template vue

您可以通过以下命令来创建项目,通过选项选择自己需要的资源即可

npm init vite

然后,cd 进项目目录

cd my-vue-app
//安装环境
npm install
//运行
npm run dev

可使用快捷键 Ctrl+C,暂停运行。

一个完整的 Vue 模版工程相对原生的 HTML 工程要复杂很多,后面会介绍工程中默认生成的文件以及文件的意义和用法。

9.2 Vite 项目模版工程

9.2.1 模版工程的目录结构

文件
  • .gitignore
  • package.json
  • README.md

.gitignore是一个隐藏文件,用来配置 Git 版本管理工具需要忽略的文件或文件夹,在创建工程时,其默认会配置好,将一些依赖、编译产物、log 日志等文件忽略,我们无需修改

package.json是一个相对比较重要的文件,其中存储的撒一个 JSON 对数据,用来配置当前的项目名称、版本号、脚本命令以及模块依赖等。当我们需要向项目中添加额外的依赖时,其就会被记录到这个文件默认的模版工程中,生产环境的依赖如下:

  "dependencies": {
    "element-plus": "^2.2.19",
    "mockjs": "^1.1.0",
    "pinia": "^2.0.23",
    "vue": "^3.2.37",
    "vue-router": "^4.1.6"
  },

开发环境的依赖如下:

  "devDependencies": {
    "@vitejs/plugin-vue": "^3.1.0",
    "vite": "^3.1.0"
  }

README.md是一个 MarkDown 格式的文件,其中记录了项目的编译和调试方式。我们也可以将项目的介绍编写中这个文件中。

文件夹
  • node_modules
  • public
  • src

node_modules 文件夹下存放的是 NPM 安装的依赖模块,这个文件夹默认会被 Git 版本管理工具忽略,对于其中的文件,我们无需手动添加或修改

public 文件用来放置一些公有的资源文件,例如网页用到的图标、静态的 HTML 文件等

src 文件是一个重要的文件夹,核心功能代码文件都放在这个文件夹下,在默认的模版工程中,这个文件夹下还有两个子文件夹:assets 和 components。顾名思义,assets 存放资源文件,components 存放组件文件。

我们按照页面的加载流程看一下 src 文件下默认生成的几个文件。

main.js 是应用程序的入口文件,其中的代码如下:

//导入vue框架中的createApp方法
import { createApp } from "vue";
//导入我们的自定义css样式
import "./style.css";
//导入我们的自定义根组件
import App from "./App.vue";
//挂载根组件
createApp(App).mount("#app");

你可能有些疑惑,main.js 文件中怎么只有组件创建和挂载的相关逻辑,并没有对应的 HTML 代码,那么组件是挂载到哪里呢?

其实我们前面已经介绍过了,在根文件夹下会包含一个名为 index.html 的文件,它就是网页的入口文件,其中的代码如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + Vue</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

现在你应该明白了,main.js 中定义的根组件将被挂载到 ID 为”app“的 div 标签上,回到 main.js 文件,其中导入了一个名为 App 的组件作为根组件,可以看到,项目工程中有一个名为 App.vue 的文件,这其实使用了 Vue 中单文件组件的定义方法,即将组件定义在单独的文件中,以便于开发和维护。

App.vue 文件中的内容如下:

<script setup>
import HelloWorld from "./components/HelloWorld.vue";
</script>

<template>
  <div>
    <a href="https://vitejs.dev" target="_blank">
      <img src="/vite.svg" class="logo" alt="Vite logo" />
    </a>
    <a href="https://vuejs.org/" target="_blank">
      <img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
    </a>
  </div>
  <HelloWorld msg="Vite + Vue" />
</template>

<style scoped>
.logo {
  height: 6em;
  padding: 1.5em;
  will-change: filter;
}
.logo:hover {
  filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
  filter: drop-shadow(0 0 2em #42b883aa);
}
</style>

单文件组件通常需要定义 3 部分内容:template 模版部分、script 脚本代码部分和 style 样式代码部分。

如上代码所述,在 template 模版中布局了一个图标和一个自定义的 HeloWorld 组件,在 JavaScript 中将当前组件进行了导出。

其中,

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

defineProps({
  msg: String,
});

const count = ref(0);
</script>

<template>
  <h1>{{ msg }}</h1>

  <div class="card">
    <button type="button" @click="count++">count is {{ count }}</button>
    <p>
      Edit
      <code>components/HelloWorld.vue</code> to test HMR
    </p>
  </div>

  <p>
    Check out
    <a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
      >create-vue</a
    >, the official Vue + Vite starter
  </p>
  <p>
    Install
    <a href="https://github.com/johnsoncodehk/volar" target="_blank">Volar</a>
    in your IDE for a better DX
  </p>
  <p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>

<style scoped>
.read-the-docs {
  color: #888;
}
</style>

HeleWorld.vue 文件中的代码比较多,总体来看,只是定义了很多可跳转元素,并没有太多逻辑。现在我们对默认生成的模版项目已经有了初步的了解,下一节将尝试在本地运行和调试它。

9.2.2 运行 Vue 项目工程

要运行 Vue 模版项目很简单,打开终端,进入当前 Vue 项目的工程目录,执行以下命令:

npm run dev

之后,进行 Vue 项目工程的编译,并在本机启动一个开发服务器,若终端输出如下信息,则表明项目已运行完成:

  VITE v4.0.3  ready in 166 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h to show help

在浏览器中输入上面中提到的网址:http://localhost:5173/,即可查看运行效果。

我们也可以指定端口运行,如下所示:

npm run dev -- --port 9000

9.3 在项目中使用依赖

在 Vue 项目开发中,额外插件的使用必不可少。后面还会介绍各种各样的常用 Vue 插件,如网络管理插件、路由插件、状态管理插件等。本节将介绍如何使用 Vite 来安装和管理插件。

Vite 创建工程基于插件架构。查看 package.json 文件,可以发现中开发环境下,其默认安装了所需要的工具依赖。安装依赖依然使用 npm 相关命令,例如,若需安装 vue-axios 依赖,可以在项目工程目录下执行如下命令:

npm install --save axios vue-axios

需要注意,若安装工程中出现权限问题,则需要在命令前添加 sudo 再执行。

安装完成后,可以看到 package.json 文件会自动进行更新,更新后的依赖信息如下:

  "dependencies": {
    "axios": "^1.2.1",
    "mockjs": "^1.1.0",
    "vue": "^3.2.45",
    "vue-axios": "^3.5.2"
  },

其实,不止 package.json 文件会更新,在 node_modules 文件夹下也会新增 axios 和 vue-axios 相关的模块文件。

vue-axios 是一个在 Vue 中用于网络请求的依赖库,后面章节会转码介绍它。

9.4 工程构建

开发完一个 Vue 项目后,需要将其构建成可发布的代码产品,Vite 提供了对应的工具来完成这些操作。

在 Vue 工程目录下执行如下命令,可在本地预览生产构建产物

npm run preview

确认效果没问题后,在 Vue 工程目录下执行如下命令,可以直接将项目代码编译构建成生产包。

npm run build

构建可能需要一段时间,构建完成后,在工程的根目录下将会生成一个名为 dist 的文件夹,这个文件夹就是我们要发布的软件包。可以看到,这个文件夹下包含一个名为 index.html 的文件,它是项目的入口文件,除此之外,还包含一些静态资源与 CSS、JavaScript 等相关文件,这些文件中的代码都是被压缩后的。

一般是将此文件发送至服务器端提供服务的,若您需在本地预览效果,可参考如下步骤

终端执行以下命令安装 server 服务

npm install -g serve

若提示无权限,可使用如下命令,

sudo npm install -g serve

输入设备密码(密码不显示)后即可正常安装。

通过以下命令查看 serve 是否安装成功,若输出版本号,则代表安装成功

server -v
//=>14.1.2

然后通过 server 服务来启动打包好的文件

serve -s dist

提示如下:

Serving!                              │
   │                                         │
   │   - Local:    http://localhost:3000     │
   │   - Network:  http://192.168.5.2:3000   │
   │                                         │
   │   Copied local address to clipboard!

选择其中一个链接中浏览器中打开即可查看效果。

若需停止该服务,可按下快捷键 Ctrl+C 停止运行。

9.5 新一代前端构建工具 Vite

9.5.1 Vite 与 Vue CLI 的比较

Vue CLI 非常适合大型商业项目的开发,它是构建大型 Vue 项目不可或缺的工具,Vue CLI 主要包括工程脚手架、带热重载模块的开发服务器、插件系统、用户界面等功能。

Vite 不是基于 Webpackd ,它有一套自己的开发服务器,并且 Vite 本身并不想 Vue CLI 那样功能强大,它只专注于提供基本构建的功能和开发服务器。因此,Vite 更加小巧迅捷,其开发服务器比基于 Webpack 的开发服务器快 10 倍左右,这对开发者来说太重要了,开发服务器的响应速度会直接影响开发者的编程体验和开发效率。对于大型项目来说,可能会有成千上万个 JavaScript 模块,这时构建效率的速度差异就会非常明显。

虽然 Vite 在”速度“上无疑比 Vue CLI 快很多,但其没有用户界面,也没有提供插件管理系统,对于初学者来说并不友好。在实际开发项目中,并没有统一的使用标准,大家可按需选择。

第十章:UI 组件库 Element Plus

在实际的开发中,更多时候需要结合各种基于 Vue 框架开发的第三方模块来完成项目。以直接的 UI 展现为例,通过使用基于 Vue 的组件库,可以快速地搭建功能强大、样式美观的页面。本章将介绍一款名为 Element Plus 的前端 UI 框架,其本身是基于 Vue 的 UI 框架,在 Vue 项目中,我们可以完全兼容使用。

Element Plus 框架是 Vue 开发中非常流行的一款 UI 组件库,其可以带给用户全网一致的使用体验、目的清晰的控制反馈等,对于开发者来说,由 Element Plus 内置了非常丰富的样式与布局框架,使用它可大大降低开发者页面开发的成本。

10.1 Element Plus 入门

Element Plus 可以直接使用 CDN 的方式进行引入,单独地使用其提供的组件和样式,这与渐进式风格的 Vue 框架十分类似;同样,也可以使用 NPM 在 Vite 等工具中使用本框架。

10.1.1 Element Plus 的安装与使用

CDN:
<!--引入样式 -->
<link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css" />
<!-- 引入组件库 -->
<script src="https://unpkg.com/element-plus"></script>

实例:

<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
    <script src="https://unpkg.com/vue@next"></script>
    <!--引入样式 -->
    <link
      rel="stylesheet"
      href="https://unpkg.com/element-plus/dist/index.css"
    />
    <!-- 引入组件库 -->
    <script src="https://unpkg.com/element-plus"></script>
    <title>Element Plus demo</title>
  </head>

  <body>
    文档地址:https://element-plus.org/zh-CN/guide/design.html
    <div id="app">
      <el-button>{{ message }}</el-button>
      <div style="margin: 40px;">
        <el-tag>这里是模版内容:{{count}}次点击</el-tag>
      </div>
      <div>
        <el-button v-on:click="clickButton">按钮</el-button>
      </div>
    </div>
    <script>
      const App = {
        data() {
          return {
            message: "Hello Element Plus",
            count: 0,
          };
        },
        methods: {
          clickButton() {
            this.count = this.count + 1;
          },
        },
      };
      const app = Vue.createApp(App);
      app.use(ElementPlus);
      app.mount("#app");
    </script>
  </body>
</html>

el-tag 与 el-button 是 Element Plus 中提供的标签组件与按钮组件。运行上述代码,可以看到组件比较美观。

NPM

上面演示了中单文件中使用 Element Plus 框架,在完整的 Vue 工程中使用也非常方便。直接在创建好的 Vue 工程目录下执行以下命令即可。

npm install element-plus --save

注意,如果权限有问题,在上面的命令前添加 sudo 即可。执行完成后,可以看到工程下的 package.json 文件中指定依赖部分已经被添加了 Element Plus 框架:

  "dependencies": {
    "element-plus": "^2.2.27",
    "vue": "^3.2.45"
  },

项目下的 src 目录下的 main.js 文件修改为以下代码:

//导入Vue框架中的createApp方法
import { createApp } from "vue";
//导入样式文件
import "./style.css";
//导入自定义根组件
import App from "./App.vue";

//导入Element Plus模块
import ElementPlus from "element-plus";
//导入模块所需样式
import "element-plus/dist/index.css";

//挂载根组件
const app = createApp(App);

//注册Element Plus
app.use(ElementPlus);
//进行应用挂载
app.mount("#app");

现在,我们可以中项目中使用 Element Plus 组件了

修改项目目录下 src 目录下 components 目录下的 HelloWorld.vue 文件为以下代码:

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

defineProps({
  msg: String,
});

const count = ref(0);
</script>

<template>
  <h1>{{ msg }}</h1>
  <el-empty description="空空如也 ~ ~ ~ "></el-empty>

  <div class="card">
    <el-button type="button" @click="count++">count is {{ count }}</el-button>
    <p>
      Edit
      <code>components/HelloWorld.vue</code> to test HMR
    </p>
  </div>

  <p>
    Check out
    <a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
      >create-vue</a
    >, the official Vue + Vite starter
  </p>
  <p>
    Install
    <a href="https://github.com/johnsoncodehk/volar" target="_blank">Volar</a>
    in your IDE for a better DX
  </p>
  <p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>

<style scoped>
.read-the-docs {
  color: #888;
}
</style>

其中,el-empty 组件是一个空态页组件,用来展示无数据时的页面占位图,执行以下命令运行项目:

npm run dev

依据提示打开网页,即可查看到效果。

反馈组件

<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1.0" />

    <title>Element Plus demo - 反馈组件</title>
  </head>

  <body>
    文档地址:https://element-plus.org/zh-CN/guide/design.html
    <script src="https://unpkg.com/vue@next"></script>
    <!-- import CSS -->
    <link
      rel="stylesheet"
      href="https://unpkg.com/element-plus/dist/index.css"
    />
    <!-- import JavaScript -->
    <script src="https://unpkg.com/element-plus"></script>
    <div id="app">
      <el-button :plain="true" @click="test">经典用法</el-button>
      <hr />
      <el-button :plain="true" @click="open2">success</el-button>
      <el-button :plain="true" @click="open3">warning</el-button>
      <el-button :plain="true" @click="open1">message</el-button>
      <el-button :plain="true" @click="open4">error</el-button>
    </div>

    <script>
      const App = {
        setup() {
          const message = Vue.ref("1111");
          const test = () => {
            ElementPlus.ElMessage({
              message: "this is a message.",
              grouping: true,
              type: "success",
            });
          };
          const open1 = () => {
            ElementPlus.ElMessage("一个简单提示");
          };

          const open2 = () => {
            ElementPlus.ElMessage({
              message: "Congrats, this is a success message.",
              type: "success",
            });
          };
          const open3 = () => {
            ElementPlus.ElMessage({
              showClose: true,
              message: "Warning, this is a warning message.",
              type: "warning",
            });
          };
          const open4 = () => {
            ElementPlus.ElMessage.error("Oops, this is a error message.");
          };

          return {
            test,
            open1,
            open2,
            open3,
            open4,
          };
        },
      };
      const app = Vue.createApp(App);
      app.use(ElementPlus);
      app.mount("#app");
    </script>
  </body>
</html>

图标

<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1.0" />

    <title>Element Plus demo - 图标</title>
  </head>

  <body>
    <script src="https://unpkg.com/vue@next"></script>
    <link
      rel="stylesheet"
      href="https://unpkg.com/element-plus/dist/index.css"
    />
    <script src="https://unpkg.com/element-plus"></script>
    <script src="https://unpkg.com/@element-plus/icons-vue"></script>
    图标介绍: https://element-plus.org/zh-CN/component/icon.html#api

    <div id="app">
      <el-icon>
        <element-plus></element-plus>
      </el-icon>
    </div>
    <script>
      const App = {};
      const app = Vue.createApp(App);
      for ([name, comp] of Object.entries(ElementPlusIconsVue)) {
        app.component(name, comp);
      }
      app.use(ElementPlus);
      app.mount("#app");
    </script>
  </body>
</html>

第十一章:基于 Vue 的网络框架 vue-axios

互联网应用自然离不开网络,我们在浏览器中浏览的任何网页数据几乎都是通过网络传输的,对于开发独立的网站应用来说,页面本身和页面要渲染的数据通常是分别传输的。浏览器首先根据用户输入的地址来获取静态的网页文件、依赖的相关脚本代码等,之后再由脚本代码进行其他数据的逻辑获取。对于 Vue 应用来说,我们通常使用vue-axios来进行网络数据的请求。

  • 如何利用互联网上的接口数据来构建自己的应用
  • vue-axios 模块的安装和数据请求
  • vue-axios 的接口配置与高级用法
  • 具有网络功能的 Vue 应用基本开发方法

链接

11.1 使用 vue-axios 请求天气数据

本节将介绍如何使用互联网上提供的免费 API 资源来实现生活小工具类应用。互联网的免费资源其实非常多,我们可以用其来实现如新闻推荐、天气预报、问答机器人等有趣的应用。本节将以天气预报数据为例介绍如何使用 vue-axios 获取这些数据。

11.1.1 使用互联网上免费的数据服务

互联网上有许多第三方的 API 接口服务,使用这些服务我们可以方便地开发个人使用的小工具应用,也可以方便地进行编程技能的学习和测试。

”聚合数据“是一个非常优秀的数据提供商,其可以提供涵盖各个领域的数据接口服务,官网如下:

https://www.juhe.cn

注册账号 - 实名认证 - 申请使用天气预报服务。

个人中心 - 数据中心 - 我的 API 栏目中看到此服务。

点击进入”天气预报“服务详情页,可看到接口地址、请求方式、请求参数说明和返回参数说明等信息,我们需要根据这些信息来进行应用的开发

尝试在终端进行接口的请求测试,输入以下指令:

curl http://apis.juhe.cn/simpleWeather/query\?city\=%E8%8B%8F%E5%B7%9E\&key\=0f8a4915ea99cfc2dfd9f51e1cxxx

注意,其中参数 key 对应的值为我们前面记录的应用的Key值,在”我的 API 栏目“对应项目上,可看到。

city对应的是要查询的城市名称,需要对其进行urlencode编码。

如果终端正确输出了我们请求的天气预报信息,

{"reason":"查询成功!","result":{"city":"苏州","realtime":{"temperature":"5","humidity":"53","info":"多云","wid":"01","direct":"西风","power":"2级","aqi":"113"},"future":[{"date":"2022-12-27","temperature":"2\/8℃","weather":"多云","wid":{"day":"01","night":"01"},"direct":"北风"},{"date":"2022-12-28","temperature":"3\/7℃","weather":"多云","wid":{"day":"01","night":"01"},"direct":"北风转东北风"},{"date":"2022-12-29","temperature":"3\/7℃","weather":"阴转多云","wid":{"day":"02","night":"01"},"direct":"东北风"},{"date":"2022-12-30","temperature":"2\/10℃","weather":"多云","wid":{"day":"01","night":"01"},"direct":"东北风"},{"date":"2022-12-31","temperature":"3\/11℃","weather":"阴转多云","wid":{"day":"02","night":"01"},"direct":"东北风转东风"}]},"error_code":0}%

优化显示

{
  "reason": "查询成功!",
  "result": {
    "city": "苏州",
    "realtime": {
      "temperature": "5",
      "humidity": "53",
      "info": "多云",
      "wid": "01",
      "direct": "西风",
      "power": "2级",
      "aqi": "113"
    },
    "future": [
      {
        "date": "2022-12-27",
        "temperature": "2/8℃",
        "weather": "多云",
        "wid": {
          "day": "01",
          "night": "01"
        },
        "direct": "北风"
      },
      {
        "date": "2022-12-28",
        "temperature": "3/7℃",
        "weather": "多云",
        "wid": {
          "day": "01",
          "night": "01"
        },
        "direct": "北风转东北风"
      },
      {
        "date": "2022-12-29",
        "temperature": "3/7℃",
        "weather": "阴转多云",
        "wid": {
          "day": "02",
          "night": "01"
        },
        "direct": "东北风"
      },
      {
        "date": "2022-12-30",
        "temperature": "2/10℃",
        "weather": "多云",
        "wid": {
          "day": "01",
          "night": "01"
        },
        "direct": "东北风"
      },
      {
        "date": "2022-12-31",
        "temperature": "3/11℃",
        "weather": "阴转多云",
        "wid": {
          "day": "02",
          "night": "01"
        },
        "direct": "东北风转东风"
      }
    ]
  },
  "error_code": 0
}

恭喜你,已经成功完成了准备工作,可以进行后面的学习了。

11.1.2 使用 vue-axios 进行数据请求

axios本身是一个基于promise的 HTTP 客户端工具,vue-axios针对 axios 进行了一层简单的包装。在 Vue 应用中,使用其进行网络数据的请求非常简单。

CDN
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
NPM

首先,在 Vue 项目工程下执行如下指令进行vue-axios模块的安装

npm install --save axios vue-axios

安装完成后,可以检查package.json文件中是否已经添加了vue-axios的依赖。还记得我们使用Element Plus框架时的步骤吗?使用vue-axios与其类似,首先需要在main.js文件中对其进行导入和注册,代码如下:

//导入Vue框架中的createApp方法
import { createApp } from "vue";
//导入样式文件
import "./style.css";
//导入自定义根组件
import App from "./App.vue";

//导入vue-axios模块
import axios from "axios";
import VueAxios from "vue-axios";

//挂载根组件
const app = createApp(App);
//注册axios
app.use(VueAxios, axios);
//进行应用挂载
app.mount("#app");

需要注意,在导入我们自定义的组件之前进行vue-axios模块的导入。之后,可以任意一个组件为例,在其生命周期方法中编写如下代码进行请求测试:

let api =
  "http://apis.juhe.cn/simpleWeather/query?city=%E8%8B%8F%E5%B7%9E&key=0f8a4915ea99cfc2dfd9f51e1cxxx";
axios.get(api).then((response) => {
  console.log(response);
});

运行代码,打开浏览器控制台,你会发现请求并没有按照我们的预期方式成功完成,控制台会输出如下信息:

Access to XMLHttpRequest at 'http://apis.juhe.cn/simpleWeather/query?city=%E8%8B%8F%E5%B7%9E&key=0f8a4915ea99cfc2dfd9f51e1cxxx' from origin 'http://localhost:5173' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

出现此问题的原因是产生了跨域请求,我们在 Vite 创建的项目中更改全局配置即可解决此问题。打开项目目录下的vite.config.js文件,编写如下配置:

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  server: {
    proxy: {
      //"/myapi": "http://apis.juhe.cn/",
      //对以/myapi开头的请求进行代理
      "/myApi": {
        // 将请求目标指定到接口服务地址
        target: "http://apis.juhe.cn/",
        //设置允许跨域
        changeOrigin: true,
        //设置非HTTPS请求
        secure: false,
        // 路径重写,将/myApi(即之前的内容)清除
        rewrite: (path) => path.replace(/^\/myApi/, ""),
      },
    },
  },
});

修改请求数据的代码如下:

let city = ref("上海");
//对输入的城市名转码
let citys = encodeURI(city.value);

let api = `/simpleWeather/query?city=${citys}&key=0f8a4915ea99cfc2dfd9f51e1c555321`;
console.log(api);
axios.get("/myApi" + api).then((response) => {
  console.log(response.data);
});

如上代码所示,将请求的API接口前的地址强制替换成了字符串”/myApi”,这样请求就能进入我们配置的代理逻辑中。实现跨域请求还有一点需要注意,我们要请求的城市是上海,真正发起请求时,需要将城市进行 URI 编码。重新运行 Vue 项目,在浏览器控制台可以看到,已经能够正常地访问接口服务了,如下所示:

{
    "reason": "查询成功!",
    "result": {
        "city": "上海",
        "realtime": {
            "temperature": "6",
            "humidity": "69",
            "info": "阴",
            "wid": "02",
            "direct": "东南风",
            "power": "2级",
            "aqi": "135"
        },
        "future": [
            {
                "date": "2022-12-28",
                "temperature": "5/8℃",
                "weather": "阴转多云",
                "wid": {
                    "day": "02",
                    "night": "01"
                },
                "direct": "北风"
            },
            {
                "date": "2022-12-29",
                "temperature": "5/8℃",
                "weather": "多云转阴",
                "wid": {
                    "day": "01",
                    "night": "02"
                },
                "direct": "东北风转北风"
            },
            {
                "date": "2022-12-30",
                "temperature": "3/10℃",
                "weather": "多云转晴",
                "wid": {
                    "day": "01",
                    "night": "00"
                },
                "direct": "东北风"
            },
            {
                "date": "2022-12-31",
                "temperature": "4/10℃",
                "weather": "晴转多云",
                "wid": {
                    "day": "00",
                    "night": "01"
                },
                "direct": "东北风"
            },
            {
                "date": "2023-01-01",
                "temperature": "6/11℃",
                "weather": "阴转小雨",
                "wid": {
                    "day": "02",
                    "night": "07"
                },
                "direct": "东北风转北风"
            }
        ]
    },
    "error_code": 0
}

通过实例代码可以看到,使用vue-axios进行数据的请求非常简单,在组件内部直接使用axios.get方法即可发起GET请求,当然也可以使用axios.post方法来发起POST请求,此方法会返回Promise对象,后续可以获取请求成功后的数据或失败的原因。

11.2 vue-axios 实用功能介绍

本节将介绍vue-axios中提供的功能接口,这些 API 接口可以帮助开发者快速地对请进行配置和处理。

11.2.1 通过配置的方式进行数据请求

vue-axios中提供了许多快捷的请求方法,在上一节中我们使用的就是快捷方法。如果要直接进行GET请求,使用如下方法即可:

axios.get( url[,config])

其中 url 参数是要请求的接口;config参数是选填的,用来配置请求的额外选项。与此方法类似,vue-axios中还提供了下面的常用快捷方法:

//快捷发起POST请求,data设置请求参数
axios.post( url[,data[, config]])

//快捷发起DELETE请求
axios.delete( url[,config])

//快捷发起HEAD请求
axios.head( url[,config])

//快捷发起OPTIONS请求
axios.options( url[,config])

//快捷发起PUT请求
axios.put( url[,data[, config]])

//快捷发起PATCH请求
axios.patch( url[,data[, config]])

除了使用这些快捷方法外,也可以完全通过自己的配置来进行数据请求,示例如下:

let city = ref("上海");
//对输入的城市名转码
let citys = encodeURI(city.value);

let api = `/simpleWeather/query?city=${citys}&key=0f8a4915ea99cfc2dfd9f51e1c555321`;
console.log(api);

axios({
  method: "get",
  url: "/myApi" + api,
}).then((response) => {
  console.log(response.data);
});

通过这种方式进行的数据请求效果与使用快捷方法一致。需要注意,在配置时必须设置请求的method方法。

大多数时候,在同一个项目中,使用的请求很多配置都是相同的。对于这种情况,可以创建一个新的 axios 请求实例,之后所有的请求都使用这个实例来发起,实例本身的配置会与快捷方法的配置合并,这样既能够复用大多数相似的配置,又可以实现某些请求的定制化。示例如下:

//统一配置URL前缀、超时时间和自定义的header
const instance = axios.create({
  baseURL: "/myApi",
  timeout: 1000,
  headers: { "X-Custom-Header": "custom" },
});

let city = ref("上海");
//对输入的城市名转码
let citys = encodeURI(city.value);

let api = `/simpleWeather/query?city=${citys}&key=0f8a4915ea99cfc2dfd9f51e1c555321`;
console.log(api);
instance.get(api).then((response) => {
  console.log(response.data);
});

如果需要让某些配置作用于所有请求,即需要重设axios的默认配置,可以使用axiosdefaults属性进行配置,例如:

//属性配置
axios.defaults.baseURL = "/myApi";
let city = ref("上海");
//对输入的城市名转码
let citys = encodeURI(city.value);

let api = `/simpleWeather/query?city=${citys}&key=0f8a4915ea99cfc2dfd9f51e1c555321`;

axios.get(api).then((response) => {
  console.log(response.data);
});

在对请求配置进行合并时,会按照一定的优先级进行选择,优先级排序如下:

axios默认配置 < defaults属性配置 < 请求时的config参数配置

11.2.2 请求的配置与响应数据结构

在 axios 中,无论我们使用配置的方式进行数据请求还是使用快捷方法进行数据请求,都可以传一个配置对象来对请求进行配置,此配置对象可配置的参数非常丰富,如下表所示:

参数意义
url设置请求的接口 URL字符串
method设置请求方法字符串,默认为“get”
baseURL设置请求的接口前缀,会拼接在 URL 之前字符串
transformRequest用来拦截请求,在发起请求前进行数据修改函数,此函数会传入 data、headers 两个参数,将修改后的 data 进行返回即可
transformResponse用来拦截请求回执,在收到请求回执后会调用函数,此函数会传入 data 作为参数,将修改后的 data 进行返回即可
headers自定义请求头数对象
paramsSerializer自定义参数的序列化方法函数
data设置请求要发送的数据字符、对象、数组等
timeout设置请求的超时时间数值,单位为毫秒,若设置为 0 则永不超时
withCredentials设置跨域请求时是否需要凭证布尔值
auth设置用户信息对象
responseType设置响应数据的数据类型字符串,默认为“json”
responseEncoding设置响应数据的编码方式字符串,默认为“utf8”
maxContentLength设置允许响应的最大字节数数值
maxBodyLength设置请求内容的最大字节数数值
validateStatus自定义请求结束的状态是成功还是失败函数,会传入请求到 status 状态码作为参数,需要返回布尔值决定请求是否成功

通过上表列出的配置属性,基本可以满足各种场景下的数据请求需求。当一个请求被发出后,axios 会返回一个 Promise 对象,通过此 Promise 对象可以异步地等待数据的返回,axios 返回的数据是一个包装好的对象,其中包装的属性列举如下表:

属性意义
data接口服务返回的响应数据对象
status接口服务返回的 HTTP 状态码数值
statusText接口服务返回的 HTTP 状态信息字符串
headers响应头数据对象
configaxios 设置的请求配置信息对象
request请求实例对象

你可以尝试在浏览器中打印这些数据,观察这些数据中的信息。

11.2.3 拦截器的使用

拦截器的功能在于其允许开发者在请求发起前或请求完成后进行拦截,从而在这些时候添加一下定制化的逻辑。举一个很简单的例子,在请求发送前,可能需要激活页面的 Loading 特效,在请求完成后移出 Loading 特效,同时。如果请求的结果是异常的,可能还需要进行一个弹窗提示,而这些逻辑对于项目中的大部分请求来说都是通用的,这时就可以使用拦截器。

要对请求的开始进行拦截,示例如下:

//对请求的开始进行拦截
axios.interceptors.request.use(
  (config) => {
    alert("请求将要开始");
    return config;
  },
  (error) => {
    alert("请求出现错误");
    return Promise.reject(error);
  }
);

//属性配置
axios.defaults.baseURL = "/myApi";
let city = ref("上海");
//对输入的城市名转码
let citys = encodeURI(city.value);

let api = `/simpleWeather/query?city=${citys}&key=0f8a4915ea99cfc2dfd9f51e1c555321`;

axios.get(api).then((response) => {
  console.log(response.data);
});

运行上面的代码,在请求开始前会有弹窗提示。

也可以对请求完成后进行拦截,实例代码如下:

//请求完成后进行拦截
axios.interceptors.response.use(
  (response) => {
    alert(response.status);
    return response;
  },
  (error) => {
    return Promise.reject(error);
  }
);

//属性配置
axios.defaults.baseURL = "/myApi";
let city = ref("上海");
//对输入的城市名转码
let citys = encodeURI(city.value);

let api = `/simpleWeather/query?city=${citys}&key=0f8a4915ea99cfc2dfd9f51e1c555321`;

axios.get(api).then((response) => {
  console.log(response.data);
});

在拦截器中,也可以对响应数据进行修改,将修改后的数据返回给请求调用处使用。

需要注意,请求拦截器的添加是和 axios 请求实例绑定的,后续此实例发起的请求都会被拦截器拦截,但是可以用如下方式在不需要拦截器的时候将其移除:

//移除拦截器
let i = axios.interceptors.request.use(
  (config) => {
    alert("请求将要开始");
    return config;
  },
  (error) => {
    alert("请求出现错误");
    return Promise.reject(error);
  }
);
//执行以下代码即可移除拦截器
axios.interceptors.request.eject(i);

11.3 范例:实现一个天气应用

vite.config.js

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  server: {
    proxy: {
      //"/api": "http://t.weather.itboy.net/",
      //对以/api开头的请求进行代理
      "/myApi": {
        // 代理的目标地址
        target: "http://apis.juhe.cn/",
        //设置允许跨域
        changeOrigin: true,
        // 路径重写
        rewrite: (path) => path.replace(/^\/myApi/, ""),
      },
    },
  },
});

核心代码

<template>
  <el-container class="container">
    <el-header>
      <el-input placeholder="请输入" class="input" v-model.lazy="city">
        <template #prepend>城市名</template>
      </el-input>
    </el-header>

    <el-main class="main">
      <div class="today" v-for="todays in todayData">
        今天:
        <span>
          {{ todays.weather ?? plc }}
          {{ todays.temperature ?? plc }}
        </span>

        <span style="margin-left: 20px">{{ todays.direct ?? plc }}</span>
        <span style="margin-left: 100px">{{ todays.date }}</span>
      </div>

      <div class="real" v-for="time in realtime">
        <span class="temp">{{ time.temperature ?? plc }}℃</span>
        <span class="realInfo">{{ time.info ?? plc }}</span>
        <span class="realInfo" style="margin-left: 20px">{{
          time.direct ?? plc
        }}</span>
        <span class="realInfo" style="margin-left: 20px">{{
          time.power ?? plc
        }}</span>
      </div>

      <div class="real" v-for="time in realtime">
        <span class="realInfo">空气质量:{{ time.aqi ?? plc }}</span>
        <span class="realInfo" style="margin-left: 20px"
          >湿度:{{ time.humidity ?? plc }}</span
        >
      </div>

      <div class="future">
        <div class="header">5日天气预报</div>
        <el-table :data="futureData.value" style="margin-top: 30px">
          <el-table-column prop="date" label="日期"></el-table-column>
          <el-table-column prop="temperature" label="温度"></el-table-column>
          <el-table-column prop="weather" label="天气"></el-table-column>
          <el-table-column prop="direct" label="风向"></el-table-column>
        </el-table>
      </div>
    </el-main>
  </el-container>
</template>

<script setup>
import axios from "axios";
import { ref, reactive, onMounted, watchEffect } from "vue";

const city = ref("上海");
const weatherData = reactive({});
const todayData = reactive({});
const plc = ref("暂无数据");
const realtime = reactive({});
const futureData = reactive({});

onMounted(() => {
  //组件挂载时,进行默认数据的初始化
  requestData();
});

//方法
const requestData = () => {
  //对输入的城市名转码
  const citys = encodeURI(city.value);

  //重设默认的接口前缀,会拼接在URL之前
  axios.defaults.baseURL = "/myApi";
  const api = `simpleWeather/query?city=${citys}&key=0f8a4915ea99cfc2dfd9f51e1cxxx`;
  console.log(api);

  //通过axios拿到接口
  axios.get(api).then((response) => {
    weatherData.value = response.data;
    todayData.value = weatherData.value.result.future[0];
    realtime.value = weatherData.value.result.realtime;
    futureData.value = weatherData.value.result.future;
  });
};

watchEffect(() => {
  //当城市变化时,执行当前函数
  requestData();
});
</script>

<style scoped>
.container {
  background: linear-gradient(rgb(13, 104, 188), rgb(54, 131, 195));
}

.input {
  width: 300px;
  margin-top: 20px;
}

.today {
  font-size: 20px;
  color: white;
}

.temp {
  font-size: 79px;
  color: white;
}

.real {
  padding-top: 53px;
  text-align: initial;
}

.realInfo {
  color: white;
}

.future {
  margin-top: 40px;
}

.header {
  color: white;
  font-size: 27px;
}
</style>

11.4 CDN

引入

<script src="https://unpkg.com/axios/dist/axios.min.js"></script>

实例:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>VUE学习 - 数据请求</title>
  </head>

  <body>
    <!--载入vue.js-->
    <script src="https://unpkg.com/vue@next"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <div id="Application">
      您的名字是:{{data.name}} <br />
      您的ID是:{{data.id}}<br />
      您的年龄是:{{data.age}}<br />
      您的性别是:{{data.sex}}<br />
      <ul v-for="item in data.list">
        <li>{{item}}</li>
      </ul>
    </div>
    <script>
      const app = Vue.createApp({
        setup() {
          const data = Vue.ref("");
          axios.get("https://rouse.npc.ink/api/data.php").then((response) => {
            data.value = response.data;
          });

          return { data };
        },
      });

      app.mount("#Application");
    </script>
  </body>
</html>

PHP 接口文件

<?php
    header('Access-Control-Allow-Origin:*');
    header('Content-Type:application/json');//加上这行,前端那边就不需要var result = $.parseJSON(data);

    //创建数组
    $xcx_url=array(
        "name"=>"李长洲",
       "id"=>"9527",
       "age"=>"28",
       "sex"=>"man",
       "list" => array(
            "1"=>"您的list值为“1”",
            "2"=>"您的list值为“2”",
            "3"=>"您的list值为“3”",
            "4"=>"您的list值为“4”",
           )
    );

    //打印数组,供前端调用
    echo json_encode($xcx_url,true);
?>

解构赋值

我不知道这有啥用,但很有意思,可以了解一下

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>VUE学习 - 请求的数据解构赋值</title>
  </head>

  <body>
    <!--载入vue.js-->
    <script src="https://unpkg.com/vue@next"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <div id="Application">
      参考:Setup使用axios请求获取的值
      http://www.qb5200.com/article/533160.html<br />

      您的名字是:{{name}} <br />
      您的ID是:{{id}}<br />
    </div>
    <script>
      const app = Vue.createApp({
        setup() {
          //这里定义一个结构体,用来保存项目信息
          var Data = Vue.reactive({
            name: "",
            id: "",
            age: "",
          });

          axios.get("https://rouse.npc.ink/api/data.php").then((response) => {
            const { name, id, age } = response.data;
            Data.name = name;
            Data.id = id;
            Data.age = age;
          });
          return { ...Vue.toRefs(Data) };
        },
      });

      app.mount("#Application");
    </script>
  </body>
</html>

第十二章:Vue 路由管理

Vue 路由是用来管理页面切换或跳转的一种方式。Vue 十分适合用来创建单页面应用。所谓单页面应用,不是指“只有一个用户页面”的应用,而是从开发角度上讲的一种架构方式,单页面只有一个主应用入口,通过组件的切换来渲染不同的功能页面。当然,对于组件的切换,我们可以借助 Vue 的动态组件功能,单其管理起来非常麻烦,且不易维护。幸运的是,Vue 有配套的路由管理方案——Vue Router,使得我们可以更加自然地进行功能页面的管理。

  • Vue Router 模块的安装与简单应用
  • 动态路由、嵌套路由的用法
  • 路由的传参方法
  • 为路由添加导航守卫
  • Vue Router 的进阶用法

链接

12.1 Vue Router 的安装与简单使用

Vue Router 是 Vue 官方的路由管理器,与 Vue 框架本身深度契合。Vue Router 主要包含如下功能:

  • 路由支持嵌套
  • 可以模块化地进行路由配置
  • 支持路由参数、查询和通配符
  • 提供了视图过渡效果
  • 能够精准地进行导航控制

本节就来一起安装 Vue Router 模块,并对其功能进行简单的体验。

12.1.1 Vue Router 的安装

与 Vue 框架本身一样,Vue Router 支持使用 CDN 的方式引入,也支持使用 NPM 的方式进行安装。在本章示例中,我们使用 Vite 项目来做演示,将采用 NPM 的方式来安装,如果需要使用 CDN 的方式引入,地址如下:

<script src="https://unpkg.com/vue-router@4"></script>

使用 Vite 新建一个工程,终端在项目的根目录下执行以下命令来安装 Vue Router 模块:

npm install vue-router

稍等片刻,安装完成后在项目的 package.json 文件中会自动添加 Vue Router 的依赖,如下:

  "dependencies": {
    "vue": "^3.2.45",
    "vue-router": "^4.1.6"
  },

注意,你安装的 Vue Router 版本并不一定需要和书中一样,但要求是 4.x.x 系列的版本,因为大版本不同的 Vue Router 模块接口和功能差异较大。

做完这些准备工作,就可以尝试对 Vue Router 进行简单的使用了。

12.1.2 一个简单的 Vue Router 使用示例

我们一直在讲路由的作用是页面管理,在实际应用中,需要做的其实非常简单:将定义好的 Vue 组件绑定到指定的路由,然后通过路由指定在何时或何处渲染这个组件。

我们先在components文件夹下新建两个文件,分别命名为 Demo1.vue 和 Demo2.vue,在其中编写以下代码

<template>
  <h1>示例页面1 - Demo1</h1>
</template>
//Demo1.vue

<template>
  <h1>示例页面2 - Demo2</h1>
</template>
//Demo2.vue

Demo1 和 Demo2 这两个组件作为示例使用,非常简单。修改App.vue文件如下:

<script setup></script>

<template>
  <h1>Hello World</h1>
  <p>
    <!-- route-link是路由跳转组件,用to来指定要跳转的路由-->
    <router-link to="/demo1">页面1</router-link>
    <router-link to="/demo2">页面2</router-link>
  </p>
  <!-- router-view是路由页面的出口,路由匹配到的组件会渲染在此-->
  <router-view></router-view>
</template>

<style scoped></style>

如以上代码所示,router-link组件是一个自定义的链接组件,它比常规的 a 标签要强大很多,其允许在不重新加载页面的情况下更改页面的 URL。router-view用来渲染与当前 URL 对应的组件,我们可以将其放在任何位置,例如带顶部导航栏的应用,其页面主体内容部分就可以放置router-view组件,通过导航栏上按钮的切换来替换内容组件。

修改项目中的main.js文件,在其中进行路由的定义与注册,示例代码如下:

//导入Vue框架中的createApp方法
import { createApp } from "vue";
//导入样式文件
import "./style.css";
//导入自定义根组件
import App from "./App.vue";

//导入Vue Router模块中的两个方法
import { createRouter, createWebHashHistory } from "vue-router";
//导入路由需要用到的自定义组件
import Demo1 from "./components/Demo1.vue";
import Demo2 from "./components/Demo2.vue";

//挂载根组件
const app = createApp(App);
//定义路由
const routes = [
  { path: "/demo1", component: Demo1 },
  { path: "/demo2", component: Demo2 },
];

//创建路由对象
const router = createRouter({
  history: createWebHashHistory(),
  routes: routes,
});

//注册路由
app.use(router);

//进行应用挂载
app.mount("#app");

运行上面的代码,单击页面中的两个切换按钮,可以看到对应的内容组件也会发生切换。

12.2 带参数的动态路由

我们已经了解到,不同的路由可以匹配到不同的组件,从而实现页面的切换。有些时候,我们需要将同一类型的路由匹配到同一个组件,通过路由的参数来控制组件的渲染。例如对于“用户中心”这类组件,不同的用户渲染的信息是不同的,这时就可以通过为路由添加参数来实现。

12.2.1 路由参数匹配

我们先编写一个示例的用户中心组件,此组件非常简单,直接通过解析路由中的参数来显示当前用户的昵称和编号。在工程components文件夹下新建一个名为User.vue的文件,在其中编写如下代码:

<template>
  用户中心页面 - 带参数的动态路由
  <h1>用户中心</h1>
  <h3>姓名:{{ $route.params.username }}</h3>
  <h3>ID:{{ $route.params.id }}</h3>
</template>
//User.vue

如上代码所示,在组件内部可以使用$route属性获取全局的路由对象,路由中定义的参数可以在此对象的params属性中获取到,在main.js中定义路由如下:

//12_2_1-路由参数匹配
import User from "./components/User.vue";

//定义路由
const routes = [
  //12_2_1-路由参数匹配
  { path: "/user/:username/:id", component: User },
];
//main.js

在定义路由的路径path时,使用冒号来标记参数,如以上代码中定义的路由路径,usernameid都是路由参数,以下路径会被自动匹配:

/user/小王/8888

其中,"小王"会被解析到路由的username属性,“8888”会被解析到路由的 id 属性。

你可以在同一个路由中设置有多个 路径参数,它们会映射到 $route.params 上的相应字段。例如:

匹配模式匹配路径$route.params
/users/:username/users/eduardo{ username: 'eduardo' }
/users/:username/posts/:postId/users/eduardo/posts/123{ username: 'eduardo', postId: '123' }

现在,运行 Vite 工程,尝试在浏览器中输入如下格式的地址:

http://localhost:5173/#/user/小王/8888

需要注意,在使用带参数的路由时,对应相同组件的路由中进行导航切换时,相同的组件并不会被销毁再创建,这种复用机制使得页面的加载效率更高。但这也表明,页面切换时,组件的声明周期方法都不会被再次调用,如果需要通过路由参数来请求数据,之后渲染页面需要特别注意,不能在生命周期方法中进行数据请求逻辑。

因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会被调用

例如,我们修改User.vue的代码如下:

<script setup>
import { onMounted } from "vue";
import { useRoute } from "vue-router";
const route = useRoute();
onMounted(() => {
  //组件挂载时,进行默认数据的初始化
  let id = route.params.id;
  alert("你的路由ID是:" + id);
});
</script>
<template>
  <h2>用户中心页面 - 带参数的动态路由</h2>
  <h3>姓名:{{ $route.params.username }}</h3>
  <h3>ID:{{ $route.params.id }}</h3>
</template>

我们模拟中组件挂载时根据路由参数来进行数据的请求。运行代码可以看到,修改链接中ID的数值,User组件中显示的内容会实时更新,但是 alert 弹窗只有在User组件第一次加载时才会弹出,后续不会再弹出。

对于这种场景,我们可以参与导航守卫的方式来处理,每次路由参数有更新,都会回调守卫函数。

修改User.vue组件的代码如下:

<script setup>
import { watchEffect } from "vue";
import { useRoute, onBeforeRouteUpdate, onBeforeRouteLeave } from "vue-router";

//实例化
const route = useRoute();
// 与 beforeRouteLeave 相同,无法访问 `this`
onBeforeRouteLeave((to, from) => {
  const answer = window.confirm("你真的想离开吗?您有未保存的更改!");
  // 取消导航并停留在同一页面上
  if (!answer) return false;
});

// 与 beforeRouteUpdate 相同,无法访问 `this`
onBeforeRouteUpdate(async (to, from) => {
  //仅当 id 更改时才获取用户,例如仅 query 或 hash 值已更改
  if (to.params.id !== from.params.id) {
    let id = route.params.id;
    alert("你的用户ID是:" + id);
  }
});

// 当参数更改时获取用户信息

//监听
watchEffect(() => {
  //当数据有更改时执行
  let id = route.params.id;
  console.log("你的下一个用户ID是:", id);
});
</script>
<template>
  <h2>用户中心页面 - 带参数的动态路由</h2>
  <h3>姓名:{{ $route.params.username }}</h3>
  <h3>ID:{{ $route.params.id }}</h3>
</template>

再次运行代码,当同一个路由的参数发生变化时,也会有alert弹出提示。

http://localhost:5173/#/user/小王/666
http://localhost:5173/#/user/小红/888

beforeRouteUpdate函数在路由将要更新时会调用。


onBeforeRouteUpdate

添加一个导航守卫,在当前位置即将更新时触发。类似于 beforeRouteUpdate,但它可以在任何组件中使用。当组件被卸载时,导航守卫将被移除。

12.2.2 路由匹配的语法规则

在进行路由参数匹配时,Vue Router 允许参数内部使用正则表达式来进行匹配。首先来看一个例子。在上一节中,我们提供了User组件做路由示范,将其修改如下:

<script setup></script>
<template>
  <h1>用户中心</h1>
  <h3>姓名:{{ $route.params.username }}</h3>
</template>

同时,在components 文件夹下新建一个名为UserSetting.vue的文件,编写如下代码:

<script setup></script>
<template>
  <h1>用户设置</h1>
  <h3>ID:{{ $route.params.id }}</h3>
</template>

我们将User组件作为用户中心页面来使用,而UserSetting组件作为用户设置页面来使用。这两个页面所需要的参数不同,用户中心页面需要用户名参数,用户设置页面需要用户编号参数。我们可以从main.js文件中定义路由:

//定义路由
const routes = [
  //12_2_2路由匹配的语法规则
  { path: "/user/:username", component: User },
  { path: "/user/:id", component: UserSetting },
];

你会发现,上面的代码中定义的两个路由除了参数名不同外,格式完全一样。这种情况下,我们无法访问用户设置页面,所有符合UserSetting组件的路由规则同时也符合User组件。为了解决这一问题,最简单的方式是加一个静态的前缀路由,例如:

//定义路由
const routes = [
  //12_2_2路由匹配的语法规则
  { path: "/user/info/:username", component: User },
  { path: "/user/setting/:id", component: UserSetting },
];

这是一个好方法,但并不是唯一的方法。

对于本示例来说,用户中心页面和用户设置页面所需要的参数的类型有明显的差异,用户编号必须是数值,用户名则不能是数字。因此,可以通过正则约束来实现不同类型的参数匹配到对应的路由组件。

//定义路由
const routes = [
  //12_2_2路由匹配的语法规则
  { path: "/user/:username", component: User },
  { path: "/user/:id(\\d+)", component: UserSetting },
];

这样,对于“/user/6666“这样的路由就会匹配到UserSetting组件,”/user/小王“这样的路由机会匹配到 User 组件。

在正则表达式中,”*“可以用来匹配 0 个或多个,”+“可以用来匹配 1 个或多个。这样的路由定义时使用这两个符号可以实现多级参数。在components文件夹下新建一个名为Category.vue的示例组件,编写如下代码:

<script setup></script>
<template>
  <h1>类别</h1>
  <h2>{{ $route.params.cat }}</h2>
</template>

main.js中增加如下路由定义:

    //12_2_2路由多级匹配的语法规则
    { path: '/category/:cat*', component: Category },

Category组件的引入

//12_2_2路由多级匹配的语法规则
import Category from "./components/Cagetory.vue";

当我们采用多级匹配的方式来定义路由时,路由中传递的参数会自动转换成一个数组,例如路由”/Category/一级/二级/三级“可以匹配到上面定义的路由,匹配成功后,cat 参数为一个数组,其中数据为["一级", "二级", "三级"]

示例:

访问:http://localhost:5173/#/category/one/two/three
显示:[ "one", "two", "three" ]

有时候,页面组件所需要的参数并不都是必传的,以用户中心页面为例,当传了用户名参数时,其需要渲染登录后的用户中心状态,当没有传用户名参数时,其可能需要渲染未登录时的状态。此时,可以将username参数设为可选的,示例如下:

{ path: '/user/:username?', component: User },

参数被定义为可选后,路由中不包含此参数的时候也可以正常匹配到指定的组件。

12.2.3 路由的嵌套

前面定义了很多路由,但是真正渲染路由的地方只有一个,即只有一个<router-view></router-view>出口,这类路由实际上都是顶级路由。在实际开发中,我们的项目可能非常复杂,除了根组件中需要路由外,一些子组件中可能也需要路由,Vue Router 提供了嵌套路由技术来支持这类场景。

以之前创建的User组件为例,假设组件中有一部分用来渲染用户的好友列表,这部分也可以用组件来完成。首先在components文件夹下新建一个名为Friends.vue的文件,编写如下代码:

<script setup></script>
<template>
  <h1>好友列表</h1>
  <h1>好友人数:{{ $route.params.count }}</h1>
</template>

Friends 组件只会中用户中心使用,可以将其作为一个子路由进行定义。修改User.vue代码如下:

<script setup></script>
<template>
  <h1>用户中心</h1>
  <h3>姓名:{{ $route.params.username }}</h3>
  <router-view></router-view>
</template>

需要注意,User组件本身也是由路由管理的,在User组件内部再使用的 <router-view></router-view>标签实际上定义的撒二级路由的页面出口。在main.js中定义二级路由如下:

    //12_2_2路由嵌套
    {
        path: '/user/:username?',
        component: User,
        children: [
            {
                path: 'friends/:count',
                component: Friends,
            }
        ]
    },

引入组件

//12_2_2路由嵌套
import Friends from "./components/Friends.vue";

之前定义路由时都只使用了pathcomponent属性,其实每个路由对象本身也可以定义子路由对象。理论上讲,可以根据自己的需要来定义路由嵌套的层数,通过路由的嵌套,可以更好地对路由进行分层管理。

如上代码所示,当我们访问如下路径时,可看到页面效果。

/user/小王/friends/6

12.3 页面导航

导航本身是指页面间的跳转和切换。router-link组件就是一种导航组件。我们可以设置其属性to来指定要执行的路由。除了使用route-link组件外,还有其他的方式进行路由控制,任意可以添加交互方法的组件都可以实现路由管理。本节将介绍通过函数的方式进行路由跳转。

12.3.1 使用路由方法

当我们成功向 Vue 应用注册路由后,在任何 Vue 实例中,都可以通过$route属性访问路由对象。通过调用路由对象的push方法可以向history栈中添加一个新的记录。也就是说,用户可以通过浏览器的返回按钮返回上一个路由的 URL。

APP.vue中编写如下代码

<script setup>
import { useRouter } from "vue-router";
//实例化
const router = useRouter();
const toUser = () => {
  router.push({
    path: "/user/小王",
  });
};
</script>

<template>
  <h1>Hello World</h1>
  <p>
    <!-- route-link是路由跳转组件,用to来指定要跳转的路由-->
    <router-link to="/demo1">页面1</router-link><br />
    <router-link to="/demo2">页面2</router-link><br />
  </p>
  <!-- router-view是路由页面的出口,路由匹配到的组件会渲染在此-->
  <router-view></router-view>

  <p><button type="primary" @click="toUser">用户中心</button></p>
</template>

<style scoped></style>

如上代码所示,我们使用按钮组件代替之前的router-link组件,在按钮的单击方法中进行路由的跳转操作。push方法可以接收一个对象,对象中通过path属性配置其 URL 路径。push方法也支持直接传入一个字符串作为 URL 路径,代码如下:

<script setup>
import { useRouter } from "vue-router";
//实例化
const router = useRouter();
const toUser = () => {
  router.push("/user/小王");
};
</script>

也可以通过路由名加参数的方式让 Vue Router 自动生成 URL,要使用这种方法进行路由跳转,在定义路由的时候需要对路由进行命名,代码如下:

    //12_3_1使用路由方法
    {
        path: '/user/:username?',
        name: 'user',
        component: User,
    }

之后,可以使用如下方式进行路由跳转

<script setup>
import { useRouter } from "vue-router";
//实例化
const router = useRouter();
const toUser = () => {
  router.push({
    name: "user",
    params: {
      username: "小王",
    },
  });
};
</script>

如果路由需要查询参数,可以通过query属性进行设置,示例如下:

<script setup>
import { useRouter } from "vue-router";
//实例化
const router = useRouter();
const toUser = () => {
  router.push({
    path: "/user",
    query: {
      name: "xixi",
    },
  });
};
</script>

需要注意,在调用push方法配置路由对象时,如果设置了path属性,则params属性会被自动忽略。push方法本身也会返回一个Promise对象,可以用其来处理路由跳转成功之后的逻辑,示例如下:

<script setup>
import { useRouter } from "vue-router";
//实例化
const router = useRouter();
const toUser = () => {
  router
    .push({
      name: "user",
      params: {
        username: "小王",
      },
    })
    .then(() => {
      alert("跳转成功");
    });
};
</script>

12.3.2 导航历史控制

当我们使用route-link组件或push方法切换页面时,新的路由实际上会被放入history导航栈中,用户可以灵活地使用浏览器的前进和后退功能在导航栈路由中进行切换。对于有些场景,我们不希望导航栈中的路由增加,这时可以配置replace 参数或直接调用replace方法来进行路由跳转,这种方式跳转的页面会直接替换当前页面,即跳转前页面的路由从导航栈中删除。

<script setup>
import { useRouter } from "vue-router";
//实例化
const router = useRouter();
const toUser = () => {
  router.push({
    path: "/user/小王",
    replace: true,
  });
  router.replace({
    path: "/user/小王",
  });
};
</script>

Vue Router 提供了另一个方法,让我们可以灵活地选择跳转到导航栈中的某个位置,示例如下:

<script setup>
import { useRouter } from "vue-router";
//实例化
const router = useRouter();

//跳转到后一个记录
const go_after_one = () => {
  router.go(1);
};
//跳转到后3个记录
const go_after_three = () => {
  router.go(3);
};
//跳转到前一个记录
const go_first_one = () => {
  router.go(-1);
};
</script>

12.4 关于路由的命名

我们知道,在定义路由时,除了path之外,还可以设置name属性,name属性为路由提供了名称,使用名称进行路由切换比直接使用path进行切换有很明显的优势:避免硬编码 URL、可以自动处理参数的编码等。

12.4.1 使用名称进行路由切换

与使用path路径进行路由切换类似,router-link组件和path方法都可以根据名称进行路由切换。以前面编写的代码为例,定义用户中心的名称为user,使用如下方法可以直接进行切换:

<script setup>
import { useRouter } from "vue-router";
//实例化
const router = useRouter();
const toUser = () => {
  router.push({
    name: "user",
    params: {
      username: "小王",
    },
  });
};
</script>

使用router-link组件切换的示例如下:

<router-link
  :to="{ name: 'user', params: { username: '小王' } }"
>小王</router-link>

12.4.2 路由视图命名

路由视图命名是指对router-view组件进行命名,router-view组件用来定义路由组件的出口,前面我们讲过,路由支持嵌套,router-view可以进行嵌套。通过嵌套,允许我们的 Vue 应用中出现多个router-view组件。但是对于有些场景,可能需要同级地展示多个路由视图,例如顶部导航区和主内容区两部分都需要使用路由组件,这时候就需要同级地使用router-view组件,要定义同级的每个router-view要展示的组件,可以对其进行命名。

修改App.vue文件,将页面的布局氛围头部和内容主体两部分,代码如下:

<script setup>
import { useRouter } from "vue-router";
//实例化
const router = useRouter();
const toUser = () => {
  router.push({
    name: "user",
    params: {
      username: "小王",
    },
  });
};
</script>

<template>
  <router-link to="/">首页</router-link> <br />
  <router-link to="/home/小王/666">小王-666</router-link>
  <!-- router-view是路由页面的出口,路由匹配到的组件会渲染在此-->
  <router-view name="topBar"></router-view>
  <hr />
  <router-view name="main"></router-view>
</template>

<style scoped></style>

main.js文件中定义一个新的路由,设置如下:

    //12_4_2 路由视图命名
    {
        path: '/home/:username/:id',
        components: {
            topBar: User,
            main: UserSetting
        }
    },

之前定义路由时,一个路径只对应一个组件,其实也可以通过components来设置一组组件,components需要设为一个对象,其中的键表示页面中路由视图的名称,值为要渲染的组件。在上面的例子中,页面的头部会被渲染未User组件,主体部分会被渲染未UserSetting组件,访问如下链接即可看到效果

/home/小王/666

需要注意,对于没有命名的router-view组件,其名字会被默认分配为default,如果编写组件模版如下:

<template>
  <router-link to="/">首页</router-link> <br />
  <router-link to="/home/小王/666">小王-666</router-link>
  <!-- router-view是路由页面的出口,路由匹配到的组件会渲染在此-->
  <router-view name="topBar"></router-view>
  <hr />
  <router-view></router-view>
</template>

使用如下方式定义路由效果是一样的:

    //12_4_2 路由视图命名
    {
        path: '/home/:username/:id',
        components: {
            topBar: User,
            default: UserSetting
        }
    },

在嵌套的子路由中,也可以使用视图命名路由,对于结构复杂的页面,可以先将其按照模块进行拆分,梳理清晰路由的组织关系再进行开发。

12.4.3 使用别名

别名提供了一种路由路径映射的方式,也就是我们可以自由地将组件映射到一个任意的路径上,而不用受到嵌套结构的限制。

首先,可以尝试为一个简单的一级路由来设置别名,修改用户设置页面的路由定义如下:

    //12_4_3 使用别名
    { path: '/user/:id(\\d+)', component: UserSetting, alias: '/setting/:id' },

之后,下面两个路径的页面渲染效果将完全一样:

#/user/666
#/setting/666

需要注意,别名和重定向并不完全一样,别名不会改变用户在浏览器中输入的路径本身,对于多级嵌套的路由来说,可以使用别名在路径上对其进行简化。如果原路由有参数配置,一定要注意别名也需要对应地包含这些参数。在为路由配置别名时,alias属性可以直接设置为别名字符串,也可以设置为数组同时配置一组别名。例如:

    //12_4_3 使用别名
    { path: '/user/:id(\\d+)', component: UserSetting, alias: ['/setting/:id', '/s/:id'] },

12.4.4 路由重定向

重定向也是通过路由配置来完成的,与别名的区别在于,重定向会将当前路由映射到另一个路由上,页面的 URL 会产生改变。例如当用户访问路由‘d/1’时,需要页面渲染‘/demo1’路由对应的组件,配置方式如下:

    //12_4_4 路由重定向
    { path: '/demo1', component: Demo1 },
    { path: '/d/1', redirect: '/demo1' },

redirect 也支持配置为对象,设置对象的name属性可以直接指定命名路由,例如:

    //12_4_4 路由重定向
    { path: '/demo1', component: Demo1, name: 'Demo1' },
    { path: '/d/1', redirect: { name: 'Demo1' } },

上面的示例代码都是采用静态的方式配置路由重定向的,在实际开发中,更多时候会采用动态的方式配置重定向,例如对于需要用户登录才能访问的页面,当未登录的用户访问此路由时,我们自动将其重定向到登录页面,示例

    //12_4_4 路由重定向
    { path: '/demo1', component: Demo1, name: 'Demo1' },
    { path: '/demo2', component: Demo2,  },
    {
        path: '/d', redirect: to => {
            console.log(to)//to是路由对象
            //随机数模拟登录状态
            let login = Math.random() > 0.5
            if (login) {
                return { path: '/demo1' }
            } else {
                return { path: '/demo2' }
            }
        }
    },

12.5 关于路由传参

通过前面的学习,我们对 Vue Router 的基本使用已经有了初步的了解,在进行路由跳转时,可以通过参数的传递来进行后续的逻辑处理。在组件内部,之前使用$route.params的方式来获取路由传递的参数,这种方式虽然可行,但组件与路由紧紧地耦合在了一起,并不利于组件的复用性。本节来讨论一下路由的另一种传参方式——使用属性的方式进行参数传递。

还记得我们编写的用户设置页面是如何获取路由传递的 ID 参数的吗?代码如下:

<script setup></script>
<template>
  <h1>用户设置</h1>
  <h3>ID:{{ $route.params.id }}</h3>
</template>

由于之前在组件的模版内部使用了$route属性,这导致此组件的通用性大大降低,首先将其所有耦合路由的地方去除掉,修改如下:

<script setup>
defineProps({
  id: Number,
});
</script>
<template>
  <h1>用户设置</h1>
  <h3>ID:{{ id }}</h3>
</template>

现在,UserSetting组件能够通过外部传递的属性来做内部逻辑,后面需要做的只是将路由的传参映射到外部属性上。Vue Router 默认支持这一功能,路由配置方式如下:

    //12_5 路由传参
    { path: '/user/:id(\\d+)', component: UserSetting, props: true },

在定义路由时,将props设置为true,则路由中传递的参数会自动映射到组件定义的外部属性。使用十分方便。

对于有多个页面出口的同级命名视图,我们需要对每个视图的props单独进行设置,示例如下:

    {
        path: '/home/:username/:id',
        components: {
            topBar: User,
            default: UserSetting
        },
        props: {
            topBar: true,
            default: true
        }
    },

如果组件内部需要的参数与路由本身并没有直接关系,也可以将 props 设置为对象,此时 props 设置的数据将原样传递给组件的外部属性,例如:

    { path: '/user/:id(\\d+)', component: UserSetting, props: { id: '000' } },

如以上代码所示,此时路由中的参数将被弃用,组件中获取到的 id 属性值将固定为”000“。

props还有一种更强大的使用方式,可以直接将其设置为一个函数,函数中返回要传递到组件的外部属性对象,这种方式动态性很好,示例如下:

    //12_5 路由传参
    {
        path: '/user/:id(\\d+)', component: UserSetting, props: route => {
            return {
                id: route.params.id,
                other: 'other'
            }
        }
    },

调用:

<script setup>
defineProps({
  id: Number,
  other: String,
});
</script>
<template>
  <h1>用户设置</h1>
  <h3>ID:{{ id }}</h3>
  {{ other }}
</template>

12.6 路由导航守卫

导航守卫的主要作用是中进行路由跳转时决定通过此次跳转或拒绝此次跳转。在 Vue Router 中有多种方式来定义导航守卫。

12.6.1 定义全局的导航守卫

main.js文件中,我们使用createRouter方法来创建路由实例,此路由实例可以使用beforeEach方法来注册全局的前置导航守卫,之后当触发导航跳转时,都会被此导航守卫捕获,示例如下:

//创建路由对象
const router = createRouter({
  history: createWebHashHistory(),
  routes: routes, //我们定义的路由配置对象
});

//定义全局的前置导航守卫
router.beforeEach((to, from) => {
  console.log(to); //将要跳转到的路由对象
  console.log(from); //当前将要离开的路由对象
  return false; //返回true表示允许此次跳转,返回false表示禁止此次跳转
});

当注册的beforeEach方法返回的是布尔值时,其用来决定是否允许此次跳转,如以上代码所示,所有的路由跳转都将被禁止。

更多时候,我们会在beforeEach方法中返回一个路由配置对象来决定要跳转的页面,这种方式更加灵活,例如可以将登录状态校验逻辑放在全局的前置守卫中处理,非常方便,示例如下:

    //12_6_1 定义全局的导航守卫
    { path: '/user/:id(\\d+)', name: 'setting', component: UserSetting, props: true },


//创建路由对象
const router = createRouter({
    history: createWebHashHistory(),
    routes: routes//我们定义的路由配置对象
})

//定义全局的前置导航守卫
router.beforeEach((to, from) => {
    console.log(to) //将要跳转到的路由对象
    console.log(from)//当前将要离开的路由对象
    if (to.name != 'setting') {//防止无限循环
        return { name: 'setting', params: { id: '000' } }//返回要跳转的路由
    }
    //返回true表示允许此次跳转,返回false表示禁止此次跳转
})

与定义全局前置守卫类似,也可以注册全局的导航后置回调。与前置守卫不同的是,后置回调不会改变导航本身,但是其对页面的分析和监控十分有用。示例如下:

//定义全局的后置导航守卫
router.afterEach((to, from, failure) => {
  console.log("跳转结束");
  console.log(to);
  console.log(from);
  console.log(failure);
});

路由实例的afterEach方法中设置的回调函数除了接收tofrom参数外,还会接收一个failure参数,通过它,开发者可以对导航的异常信息进行记录。

12.6.2 为特定的路由注册导航守卫

如果只有特定的场景需要在页面跳转过程中实现相关逻辑,也可以为指定的路由注册导航守卫。有两种注册方式,一种是中配置路由时进行定义,另一种是在组件中进行定义。

在对导航进行配置时,可以直接为其设置为bforeEnter属性,示例如下:

    //12_6_2 为特定的路由注册导航守卫
    {
        path: '/demo1', component: Demo1, name: 'Demo1', beforeEnter: router => {
            console.log(router)
            return false
        }
    },

如以上代码所示,当用户访问“/demo1”路由对应的组件时都会被拒绝掉。需要注意,beforeEnter设置的守卫只有中进入路由时会触发,路由的参数变化并不会触发此守卫。

在编写组件时,也可以实现一些方法来为组件定制守卫函数,示例代码如下:

<script setup>
import { onBeforeRouteLeave, onBeforeRouteUpdate } from "vue-router";

onBeforeRouteUpdate((to, from) => {
  console.log(to, from, "路由参数有更新时的守卫");
});
onBeforeRouteLeave((to, from) => {
  console.log(to, from, "离开页面");
});
</script>
<template>
  <h1>示例页面1 - Demo1</h1>
</template>

如上代码所示,beforeRouteEnter是组件的导航前置守卫,在通过路由将要切换到当前组件时调用,在这个函数中,可以做拦截操作,也可以做重定向操作,需要注意此方法只有中第一次切换此组件时会被调用,路由参数的变化不会重复调用此方法。

beforeRouteUpdata方法在当前路由发生变化时会被调用,例如路由参数的变化等都可以在此方法中捕获到。beforeRouteLeave方法会在将要离开当前页面时被调用。

还有一点需要注意,在beforeRouteEnter方法中不能使用 this 获取当前组件实例,因为在导航守卫确认通过前,新的组件还没有被创建。如果一定要在导航被确认时使用当前组件实例处理一些逻辑,可以通过next参数注册回调方法,示例如下:

            beforeRouteEnter(to, from, next) {
                // 在渲染该组件的对应路由被验证前调用
                // 不能获取组件实例 `this` !
                // 因为当守卫执行时,组件实例还没被创建!
                console.log(to, from, "前置守卫");
                next((w) => {
                    console.log(w);//w为当前组件实例
                })
                return true;
            },

当前置守卫确认了此次跳转后,next参数注册的回调方法会被执行,并且会将当前组件的实例作为参数传入。在beforeRouterUpdatabeforeRouteLeave方法中可以直接使用this关键字来获取当前组件实例,无需额外操作。

下面来总结 Vue Router 导航跳转的全过程。

完整的导航解析流程

  1. 导航被触发可以通过 router-link 组件触发,也可以通过$route.push 或直接改变 URL 触发。
  2. 在将要失活的组件里调用 beforeRouteLeave 守卫。
  3. 调用全局注册的 beforeEach 守卫。
  4. 在重用的组件(当前使用的组件无变化)里调用组件内的 beforeRouteUpdate 守卫(2.2+)。
  5. 调用在定义路由时配置的 beforeEnter守卫函数。
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter守卫。
  8. 调用全局的 beforeResolve 守卫(2.5+)。
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 触发 DOM 更新。页面进行更新
  12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

12.7 动态路由

截止目前,我们所有使用到的路由都是采用静态配置的方式定义的,即先在main.js中完成路由配置,再在项目中使用。但某些情况下,可能需要在运行的过程中动态添加或删除路由,Vue Router 中也提供了方法支持动态地对路由进行操作。

12.7.1 动态添加与删除路由

在 Vue Router 中,动态操作路由的方法主要有两个:addRouteremoveRouteaddRoute用来动态添加一条路由,对应的removeRoute用来动态删除一条路由。

首先,修改Demo1.vue 文件如下:

<script setup>
import Demo2 from "./Demo2.vue";
import { useRouter } from "vue-router";
//实例化
const router = useRouter();
router.addRoute({
  path: "/demo2",
  component: Demo2,
});
const click = () => {
  router.push("/demo2");
};
</script>
<template>
  <h1>示例页面1 - Demo1</h1>
  <button type="button" @click="click">跳转Demo2</button>
</template>

我们在Demo1组件中布局了一个按钮元素,在Demo1组件创建完成后,使用addRoute方法动态添加了一条路由,当单击页面上的按钮时,切换到Demo2组件。

修改main.js文件中配置路由的部分如下:

const routes = [{ path: "/demo1", component: Demo1 }];

//创建路由对象
const router = createRouter({
  history: createWebHashHistory(),
  routes: routes, //我们定义的路由配置对象
});

可以尝试下,如果直接在浏览器中访问“/demo2”页面会报错,因为此时注册的路由列表中并没有此项路由记录,但是如果先访问“/demo1”页面,再单击页面上的按钮进行路由跳转,则能构正常跳转。

在下面几种场景下会触发路由的删除。

当使用addRoute方法动态添加路由时,如果添加了重名的路由,旧的会被删除,例如

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

上面的代码中,路径为“/demo”的路由将会被删除。

在调用 addRoute 方法是,它其实会返回一个删除回调,我们也可以通过此删除回调来直接删除所添加的路由,代码如下:

<script setup>
import Demo2 from "./Demo2.vue";
import { useRouter } from "vue-router";
//实例化
const router = useRouter();
let call = router.addRoute({
  path: "/demo2",
  name: Demo2,
  component: Demo2,
});
//直接移除此路由
call();

const click = () => {
  router.push("/demo2");
};
</script>

另外,对于已经命名的路由,也可以通过名称来对路由进行删除,示例如下:

<script setup>
import Demo2 from "./Demo2.vue";
import { useRouter } from "vue-router";
//实例化
const router = useRouter();
router.addRoute({
  path: "/demo2",
  name: Demo2,
  component: Demo2,
});
//移除此路由
router.removeRoute("Demo2");


const click = () => {
  router.push("/demo2");
};

</script>

需要注意,当路由被删除时,其所有的别名和子路由也会被同步删除。

在 Vue Router 中,还提供了方法来获取现有路由,例如:

console.log(router.hasRoute("Demo1"));
console.log(router.getRoutes());

其中,hasRouter 方法用来检查当前已经注册的路由中是否包含某个路由,getRouter 方法用来获取包含所有路由的列表。

12.8 HTML 版路由实现

12.8.1 一个简单的 Vue Router 使用示例

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>12_1_2-一个简单的Vue Router使用示例</title>
  </head>

  <body>
    <script src="https://unpkg.com/vue@3"></script>
    <script src="https://unpkg.com/vue-router@4"></script>

    <div id="app">
      <h1>你好呀,路由!</h1>
      <!--使用 router-link 组件进行导航 -->
      <!--通过传递 `to` 来指定链接 -->
      <!--`<router-link>` 将呈现一个带有正确 `href` 属性的 `<a>` 标签-->
      <router-link to="/demo1">页面一</router-link> <br />
      <router-link to="/demo2">页面二</router-link> <br />
      <!-- 路由出口 -->
      <!-- 路由匹配到的组件将渲染在这里 -->
      <router-view></router-view>
    </div>
    <script>
      // 1. 定义路由组件.
      // 也可以从其他文件导入
      const Demo1 = { template: "<div>示例页面1 - Demo1</div>" };
      const Demo2 = { template: "<div>示例页面2 - Demo2</div>" };

      // 2. 定义一些路由
      // 每个路由都需要映射到一个组件。
      const routes = [
        { path: "/demo1", component: Demo1 },
        { path: "/demo2", component: Demo2 },
      ];
      // 3. 创建路由实例并传递 `routes` 配置
      // 你可以在这里输入更多的配置,但我们在这里
      // 暂时保持简单
      const router = VueRouter.createRouter({
        // 4. 内部提供了 history 模式的实现。为了简单起见,我们在这里使用 hash 模式。
        history: VueRouter.createWebHashHistory(),
        routes, // `routes: routes` 的缩写
      });

      // 5. 创建并挂载根实例
      const app = Vue.createApp({});
      //确保 _use_ 路由实例使
      //整个应用支持路由。
      app.use(router);

      app.mount("#app");

      // 现在,应用已经启动了!
    </script>
  </body>
</html>

12.8.2 动态添加路由

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>12_7_1-1-动态添加路由</title>
  </head>

  <body>
    <script src="https://unpkg.com/vue@3"></script>
    <script src="https://unpkg.com/vue-router@4"></script>

    <div id="app">
      <h1>你好呀,路由!</h1>
      <!--使用 router-link 组件进行导航 -->
      <!--通过传递 `to` 来指定链接 -->
      <!--`<router-link>` 将呈现一个带有正确 `href` 属性的 `<a>` 标签-->
      <router-link to="/">回到首页</router-link> <br />
      <router-link to="/user/小王">用户名页 - 小王</router-link> <br />
      <router-link to="/user/666">用户ID页 - 666</router-link> <br />
      <!-- 路由出口 -->
      <!-- 路由匹配到的组件将渲染在这里 -->
      <router-view></router-view>
    </div>
    <script>
      // 1. 定义路由组件.
      // 也可以从其他文件导入
      const Home = { template: "<div>欢迎来到首页</div>" };
      //const Demo1 = {
      //    template: '<h1>这里是用户名页面</h1><h2>您的用户名是:{{ $route.params.name }}</h2><button type="button" @click="click">跳转Demo2页面</button>',
      //}
      const Demo1 = {
        setup() {
          //实例化
          const router = VueRouter.useRouter();
          router.addRoute({
            path: "/user/:id(\\d+)",
            component: Demo2,
          });
          const click = () => {
            router.push("/user/888");
          };

          return {
            click,
          };
        },
        template:
          '<h1>Demo1-这里是用户名页面</h1><h2>您的用户名是:{{ $route.params.name }}</h2><button type="button" @click="click">跳转Demo2页面</button>',
      };

      const Demo2 = {
        template:
          "<h1>Demo2-这里是设置页面</h1><h2>您的ID是:{{ $route.params.id }}</h2>",
      };

      // 2. 定义一些路由
      // 每个路由都需要映射到一个组件。
      const routes = [
        { path: "/", component: Home },
        { path: "/user/:name", component: Demo1 },
        //{ path: '/user/:id(\\d+)', component: Demo2 },
      ];
      // 3. 创建路由实例并传递 `routes` 配置
      // 你可以在这里输入更多的配置,但我们在这里
      // 暂时保持简单
      const router = VueRouter.createRouter({
        // 4. 内部提供了 history 模式的实现。为了简单起见,我们在这里使用 hash 模式。
        history: VueRouter.createWebHashHistory(),
        routes, // `routes: routes` 的缩写
      });

      // 5. 创建并挂载根实例
      const app = Vue.createApp({});
      //确保 _use_ 路由实例使
      //整个应用支持路由。
      app.use(router);

      app.mount("#app");

      // 现在,应用已经启动了!
    </script>
  </body>
</html>

小结

路由技术在实际项目开发中应用广泛,随着网页应用的功能越来越强大,前端代码也将越来越复杂,因此如何高效清晰地根据业务模块组织代码变得十分重要。路由就是一种非常优秀的页面组织方式,通过路由可以将页面按照组件的方式进行拆分,组件内只关注内部业务逻辑,组件间通过路由来进行交互和跳转。

通过本章学习,相信你已经有了开发大型前端应用的基本能力,可以尝试模仿流行的互联网应用,通过路由来搭建一些页面进行联系。

练习:在实际的应用开发中,只有一个页面的应用很少,页面间的组织与管理非常重要。你能简述路由管理在前端开发中解决了什么问题吗?

温馨提示:可以从解耦、模块化等方面进行分析。

第十三章:Vue 状态管理 Pinia

首先,Vue 框架本身就有状态管理能力的,我们在开发 Vue 应用页面时,视图上渲染的数据就是通过状态来驱动的。本章主要讨论基于 Vue 的状态管理框架 Pinia。Pinia 是一个=专为 Vue 定制的状态管理模块,其集中式地存储和管理应用的所有组件状态,使这些状态数据可以按照我们预期的方式变化。

当然,并非所有 Vue 应用的开发都需要使用 Pinia 来进行状态管理,对于小型的、简单的 Vue 应用,我们使用 Vue 自身的状态管理功能就已经足够,但是对于复杂度较高、组件繁多的 Vue 应用,组件间的交互会使得状态的管理变的困难,这时就需要 Pinia 的帮助了。

学习

  • Pinia 框架的安装与简单使用
  • 多组件共享状态的管理方法
  • 多组件驱动同一状态的方法

网址

13.1 认识 Pinia 框架

Pinia 采用集中式的方式管理所有组件的状态,相较于”集中式“而言,Vue 本身对状态的管理是”独立式“的,即每个组件只负责维护自身的状态。

13.1.1 关于状态管理

我们先从一个简单的示例组件来理解状态管理,使用 Vite 新建一个 Vue 项目工程,修改 APP.vue 代码如下:

<script setup>
import HelloWorld from "./components/HelloWorld.vue";
</script>

<template>
  <HelloWorld msg="Vite + Vue" />
</template>

修改 HelloWorld.vue 文件如下:

<script setup>
import { ref } from "vue";
const count = ref(0);
</script>

<template>
  <div class="card">
    <button type="button" @click="count++">count is {{ count }}</button>
  </div>
</template>

上面的代码逻辑非常简单,页面上渲染了一个按钮组件和一个文本标题,当用户单击按钮时,标题上的显示的计数器会自增。分析上面的代码可以发现,在 Vue 应用中,组件状态的管理由如下几部分组成:

1.状态数据

状态数据是指定义的 count 中返回的数据,这些数据是响应式的,由其来对视图的展现进行驱动。

2.视图

视图是指 template 里面定义的视图模版,其通过声明的方式将状态映射到视图上。

3.动作

动作是指会引起状态变化的行为,即上面代码中@click="count++"定义的方法,这些方法用来改变状态数据,状态数据的改动最终驱动视图的刷新。

上面 3 部分的协同工作就是 Vue 状态管理的核心。总体来看,在这个状态管理模式中,数据的流向是单向的、私有的、由视图触发动作,由动作改变状态,由状态驱动视图。

flowchart TB

id1((视图))-->id2((动作))-->id3((状态))-->id1((视图))

单向数据流这种状态管理模式非常简洁,对于组件不多的简单 Vue 应用来说,这种模式非常搞笑,但是对于多组件复杂交互的场景,使用这种方式进行状态管理就会比较困难。

我们思考下面两种状况:

(1)有多个组件依赖于同一状态

(2)多个组件都可能触发动作变更一个状态

对于状况(1),使用上面所述的状态管理方法很难实现吗,对于嵌套的多个组件,我们还可以通过传值的方式来传递状态,但是对于多个平级的多个组件,共享同一个状态是非常困难的。

对于状况(2),不同的组件若要更改同一个状态,最直接的方式是将触发动作交给上层,对于多层嵌套的组件,则需要一层一层地向上传递事件,在最上层统一处理状态的变更,这会使代码的维护难度大大增加。

Pinia 就是基于这种应用场景产生的,在 Pinia 中,可以将需要组件间共享的状态抽取出来,以一个全局的单例模式进行管理。在这种模式下,视图无论在视图树中的那个位置,都可以直接获取这些共享的状态,也可以直接触发修改动作来动态改变这些共享状态。

13.1.2 安装与体验 Pinia

与前面我们使用过的模块的安装方式类似,使用 npm 命令可以非常方便地为工程安装 Pinia 模块,终端命令如下:

npm install pinia

在安装过程中,如果有权限相关错误产生,可在命令前添加 sudo。安装完成后,即可在工程的 package.json 文件中看到相关依赖配置。

  "dependencies": {
    "pinia": "^2.0.28",
    "vue": "^3.2.45"
  },

在 main.js 文件中编写如下代码:

import { createApp } from "vue";
import "./style.css";
import App from "./App.vue";
import { createPinia } from "pinia";

const pinia = createPinia();
const app = createApp(App);
app.use(pinia);
app.mount("#app");

我们尝试下 Pinia 状态管理的基本功能,仿照 HelloWorld 组件创建一个新的组件,HelloWorld2.vue,功能是一个简单的计数器,代码如下:

<script setup>
import { ref } from "vue";
const count = ref(0);
const click = () => {
  count.value++;
};
</script>

<template>
  <h2>简单的计数器</h2>

  <button type="button" @click="click">增加 {{ count }}</button>
</template>

修改 App.vue 为以下代码

<script setup>
import HelloWorld2 from "./components/HelloWorld2.vue";
</script>

<template>
  <HelloWorld2 />
  <HelloWorld2 />
</template>

运行此工程,此时,这两个计数器组件是相互独立的,即单击第一个按钮只会增加第一个计数器的值,单击第二个按钮,只会增加第二个按钮的值。如果需要让这两个计数器共享一个状态,且同时操作此状态,则需要 Pinia 出马。

Pinia 的核心是 store,即仓库。简单理解,store 本身就是一个容器,其内存储和管理的应用中需要多组件共享的状态。

Pinia 中的 store 很强大,其中存储的状态是响应式的,若 store 中的状态数据发生变化,其会自动反映到对应的组件视图上。

并且,store 中的状态数据并不允许开发者直接进行修改,改变 store 中状态数据的唯一办法是提交 mutation 操作,

通过这样严格的管理,可以更加方便地追踪每一个状态的变化过程,帮助我们进行应用调试。

我们使用 Pinia 对上面的代码进行改写,

在项目根目录下 src 文件夹下新建文件夹 store,store 文件夹下新建文件 index.js,写入以下代码:

import { defineStore } from "pinia";
import { ref } from "vue";

export const mainStore = defineStore("main", () => {
  let msg = ref("Hello world");
  let count = ref(0);
  const click = () => {
    count.value++;
  };
  return { msg, count, click };
});

之后就可以在组件中共享 count 的状态了,并且通过提交 increment 操作来修改此状态,修改 HelloWorld.vue 代码如下:

<script setup>
import { ref } from "vue";
import { mainStore } from "../store/index";
const count = ref(0);

const store = mainStore();
</script>

<template>
  <h2>{{ store.msg }}</h2>
  <h5>未用store</h5>
  <div class="card">
    <button type="button" @click="count++">count is {{ count }}</button>
  </div>

  <h5>用了store</h5>
  <!--两个点击按钮共享一个数据-->
  <button type="button" @click="store.click">
    简单计数:{{ store.count }}
  </button>
</template>

修改 App.vue 文件为如下代码:

<script setup>
import HelloWorld from "./components/HelloWorld.vue";
</script>

<template>
  <HelloWorld />
  <HelloWorld />
</template>

可以看到,在组件中使用$store 属性可以直接获取到 store 实例,此实例的 state 属性中存储着所有共享的状态数据,且是响应式的,可以直接绑定到组件的视图进行使用。当需要对状态进行修改时,需要调用 store 实例的 commit 方法来提交变更操作,在这个方法中直接传入要执行更改操作的方法名即可。

再次运行工程,你会发现页面上的两个计数器的状态已经能够联动起来。后续,我们将讨论更多 Pinia 的核心

13.2 Pinio 中的一些核心概念

讨论 Pinia 中的 4 个核心概念

13.2.1 状态 state

我们知道,状态实际上就是应用中组件需要共享的数据,在 Pinia 中采用单一状态树来存储状态数据,也就是说我们的数据源是唯一的。在任何组件中,都可以使用如下方式来获取任何一个状态树中的数据:

store.count;

在深入研究核心概念之前,我们得知道 Store 是用 defineStore() 定义的,它的第一个参数要求是一个独一无二的名字:

import { defineStore } from "pinia";

// 你可以对 `defineStore()` 的返回值进行任意命名,但最好使用 store 的名字,同时以 `use` 开头且以 `Store` 结尾。(比如 `useUserStore`,`useCartStore`,`useProductStore`)
// 第一个参数是你的应用中 Store 的唯一 ID。
export const useStore = defineStore("main", {
  // 其他配置...
});

这个名字 ,也被用作 id ,是必须传入的, Pinia 将用它来连接 store 和 devtools。为了养成习惯性的用法,将返回的函数命名为 use... 是一个符合组合式函数风格的约定。

defineStore() 的第二个参数可接受两类值:Setup 函数或 Option 对象。

Setup Store

也存在另一种定义 store 的可用语法。与 Vue 组合式 API 的 setup 函数 相似,我们可以传入一个函数,该函数定义了一些响应式属性和方法,并且返回一个带有我们想暴露出去的属性和方法的对象。

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  function increment() {
    count.value++
  }

  return { count, increment }
})

Setup Store 中:

  • ref() 就是 state 属性
  • computed() 就是 getters
  • function() 就是 actions

Setup store 比 Option Store 带来了更多的灵活性,因为你可以在一个 store 内创建侦听器,并自由地使用任何组合式函数。不过,请记住,使用组合式函数会让 SSR 变得更加复杂。

使用 Store

虽然我们前面定义了一个 store,但在 setup() 调用 useStore() 之前,store 实例是不会被创建的:

import { useCounterStore } from '@/stores/counter'

export default {
  setup() {
    const store = useCounterStore()

    return {
      // 为了能在模板中使用它,你可以返回整个 Store 实例。
      store,
    }
  },
}

你可以定义任意多的 store,但为了让使用 pinia 的益处最大化(比如允许构建工具自动进行代码分割以及 TypeScript 推断),你应该在不同的文件中去定义 store

虽然使用 Pinia 管理状态非常方便,但是这并不意味着需要将组件所有使用到的数据都放在 stor 中,这会使 store 仓库变的巨大且容易产生冲突。对于那些完全是组件内部使用的数据,还是应该将其定义为局部状态。

state - 数据

在大多数情况下,state 都是你的 store 的核心。人们通常会先定义能代表他们 APP 的 state。在 Pinia 中,state 被定义为一个返回初始状态的函数。这使得 Pinia 可以同时支持服务端和客户端。

import { defineStore } from "pinia";

const useStore = defineStore("storeId", {
  // 为了完整类型推理,推荐使用箭头函数
  state: () => {
    return {
      // 所有这些属性都将自动推断出它们的类型
      count: 0,
      name: "Eduardo",
      isAdmin: true,
      items: [],
      hasChanged: true,
    };
  },
});

13.2.2 Getter 方法 - 计算

Setup Store 中:computed() 就是 getters

在 Vue 中,计算属性实际上就是 Getter 方法,当我们需要将数据处理过再进行使用时,就可以使用计算属性。对于 Pinia 来说,

但是如果有些计算属性是通用的,或者这些计算属性也是多组件共享的,此时在这些组件中都实现一遍这些计算方法就显的非常多余。Pinia 允许我们在定义 store 实例时添加一些仓库本身的计算属性,即 Getter 方法。

Getter 完全等同于 store 的 state 的计算值。可以通过 defineStore() 中的 getters 属性来定义它们。推荐使用箭头函数,并且它将接收 state 作为第一个参数:

export const useStore = defineStore("main", {
  state: () => ({
    count: 0,
  }),
  getters: {
    doubleCount: (state) => state.count * 2,
  },
});

大多数时候,getter 仅依赖 state,不过,有时它们也可能会使用其他 getter。因此,即使在使用常规函数定义 getter 时,我们也可以通过 this 访问到整个 store 实例但(在 TypeScript 中)必须定义返回类型。这是为了避免 TypeScript 的已知缺陷,不过这不影响用箭头函数定义的 getter,也不会影响不使用 this 的 getter

以上一节编写的示例代码为基础,修改 store 定义如下

import { defineStore } from "pinia";
import { computed, ref } from "vue";

export const mainStore = defineStore("main", () => {
  let msg = ref("Hello world");
  let count = ref(0);
  const click = () => {
    count.value++;
  };
  //Getter方法 - 计算
  let getSum = computed(() => {
    return count.value + 10;
  });
  return { msg, count, click, getSum };
});

Getter 方法本身也是具有响应性的,当期内部使用的状态发生改变时,其也会触发所绑定组件的更新,在组件中使用 store 的 Gtter 数据方法如下:

<script setup>
import { ref } from "vue";
import { mainStore } from "../store/index";
const store = mainStore();
</script>

<template>
  <h5>用了store</h5>

  <p>简单测试下:{{ store.getSum }}</p>
</template>

Geter 方法中也支持参数的传递,这时需要让其返回一个函数,在组件对其进行使用时非常灵活,例如修改 countText 方法如下:

通过如下方式使用即可:

对于 Getter 方法,Pinia 中也提供了一个方法用来将其映射到组件内部的计算属性中,示例如下:

13.2.4 Action - 方法

Action 是我们将要接触到的一个新的 Pinia 核心概念。在实际开发中,并非所有修改数据的场景都是同步的,例如从网络请求获取数据,之后刷新页面。当然,也可以将异步的操作放到组件内部处理,异步操作结束后再提交修改到 store 仓库,但这样可能会使本来可以复用的代码要在多个组件中分别写一遍。Pinia 提供了 Action 来处理这种场景。

Action 不会直接修改数据,在 Action 定义的方法中,允许我们包含任意一步操作。

以前面编写的示例代码为例,修改 store 实例的定义如下:

import { defineStore } from "pinia";
import { ref } from "vue";

export const mainStore = defineStore("main", () => {
  let count = ref(0);
  const click = () => {
    count.value++;
  };

  return { count, click };
});

使用

<script setup>
import { ref } from "vue";
import { mainStore } from "../store/index";
const store = mainStore();
</script>

<template>
  <button type="button" @click="store.click">
    简单计数:{{ store.count }}
  </button>
</template>

本章介绍了在 Vue 项目开发中常用的状态管理框架 Pinia 的应用。有效的状态管理可以帮助我们更加顺畅地开发大型应用。Pinia 的状态管理功能主要解决了 VU 组件间的通信问题,让跨层级共享数据或平级组件共享数据变的非常容易。

练习:大多数时候,Pinia 管理的状态都是全局状态,Vue 组件内自己维护的状态是局部状态,请简述它们之间的异同和适用的场景。

第十四章:开发一个文档学习网站

第十五章:Typescript

使用 Typescript 可以很好地帮助我们判断变量类型,提升工作效率

准备

新建文件夹,进入后终端执行以下命令:

npm create vite@latest

依次输入自己的

  1. 项目名
  2. 包名
  3. 选择框架:vue
  4. 选择类型:Typescript,
  5. 即可

然后依照提示进入对应目录,配置环境和运行

案例 1:传文本给子组件

我们编写子组件:

<script setup lang="ts">
import { ref } from "vue";

defineProps<{ msg: string }>();

const count = ref(0);
</script>

<template>
  <h1>{{ msg }}</h1>

  <div class="card">
    <button type="button" @click="count++">count is {{ count }}</button>
  </div>
</template>

<style scoped>
.read-the-docs {
  color: #888;
}
</style>

父组件使用:

<HelloWorld msg="Vite + Vue" />

案例 2:传 Number 给子组件

撰写子组件:

<script setup lang="ts">
//接受父组件传值
import { ref } from "vue";
//let prop = defineProps<{ count: number }>();
//解构
const { count } = defineProps<{ count: number }>();

let counts = ref(0);
counts.value = count;
const click = () => {
  counts.value++;
};
</script>
<template>
  <h3>这里有一个简单的按钮,初始值由父组件提供</h3>

  <button type="button" @click="click">{{ counts }}</button>
</template>

父组件调用

<script setup lang="ts">
import { ref } from "vue";

//准备一个数值,传给子组件
let tst = ref(10);
</script>

<template>
  <ClickTransferVue :count="tst" />
</template>

补充

CSS

颜色切换

通过js控制CSS

基础用法
<script src="https://unpkg.com/vue@next"></script>

    <div id="Application">
        <div class="box" :style="styleVar">
            简简单单的晚饭
        </div>
    </div>
    <script>
        const App = Vue.createApp({
            setup() {
                const height = Vue.ref(94)
                const whith = Vue.ref(200)
                const styleVar = Vue.computed(() => {
                    return {
                        '--box-width': whith.value + 'px',
                        '--box-height': height.value + 'px'
                    }
                })

                return {
                    styleVar
                };
            },
        });
        App.mount("#Application");
    </script>
    <style>
        .box {
            height: var(--box-height);
            width: var(--box-width);
            background: red;
        }
    </style>

这样就在vue中实现了在样式里使用js变量的方法,及通过css定义变量的方式,将变量在行内注入,然后在style中使用var()获取我们在行内设置的数据即可。以后,在封装一些需要动态传入样式参数的ui组件是不是简便了不少。

ref
 <script src="https://unpkg.com/vue@next"></script>

    <div id="Application">
        <div class="demo">
            <button v-for="(item, index) in btns" :key="index" @click="onBtnClick(item.bgColor, item.textColor)">{{
                item.title }}</button>

            <div>
                <div class="example" ref="exeampleRef">Hello World</div>
            </div>
        </div>
    </div>
    <script>
        const App = Vue.createApp({
            setup() {
                const btns = [
                    { title: '红色主题', bgColor: '#FF9191', textColor: '#FF0000' },
                    { title: '蓝色主题', bgColor: '#B3C4FF', textColor: '#042BA9' },
                    { title: '默认主题', bgColor: '#333333', textColor: '#FFFFFF' }
                ]
                //拿到该div的引用
                const exeampleRef = Vue.ref()
                const onBtnClick = (bgColor, textColor) => {
                    console.log(bgColor, textColor, exeampleRef.value)
                    if (exeampleRef.value) {
                        exeampleRef.value?.style.setProperty('--textColor', textColor)
                        exeampleRef.value?.style.setProperty('--bgColor', bgColor)
                    }
                }

                return {
                    btns,
                    exeampleRef,
                    onBtnClick
                };
            },
        });
        App.mount("#Application");
    </script>
    <style scoped lang="scss">
        .demo {
            padding: 10px;
        }

        .demo .example {
            --textColor: #FFFFFF;
            --bgColor: #333333;

            display: inline-block;
            margin-top: 20px;
            font-size: 20px;
            padding: 20px 50px;
            color: var(--textColor);
            background: var(--bgColor);
        }
    </style>
v-bind
<script src="https://unpkg.com/vue@next"></script>

    <div id="Application">
      <div class="demo">
        <button
          v-for="(item, index) in btns"
          :key="index"
          @click="onBtnClick(item.bgColor, item.textColor)"
        >
          {{ item.title }}
        </button>

        <div>
          <div class="example" :style="styleVar">Hello World</div>
        </div>
      </div>
    </div>
    <script>
      const App = Vue.createApp({
        setup() {
          const currentBgColor = Vue.ref("#333333");
          const currentTextColor = Vue.ref("#FFFFFF");

          const btns = [
            { title: "红色主题", bgColor: "#FF9191", textColor: "#FF0000" },
            { title: "蓝色主题", bgColor: "#B3C4FF", textColor: "#042BA9" },
            { title: "默认主题", bgColor: "#333333", textColor: "#FFFFFF" },
          ];
          const onBtnClick = (bgColor, textColor) => {
            console.log(bgColor, textColor);
            currentBgColor.value = bgColor;
            currentTextColor.value = textColor;
          };
          const styleVar = Vue.computed(() => {
            return {
              "--textColor": currentTextColor.value,
              "--bgColor": currentBgColor.value,
            };
          });

          return {
            btns,
            onBtnClick,
            styleVar,
          };
        },
      });
      App.mount("#Application");
    </script>
    <style scoped lang="scss">
      .demo {
        padding: 10px;
      }

      .demo .example {
        display: inline-block;
        margin-top: 20px;
        font-size: 20px;
        padding: 20px 50px;
        color: var(--textColor);
        background: var(--bgColor);
      }
    </style>
单文件组件
<template>
  <div class="text">hello</div>
</template>

<script>
export default {
  data() {
    return {
      color: 'red'
    }
  }
}
</script>

<style>
.text {
  color: v-bind(color);
}
</style>

技巧

延时加载

function openAd() {
  showAd.value = true;
}

//延时加载
setTimeout(() => {
  qrShow();
}, 10);

语法:setTimeout(code,millisec)

  • code:要调用的函数后要执行的 JavaScript 代码串

  • millisec:在执行代码前需等待的毫秒数

默认值

{
  {
    a ?? b;
  }
}
<script src="https://unpkg.com/vue@next"></script>

<div id="Application">
  <h2>{{a ?? b}}</h2>
  <button @click="clear()">清除内容</button>
</div>

<script>
  const App = Vue.createApp({
    setup() {
      const a = Vue.ref("我是内容文本");
      const b = Vue.ref("我是默认内容");
      const clear = () => {
        a.value = undefined;
      };
      return {
        a,
        b,
        clear,
      };
    },
  });
  App.mount("#Application");
</script>