Vue 核心技术

一、Vue介绍

1、Vue概念

Vue 是一个 渐进式的 JavaScript 框架

官网地址: https://cn.vuejs.org/

渐进式的理解

渐进式:逐渐增强Vue不强求你一次性在网站中运用学习所有的语法,可以学一点用一点

库和框架的理解

库:本质上是一些方法的集合。每次调用方法,实现一个特定的功能。

框架:是一套完整的解决方案。框架实现了大部分的功能,我们需要按照框架的规则写代码

2、Vue是一个MVVM的框架

什么是MVVM呢?

MModel数据模型(ajax获取到的数据)

V: View视图(页面)

VMViewModel 视图模型 (操作视图+模型)

在以前的开发中,我们只用到了View视图页面和Model数据,通过ajax请求获取到数据以后,在通过DOM的方式将数据渲染到View页面中。但是这种开发方式有一个问题,需要我们手动操作DOM,这样导致了开发效率比较低。

为了能够提高开发效率是否可以将DOM的操作做成自动的方式呢?也就是说,当数据变化了自动的渲染到页面中,不需要开发人员在操作DOM.

为了满足这个要求,出现了VM(ViewModel),它可以操作View(视图)也可以操作Model(数据)。当数据(Model)变化的时候,ViewModel能够监听到这种变化,并及时通知View视图做出修改。同样的,当页面有事件触发的时候,ViewModel也能够监听到事件,并通知数据(Model)进行响应。所以ViewModel就相当于一个观察者,监控着双方的动作,并及时通知对方进行相应的操作。

总结:

之前的开发方式是原生DOM驱动,无论修改页面中的什么内容,先找到对象,再去操作DOM

现在的开发方式是,**Vue 数据驱动**,要想更新页面,直接操作数据就可以了,数据变化了,视图会自动更新。

3、Vue组件化思想

我们知道一个网页会包含三部分,分别是HTMLCSS,JS. 在开发一个页面的时候,如下网站

以上是网易网站中的国内新闻页面,

我们需要将html代码写到一个html网页文件中,然后把css代码写到一个css样式文件中,把javascript代码写到一个js文件中。

然后在html网页文件中去引入css文件和js文件。

这样会带来一个问题,不利于代码的维护,假如html文件中的代码3000行,你要找其中某个版块的结构,这样查找起来比较麻烦。

为了解决这个问题,我们是否可以将上面的网页进行拆分,例如将头部区域的html,css,js作为一个整体,单独的去维护,这样代码量相对来说比较少,查找方便,容易维护。

这样也带来了另外一个好处,如果要开发另外一个新的页面,如下所示:

以上是网易国际页面

依据:功能单一 函数

我们发现上面的页面与前面页面头部是一样的,所以可以共用,不用单独的再去开发头 部版块了。

这样,我们可以将一个大的页面拆分成不同的小的模块(每个模块都是单独的一个文件),每个模块中都包含了html,css,js

而这些小的模块,在Vue中称作组件,如下图所示:

一个大的页面就是有不同的组件组合而成,而这就是Vue组件化的思想。

所谓组件化,指的就是将一个完整的页面拆分成多个组件,每个组件都会包含html,css,js.

综上所述组件化的好处:

第一:容易维护

第二:便于复用。

二、脚手架

1、脚手架基本介绍

关于Vue的开发方式有两种

第一:传统开发模式,在html文件中引入vue文件

第二:工程化开发方式:在webpack环境中开发Vue(推荐),后面的开发采用该方式。

webpack的作用:

javascript 应用程序的 静态模块打包器 (module bundler)

其中功能:

  • less/sass -> css

  • ES6/7/8 -> ES5 处理js兼容
  • 支持js模块化 export import
  • 处理css兼容性
  • html/css/js -> 压缩合并

问题:自己配置Webpack非常痛苦,有没有一套搭建好的,拿来即用的环境呢?

这就是脚手架的工作。

脚手架,在日常生活中脚手架非常常见,主要作用就是为了保证各施工过程顺利进行而搭设的工作平台,

Vue脚手架可以帮助我们快速的创建一个Vue项目的基础架子。Vue脚手架叫做@vue/cli,它是Vue官方所提供的一个全局命令工具。

Vue脚手架有什么好处呢?

第一:开箱即用

第二:零配置(不用你配置webpack)

第三:内置了babel等工具。

2、Vue脚手架的基本使用

要想使用@vue/cli脚手架搭建Vue项目,需要先进行安装。

2.1 安装方式采用全局安装

命令如下:

1
2
3
npm i @vue/cli -g 或  yarn global add @vue/cli


当然,首先必须在电脑中安装node.js环境

查看nodenpm的版本

1
2
node -v #查看node版本
npm -v #查看npm版本

**npm淘宝镜像**

npm是非常重要的npm管理工具,由于npm的服务器位于国外, 所以一般建议 将 npm设置成国内的淘宝镜像

设置淘宝镜像

1
2
3
4
5
$ npm config set registry  https://registry.npm.taobao.org/  #设置淘宝镜像地址

npm config set registry https://registry.npmmirror.com #设置淘宝镜像地址(最新镜像地址)

$ npm config get registry #查看镜像地址

查看脚手架版本:

1
vue --version

2.2、使用脚手架搭建Vue3开发环境

在指定的目录下面执行

1
vue create 项目名称(项目名称自己定义,并且不能出现中文,以及大写字母)

使用以上命令创建项目的时候,有可能会出现了如下错误

1
vue : 无法加载文件 C:\XXX\AppData\Roaming\npm\vue.ps1,因为在此系统上禁止运行脚本。

参考解决方案:

1
https://blog.csdn.net/ying456baby/article/details/126379392

第一:以管理身份启动命令窗口

第二:在终端窗口输入 **set-ExecutionPolicy RemoteSigned**

输入命令查看 **get-ExecutionPolicy**“**RemoteSigned**”

项目创建的步骤如下

第一步:打开命令行窗口(以管理员身份)

第二步:在打开的命令行窗口中输入vue create 项目名称

第三步: 选择Vue3项目(关于自定义的方式,我们后面做项目的时候,在进行讲解)

2.3 项目创建好以后 cd 进入目录,启动项目, 打包项

1
2
启动项目:yarn serve 或 npm run serve
打包项目:yarn build 或 npm run buil

3、如何覆盖脚手架下的webpack配置`

我们说 @vue/cli 脚手架集成了 webpack,但是我们怎么没有看到 webpack.config.js 呢?

注意:我们在项目无法找到webpack.config.js文件,因为vue把它隐藏了

如果需要覆盖webpack的配置,可以修改vue.config.js文件,覆盖webpack配置

注意:修改了配置,需要重新启动项目。

4、脚手架目录分析

问题: 脚手架所创建的Vue项目里各个文件及代码都有什么作用呢?

下面,我们就来了解一下项目中文件夹和文件的含义。

src目录是我们后面编程中经常使用到的目录。这里我们可以暂时先将该目录中使用不到的文件删除掉。

  1. public/index.html不用动,提供一个最基础的页面

  2. src/main.js不用动, 渲染了App.vue组件

  3. src/App.vue默认有很多的内容,可以全部删除

  4. assets 文件夹 和 components 直接删除

通过本小节的学习,大家需要对脚手架里主要文件的左右有一个基本的了解

这里还需要注意一个小的问题:在使用vscode编写vue代码之前,最好在vscode中安装相应的插件,方便有代码提示

5、单文件组件

.vue 文件是什么文件呢? 以前怎么没见过呢

一个单文件组件由三部分组成:

template:表示结构 (有且只能一个根元素)

script:表示js逻辑

style: 表示样式

三、Vue插值表达式

1、Vue如何提供数据

问题: vue是数据驱动的,应该如何提供数据,将来控制视图呢?

第一:通过 data 属性可以提供数据, data属性必须是一个函数

第二:这个函数需要返回一个对象,这个对象就代表vue提供的数据

使用插值表达式,可以在模板中渲染数据:

2、插值表达式的语法

插值表达式的作用就是使用data中的数据来渲染视图(模板),它的语法如下所示:

1
2
3
4
5
6
7
8
9
通过上图可以看到,插值表达式也是支持三元运算符的。

在使用插值表达式的时候,还需要注意以下三点:

(1)使用数据在` data` 中必须存在

(2)能使用表达式,但是不能使用语句` if for ... `

(3)不能在标签属性中使用 {{ }}。

具体代码演示如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export default{
name: 'App',
data(){
return{
money:100,
username:"zs",
obj:{
name:'lisi',
age:16,
desc:'大帅哥'
}
}
}
}

App.vue组件中提供了相应的数据,下面看一下模板中是怎样使用插值表达式将数据渲染到模板中的。

1
2
3
4
5
6
7
8
9
<template>
<img alt="Vue logo" src="./assets/logo.png">
<span>{{ money }}</span>
<span>{{ username }}</span>
<div>
{{ obj.name }}
{{ obj.age>18?"成年":"未成年" }}
</div>
</template>

四、指令

1、什么是指令

Vue指令:特殊的 html 标签属性, 特点:v-开头

每个 v- 开头的指令, 都有着自己独立的功能, 将来vue解析时, 会根据不同的指令提供不同的功能

1
<div v-text="username"></div>

2、v-bind指令

插值表达式不能用在html的属性上,想要动态的设置html元素属性,需要使用v-bind指令

所以说,v-bind指令的作用就是用来动态设置html标签属性。

它的语法是:·v-bind:属性名 = "值"

我们一般都是使用简写的形式::属性名 = "值"

1
<div v-bind:title="obj.desc">hello</div>

下面再来看一下简写的形式:

1
<div :title="obj.desc">hello</div>
1
<a :href="link">百度</a>
1
2
3
4
5
6
7
8
9
10
11
12
data(){
return{
money:100,
username:"zs",
obj:{
name:'lisi',
age:16,
desc:'大帅哥'
},
link:"https://www.baidu.com"
}
}

注意:在给标签添加属性的时候,如果是采用如下的写法:

1
<div title='msg'> </div>

表示的是给div这个标签设置了title属性,该属性的值是字符串msg.

但是如果采用如下的写法:

1
<div :title='msg'></div>

表示的是给div这个标签设置了title属性,该属性的值是来自msg这个变量,而该变量需要在data中进行定义。

3、v-on指令

这里有一个问题,需要我们思考一下:在Vue中如何给按钮绑定点击事件呢?

这里需要使用到v-on指令,该指令的作用就是注册事件。

该指令有三种使用方式,如下所示:

第一:v-on:事件名=“要执行的少量代码”

第二:v-on:事件名=”methods中的函数名”

第三:v-on:事件名=“methods中的函数名(实参)”

下面我们先来看第一种方式的使用情况,

1
2
<button v-on:click="obj.age++">搬砖</button>
年龄{{ obj.age }}

通过上图展示的代码,我们可以看到,如果计算比较简单,只需要少量代码就可以完成,这样就没有必要在将代码封装到方法里了,只需要写到模板中就可以了。

下面我们再来看第二种使用方式

1
2
3
4
5
 <button v-on:click="obj.age++">搬砖</button>
年龄{{ obj.age }}
<hr/>
<!--第二种方式-->
<button v-on:click="addAge">计算年龄</button>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
data(){
return{
money:100,
username:"zs",
obj:{
name:'lisi',
age:16,
desc:'大帅哥'
},
link:"https://www.baidu.com"
}
},
// 添加mehtods属性,指定方法
methods:{
addAge(){
this.obj.age=this.obj.age+1;
}
}

在模板中,给v-on:click指定的是addAge这个方法名。问题是addAge这个方法应该定义在什么地方呢?

这里需要注意的就是方法需要定义在methods中,methods是一个属性,该属性的值是一个对象,在对象中定义了方法。

同时data这个选项和methods这个选项之间需要使用英文的逗号进行分隔,如果漏掉英文的逗号,程序会出现错误。

addAge这个方法中,我们需要对定义在data中的数据进行计算,这里就涉及到一个问题,怎样获取data中定义的数据呢?

methods选项中定义的所有方法,如果要访问或者是修改data中的数据,需要使用到this这个关键字,this表示的是当前vue实例。

通过this关键字就可以获取到data中定义的数据。

但是,这里需要大家注意的一点是,整个过程中我们并没有操作DOM,而只是操作数据,当数据发生了改变,视图也会发生相应的变化。

下面我们再来看第三种使用方式

1
<button v-on:click="addAge(2)">计算年龄</button>
1
2
3
4
5
methods:{
addAge(age){
this.obj.age=this.obj.age+age;
}
}

注意:如果在methods中定义的不同方法之间也要使用英文的逗号进行分隔。

当然,需要传递多少个参数,根据实际情况来确定。

每次使用v-on的时候写起来比较麻烦,我们可以直接将其简写为@事件名的形式, 如下图所示:

1
<button @click="addAge(2)">计算年龄</button>

4、如何获取事件对象

默认a标签点击会跳走, 希望阻止默认的跳转, 应该如何操作呢?如何阻止默认行为呢?

语法:e.preventDefault()

vue中如何获取事件对象呢?

关于Vue中获取事件对象,有两种情况

第一:如果没有传参,通过形参接收e

第二:如果传参了,通过$event指定事件对象e.

1
<a :href="link" @click="fn">百度</a>
1
2
3
4
5
6
7
8
9
10
methods:{
addAge(age){
this.obj.age=this.obj.age+age;
}, // 注意分号
// 定义了fn方法,通过参数e指定事件对象
fn(e){
e.preventDefault();
console.log("单击了超链接");
}
}

下面看一下第二种情况

1
<a :href="link" @click="fn(100,$event)">百度</a>
1
2
3
4
fn(num,e){
e.preventDefault();
console.log("num参数的值是:"+num);
}

当单击去百度这链接的时候,会触发单击事件click,从而调用fn这个方法,在调用该方法的时候,没有传递任何的参数,所以在fn这个方法中通过形参e来获取事件对象。

而当单击去百度2这个链接的时候,也会触发单击事件click,调用fn2这个方法,这时候需要通过$event指定事件对象e.

5、事件修饰符

e.preventDefault()单词很长不好写吧?

有没有一种更简单的方式实现呢?

这里就需要使用到事件修饰符

事件修饰符:vue提供事件修饰符,可以快速阻止默认行为或阻止冒泡

.prevent 阻止默认行为,.stop阻止冒泡

使用方式:@事件名.prevent@事件名.stop

1
2
3
<div @click="fatherFn">
<a href="https://www.jd.com" @click.prevent.stop="fn1">京东</a>
</div>
1
2
3
4
5
6
fn1(){
console.log("去京东");
},
fatherFn(){
console.log('父元素');
}

在第一个超链接和第二个超链接中,即指定了.prevent又指定了.stop.表示的含义是:当单击两个链接的时候,分别会执行fn1fn2两个函数,但是由于添加了.prevent事件修饰符,所以不会跳转到京东与淘宝。同时由于添加了.stop这个事件修饰符,这里可以阻止冒泡,所以对应的两个a元素的父元素div中的单击事件不会触发,对应的fatherFn这个方法不会被调用。

注意:以上事件修饰符的使用方式,采用的是链式的写法,但是.prevent.stop两个修饰符没有顺序的限制。

6、按键修饰符

我想判断用户是否按下回车了怎么做?

在监听键盘事件时,我们经常需要判断详细的按键。可用按键修饰符。

需求: 用户输入内容, 回车时, 打印输入的内容

@keyup.enter 监听回车键

@keyup.esc 监听返回键

下面看一下具体的代码实现

1
2
<hr/>
<input @keyup="handleKeyUp" />
1
2
3
handleKeyUp(){
console.log("开始搜索");
}

注意:在上图所展示的代码中,我们仅仅给input标签指定了@keyup事件。这样导致的结果是:当在文本框中输入任何内容的时候,都会调用handleKeyup这个方法,原因是@keyup监听的是键盘的弹起,也就是说键盘上的任意按键的弹起都会被监听到。

而这里,我们要求的是,用户在文本框中输入了内容后,按下回车键的时候,才会去调用handleKeyup这个方法打印相关的内容。

修改后的代码如下所示:

1
2
3
4
5
6
handleKeyUp(e){
if(e.key==='Enter'){
console.log("开始搜索");
}

}

在上面的代码中,我们是通过事件对象e来获取对应的key属性的值,判断是否是Enter。如果是,表示用户按下了回车键,这时候才会打印相关的内容。

这种处理方式可以满足我们的需求,但是写法上比较繁琐。

下面使用按键修饰符:

1
2
<hr/>
<input @keyup.enter="handleKeyUp" />
1
2
3
4
5
6
handleKeyUp(){
// if(e.key==='Enter'){
// console.log("开始搜索");
// }
console.log("开始搜索");
}

在这里我们给input文本框添加了@keyup.enter,表示只有当用户按下了回车键后,才会执行handleKeyup这个方法打印相关的内容。当然在handleKeyup方法中不用在通过事件对象做按键的判断了。

这种写法更加的简洁。

如果想获取到用户在文本框中输入的内容,还是需要通过事件对象来完成

1
2
3
4
5
6
7
handleKeyUp(e){
// if(e.key==='Enter'){
// console.log("开始搜索");
// }

console.log("开始搜索"+e.target.value);
}

Vue内置的按键修饰符列表如下所示:

通过上图我们可以看到,在Vue中只是提供了常用了的按键修饰符,如果你想对其它的一些按键做判断,只能采用事件对象的方式,来获取对应的键盘码或者键盘标识进行判断。

7、v-showv-if

问题:在Vue中如何控制盒子的显示与隐藏呢?

这里可以通过v-show或者v-if指令来实现。

下面我们先来看一下v-show指令。

v-show指令的语法:

1
v-show="布尔值" (true显示, false隐藏)

v-show指令的取值是布尔值,如果是true表示显示盒子,false表示隐藏盒子。

它的实现原理:实质是在控制元素的 css 样式, 通过display:none隐藏元素

再来看一下v-if指令

v-if指令的语法如下:

1
v-if="布尔值" (true显示, false隐藏)

v-if指令的取值也是布尔值,true表示元素的显示,false表示元素的隐藏。

它的实现原理:实质是在动态的创建 或者 删除元素节点

下面看一下具体的代码实现

1
2
3
4
5
6
7
8
9
10
<script>
export default{
name:"App",
data(){
return{
isShow:true
}
}
}
</script>
1
2
3
4
5
6
7
<template>
<div>
<button @click="isShow=!isShow">控制显示与隐藏</button>
<div v-show="isShow">v-show控制的盒子</div> <!--v-show指令-->
<div v-if="isShow">v-if控制的盒子</div> <!--v-if指令-->
</div>
</template>

这里在data函数中定义了具体的数据isShow,默认值是true

在模板中两个h3元素分别通过v-showv-if两个指令来控制显示与隐藏,两个指令的值就是isShow

当单击button按钮的时候,触发点击事件,将改变isShow数据的值。

返回到浏览器中进行测试的时候,发现当不断单击按钮时候,两个h3标签会不断出现显示与隐藏的效果,这里大家可以在浏览器中审查元素,就会发现两个指令在显示与隐藏元素实现原理上的区别。

问题:两个指令的应用场景是什么?

第一:如果是频繁的切换显示隐藏,用v-show.

因为v-show, 只是控制css样式,而v-if, 频繁切换会大量的创建和删除元素, 消耗性能

第二:如果是不用频繁切换,而是出现了 要么显示, 要么隐藏的情况, 适合于用 v-if

v-if 是惰性的, 如果初始值为 false, 那么这些元素就直接不创建了, 节省一些初始渲染开销

8、v-elsev-else-if

关于v-elsev-else-if两个指令,与js中的if..else/else if执行流程是一样的、

例如上图展示的代码中,先判断flag是否为true,如果为true展示的内容就是尊敬的超级vip,里面请,而后面的内容不会展示,如果flagfalse,则展示的内容就是你谁呀,请先登录

下面紧跟着判断age的取值范围,根据age不同的值,展示不同的内容。当前定义的age默认值是12,所以打开浏览器后,在页面上默认展示的内容就是:11岁-20岁 蹦迪

最后需要注意的一点就是v-if,v-else,v-else-if指令之间需要连接写,如下的写法是错误的。

v-ifv-else-if之间添加了div标签,这时候就出错了。

9、v-model

v-model指令是给表单元素使用,可以实现双向数据绑定。

什么是双向数据绑定?

(1) 当数据发生了变化,视图会跟着变化。

(2) 当视图变了,数据也会跟着变化。

语法: v-model ='变量值'

代码如下所示:

1
2
3
4
5
6
data(){
return{
isShow:true,
msg:'abc'
}
}
1
2
3
4
<hr/>
<input v-model="msg" />
<button @click="msg='hello'">修改msg</button>
<input v-model="msg"/>

我们看到,在上图的代码中,我们在data中定义l了msg,初始值是abc字符串。

然后在模板中两个文本框通过v-model都绑定了msg,所以在页面中展示的两个文本框中的值是abc字符串。

当我们修改视图中一个文本框中的值时,另外一个文本框中的值也发生了变化,说明:修改视图,数据会发生改变。

当单击了修改msg按钮的时候,修改了msg数据,视图中文本框的值也发生了变化。

这就是我们所说的双向数据绑定。

当然,这里让两个文本框绑定了同一个数据,但是在实际的应用中,每个文本框都会单独的绑定一个数据。

1
2
3
4
5
6
<hr/>
<div>
姓名:<input v-model="username" />
密码:<input v-model="password" type="password"/>
<button @click="login">登录</button>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
data(){
return{
isShow:true,
msg:'abc',
username:'',
password:''
}
},
methods:{
login(){
console.log("用户登录:",this.username,this.password);
}
}

在模板中添加了登录按钮,并且注册了单击事件,事件触发以后会调用login方法。在该方法中可以通过this.usernamethis.password来获取用户输入的姓名和密码。这就是视图发生了变化,数据也会进行改变。这里大家可以与传统的js编程做一下对比,这里不需要操作DOM,直接操作数据,更加的方便。

综上所述,v-model可以快速的收集和设置表单中的数据。

10、v-model 处理其他表单元素

在这一小节中,我们看一下v-model处理其他的表单元素。

先来看一下对下拉框的处理

1
2
3
4
5
6
7
<hr/>
<select v-model="cityId">
<option value="101">北京</option>
<option value="102">上海</option>
<option value="103">广州</option>
<option value="104">深圳</option>
</select>{{cityId}}
1
2
3
4
5
6
7
8
9
data(){
return{
isShow:true,
msg:'abc',
username:'',
password:'',
cityId:103 // 定义了cityId,默认值是103
}
},

这里定义了cityId数据,初始值是103,并且通过v-modelselect标签进行了绑定。

这时候,打开浏览器查看,就会发现下拉框中默认选中的就是广州这一项,因为它的option选项中value属性的值就是103.

并且在下拉框旁边会展示出cityId的值。

下面再来看一下复选框的使用

1
2
<hr/>
是否单身:<input type="checkbox" v-model="isSingle"/> {{ isSingle }}
1
2
3
4
5
6
7
8
9
10
data(){
return{
isShow:true,
msg:'abc',
username:'',
password:'',
cityId:103,
isSingle:false // 定义了isSingle属性
}
},

这里定义了isSingle数据,初始值是false,并且通过v-model与复选框进行了绑定,看一下效果,这时候,我们可以看到复选框是不会被选中的。

当选中了复选框以后,isSingle的值就会变成true,也就表示复选框被选中了。

关于其它的表单元素,在后面使用到的时候在进行讲解。

11、v-model修饰符

语法: v-model.修饰符=”数据变量”

(1) .number转数字,以parseFloatt转成数字类型

(2) .trim 去除首尾空白字符

(3) .lazychange时触发而非input

下面先来看一下.number的使用,

1
2
<hr/>
年龄:<input type="text" v-model="age"/>{{ age }}
1
2
3
4
5
6
7
8
9
10
11
data(){
return{
isShow:true,
msg:'abc',
username:'',
password:'',
cityId:103,
isSingle:false,
age:18 // 定义了age属性
}
},

在上面的代码中,定义了age数据,默认值是18,并且与年龄文本框进行了绑定,这时候文本框中会展示18.

这里如果我们在文本框中输入的字符串也是没有问题的。

1
年龄:<input type="text" v-model.number="age"/>{{  age }}

这时候在输入字符串,视图页面中没有变化(注意:在数字后面输入字符串)

注意:如果用户在年纪文本框中输入了一个字符串abc,这时候是无法进行转换的,这时候age中的值就是abc字符串,当然,后面我们也会对文本框进行校验。

下面再来看一下.trim修饰符的使用

1
2
<hr/>
标题:<input v-model="title"/>
1
2
3
4
5
6
7
8
9
10
11
12
data(){
return{
isShow:true,
msg:'abc',
username:'',
password:'',
cityId:103,
isSingle:false,
age:18,
title:'hello' // 定义了title
}
},

这时候在文本框中会默认显示hello这个字符串,当然,这里我们可以在该字符串前面输入空格

1
2
<hr/>
标题:<input v-model.trim="title"/>

当添加了.trim这个修饰符以后,在hello`这个字符串前输入空格的时候,会自动的清除掉空格。

最后再来看一下.lazy修饰符的使用

1
2
<hr/>
描述:<input v-model="desc" />{{ desc }}
1
2
3
4
5
6
7
8
9
10
11
12
13
data(){
return{
isShow:true,
msg:'abc',
username:'',
password:'',
cityId:103,
isSingle:false,
age:18,
title:'hello',
desc:'' // 定义desc属性
}
},

默认情况下描述这个文本框只是通过v-modeldesc进行了绑定,这时候在文本框中输入内容desc中的数据就会发生变化

出现这个情况的原因是,当给文本框添加了v-model以后,Vue会给文本框添加一个input事件,这样用户只要在文本框中输入内容,input事件就会触发。也就是说input事件是实时触发,只要修改文本框内容,就会触发。

还有一个事件是change事件,当在文本框中输入完内容并且失去焦点后或者是按下回车键的时候,触发change事件。要想实现该效果就需要添加.lazy修饰符。

1
2
<hr/>
描述:<input v-model.lazy="desc" />{{ desc }}

现在在描述文本框中输入了新的数据,但是文本框还没有失去焦点,这时候desc中的数据并没有发生改变。

当输入完内容按下回车键或者是文本框失去焦点后,desc中的内容就会发生改变了。

最后总结一下:

(1).number – 转成数值类型赋予给Vue数据变量

(2) .trim – 去除左右两边空格后把值赋予给Vue数据变量

(3).lazy– 等表单失去焦点, 才把值赋予给Vue数据变量

12、v-textv-html

v-textv-html的作用是:更新元素的innerText/innerHTML

1
2
3
<hr/>
<div v-text="str1"></div>
<div v-html="str2"></div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
data(){
return{
isShow:true,
msg:'abc',
username:'',
password:'',
cityId:103,
isSingle:false,
age:18,
title:'hello',
desc:'',
str1:'<a href="#">百度</a>',
str2:'<a href="#">百度</a>'

}
},

通过执行效果,我们可以看到v-text是不解析标签的,而v-html会解析标签。所以说,v-text就是innerText,而v-html就是innerHTML

关于v-text指令我们不常用,一般都是使用插值表达式。

而当系统后台返回一些标签节点的时候,我们经常会使用v-html指令进行动态解析渲染。但是这里需要注意的是,永远不要直接将用户输入的内容直接作为v-html指令的值,容易造成XSS攻击(跨站脚本攻击)。因为用户的输入中可能包含js脚本,v-html会解析js脚本,从而导致js被执行,从而产生攻击漏洞。

13、v-for 指令

v-for的作用:可以遍历 数组 或者 对象,用于渲染结构

下面先来看一下关于数组的遍历

遍历数组的语法,如下:

1
2
v-for="item in 数组名"
v-for="(item, index) in 数组名

具体代码实现如下所示:

1
2
3
4
<hr/>
<ul>
<li v-for="item in arrList" :key="item">{{ item }}</li>
</ul>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
data(){
return{
isShow:true,
msg:'abc',
username:'',
password:'',
cityId:103,
isSingle:false,
age:18,
title:'hello',
desc:'',
str1:'<a href="#">百度</a>',
str2:'<a href="#">百度</a>',
arrList:['苹果','桃子','香蕉'] // 定义数组

}
}

在上面的代码中,定义了数据arrList,它是一个数组。在视图中可以通过v-for来遍历arrList这个数组,而item就是数组中的每一项。所以将item中存储的数据展示到了li标签中。注意这里需要指定key属性,它的值暂时为item.key属性在这里起到唯一标识的作用

下面我们再来看一下v-for遍历数组的第二种使用方式

1
v-for="(item, index) in 数组名
1
2
3
<ul>
<li v-for="(item,index) in arrList" :key="index">{{ item }}---{{ index }}</li>
</ul>

在上面的代码中,指定了index,并且在展示完了item中的数据后又展示了index中的数据

这里不仅展示了数组中的每一项,同时将其下标也给展示出来了。

注意:itemindex的名称可以根据自己的情况随意命名,但是两者的顺序不能更换。第一个表示的就是数组中的每一项,第二个表示的就是数组中每一项的下标。

下面看一下针对对象的遍历

遍历对象的语法

1
v-for = "(value, key) in 对象名"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
data(){
return{
isShow:true,
msg:'abc',
username:'',
password:'',
cityId:103,
isSingle:false,
age:18,
title:'hello',
desc:'',
str1:'<a href="#">百度</a>',
str2:'<a href="#">百度</a>',
arrList:['苹果','桃子','香蕉'],
// 定义对象
grilFriend:{
name:"小于",
age:23,
desc:"是一个贤惠姑娘"
}

}
1
2
3
<ul>
<li v-for="(value,key) in grilFriend" :key="key">{{ value }}</li>
</ul>

在上面的代码中定义了girlFriend对象,在视图中对该对象进行遍历。其中的value中存储的是属性值,而key中存储的是属性名。

同时也需要给v-for添加key这个属性作为唯一标识

最后再来看一下遍历数字,对应的语法如下

1
v-for = "item in 数字
1
<div v-for="item in 30" :key="item">{{ item }}</div>

这里通过v-for遍历数字,item中存储的就是1--20的数字。

以上就是v-for指令的基本使用。

14、虚拟Dom

html 渲染出来的 真实dom树,是个树形结构(复杂)。每个标签,都只是树的某个节点

每个标签,虽然只是树形结构的一个小节点,但属性也非常多。=> 遍历真实dom找差异,非常费时!

真实DOM属性过多, 有很多无用的属性 ,无需遍历对比。

如何优化呢?对比属性少的虚拟dom

虚拟dom:本质就是 保存节点信息, 描述真实dom JS 对象

虚拟dom(一个js对象): 可以用最少的属性结构,描述真实的dom

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta charset="utf-8">
<script type="text/javascript" src="https://code.jquery.com/jquery-1.11.3.js"></script>
</head>
<body>
<ul id="app">
1,2,3,4,5,6,8,7
</ul>
<button id="sort" style="margin-top: 20px;">按年纪排序</button>
<script type="text/javascript">
var datas = [
{ 'name': 'kongzhi11', 'age': 32 },
{ 'name': 'kongzhi44', 'age': 29 },
{ 'name': 'kongzhi22', 'age': 31 },
{ 'name': 'kongzhi33', 'age': 30 }
];
var render = function() {
var html = '';
datas.forEach(function(item, index) {
html += `<li>
<div class="u-cls">
<span class="name">姓名:${item.name}</span>
<span class="age" style="margin-left:20px;">年龄:${item.age}</span>
<span class="closed">x</span>
</div>
</li>`;
});
return html;
};
$("#app").html(render()); // document.querySeletor("#app").innerHTML=render()
$('#sort').on('click', function() {
datas = datas.sort(function(a, b) {
return a.age - b.age;
});
$('#app').html(render());
})
</script>
</body>
</html>

如上demo排序,虽然在使用jquery时代这种方式是可行的,我们点击按钮,它就可以从小到大的排序,但是它比较暴力,它会将之前的dom全部删除,然后重新渲染新的dom节点,我们知道,操作DOM会影响页面的性能,并且有时候数据根本就没有发生改变,我们希望未更改的数据不需要重新渲染操作。

因此虚拟DOM的思想就出来了,虚拟DOM的思想是先控制数据再到视图,但是数据状态是通过diff比对,它会比对新旧虚拟DOM节点,然后找出两者之前的不同,然后再把不同的节点再发生渲染操作。

所以可以概括的说,虚拟DOM,就是为了最小化找出差异这一步的性能损耗而出现的.

下面我们看一个案例,该案例的功能比较简单,单击按钮后,更新div中的内容。

1
2
3
4
5
6
<div>aaa</div>
const div=document.querySelector('#app')
const btn=document.querySelectory('#btn')
btn.onclick=function(){
div.textContent='Hello World'
}

以上代码非常简单,而且是使用DOM操作的方式来实现的。

如果上面的案例,我们使用虚拟DOM来实现,应该怎样处理呢?首先,我们要创建一个虚拟DOM的对象,

虚拟DOM对象就是一个普通的JS对象。当单击按钮的时候,需要对比两次状态的差异。所以说,仅仅是该案例,

我们使用虚拟DOM的方式来实现,要比使用纯DOM的方式来实现,性能要低。

所以说,并不是所有的情况下使用虚拟DOM都会提升性能的。只有在视图比较复杂的情况下使用虚拟DOM才会提升渲染的性能。

虚拟DOM除了渲染DOM以外,还可以实现渲染到其它的平台,例如可以实现服务端渲染(ssr),原生应用(React Native),小程序(uni-app等)。以上列举的案例中,内部都使用了虚拟DOM.

15、Diff算法

明确:通过对比 新旧虚拟dom,提高了对比性能。 但是就算是虚拟dom,和真实dom一样,也是树形结构 内部又是如何对比的呢?

内存中创建虚拟dom,快速比较变化, 给真实DOM打补丁(更新).

1
diff算法可以看作是一种对比算法,对比的对象是新旧虚拟Dom。顾名思义,diff算法可以找到新旧虚拟Dom之间的差异,但diff算法中其实并不是只有对比虚拟Dom,还有根据对比后的结果更新真实Dom。

在这有一个问题,虚拟DOM在什么时候会进行比对?

当数据发生了变化的时候,就要进行虚拟DOM的比对了。

什么时候会发生数据的改变呢?当data中的数据发生了变化。这时就需要使用Diff算法进行比对了。

策略1:先同层级根元素比较。 => 如果根元素变化,那么不考虑复用,整个dom树删除重建

策略1:先同层级根元素比较。 => 如果根元素不变,对比出属性的变化更新,并考虑往下递归复用。

策略2:对比同级兄弟元素时,默认按照下标进行对比复用

对比同级兄弟元素时,如果指定了 key,就会按照相同 key 的元素来进行对比

小结:

1
2
3
4
5
6
7
1. 同层级根元素先比较
(1)如果根元素变了,删除重建dom树
(2)如果根元素没变,对比属性。并考虑往下递归复用。
2. 兄弟元素比较
(1)默认按照下标,进行对比复用
(2)如果设置了key,就会按照相同key的元素进行复用

思考:

1
2
3
4
同层级兄弟元素比较新旧变化,默认按下标比较,
如果设置了key,则优先相同key的兄弟元素比较。
那么设置 key 和 不设置key 有什么区别呢? 设置key有什么用呢?

无key的情况:默认diff更新算法,是同级兄弟,按照 下标 对比新旧dom的差异

以上没有给元素指定key,只能通过下标来对比新旧元素,性能相对来讲比较低

有key的情况:根据diff更新算法,同级兄弟元素,在设置了key后,会让相同key的元素进行对比

key的要求:必须是字符串 或者 数字,且要保证唯一性!(标准的key需要指定成 id)

通过上图,我们可以看到,老大,老二,老三,在新的虚拟DOM结构中key都是一样的,没有发生变化,这时候,新的DOM结构中新增了一个元素

列表循环加:key=”唯一标识”,可以标识元素的唯一性,可以更好地区别各个元素。

key的作用:提高虚拟DOM的对比复用性能

小节:

1
2
3
4
5
6
7
8
9
1. 设置 和 不设置 key 有什么区别?
不设置 key, 默认同级兄弟元素 按照下标 进行比较。
设置了key,按照 相同key 的新旧元素比较。
2. key值要求是?
字符串或者数值,唯一不重复
有 id 用 id, 有唯一值用唯一值,实在都没有,才用索引
4. key的好处?
key的作用:提高虚拟DOM的对比复用性能

但是有一点要注意,就是这个key值尽量不能为数组的索引的值,因为,如果key的值为index的话,就无法保证原始的虚拟DOM上的key值和新的虚拟DOM上的key值相同了,因为数组的index的值是随着数组里元素的添加或者是删除,会发生变化的。

所以,尽量不要使用index作为key值。

key实际工作中应用的案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<template>
<div>
<ul>
<li v-for="item in arr" :key="item.id">
{{ item.name }}
</li>
</ul>
<button @click="fn">新来的</button>
</div>
</template>
<script>
export default {
name:'App',
data(){
return{
arr: [
{ id: 1, name: '小明', age: 6 },
{ id: 2, name: '小花', age: 4 },
{ id: 3, name: '小美', age: 8 },
]
}
},
methods:{
fn(){
// push pop unshift shift
// arr.splice(从哪开始删除,删几个,添加项1,添加项2, ...)
this.arr.splice(1,0,{
id:this.arr[this.arr.length-1]+1,
name:'小兰',
age:10
})
}
}
}
</script>

16、样式控制

16.1 控制类名

控制样式,要么操作类,要么操作行内样式, 在vue中,应该如何操作 class 类呢?如何操作style行内样式呢

1
2
3
4
语法 :class="对象/数组"
对象:如果键值对的值为true,那么就有这个类,否则没有这个类
数组:数组中所有的类,都会添加到盒子上
v-bind 对于类名操作的增强, 注意点 :class 不会影响到原来的 class 属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<template>
<div>
<!--直接使用了box样式-->
<!--动态绑定了obj对象,如果值,是true,那么就会有这个类样式,类名就是对象的键-->
<!--这里动态添加的class,没有对原来的class="box"造成影响-->
<div class="box" :class="obj"></div>
</div>
</template>
<script>
export default {
name:'App',
data(){
return {
obj:{
red:true, // obj对象中的red属性与css类名同名,并且值为true,说明会使用该类样式
}
}
}
}
</script>
<style>
.box {
width: 100px;
height: 100px;
border: 2px solid #000;
margin: 10px;
}
.red {
background-color: red;
}
.orange {
background-color: orange;
}
.big {
width: 200px;
height: 200px;
}
.circle {
border-radius: 50%;
}
</style>

下面再来看另外一种比较常用的使用方式:

根据属性的值是true还是false来决定是否添加某个类样式

1
2
3
4
5
6
7
8
data(){
return {
obj:{
red:true,
},
isOrage:true
}
}

这里定义了isOrage属性,根据该属性的取值来决定是否采用某个类样式

1
<div class="box" :class="{orange:isOrage}" ></div>

如果isOrage这个属性的值是true,就给div添加orange这个类样式,如果为false就不添加orange这个类样式

下面再来看一下数组的使用

1
2
3
4
5
6
7
8
9
data(){
return {
obj:{
red:true,
},
isOrage:true,
arr: ['orange', 'big', 'circle'] // 定义了arr属性,它的取值是一个数组,数组中的元素都是类样式名称
}
}
1
2
<!-- [类名1, 类名2, 类名3, ...]  数组里面有的项,就是要添加的类 (适合批量添加多个类)--->   
<div class="box" :class="arr"></div>

16.2 控制行内样式

v-bind 动态设置标签的style 行内样式

语法:

1
:style="对象/数组"
1
<div :style="{ background: 'red', width: '200px', height: '100px'}"></div>

下面看一下另外一种用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
data(){
return {
obj:{
red:true,
},
isOrage:true,
arr: ['orange', 'big', 'circle'],
// 定义了styleObj对象,包含了样式属性
styleObj:{
width: '200px',
height: '200px',
backgroundColor: 'blue'
},
// 定义了obj2对象
obj2: {
borderRadius: '50%'
}
}
}
1
<div :style="styleObj"></div>  

通过style动态绑定了styleObj对象

1
<div :style="[styleObj, obj2]"></div>

通过style动态绑定了一个数组

五、Composition API

1、Composition API设计动机

Vue2.x在设计中小型项目的时候,使用非常方便,开发效率也高。但是在开发一些大型项目的时候也会带来一定的限制,

Vue2.x中使用的APIOptions API,该类型的API包含一个描述组件选项(data,methods,props等)的对象,在使用Options API开发复杂的组件的时候,同一个功能逻辑的代码被拆分到不同的选项中,这样在代码量比较多的情况下就会导致不停的拖动滚动条才能把代码全部看清,非常的不方便。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div>
{{ msg }}
</div>
</template>
<script>
export default{
name:'App',
setup(){
const msg = "hello";
return {msg};
}
}
</script>

可以将业务写到setup入口函数中,如果业务比较复杂,可以单独的在封装函数,然后在setup中进行调用。

2、reactive/toRefs/ref

2.1 reactive函数基本使用

在上面的案例中定义的msg是一个普通的数据,并不是响应式的。而放在data函数中的数据都是响应式的。

而在Vue3中怎样创建响应式数据呢?

Compositiont API中的三个函数reactive/toRefs/ref创建响应式数据

reactive函数,可以将对象转换成响应式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<template>
<div>
<p>年龄:{{ obj.age }}</p>
<button @click="addAge">计算年龄</button>
</div>
</template>
<script>
import { reactive } from 'vue'; // 注意:需要从vue中导入reactive函数

export default{
name:'App',
setup(){
let obj =reactive({
age:10
})
const addAge=()=>{
obj.age++;
}
return {
obj,
addAge
}
}
}
</script>

单击计算年龄按钮 的时候,调用addAge函数,在该函数中完成了age属性值的更新,同时视图中数据也发生了相应的变化。

2.2 ref函数基本使用

ref函数的作用就是把普通数据转换成响应式数据。与reactive不同的是,reactive是把一个对象转换成响应式数据。

ref可以把一个基本类型的数据包装成响应式对象。

KilgourNote:

插值表达式会自动将ref过的对象取value

下面,我们使用ref函数实现一个自增的案例。

1
2
3
4
5
setup(){
let obj =reactive({
age:10
})
const num =ref(0); // 定义num

通过ref函数创建一个num,初始值是0

1
2
3
4
5
6
7
8
9
10
// 完成num值累加操作  
const addNum=(n)=>{
num.value += n;
}
return {
obj,
addAge,
num,
addNum
}

创建一个addNum函数,接收传递过来的数值,然后完成累加操作。

注意:获取num的值需要通过value属性。

最后返回numaddNum函数,这样在模版中才能够使用。

1
2
<p>num:{{ num }}</p>
<button @click="addNum(3)">计算</button>

注意: 需要从vue中导入ref函数。

2.3 toRefs函数基本使用

1
<p>年龄:{{ age }}</p>

这里如果想在模版中展示obj对象中age属性,可以简写成如上形式

1
2
3
4
5
6
return {
...obj, //解构
addAge,
num,
addNum
}

这里就需要将obj对象进行解构。

但是,当单击计算年龄按钮的时候,模版中的年龄数据没有变化,说明解构完以后,数据不是响应式的。

为了解决这个问题,需要使用到toRefs函数。

1
import { reactive, ref,toRefs } from 'vue';

导入toRefs函数

1
2
3
4
5
6
return {
...toRefs(obj), // 使用toRefs函数
addAge,
num,
addNum
}

通过toRefs函数包裹了obj对象。

这时候解构出的数据就是响应式的。

3、学生列表案例

3.1 基本结构

创建一个学生列表.vue组件。

同时修改一下main.js入口文件中的代码

1
2
3
4
import { createApp } from 'vue'
import App from './学生列表.vue'

createApp(App).mount('#app')

上面导入的是学生列表组件

该组件中的基本结构代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
<template>
<div class="score-case">
<div class="table">
<table>
<thead>
<tr>
<th>编号</th>
<th>科目</th>
<th>成绩</th>
<th>考试时间</th>
<th>操作</th>
</tr>
</thead>

<tfoot>
<tr>
<td colspan="5">
<span>总分:321</span>
<span style="margin-left:50px">平均分:80.25</span>
</td>
</tr>
</tfoot>
</table>
</div>
<div class="form">
<div class="form-item">
<div class="label">科目:</div>
<div class="input">
<input type="text" placeholder="请输入科目" />
</div>
</div>
<div class="form-item">
<div class="label">分数:</div>
<div class="input">
<input type="text" placeholder="请输入分数" />
</div>
</div>
<div class="form-item">
<div class="label"></div>
<div class="input">
<button class="submit">添加</button>
</div>
</div>
</div>
</div>

</template>
<script>
export default {
name:'StudList',
setup(){

}
}
</script>
<style lang="less">
.score-case {
width: 1000px;
margin: 50px auto;
display: flex;
.table {
flex: 4;
table {
width: 100%;
border-spacing: 0;
border-top: 1px solid #ccc;
border-left: 1px solid #ccc;
th {
background: #f5f5f5;
}
tr:hover td {
background: #f5f5f5;
}
td,
th {
border-bottom: 1px solid #ccc;
border-right: 1px solid #ccc;
text-align: center;
padding: 10px;
&.red {
color: red;
}
}
}
.none {
height: 100px;
line-height: 100px;
color: #999;
}
}
.form {
flex: 1;
padding: 20px;
.form-item {
display: flex;
margin-bottom: 20px;
align-items: center;
}
.form-item .label {
width: 60px;
text-align: right;
font-size: 14px;
}
.form-item .input {
flex: 1;
}
.form-item input,
.form-item select {
appearance: none;
outline: none;
border: 1px solid #ccc;
width: 200px;
height: 40px;
box-sizing: border-box;
padding: 10px;
color: #666;
}
.form-item input::placeholder {
color: #666;
}
.form-item .cancel,
.form-item .submit {
appearance: none;
outline: none;
border: 1px solid #ccc;
border-radius: 4px;
padding: 4px 10px;
margin-right: 10px;
font-size: 12px;
background: #ccc;
}
.form-item .submit {
border-color: #069;
background: #069;
color: #fff;
}
}
}
</style>

上面使用的是less构建的样式,为了能够进行解析,需要安装如下包

1
npm install  less-loader less

3.2 展示数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { reactive,toRefs } from 'vue'; 
setup(){
const list=reactive({
stuList:[
{id: 15, subject: '语文', score: 89, date: new Date('2022/06/07 10:00:00')},
{id: 27, subject: '数学', score: 100, date: new Date('2022/06/07 15:00:00')},
{id: 32, subject: '英语', score: 56, date: new Date('2022/06/08 10:00:00')},
{id: 41, subject: '物理', score: 76, date: new Date('2022/06/08 10:00:00')}

]
})
return {
...toRefs(list)
}
}

构建数据

模版中的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<thead>
<tr>
<th>编号</th>
<th>科目</th>
<th>成绩</th>
<th>考试时间</th>
<th>操作</th>
</tr>
</thead>
<!--判断是否有数据--->
<tbody v-if="stuList.length>0">
<!--数据遍历-->
<tr v-for="(item,index) in stuList" :key="item.id">
<td>
{{ index+1 }}
</td>
<td>
{{ item.subject }}
</td>
<td>
{{ item.score }}
</td>
<td>
{{ item.date }}
</td>
<td>
<a href="#">删除</a>
</td>
</tr>
</tbody>
<!--没有数据的处理-->
<tbody v-else>
<tr>
<td colspan="5">
<span class="none">暂无数据</span>
</td>
</tr>
</tbody>

进行日期格式的处理。(https://momentjs.com/

安装日期处理的包moment

1
npm install moment

定义日期处理的函数,并且返回

1
2
3
4
5
6
7
8
9
10
11
import moment  from 'moment' 

// 处理日期格式
const format=(date)=>{
return monment(date).format('YYYY/MM/DD HH:mm:ss');
}
return {
...toRefs(list),
add,
format // 返回format函数
}

修改模版中的代码,调用format函数

1
2
3
<td>
{{ format(item.date) }}
</td>

最后,将小于60分的成绩,添加样式

1
2
3
<td :class="{red:item.score < 60  }">
{{ item.score }}
</td>

判断的条件成立,将使用red样式

3.3 添加数据

1
2
3
4
5
6
7
8
9
10
11
const list=reactive({
stuList:[
{id: 15, subject: '语文', score: 89, date: new Date('2022/06/07 10:00:00')},
{id: 27, subject: '数学', score: 100, date: new Date('2022/06/07 15:00:00')},
{id: 32, subject: '英语', score: 56, date: new Date('2022/06/08 10:00:00')},
{id: 41, subject: '物理', score: 76, date: new Date('2022/06/08 10:00:00')}

],
subject:'',// 科目
score:0 // 成绩
})

定义响应数据subject,score,然后与视图中的文本框进行绑定。

1
2
3
<input  type="text" placeholder="请输入科目"   v-model.trim="subject"/>

<input type="text" placeholder="请输入分数" v-model.number="score" />

给按钮注册单击事件

1
<button class="submit" @click="add">添加</button>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 添加数据
const add=()=>{
if(list.subject===''||list.score > 100){
alert("请输入正确的数据");
return;
}
list.stuList.push({
id:list.stuList[list.stuList.length-1]+1,
subject:list.subject,
score:list.score,
date:new Date()
})
list.score = 0;
list.subject = "";

}
return {
...toRefs(list),
add // 返回add方法
}
}

完成数据的添加

3.4 数据删除

1
2
3
<td>
<a href="#" @click.prevent="del(item.id)">删除</a>
</td>

给删除的链接,注册单击事件,去掉默认行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 删除数据
const del=(id)=>{
if(confirm(`确定要删除编号${id}的数据吗?`)){
// 根据 id 唯一标识(发请求 / 从数组直接删)
// 删除某一id的项 => 过滤出所有不是该 id 的项 filter
list.stuList = list.stuList.filter(item=>item.id !==id)
}
}
return {
...toRefs(list),
add,
del, // 返回del方法
format
}

六、计算属性

1、计算属性说明

计算属性: 一个特殊属性, 值依赖于另外一些数据变化动态计算出来

1
2
3
4
注意:
1. 计算属性必须定义在 computed 节点中
2. 计算属性必须是一个 function或者是对象,计算属性必须有返回值
3. 计算属性不能被当作方法调用, 要作为属性来

计算属性特点:所使用的变量改变, 重新计算结果返回

参考文档:https://cn.vuejs.org/guide/essentials/computed.html(根据文档进行说明)

KilgourNote:

只要template中的属性发生变化,则全部的template代码都得执行一遍,因此,使用累加、计算总和这样的函数就会被重新执行一遍。

这大大增加了内存的负担,计算属性也就应运而生了。

2、通过计算属性计算总分与平均分

计算总分

1
import { computed, reactive,toRefs } from 'vue';

导入computed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 通过计算属性完成总分与平均分的计算
const total = computed(()=>{
// arr.reduce((上一次累加的结果, 每一项, 下标) => {
// return 累加的结果
// }, 起始累加值)
const result = list.stuList.reduce((sum,item)=>{
return sum + item.score
},0);
return result
})
return {
...toRefs(list),
add,
del,
format,
total // 返回total
}

模版中的代码

1
<span>总分:{{ total }}</span>

计算平均分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 计算平均分  
const avg = computed(()=>{
// 获取total中的值时,是通过value
const result = list.stuList.length >0 ? total.value/list.stuList.length:0;
return result.toFixed(2);
})
return {
...toRefs(list),
add,
del,
format,
total,
avg // 返回avg
}

模版中的代码:

1
<span style="margin-left:50px">平均分:{{avg}}</span>

3、计算属性缓存

若我们将同样的函数定义为一个方法而不是计算属性,两种方式在结果上确实是完全相同的,不同之处在于计算属性值会基于其响应式依赖被缓存。一个计算属性仅会在其响应式依赖更新时才重新计算。

例如上面的代码中,只要计算属性所依赖的list.stuList中的数据没有变化,也就是不会发生改变,不管访问多少次,都会立即返回之前的计算结果,而不是重复执行计算。实现了缓存的效果。

但是,如果定义成普通的方法来实现,就会每次访问的时候,都会调用所定义的方法重新计算,不管list.stuList中的数据是否发生变化。

所以计算属性提升了性能。

总结:

1
2
3
4
计算属性特点?
1 计算完了一次,就会自动进行缓存
2 如果依赖项不变, 下次使用直接从缓存取
3 直到依赖项改变, 函数才会重新执行并重新缓

4、计算属性完整写法

计算属性默认是只读的。当你尝试修改一个计算属性时,你会收到一个运行时警告。只在某些特殊场景中你可能才需要用到“可写”的属性,你可以通过同时提供 getter setter 来创建:

创建一个计算属性.vue组件,进行演示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<template>
<div>
<input type="text" v-model="name1" />
<input type="text" v-model="name2"/>
<span>{{ fullName }}</span>
</div>
</template>
<script>
import { computed, reactive, toRefs } from 'vue';

export default{
name:"ComputedApp",
setup(){
const obj = reactive({
name1:'李',
name2:'霸天'
})
// 这里computed需要的参数是一个对象
const fullName = computed({
get(){
return obj.name1+"-"+obj.name2;
},
set(value){
console.log("value=",value);
}
})

return {
...toRefs(obj),
fullName
}
}
}
</script>

上面代码中,定义了obj对象。并且将该对象中的属性与视图中的文本框进行了绑定。

这里创建了一个计算属性fullName,当在视图中展示fullName 的时候,会执行get函数。

问题:什么时候执行set函数呢?

当更新计算属性的时候,才会执行。

1
2
<span>{{ fullName }}</span>
<button @click="changeName">更改姓名</button>

在模版中添加了更改姓名按钮

1
2
3
4
5
6
7
8
9
// 更改姓名
const changeName=()=>{
fullName.value="张-三丰"
}
return {
...toRefs(obj),
fullName,
changeName // 返回changeName方法
}

changeName方法中,完成了对fullName值的修改。

这时候会执行set方法。

下面要做的就是完善set方法,给obj对象中的name1和name2属性进行赋值,

1
2
3
4
5
6
7
8
9
10
const fullName = computed({
get(){
return obj.name1+"-"+obj.name2;
},
set(value){
// console.log("value=",value);
obj.name1 = value.split('-')[0];
obj.name2 = value.split('-')[1];
}
})

set方法执行完毕以后,文本框中的内容也发生了变化。

5、实现全选与反选

展示基本的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<template>
<div>
<ul>
<li v-for="item in userList" :key="item.id">
<input type="checkbox" v-model="item.flag"/>
<span>{{ item.username }}</span>
</li>
</ul>
</div>
</template>
<script>
import { reactive, toRefs } from 'vue';

export default{
name:'SelectAll',
setup(){
const list=reactive({
userList:[
{
id:1,
username:'张三',
flag:false
},
{
id:2,
username:'李四',
flag:true
},
{
id:3,
username:'王五',
flag:false
}
]
})
return {
...toRefs(list)
}
}
}
</script>

模版中添加一个全选的复选框

1
2
3
<div>
<span>全选</span>
<input type="checkbox" v-model="isAll"/>

这里给复选框双向绑定了isAll这个计算属性。当复选框的状态发生变化的时候,会执行isAll这个计算属性的set方法(修改计算属性的值)。

例如:最开始的时候,全选复选框没有选中,当单击该复选框,将其选中,会执行set方法,在该方法中的value参数值就是true

这样数据中的flag属性都是true,所有的小复选框都被选中了。

当然,第一次打开页面的时候,也会执行isAll这个计算属性的get方法,这时候,我们发现,有的小复选框没有选中,这样every返回的就是false,从而全选复选框也不会被选中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 计算属性
const isAll=computed({
get(){
// every 必须每一个小复选框,都选中,isAll才为true
return list.userList.every(item=>item.flag===true);
},
set(value){
//console.log(value);
// value 参数就是设置全选框的新状态
list.userList.forEach(item=>item.flag = value);
}
})
return {
...toRefs(list),
isAll
}
1
2
3
 总结:计算属性使用场景:
1. 求和,求总价 计算求值...
2. 全选反选 ...

七、侦听器

vue中想监听数据的变化,应该怎么办?(监听器)

通过watch可以侦听到数据的变化。

我们可以在setup函数中使用watch函数创建一个侦听器。监听响应式数据的变化,然后执行一个回调函数,可以获取到监听的数据的新值与旧值。

watch的三个参数

第一个参数:要监听的数据

第二个参数:监听到的数据变化后执行的函数,这个函数有两个参数,分别是新值和旧值。

第三个参数:选项对象,deep(深度监听)和immediate(立即执行)

Watch函数的返回值是一个函数,作用是用来取消监听。

基本使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<template>
<div>

<!--这里使用了lazy,保证当文本框失去焦点后,才去执行对应操作-->
<span><input type="text" v-model.lazy="question" /></span>
<span>{{answer}}</span>
</div>
</template>
<script>
import { ref, watch } from 'vue';

export default{
name:'WatchApp',
setup(){
const question = ref('');
const answer = ref('');
watch(question,async(newValue,oldValue)=>{
// console.log('newValue=',newValue);
// console.log("oldValue=",oldValue);
const response = await fetch('https://www.yesno.wtf/api');
const data = await response.json();
answer.value = data.answer;

})
return {
question,
answer
}
}
}
</script>

在上面的代码中,setup函数内,定义两个响应式对象分别是questionanswer,这两项的内容都是字符串,所以通过ref来创建。使用watch来监听quest的变化,由于question与文本框进行了双向绑定,当在文本框中输入内容后,question的值会发生变化,发生变化后,就会执行watch的第二个参数,也就是回调函数。该回调函数内,发送一个异步请求,注意这里使用了fetch方法,发送请求,该方法返回的是一个promise对象,所以这里使用了await,下面调用json方法获取json对象,注意该方法返回的也是promise,所以这里也使用了await,最后把获取到的数据给了answer中的value属性。

1、立即执行

通过上面的案例,我们可以看到,当访问该页面的时候,watch方法并没有执行。

而是等待数据发生了变化以后,才执行的。

如果想立即执行,需要通过immediate:true来完成配置。

1
2
3
4
5
6
7
8
9
10
watch(question,async(newValue,oldValue)=>{
console.log('newValue=',newValue);
console.log("oldValue=",oldValue);
const response = await fetch('https://www.yesno.wtf/api');
const data = await response.json();
answer.value = data.answer;

},{
immediate: true
})

这里给watch函数添加了第三个参数,也是一个对象,在该对象中配置了immediate

2、深度侦听

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<template>
<div>
<button @click="person.user.userInfo.username = 'wangwu'">改名</button>
<button @click="person.user.userInfo.age = 80">改年纪</button>
</div>
</template>
<script>
import { watch, ref } from 'vue';

export default{
name:'WatchDemo',
setup(){
//(1)ref函数参数可以是基本数据类型,也可以接受对象类型;
//(2)如果参数是对象类型时,其实底层的本质还是reactive,
const person = ref(
{
user:{
userInfo: {
username:"zhangsan"
},
age:18,
address:'北京'
}
}
)
watch(person.value.user.userInfo,(newValue,oldValue)=>{
//对于复杂类型的监视,oldValue 和 newValue 会指向同一个对象, 所以值相同,oldValue无意义
console.log("newValue = ",newValue);
console.log("oldValue = ",oldValue);
},{
deep:true // 指定了深度监听
})
return {
person
}
}
}
</script>

reactive的数据,用不用deep:true是没有影响的

非业务需求:尽可能不要直接深度监视 => 相对消耗性能

如果是业务需求,就需要深度监视,就正常监视即可

最后我们把计算属性与侦听器做一个总结,看一下它们的应用场景。

第一点:语境上的差异

watch适合一个值发生了变化,对应的要做一些其它的事情

而计算属性computed:一个值由其它的值得来,其它值发生了变化,对应的值也会变化

第二点:计算属性有缓存性,侦听器没有

由于这个特点,我们在实际的应用中,能用计算属性的,会首先考虑先使用计算属性。

第三点:侦听器选项提供了更加通用的方法,适合执行异步操作或者较大开销操作。

八、组件基础

1、为什么使用组件

问题1: 以前遇到重复的结构代码, 怎么做的?

问题2: 复制粘贴? 可维护性高吗 ?

为什么使用组件?

组件的好处: 各自独立, 便于复用,可维护性高

2、什么是组件化开发

1
2
3
4
5
1. 组件是可复用的 Vue 实例, 封装标签, 样式和JS代码
2. 组件化 :封装的思想,把页面上 `可重用的部分` 封装为 `组件`,从而方便项目的 开发 和 维护
3. 一个页面, 可以拆分成一个个组件,一个组件就是一个整体
4. 每个组件可以有自己独立的 结构 样式 和 行为(html, css和js)
5. 例如:http://www.bootstrap.cn/ 所展示的效果,就契合了组件化开发的思想。

问题是,如何确定页面中哪些内容划分到一个组件中呢?

但你如何确定应该将哪些部分划分到一个组件中呢?你可以将组件当作一种函数或者是对象来考虑(函数的功能是单一的),根据[单一功能原则]来判定组件的范围。也就是说,一个组件原则上只能负责一个功能。如果它需要负责更多的功能,这时候就应该考虑将它拆分成更小的组件。

3、如何创建和使用组件

App.vue 是根组件, 这个比较特殊, 是最大的一个根组件。其实里面还可以注册使用其他小组件

使用组件的四步 单页面

1
2
3
4
5
1. 创建组件, 封装要复用的标签, 样式, JS代码
2. 引入组件
3. 注册组件
4. 使用组件:组件名当成标签使用即可
<组件名></组件名>

compnents目录下面创建一个组件:Header.vue,该组件中的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div class="hm-header">
头部区域
</div>
</template>
<script>
export default {
name:'HeaderComp'
}
</script>
<style lang="less">
.hm-header {
height: 150px;
border: 3px solid #000;
margin: 10px;
}
div {
background-color: red;
}
</style>

App.vue组件中使用上面定义的Header组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div>

<Header></Header>
<hr/>
App
</div>
</template>
<script>
// 导入
import Header from './components/Header.vue'
export default{
name:"App",
// 注册
components:{
Header
}
}
</script>

4、组件样式处理

默认情况下,写在组件中的样式会 全局生效,因此很容易造成多个组件之间的样式冲突问题

例如,上一小节案例中,我们在Header.vue这个子组件中,创建了

1
2
3
div {
background-color: red;
}

然后在App.vue这个父组件中使用了Header.vue这个子组件以后,以上样式不仅对Header.vue组件起作用,对App.vue组件也是起作用。

如果在Header.vue这个组件中定义的样式,只在该组件中起作用,应该怎样处理?

可以给组件加上 scoped 属性, 可以让样式只作用于当前组件

1
2
3
4
5
6
7
8
9
10
<style lang="less" scoped>
.hm-header {
height: 150px;
border: 3px solid #000;
margin: 10px;
}
div {
background-color: red;
}
</style>

scoped原理

1
2
(1)当前组件内标签都被添加 data-v-hash值 的属性
(2)css选择器都被添加 [data-v-hash值] 的属性选择

最终效果: 必须是当前组件的元素, 才会有这个自定义属性, 才会被这个样式作用到

九、组件通信

每个组件有自己的数据

每个组件数据独立, 组件数据无法互相直接访问 (合理的)

但是如果需要跨组件访问数据, 怎么办呢? => 组件通信

组件通信的方式有很多: 现在先关注两种, 父传子 子传

1、组件通信 - 父传子

首先明确父和子是谁

父传子语法

1
<Son price="100" title="不错" :info="msg"></Son

App.vue组件中的代码,如下所示:

1
<Header msg="hello" :title="title"></Header>
1
2
3
4
5
6
setup(){
const title = ref("Vue");
return{
title
}
}

Header.vue子组件中的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script>
export default {
name:'HeaderComp',
// 通过props进行接收
props:{
msg:{
type:String,
default:''
},
title:{
type: String,
required:true

}
}
}
</script>
1
2
3
<div class="hm-header">
头部区域:{{ msg }},标题:{{ title }}
</div>

总结:

1
2
3
父传子的基本步骤是什么?
父组件内, 给子组件添加属性的方式传值
子组件内, 通过 props 接
1
2
3
4
以后的数据,肯定是后台回来的,商品应该是动态渲染的
问题1: 一个个的写组件合理么?能循环使用组件吗?
问题2: 循环使用组件, 又如何向组件内传值呢?

2、v-for 遍历展示组件练习

components目录下面创建MyProduct.vue组件,该组件中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div>
<h3>标题:{{ title }}</h3>
<p>价格: {{ price }} 元一根</p>
<p>描述: {{ info }} </p>
</div>
</template>
<script>
export default{
name:'MyProduct',
props:["title","price","info","pid"],
setup(){

}
}
</script>

App.vue组件中的代码

1
2
import MyProduct from './components/MyProduct.vue';

注册组件

1
2
3
4
components:{
Header,
MyProduct
},

准备数据源

1
2
3
4
5
6
7
8
9
10
11
const list =reactive({
productList:[
{ id: 1, proname: '超级好吃棒棒糖', proprice: 188, low: 160, desc: '甜' },
{ id: 2, proname: '超级好吃的大鸡腿', proprice: 360, low: 320, desc: '香' },
{ id: 3, proname: '超级无敌冰激凌', proprice: 42, low: 30, desc: '冰' }
]
})
return{
title,
...toRefs(list)
}

修改模版中的代码

1
2
3
4
5
6
7
8
<hr/>
<MyProduct v-for="item in productList"
:key="item.id"
:title="item.proname"
:price="item.proprice"
:info ="item.desc"
:pid ="item.id"
/>

3、单向数据流

在vue中需要遵循单向数据流原则: (从父到子的单向数据流动, 叫单向数据流)

  1. 父组件的数据变化了,会自动向下流动影响到子组件
  2. 子组件不能直接修改父组件传递过来的 props, props是只读的!

4、组件通信 - 子传父

需求: 商品组件, 实现砍价功能

给商品这个子组件,添加一个砍价的按钮,当单击该按钮的时候,将对应商品的价格减去对应的值。

注意:商品的价格是在父组件中。

这就需要子组件向父组件中传递所砍价的钱数,以及商品的编号,这样在父组件中才会找到对应的商品,然后减去对应的钱数。

这就是子组件向父组件中传递数据。

修改/components/MyProduct.vue子组件中的代码

1
2
3
4
5
6
7
8
<template>
<div>
<h3>标题:{{ title }}</h3>
<p>价格: {{ price }} 元一根</p>
<p>描述: {{ info }} </p>
<button @click="setPrice">砍价</button>
</div>
</template>

添加了砍价按钮

setPrice方法的实现

1
2
3
4
5
6
7
8
9
10
// 第一个参数是props
// 从第二个参数中解构出emit方法
setup(props,{emit}){
const setPrice =()=>{
emit("setPrice",10,props.pid);
}
return {
setPrice
}
}

通过emit方法,触发父组件传递过来的setPrice事件,同时传递了响应的参数

下面修改父组件App.vue

1
2
3
4
5
6
7
8
<MyProduct v-for="item in productList" 
:key="item.id"
:title="item.proname"
:price="item.proprice"
:info ="item.desc"
:pid ="item.id"
@setPrice="handleSetPrice"
/>

这里为子组件MyProduct指定了事件setPrice事件,当子组件通过emit方法触发了该事件以后,会调用handleSetPrice方法,该方法中的实现代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 砍价
const handleSetPrice=(price,id)=>{
//根据id查找到对应的商品
const good = list.productList.find(item=>item.id===id);
// 砍价,条件是必须大于成本价,才能够砍价成功。
if(good.proprice-price>good.low){
good.proprice -= price;
}else{
good.price = good.low;
alert("已经是成本价了,不能在砍了");
}
}
return{
title,
...toRefs(list),
handleSetPrice
}

5、组件通信–兄弟组件通信

这里通过mitt这个第三方包来实现。

1
npm i mitt

第一:创建mitt的实例。

src下面创建untils目录,在该目录中创建 mitt.js文件,该文件中的代码:

1
2
3
import mitt from 'mitt'
const m = mitt();
export default m;

第二:在components/Header.vue组件中触发事件

1
2
3
4
<div class="hm-header">
头部区域:{{ msg }},标题:{{ title }}
<button @click="handleClick">兄弟组件</button>
</div>
1
2
3
4
5
6
7
8
9
const handleClick =()=>{

mitt.emit("handleChange",10); // 触发事件
}

return {
handleClick
}

import mitt from '@/utils/mitt

第三:在components/MyProduct.vue中处理事件

1
2
import {onMounted}from 'vue'
import mitt from "@/utils/mitt"
1
2
3
4
5
6
7
8
const someMethod=(num)=>{
console.log('num=',num);
}

onMounted(()=>{
console.log("onMounted");
mitt.on('handleChange',someMethod);
})

十、组件进阶

1、获取Dom元素

KilgourNote:

给标签加ref属性可以获取dom对象,

给组件标签加可以获取组件实例

Vue中 如何获取真实DOM元素呢 ?

利用ref可以用于 获取 dom 元素

基本使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div>
<h3 ref="myH3">标题</h3>
<button @click="handleGet">获取dom元素</button>
</div>
</template>
<script>
import {ref} from 'vue'
export default{
name:"DomApp",
setup(){
const myH3 =ref("");
const handleGet=()=>{
console.log(myH3.value);
}
return {
myH3,
handleGet
}
}
}
</script>

当单击按钮的时候,可以获取到对应的Dom元素。

1
2
3
4
const handleGet=()=>{
// console.log(myH3.value);
myH3.value.style.backgroundColor='blue'
}

这里给获取到的Dom元素设置了背景色。

通过ref属性获取组件对象

components目录下面创建Demo.vue组件,该组件中的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div>
<button @click="handleMethod">单击</button>
</div>
</template>
<script>
export default{
name:'DemoApp',
setup(){
const handleMethod=()=>{
console.log("hello demo");
}
return {
handleMethod
}
}
}
</script>

修改获取Dom元素.vue组件中的代码

1
2
3
4
5
6
import Demo from './components/Demo.vue'
export default{
name:"DomApp",
components:{
Demo
},

导入Demo组件,并且完成组件的注册

1
2
<Demo ref="demo"></Demo>
<button @click="handldeDemoGet">获取Demo组件</button>

在模版中使用了Demo组件,并且添加一个button按钮

1
2
3
setup(){
const myH3 =ref("");
const demo =ref("");

创建了demo对象,与Demo组件通过ref属性进行关联。

1
2
3
4
5
6
7
8
9
const handldeDemoGet=()=>{
demo.value.handleMethod();
}
return {
myH3,
handleGet,
demo,
handldeDemoGet
}

handleDemoGet方法中,可以调用Demo组件中的hanldeMethod方法。

2、nextTick函数

判断如下代码的输出结果?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 const count = ref(0);  
const pcount = ref("");
// 数字累加
const btnHandle = ()=>{
count.value += 1;
// 获取对应的`DOM`元素(马上获取原生的dom内容)
console.log(pcount.value.innerHTML);

}
return {
myH3,
handleGet,
demo,
handldeDemoGet,
count, // 返回count
btnHandle, // 返回
pcount// 返回
}

模版中的代码:

1
2
<p ref="pcount">数字:{{ count }}</p>
<button @click="btnHandle">计算</button>

当单击计算按钮的时候,是可以完成count值的累加。

但是问题是:在浏览器的控制台中输出的是之前的dom,造成这一问题的原因是:Vue在更新Dom的时候是异步的

也就是说,在更新完count这个状态数据以后,不是立即更新对应的dom元素。

但是,我们想更新完count这个状态数据库以后,立即获取到最新的dom元素,应该怎样处理呢?

这就需要使用到nextTick函数,如下代码:

1
2
3
4
5
6
7
8
9
10
import {ref,nextTick} from 'vue'
// 数字累加
const btnHandle = ()=>{
count.value += 1;

nextTick(()=>{
console.log(pcount.value.innerHTML);
})

}

nextTick(函数体):等DOM更新后, 才会触发执行此方法里的函数体

3、dynamic动态组件

动态组件是什么?

是 可以改变 的 组件

动态组件能解决什么需求呢 ?

解决多组件同一位置, 切换显示的需求

思考: 需要2个组件, 互斥的显示隐藏切换, 应该怎么做?

可以使用v-show,但是使用动态组件更加简单。

基本语法:

1
2
3
1. component 组件(位置) + is 属性 (哪个组件)
<component :is="要显示的组件" />
2. 修改is属性绑定的值完成组件切换

具体代码演示:

components目录下面创建UserAccount.vue组件,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<div class="user-account form-box">
<div class="form-box-item">
<label for="username">账 户: </label>
<input id="username" type="text" placeholder="请输入用户名">
</div>

<div class="form-box-item">
<label for="password">密 码: </label>
<input id="password" type="text" placeholder="请输入密码">
</div>
</div>
</template>

<script>
export default {
name: 'UserAccount'
}
</script>

<style lang="less">

</style>

再创建UserInfo.vue组件,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<template>
<div class="user-info form-box">
<div class="form-box-item">
<label for="words">人生格言: </label>
<input id="words" type="text" placeholder="请填写人生格言">
</div>

<div class="form-box-item">
<label for="info">个人简介: </label>
<input id="info" type="text" placeholder="请填写个人简介">
</div>

<div class="form-box-item">
<label for="hobby">兴趣爱好: </label>
<input id="hobby" type="text" placeholder="请填写兴趣爱好">
</div>
</div>
</template>

<script>
export default {
name: 'UserInfo'
}
</script>

<style lang="less">

</style>

然后在src目录下面创建动态组件.vue这个组件,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
<template>
<div>
<h1>动态组件dynamic的学习</h1>
<button class="my-btn-primary" @click="comName = 'UserAccount'">
账号密码填写
</button>
<button class="my-btn-success" @click="comName = 'UserInfo'">
个人信息填写
</button>
<!-- 挖个坑,在这个位置切换组件 -->
<component :is="comName"></component>
</div>
</template>
<script>
import UserInfo from './components/UserInfo.vue'
import UserAccount from './components/UserAccount.vue'
import { ref } from 'vue';
export default{
name:"DynamicApp",
components:{
UserInfo,
UserAccount
},
setup(){
const comName = ref("UserAccount");
return {
comName
}
}
}
</script>
<style lang="less">
.form-box {
box-shadow: 0 2px 12px 0 #ddd;
border-radius: 4px;
border: 1px solid #ebeef5;
background-color: #fff;
color: #303133;
transition: .3s;
min-height: 100px;
margin: 20px 0;
padding: 20px;
.form-box-item {
padding: 10px;
input {
-webkit-appearance: none;
background-color: #fff;
background-image: none;
border-radius: 4px;
border: 1px solid #dcdfe6;
box-sizing: border-box;
color: #606266;
display: inline-block;
font-size: inherit;
height: 40px;
line-height: 40px;
outline: none;
padding: 0 15px;
transition: border-color .2s cubic-bezier(.645,.045,.355,1);
margin-left: 10px;
}
}
}

.my-btn() {
display: inline-block;
line-height: 1;
white-space: nowrap;
cursor: pointer;
background: #fff;
border: 1px solid #dcdfe6;
color: #606266;
-webkit-appearance: none;
text-align: center;
box-sizing: border-box;
outline: none;
margin: 0;
transition: .1s;
font-weight: 500;
padding: 12px 20px;
font-size: 14px;
border-radius: 4px;
}
.my-btn-primary {
.my-btn();
background-color: #409eff;
border-color: #409eff;
color: #fff;
}
.my-btn-success {
.my-btn();
background-color: #67c23a;
border-color: #67c23a;
color: #fff;
}
</style>

4、插槽

需求: 要在页面中显示一个对话框, 封装成一个组件

问题:组件的内容部分,不希望写死,希望能使用的时候自定义。怎么办?

(1)父传子: 父传子固然可以完成一定层面的组件文本的定制(可以用于传值), 但是自定义性较差, 且无法自定义结构。

(2)如果希望能够自定义组件内部的一些结构 => 就需要用到插槽(可以传递结构)

4.1 默认插槽

基本使用

默认插槽:没有起名字,默认名字叫defalut

下面看一下插槽的基本使用。

components目录下面创建Dialog.vue组件,该组件中的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<template>
<div class="dialog">
<div class="dialog-header">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name:'DialogApp',
setup(){

}
}
</script>
<style>
.dialog {
width: 400px;
border: 3px solid #000;
border-radius: 5px;
margin: 10px;
padding: 0 10px;
padding-bottom: 20px;
}
</style>

插槽.vue组件中使用上面创建的Dialog.vue组件,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div>
<h3>友情提示</h3>
<Dialog>
<p>请输入手机号码</p>
</Dialog>
</div>
</template>
<script>
import Dialog from './components/Dialog.vue'
export default{
name:'SlotApp',
components:{
Dialog
},
setup(){

}
}
</script>

在使用Dialog组件的时候,指定了内容。

Dialog.vue组件中,可以指定插槽中默认显示的内容

1
2
3
4
5
<div class="dialog-header">
<slot>
<p>请输入邮箱</p>
</slot>
</div>

这样在插槽.vue组件中,如果不指定内容,显示插槽中默认的内容,如果指定了内容,就展示所指定的内容。

4.2 具名插槽

组件内有 2处以上 的内容,不确定怎么办?

可以自定义名字,通过name自定义名字,可以实现定向分发

需求:一个组件内有多处,需要外部传入标签,进行定制

语法:

1
2
3
4
语法:
1. 多个slot使用name属性区分名字 <slot name="content"></slot>
2. template配合v-slot:名字来分发对应标签
v-slot:可以简化成#

Dialog.vue组件中的代码:

1
2
3
4
5
6
7
8
9
10
11
<template>
<div class="dialog">
<div class="dialog-header">
<slot name="header" ></slot>
<slot>
<p>请输入邮箱</p>
</slot>
<slot name = "footer"></slot>
</div>
</div>
</template>

添加了两个具名插槽,分别是headerfooter

修改插槽.vue组件中的代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div>
<h3>友情提示</h3>
<Dialog>
<p>请输入手机号码</p>
</Dialog>
<Dialog >
<!--使用header插槽-->
<template v-slot:header>
<p>我是头部</p>
</template>
<!--使用footer插槽-->
<template v-slot:footer>
<p>我是底部</p>
</template>
</Dialog>
</div>
</template>

在 Vue 中,很多指令都有简写形式,v-slot:name 指令也有简写形式,比如看我们下面的示例。

原写法:

1
2
<template v-slot:footer>
</template>

简写

1
2
<template #footer>
</template>

4.3 作用域插槽

我们仔细思考插槽的使用,会发现这有一点类似于父子组件传递,只不过插槽传递的是模板内容罢了。那么涉及到传值,就会有一个作用域的问题

作用域插槽: 定义 slot 插槽的同时, 是可以传值的。给插槽上可以绑定数据,将来使用组件时可以用。

修改Dialog.vue组件

1
2
3
<slot title="标题1" msg = "hello title">
<p>请输入邮箱</p>
</slot>

这里指定了title,msg两个属性

插槽.vue组件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div>
<Dialog >
<template v-slot:header>
<p>头部布局 </p>
</template>

<template v-slot="{title,msg}">
<p>请输入用户名:{{ title }}--{{ msg }}</p>
</template>

<template v-slot:footer>
<p>底部布局 </p>
</template>

</Dialog>
</div>
</template>

通过v-slot来指定了一个值slotProps,它是一个对象,该对象中存储了对应的数据。

我们都知道对象是可以解构的,所以我们在父组件(插槽.vue)中还可以直接使用解构的写法来获取数据。

1
2
3
4
5
<h3>友情提示</h3>
<Dialog v-slot="{title,msg}">
<p>请输入手机号码</p>
<p>{{ title }},{{ msg }}</p>
</Dialog>

十一、生命周期

1、生命周期基本认知

vue组件生命周期:从创建 到 销毁 的整个过程就是 – Vue实例的 - 生命周

2、生命周期探讨

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
在这一小节中,我们看一下`vue`生命周期中其它的一些钩子函数内容。

其实`Vue`实例的生命周期,主要分为三个阶段,分别为

- 挂载(初始化相关属性,例如`watch`属性,`method`属性)
1. `beforeCreate`
2. `created`
3. `beforeMount`
4. `mounted`
- 更新(元素或组件的变更操作)
1. `beforeUpdate`
2. `updated`
- 销毁(销毁相关属性)
1. `beforeUnmount`
2. ``unmounted`
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<template>
<div>

</div>
</template>
<script>
export default{
name:'CreateLife',
beforeCreate(){
console.log("beforCreate");
},
created() {
console.log("created");
},
beforeMount() {
console.log("beforeMount");
},
mounted() {
console.log("mounted");
},
beforeUpdate() {
console.log("beforeUpdate");
},
updated() {
console.log("updated");
},
beforeUnmount() {
console.log("beforeUnmount");
},
unmounted() {
console.log("`unmounted");
},
}
</script>

在上面的代码中,我们将所有的钩子函数都添加上了,然后打开浏览器,看下执行结果:

1
2
3
4
beforCreate
created
beforeMount
mounted

beforeCreate: Vue实例初始化之后,以及事件初始化,以及组件的父子关系确定后执行该钩子函数,一般在开发中很少使用

created: 在调用该方法之前,初始化会被使用到的状态,状态包括props,methods,data,computed,watch.由于data数据已经初始化,所以可以获取到组件的数据。

beforeMount:在执行该钩子函数的时候,虚拟DOM已经创建完成,马上就要渲染了,经过这一步后,在模板中所写的{{foo}}会被具体的数据所替换掉。

所以下面执行mounted的时候,可以看到真实的数据。同时整个组件内容已经挂载到页面中了,数据以及真实DOM都已经处理好了,可以在这里操作真实DOM了,也就是在mounted的时候,页面已经被渲染完毕了,在这个钩子函数中,我们可以去发送ajax请求。

下面看一下更新的操作

1
2
3
<div>
{{ foo }}
<button @click="update">更新</button>
1
2
3
4
5
6
7
8
9
10
11
methods:{
update: function () {
this.foo = "abc";
},

},
data(){
return {
foo:'hello'
}
},

这里在data函数中定义了foo,然后单击更新按钮的时候,调用update函数完成foo的值更新。

1
2
3
4
5
当整个组件挂在完成后,有可能会进行数据的修改,当`Vue`发现`data`中的数据发生了变化,会触发对应组件的重新渲染,先后调用了`beforeUpdate` 和`updated`钩子函数。

在`updated`之前`beoreUpdate`之后有一个非常重要的操作就是虚拟`DOM`会重新构建,也就是新构建的虚拟`DOM`与上一次的虚拟`DOM`树利用`diff`算法进行对比之后重新渲染。

而到了`updated`这个方法,就表示数据已经更新完成,`dom`也重新`render`完成。

销毁:

beforeUnmount,unmounted

Vue实例已经被销毁,所有的事件监听器会被移除,所有的子实例也会被销毁。

setup是在组件初始化之前执行的,是在beforeCreatecreated之间执行的,所以beforeCreatecreated的代码都可以放到setup函数中。

所以,在上图中,我们可以看到beforeCreatecreatedsetup中没有对应的实现。

其它的都是在钩子函数名称前面添加上on,并且首字母大写。

注意:onUnmounted类似于之前的destoryed,调用组件的onUnmounted方法会触发unmounted钩子函数

十二、路由

1、单页应用程序: SPA - Single Page Application

具体使用示例: 网易云音乐 https://music.163.com

单页面应用(SPA): 所有功能在一个html页面上实现 (多页面应用程序MPA)

1
2
3
4
5
6
优点:
不整个刷新页面,每次请求仅获取需要的部分,用户体验更好
数据传递容易, 开发效率高
缺点:
开发成本高(需要学习专门知识 - 路由)
首次加载会比较慢一点。不利于seo

2、路由的介绍

前端Vue中的路由是什么

路径 和 组件的映射关系

路径 和 组件的切换匹配, 怎么实现呢?

vue-router vue-router本质是vue官方的一个路由插件,是一个第三方包

官网: https://router.vuejs.org/zh

vue-router集成了 路径 和 组件的切换匹配处理,我们只需要配置规则即可。

在具体学习vue-router如何使用之前,我们需要认知 组件的两种分类

组件分类: .vue文件分2类, 一个是页面组件, 一个是复用组件

src/views文件夹: 页面组件 - 页面展示 - 配合路由用

src/components文件夹:复用组件 - 展示数据 - 常用于复用

3、vue-router基本使用

1
npm install vue-router

首先在项目中单的src目录下面创建一个views文件夹,在该文件夹中创建以下页面组件

find.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div>
<p>发现音乐</p>
<p>发现音乐</p>
<p>发现音乐</p>
</div>
</template>

<script>
export default {
name: 'FindIndex'
}
</script>

<style>

</style>

my.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div>
<p>我的音乐</p>
<p>我的音乐</p>
<p>我的音乐</p>
</div>
</template>

<script>
export default {
name: 'MyIndex'
}
</script>

<style>

</style>

part.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div>
<p>好朋友</p>
<p>好朋友</p>
<p>好朋友</p>
</div>
</template>

<script>
export default {
name: 'PartIndex'
}
</script>

<style>

</style>

src目录下面router目录,在router目录下面创建index.js文件,该文件中的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { createRouter, createWebHashHistory } from "vue-router";

const routes =[
{
path:'/find',
name:'find',
component:()=>import("../views/find.vue")
},
{
path:'/my',
name:'my',
component:()=>import("../views/my.vue")
},
{
path:'/part',
name:'part',
component:()=>import("../views/part.vue")
}
]
const router =createRouter({
// //history模式:createWebHistory
history:createWebHashHistory(),//使用hash模式
routes
});
export default router;

main.js入口文件中导入以上创建的路由对象,并且挂在到Vue实例中

1
2
3
4
5
import { createApp } from 'vue'
import App from './App.vue'
import router from "./router"; // 导入路由实例
createApp(App).use(router).mount('#app') // 使用路由

App.vue中添加路由出口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<template>
<div>
<div class="footer_wrap">
<!-- router-link本质还是a标签,通过 to 配置属性指定跳转路径,to无需# -->

<router-link to="/find">发现音乐</router-link>
<router-link to="/my">我的音乐</router-link>
<router-link to="/part">朋友</router-link>


</div>
<div class="top">
<!-- 指定出口:决定匹配的组件,在哪渲染 -->
<router-view></router-view>
</div>
</div>
</template>
<script>
export default{
name:'App'
}
</script>
<style scoped>

.footer_wrap {
position: fixed;
left: 0;
top: 0;
display: flex;
width: 100%;
text-align: center;
background-color: #333;
color: #ccc;
}
.footer_wrap a {
flex: 1;
text-decoration: none;
padding: 20px 0;
line-height: 20px;
background-color: #333;
color: #ccc;
border: 1px solid black;
}
.footer_wrap a:hover {
background-color: #555;
}
.top {
padding-top: 62px;
}

.active {
background-color: olive!important;
color: #fff!important;
}

</style>

启动项目进行测试。

4、声明式导航 - 跳转传参

目标:在跳转路由时, 可以给路由对应的组件内传值

router-link上的to属性传值, 语法格式如下

1
2
/path?参数名=值
/path/值 – 需要路由对象提前配置 path: “/path/参数名
1
<router-link to="/my?id=1">我的音乐</router-link>

这里传递了参数id,表示音乐的编号。

看一下my.vue组件中怎样接收参数?

1
<p>我的音乐:{{ $route.query.id }}</p>

setup函数中,通过如下方方式接收参数

1
2
3
4
5
6
7
8
9
10
<script>
import { useRoute } from 'vue-router';
export default {
name: 'MyIndex',
setup(){
const route =useRoute();
console.log(route.query.id);
}
}
</script>

下面再来看另外一种传递参数的情况

1
<router-link to="/find/10">发现音乐</router-link>

修改App.vue组件中的链接

修改router/index.js定义的路由规则

1
2
3
4
5
6
const routes =[
{
path:'/find/:id',
name:'find',
component:()=>import("../views/find.vue")
},

在配置路由规则的时候,需要指定对应的参数。

修改views/find.vue组件中的代码,在该组件中接收传递过来的参数

1
2
3
<template>
<div>
<p>发现音乐:{{ $route.params.id }}</p>

在模版中通过$route.params来接收传递过来的参数

同理setup入口函数中的处理如下所示:

1
2
3
4
5
6
7
8
9
10
<script>
import { useRoute } from 'vue-router';
export default {
name: 'FindIndex',
setup(){
const route = useRoute();
console.log("id=",route.params.id);
}
}
</script>

5、重定向

匹配path后, 强制跳转path路径

修改router/index.js中所定义的路由规则:

1
2
3
4
5
6
import { createRouter, createWebHashHistory } from "vue-router";
const routes =[
{
path:'/',
redirect:"/my"
},

当在浏览器中输入地址:http://localhost:8080/#/的时候,直接调整到/my

6、404

问题: 当访问不存在的页面应该要显示什么呢?

404:当找不到路径匹配时,给个提示页面

router/index.js文件中的routes数组中添加如下路由规则

1
2
3
4
5
6
7
{
path:'/part',
name:'part',
component:()=>import("../views/part.vue")
},
// 404
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: ()=>import("../views/NotFound.vue") },

src/views目录中创建NotFound.vue组件

1
2
3
<template>
你访问的页面不存在
</template>

7、编程式导航

编程式导航:用JS代码来进行跳转

语法: path或者name任选一个 // location.href=”/home/index”

修改views/find.vue组件中的代码

1
2
<p>发现音乐</p>
<button @click="handleClick">跳转到我的音乐</button>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 这里需要导入useRouter 
import { useRoute,useRouter } from 'vue-router';

export default {
name: 'FindIndex',
setup(){
const route = useRoute();
const router = useRouter(); // 创建router实例
console.log("id=",route.params.id);
// 创建handleClick函数
const handleClick=()=>{
router.push("/my") // 根据路径进行跳转
}
return{
handleClick
}
}

或者采用如下写法:

1
2
3
4
5
const handleClick=()=>{
router.push({
path:"/my"
})
}

下面通过name的方式实现跳转

1
2
3
4
5
const handleClick=()=>{
router.push({
name:'my' // 这里指定的就是路由的名字
})
}

当然在编程式导航的时候,可以进行参数的传递。

1
2
3
4
5
6
7
8
const handleClick=()=>{
router.push({
name:'my',
query:{
"id":12
}
})
}

这里是通过query的方式来进行传参的在my.vue组件中通过$route.query.id 方式来进行接收。

8、路由嵌套

在现有的一级路由下, 再嵌套二级路

二级路由示例-网易云音乐-发现音乐下 https://music.163.com

1
2
3
4
5
6
1. 创建需要用的所有组件
src/views/Find.vue -- 发现音乐页
src/views/My.vue -- 我的音乐页
src/views/second/recommend.vue -- 发现音乐页 / 推荐页面
src/views/second/ranking.vue -- 发现音乐页 / 排行榜页面
src/views/second/songList.vue -- 发现音乐页 / 歌单页

views目录下面创建second目录,并且在该目录下面创建以上对应的组件

修改router/index.js文件中所定义的路由规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const routes =[
{
path:'/',
redirect:"/my"
},
{
path:'/find/:id',
name:'find',
component:()=>import("../views/find.vue"),
// 配置子路由
children:[
{
path:'ranking',
name:'ranking',
component:()=>import("../views/second/rank.vue")
},
{
path:'recommend',
name:'recommend',
component:()=>import("../views/second/recommend.vue")
},
{
path:'songlist',
name:"songlist",
component:()=>import("../views/second/songList.vue")
}
]
},
{
path:'/my',
name:'my',
component:()=>import("../views/my.vue")
},

find.vue组件中添加router-view

1
2
3
4
5
6
7
8
9
10
11
<template>
<div>
<p>发现音乐:{{ $route.params.id }}</p>
<p>发现音乐</p>
<p>发现音乐</p>
<button @click="handleClick">跳转到我的音乐</button>
<p>
<router-view></router-view>
</p>
</div>
</template>
1
http://localhost:8080/#/find/10/recommend

输入以上地址进行测试。

有可能会出现文件名称的错误,所以在vue.config.js文件中关闭检测功能

1
2
3
4
5
6
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
lintOnSave: false // 关闭检测
})

参考:https://blog.csdn.net/qq_57587705/article/details/124674660

以上是关于路由的基本应用,关于其他的应用,在项目中会进行讲解。

十三、Vuex

1、vuex 概述

vuexvue的状态管理工具,状态即数据。 状态管理就是集中管理vue通用的 一些数据

注意(官方原文):https://vuex.vuejs.org/zh/guide/

  • 不是所有的场景都适用于vuex,只有在必要的时候才使用vuex
  • 使用了vuex之后,会附加更多的框架中的概念进来,增加了项目的复杂度 (数据的操作更便捷,数据的流动更清晰)

vuex的优点: 方便的解决多组件的共享状态

  • 它是独立于组件而单独存在的,所有的组件都可以把它当作 一座桥梁 来进行通讯。

  • 特点:

    • 响应式: 只要仓库一变化,其他所有地方都更新 (太爽了!!!)
    • 操作更简洁

什么数据适合存到vuex

一般情况下,只有 多个组件均需要共享的数据 ,才有必要存储在vuex中,

对于某个组件中的私有数据,依旧存储在组件自身的data中。

例如:

  • 对于所有组件而言,当前登陆的 用户信息 是需要在全体组件之间共享的,则它可以放在vuex中
  • 对于文章详情页组件来说,当前的用户浏览的文章列表数据则应该属于这个组件的私有数据,应该要放在这个组件data中。

2、Vuex基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Vuex是专门为Vue.js设计的状态管理库。

Vuex采用集中式的方式存储需要共享的状态。

Vuex的作用是进行状态管理,解决复杂组件通信,数据共享的问题。

什么情况下使用Vuex?

非必要的情况下不要使用Vuex.

如果是小项目,并且不会涉及到大量组件的通信,不需要使用Vuex,使用反而增加了项目的复杂度。

如果是开发大型的单页应用程序,这时会涉及到多个视图依赖于同一状态,并且来自不同视图的行为需要变更同一状态,这时就需要用到Vuex.



以上图能够理解,那么我们就理解了Vuex.

我们先来看一下State,State是全局管理的状态,把状态绑定了到了组件,也就是视图(Vue Components)上,渲染到界面上,展示给用户。用户可以和视图进行交互,例如单击视图中的按钮。这时会通过Dispatch来分发Actions.在Actions中可以进行异步操作(Backend API),例如单击按钮后,可以发送异步的ajax请求。到异步操作结束后,可以执行Commit提交Mutations的操作,记录状态的更改。Mutations必须是同步的,所有状态的更改都需要通过Mutations来完成。


下面我们再来总结一下关于Vuex中核心概念

Store:就是仓库,是使用Vuex的核心,每一个应用只有一个Store,Store是一个容器。包含了应用中的大部分的状态,当然我们是不能直接改变Store中的状态,我们要通过提交Mutation的方式来修改状态。

State:就是状态,保存在Store中,因为Store是唯一的,所以State也是唯一的。但是所有的状态都保存在State中,会让状态难以维护,我们后面可以通过模块来解决这个问题。注意这里的状态是响应式的。

Getter:就是Vuex中的计算属性,方便从一个属性,派生出其它的值。它的内部可以对计算的结果进行缓存,只有当所依赖的状态发生了改变后,才会重新进行计算。

Mutation:状态的变化必须通过提交Mutation来完成,Action与Mutation类似,不同的地方是:Action可以进行异步的操作,内部需要改变状态的时候,都要提交Mutation

Module:如果我们把所有的状态都存储到了State中,后期会变得非常难维护,Store也就变得非常臃肿,为了解决这个问题,Vuex允许我们将Store分隔成模块。每一个模块都有自己的State,Mutation,Action,Getter,在后期的案例中,我们会体会Module的使用。

在这里我们先来看一下vuex的基本使用

store/index.js文件中,添加了如下代码:

1
npm install vuex

src目录下面创建store目录,在该目录下面创建index.js文件,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
import { createStore } from "vuex";

export default createStore({
state: {
username: "张三",
},
mutations: {},
actions: {},
modules: {},
});

在仓库中添加了一个状态数据,username

下面就可以在组件中获取对应的状态数据。(创建一个App.vue组件)

1
2
3
4
5
6
7
8
9
10
<template>
<div>
<div>HelloWorld:{{$store.state.username }}</div>
</div>
</template>
<script>
export default{
name:'App'
}
</script>

main.js文件中使用vuex

1
2
3
4
5
6
import { createApp } from 'vue'
import App from './App.vue'
import store from './store'
// import router from "./router";
// createApp(App).use(router).mount('#app')
createApp(App).use(store).mount('#app')

这里为了方便测试,暂时将路由注释掉。

如果在setup入口函数中获取状态数据,可以采用如下的写法:

1
2
3
4
5
6
7
8
9
10
11
<script>
import { useStore } from "vuex";
export default{
name:'App',
setup(){
//使用vuex仓库
const store = useStore();
console.log(store.state.username);
}
}
</script>

3、getters用法

getters的作用是对state属性进行计算,类似于vue中的计算属性,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。

例如把username中的数据倒序输出,或者过滤商品数据等。

store/index.js文件中,添加对应的getters代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { createStore } from "vuex";

export default createStore({
state: {
username: "张三",
},
getters: {
newName(state) {
return state.username + "你好!";
},
},
mutations: {},
actions: {},
modules: {},
});

返回到App.vue模板中使用:

1
2
3
4
5
6
7
8
<template>
<div>
<div>HelloWorld:{{$store.state.username }}</div>
<p>
{{ $store.getters.newName }}
</p>
</div>
</template>

在上面的代码中,通过$store.getters.newName获取getters中的数据,或者是采用如下写法:

1
2
3
<p>
{{ $store.getters["newName"] }}
</p>

newName本身就是对象中的一个属性。所以可以采用如上的写法。

setup入口函数中可以采用如下的写法:

1
2
3
4
5
6
setup(){
//使用vuex仓库
const store = useStore();
console.log(store.state.username);
console.log(store.getters.newName);// 获取仓库中的getters
}

4、mutations用法

状态数据的修改必须提交Mutaion,并且Mutaion的修改必须是同步执行的。

继续修改store/index.js文件中的代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { createStore } from "vuex";

export default createStore({
state: {
username: "张三",
},
getters: {
newName(state) {
return state.username + "你好!";
},
},
mutations: {
updateName(state) {
state.username = "李四";
},
},
actions: {},
modules: {},
});

在上面的mutations中,定义了updateName方法来修改state状态中的username属性的值。

下面返回到App.vue中,我们来看一下怎样使用mutations中的updateName方法完成状态更新操作。

1
2
3
4
5
6
7
8
9
10
11
12
<template>
<div>
<div>HelloWorld:{{$store.state.username }}</div>
<p>
{{ $store.getters.newName }}
<p>
{{ $store.getters["newName"] }}
</p>
<button @click="mutationsFn">提交</button>
</p>
</div>
</template>

在模板中,我们添加了一个提交按钮,单击该按钮执行mutaionsFn这个函数。

该函数的实现如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
import { useStore } from "vuex";
export default{
name:'App',
setup(){
//使用vuex仓库
const store = useStore();
console.log(store.state.username);
console.log(store.getters.newName);// 获取仓库中的getters
const mutationsFn = () => {
store.commit("updateName");
};
return {
mutationsFn,
};
}
}
</script>

mutationsFn函数中,我们通过store.commit提交了一个updateName mutations,完成用户名的更新操作。

注意:这里需要将mutationsFn方法返回,在模板中才能使用。

5、actions用法

actions的作用:

如果需要执行异步操作,我们需要通过Action来完成。

如果异步执行完毕后,需要修改状态,需要提交Mutation来修改State状态。因为所有的状态的修改都需要通过Mutation来完成。

例如:我们需要异步获取商品数据,就需要在Action中发送请求,异步完成后,需要提交Mutation,把数据记录到State中。

下面修改store/index.js中的代码:

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

export default createStore({
state: {
username: "张三",
count:0 // 添加了count这个状态值
},
getters: {
newName(state) {
return state.username + "你好!";
},
},
mutations: {
updateName(state) {
state.username = "李四";
},
//payload是调用mutations的时候传递的额外的值。
//这里就是传递过来的要累加的值
increate(state, payload) {
state.count += payload;
},
},
actions: {
//context:上下文,该对象中包含了state,commit,getters等成员
increateAsync(context, playload) {
setTimeout(() => {
context.commit("increate", playload);
}, 1000);
},
},
modules: {},
});

在上面的代码中,我们首先定义了一个count状态属性

actions中定义了一个increateAsync方法,该方法中通过setTimeout进行异步请求的模拟,在1秒中以后提交increate这个mutations修改count的状态,这里传递了参数playload

mutations中定义了increate方法,完成了count这个状态值的更新。

返回到App.vue中。

1
2
3
4
5
<p>
{{ $store.state.count }}
</p>
<button @click="mutationsFn">提交</button>
</p>

展示了count这个状态值

1
2
3
4
5
6
7
const mutationsFn = () => {
// store.commit("updateName");
store.dispatch("increateAsync", 6);
};
return {
mutationsFn,
};

mutationsFn方法中,通过store.dispatch来派发increateAsync方法,并且传递的参数是6.

以上就是Vuex的基本使用。

6、模块基本使用

这节课我们来看一下Module模块的内容。

如果我们把所有的状态都存储到了State中,后期会变得非常难维护,Store也就变得非常臃肿,为了解决这个问题,Vuex允许我们将Store分隔成模块。每一个模块都有自己的State,Mutation,Action,Getter等。当状态内容非常多的时候,Module是非常有用的。

例如:我们这里把用户的数据放到User模块中,购物车中的数据放到Cart模块中等等。

下面我们先来定义模块的内容,当模块定义好以后,还需要在Store中进行注册。

修改store/index.js文件中的代码,如下所示:

在下面的代码中,我们定义了A模块和B模块,这两个模块中都定义了stategetters

并且B模块中开启了命名空间,A模块没有开启命名空间。

最后把两个模块都在createStore方法中的modules进行了注册。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import { createStore } from "vuex";
// A模块
const moduleA = {
state: {
username: "moduleA",
},
getters: {
newName(state) {
return state.username + "你好";
},
},
};
// B模块
const moduleB = {
namespaced: true,
state: {
username: "moduleB",
},
getters: {
newName(state) {
return state.username + "你好";
},
},
};
export default createStore({
modules: {
moduleA,
moduleB,
},
});
// export default createStore({
// state: {
// username: "张三",
// count:0
// },
// getters: {
// newName(state) {
// return state.username + "你好!";
// },
// },
// mutations: {
// updateName(state) {
// state.username = "李四";
// },
// //payload是调用mutations的时候传递的额外的值。
// //这里就是传递过来的要累加的值
// increate(state, payload) {
// state.count += payload;
// },
// },
// actions: {
// //context:上下文,该对象中包含了state,commit,getters等成员
// increateAsync(context, playload) {
// setTimeout(() => {
// context.commit("increate", playload);
// }, 1000);
// },
// },
// modules: {},
// });

下面,返回到App.vue组件中,使用一下(将原有的App.vue组件进行备份)

1
2
3
4
5
6
7
<template>
<div>
<!-- 使用A模块中的state数据 -->
<p>{{ $store.state.moduleA.username }}</p>
<p>{{ $store.getters.newName }}</p>
</div>
</template>

要获取A模块中的state数据的时候,需要添加上模块的名字,但是在获取getters中的内容的时候不需要添加模块的名字,如果添加了模块的名字,会出错。

原因:由于A模块没有添加命名空间,所以除了state需要添加模块名称,模块内部的getters,action,mutaions都是注册在全局下的。

下面我们再来看一下模块B的访问

1
2
3
4
5
6
7
8
9
10
11
<template>
<div>
<!-- 使用A模块中的state数据 -->
<p>{{ $store.state.moduleA.username }}</p>
<p>{{ $store.getters.newName }}</p>
<!-- 使用B模块中的state -->
<p>{{ $store.state.moduleB.username }}</p>
<!-- 使用B模块中的getters -->
<p>{{ $store.getters["moduleB/newName"] }}</p>
</div>
</template>

在使用B模块中的state的时候需要添加模块名称。

这里重点需要注意的是:在访问B模块中的getters的时候的写法。

1
{{ $store.getters["moduleB/newName"] }}

在方括号中,先写模块的名称,后面再写getters的名称。

下面我们在把B模块中的mutationsactions都补充一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import { createStore } from "vuex";
// A模块
const moduleA = {
state: {
username: "moduleA",
},
getters: {
newName(state) {
return state.username + "你好";
},
},
};
// B模块
const moduleB = {
namespaced: true,
state: {
username: "moduleB",
count: 0,
},
getters: {
newName(state) {
return state.username + "你好";
},
},
mutations: {
updateName(state) {
state.username = "李四";
},
//payload是调用mutations的时候传递的额外的值。
//这里就是传递过来的要累加的值
increate(state, payload) {
state.count += payload;
},
},
actions: {
//context:上下文,该对象中包含了state,commit,getters等成员
increateAsync(context, playload) {
setTimeout(() => {
context.commit("increate", playload);
}, 1000);
},
},
};
export default createStore({
modules: {
moduleA,
moduleB,
},
});

通过上面的代码,我们可以看到mutationsactions的内容和前面写的是一样的。

下面返回到App.vue组件中使用一下mutationsactions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div>
<!-- 使用A模块中的state数据 -->
<p>{{ $store.state.moduleA.username }}</p>
<p>{{ $store.getters.newName }}</p>
<!-- 使用B模块中的state -->
<p>{{ $store.state.moduleB.username }}</p>
<!-- 使用B模块中的getters -->
<p>{{ $store.getters["moduleB/newName"] }}</p>

<p>{{ $store.state.moduleB.count }}</p>
<button @click="mutationsFn">mutationsFn</button>
<button @click="actionsFn">actionsFn</button>
</div>
</template>

在上面的代码中,我们添加了count这个状态,同时添加了两个按钮。

下面看一下当按钮的单击事件触发以后,对应的方法的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script>
import { useStore } from "vuex";
export default {
name: "App",
setup() {
const store = useStore();
const mutationsFn = () => {
// 调用B模块中的mutations
store.commit("moduleB/updateName");
};
const actionsFn = () => {
// 调用B模块中的actions

store.dispatch("moduleB/increateAsync", 6);
};
return {
mutationsFn,
actionsFn,
};
},
};
</script>

在上面的代码中,不管是执行commit或者是dispatch方法,都指定了模块的名字,然后后面跟上mutations的名字或者是actions的名字。

我们在项目中都需要给模块添加命名空间。

7、Vuex数据持久化操作

在项目中,我们需要将vuex中存储的数据持久化到本地,例如:我们在项目中的用户信息需要存储到vuex中,但是需要持久化到本地,否则刷新浏览器数据会丢失。

还是就是购物车的数据,也是存储到vuex中,当然在用户未登录的情况是可以向购物车中添加数据的,而vuex中存储的购物车中的商品数据也需要持久化到本地。

关于对vuex中的数据持久化的操作,这里我们使用vuex-persistedstate插件来实现

1
npm i vuex-persistedstate

下面在src/store目录下面创建modules文件夹,在该文件夹下面创建cart.jsuser.js两个模块文件。

cart.js文件中的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
// 购物车模块
export default {
namespaced: true,
state() {
return {
// 购物车中的商品列表
list: [],
};
},
};

这里我们定义了cart这个模块,并且开启了命名空间。注意这里的state是一个方法,这是语法上的要求。最终返回的是购物车中的商品列表数据。

user.js文件中定义的内容如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 用户模块
export default {
namespaced: true,
state() {
return {
// 用户信息
profile: {
id: "",
avatar: "", // 用户头像
nickname: "", // 用户昵称
account: "", // 用户账号名称
mobile: "", // 手机号码
token: "", // token 数据
},
};
},
};

这里也是开启了命名空间,同时用户的信息存储到了profile对象中,该对象中定义的属性与服务端接口返回的内容是一致的。

这一点,后面讲解到的时候,我们可以再来看一下。

下面在添加一个category.js文件,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
// 商品分类模块
export default {
namespaced: true,
state() {
return {
// 分类信息的集合
list: [],
};
},
};

下面在修改store/index.js文件中的代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
import { createStore } from "vuex";
import cart from "./modules/cart";
import user from "./modules/user";
import category from "./modules/category";
export default createStore({
modules: {
cart,
user,
category,
},
});

在上面的代码中完成了模块的注册操作。

在进行持久化之前,我们先来做一个测试,看一下当刷新浏览器以后,vuex容器中的数据是否会丢失。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 用户模块
export default {
namespaced: true,
state() {
return {
// 用户信息
profile: {
id: "",
avatar: "", // 用户头像
nickname: "", // 用户昵称
account: "", // 用户账号名称
mobile: "", // 手机号码
token: "", // token 数据
},
};
},
mutations: {
// 更新用户的信息,payload就是用户信息对象
setUser(state, payload) {
state.profile = payload;
},
},
};

我们在store/modules/user.js文件中添加了一个mutations方法setUser,该方法的作用就是修改用户信息。

下面返回到App.vue这个组件中(这里将原来的App组件重新命名,这里创建一个新的App.vue组件),该组件中的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div>
<!-- 修改用户数据,测试是否实现了持久化 -->
{{ $store.state.user.profile.account }}
<button @click="$store.commit('user/setUser', { account: 'zhangsan' })">
更改用户信息
</button>
</div>
</template>
<script>
export default {
name: "App",
};
</script>

在模板中先获取user模块中的profile对象中的account用户名数据。

下面单击按钮以后,完成account属性值的修改,注意commit方法在提交的时候,需要指定模块的名称以及对应的mutations中的方法名。

​ 返回到浏览器中进行测试,当单击了按钮以后,页面中展示了zhangsan这个用户名,但是当刷新后,用户名信息丢失了。

所以下面需要对vuex中的数据实现持久化的操作。

要想持久化,我们可以把vuex中的数据持久化到localStorage中,但是问题是,每次写关于操作localStorage的代码也是非常麻烦的。

所以这里我们可以通过 vuex-persistedstate这个插件来实现。

这个插件安装好以后,下面看一下怎样使用?

store/index.js文件中配置该插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { createStore } from "vuex";
import cart from "./modules/cart";
import user from "./modules/user";
import category from "./modules/category";
import createPersistedState from "vuex-persistedstate"; // 导入vuex-persistedstate插件
export default createStore({
modules: {
cart,
user,
category,
},
// 配置插件
plugins: [
createPersistedState({
// 本地存储的名字
key: "shop-project-vuex-store",
// 指定要存储的模块(这里将user和cart购物车中的数据进行持久化)
paths: ["user", "cart"],
}),
],
});

在上面的代码中,我们导入了vuex-persistedstate这个插件,并且指定了存储到本地的数据的名字,以及要存储的模块。

下面返回到浏览器中进行测试,当单击了按钮以后,完成了数据的更新,然后刷新浏览器发现数据没有丢失。

数据都存储到了localStorage中了,这里可以看一下。