Vue2-基础3

Vue2基础3

条件渲染

案例:如图

使用v-if

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div id="app">
<span>现在的n值是{{n}}</span><br>
<button @click="n++">点我n自增1</button><br>
<button @click="n--">点我n自减1</button><br>
<div v-if="n<3">我在n小于3的时候出现</div>
<div v-else-if="n<7">我在n大于3小于7的时候出现</div>
<div v-else>我在n>7的时候显示</div>
</div>
<script type="text/javascript">
new Vue({
el: '#app',
data(){
return {
n: 0
}
}

})
</script>

使用v-show

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div id="app">
<span>现在的n值是{{n}}</span><br>
<button @click="n++">点我n自增1</button><br>
<button @click="n--">点我n自减1</button><br>
<div v-show="n<3">我在n小于3的时候出现</div>
<div v-show="n>=3">我在n大于3的时候出现</div>
</div>
<script type="text/javascript">
new Vue({
el: '#app',
data(){
return {
n: 0
}
}

})
</script>

区别

1
2
3
4
1.	v-show的原理是将被此指令标记的DOM元素的display属性设置为none,从而实现隐藏的效果
v-if的原理是直接将此DOM元素直接从DOM树中删除
2. 如果这个元素会频繁切换显示隐藏,则使用v-show,不需要频繁创建节点
如果这个元素不会频繁切换显示隐藏,只是在初始化的时候进行判断显示隐藏,则使用v-if

列表渲染

案例:将数组数据展示在页面上

1
2
3
4
5
6
7
8
9
data(){
return {
people: [
{id: '001',name: '张三',age: 18,sex: '男'},
{id: '002',name: '芝麻',age: 20,sex: '男'},
{id: '003',name: '李四',age: 17,sex: '女'}
]
}
}

使用v-for指令

1
2
3
4
5
<ul>
<li v-for="item in people">
{{item.name}}-{{item.age}}-{{item.sex}}
</li>
</ul>

这样就完成了最基本的遍历,如果我需要得到每一次遍历的索引,只需要加入一个形参即可

1
2
3
4
5
<ul>
<li v-for="(item,index) in people">
{{index}}-{{item.name}}-{{item.age}}-{{item.sex}}
</li>
</ul>

OK,这里其实遗漏了一个很重要很重要的一个属性key,这个key就是每一个节点的标识,所以可以直接使用数组每一个对象的id属性,或者是index

1
2
3
4
5
6
<ul>
<!--<li v-for="(item,index) in people" :key="index">-->
<li v-for="(item,index) in people" :key="item.id">
{{index}}-{{item.name}}-{{item.age}}-{{item.sex}}
</li>
</ul>

这个key是干嘛的呢?这就要好好扒一扒了,来吧,整活!

虚拟DOM的DIFF算法

啥啊这是,怎么从key直接跳到了什么虚拟DOM的DIFF算法了?走错片场了?

没有没有,要解释key就要先知道这个知识

现在的代码,很简单,就是比刚刚多了一个添加一个人的按钮和一个input框

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
<div id="app">
<button @click.once="addPerson">添加一个人</button>
<ul>
<li v-for="(item,index) in people" :key="index">
{{item.name}}-{{item.age}}-{{item.sex}}<input type="text" v-model="item.name">
</li>
</ul>
</div>
<script type="text/javascript">
new Vue({
el: '#app',
data(){
return {
people: [
{id: '001',name: '张三',age: 18,sex: '男'},
{id: '002',name: '芝麻',age: 20,sex: '男'},
{id: '003',name: '李四',age: 17,sex: '女'}
]
}
},
methods: {
addPerson(){
this.people.unshift({id: '002',name: '小马',age: 21,sex: '男'})
}
}
})
</script>

说明问题

然后点击添加一个人,你会发现一个很严重的错误,输入塌陷

加入:key="index"之后,一样出现这个错误

加入:key="item.id"之后,我们发现,这个错误就消失了

既然意识到了错误和解决错误的方法,那我们就来好好捋一下为什么,下面看图,图有点乱,请仔细观看

key为index的情况

解释几个点吧:

1、粉色的×表示匹配失败,不可以复用,蓝色的√表示匹配成功,可以复用

2、左下角的橙色的删除号表示该元素直接删除

3、下半部分的橙色箭头表示直接复用之前生成的真实DOM节点

4、左半部分的绿色箭头表示需要从虚拟节点重新生成真实DOM节点

key为id的情况
key不写的情况

如果不写key的话,Vue默认将index作为key,所以这种情况和key为index的情况是一样的

总结
1
2
3
4
5
6
7
8
9
10
11
12
13
1、key的作用
作为每一个DOM虚拟节点的唯一性标识,当数据发生改变的时
在Diff算法时,新虚拟DOM节点找到与自身key相同的旧虚拟DOM节点进行比较
如果相同,则直接将旧虚拟DOM节点生成的真实DOM节点直接拿来复用
如果不同,则将新虚拟DOM节点生成新真实DOM节点

2、用index做key会出现的问题
(1)如果出现了对顺序改变的操作,则会导致旧真实DOM无法复用,效率降低
(2)如果有输入类控件,则会出现输入塌陷的问题

3、如何设置key
(1)如果只是展示,不会产生逆序插入,逆序删除等会影响顺序的情况,则使用index即可
(2)如果会对顺序造成影响,则不能使用index作为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
<div id="app">
<input type="text" v-model="keyword" placeholder="请输入姓关键词" @keyup.enter="filterData">
<ul>
<li v-for="(item,index) in people" :key="index">
{{item.name}}-{{item.age}}-{{item.sex}}<input type="text">
</li>
</ul>
</div>
<script type="text/javascript">
new Vue({
el: '#app',
data(){
return {
keyword: '',
people: [
{id: '001',name: '张三',age: 18,sex: '男'},
{id: '002',name: '芝麻',age: 20,sex: '男'},
{id: '003',name: '李四',age: 17,sex: '女'},
],
showData: people
}
},
methods: {
filterData(){
this.showData = this.people.filter(p=>p.name.indexOf(this.keyword)!==-1);
}
}
})
</script>

列表排序

1
2
3
4
5
6
7
<button @click="sortByAge">按年龄排序</button>

methods: {
sortByAge(){
this.showData = this.people.sort((p1,p2)=>p2.age-p1.age);
}
}

修改列表第一个人的信息(奏效)

1
2
3
4
5
6
7
8
<button @click="updatePerson">修改第一个人的年龄为20</button>

methods: {
updatePerson(){
this.people[0].age = 20;
this.showData = this.people;
}
}

彻底修改列表一个人的信息(失效)

1
2
3
4
5
6
<button @click="updatePersonComplete">彻底修改一个人的信息</button>

updatePersonComplete(){
this.people[0] = {id: '004',name: '老马',age: 18,sex: '女'};
this.showData = this.people;
}

为什么这种方式修改数据,Vue没有检测到呢?

这就要讲到Vue监测数据变化(响应式)的原理了

响应式原理

之前我在讲数据代理的时候,其实已经讲到了Vue响应式的原理,现在我们来正式给小伙伴讲一讲

Vue监测数据分为两种,一种是数组,还有一种是对象。我们先从监测对象开始,然后再讲监测数组。

监测对象

前期准备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div id="app">
<div>姓名:{{name}}</div>
<div>性别:{{sex}}</div>
<div>年龄:{{age}}</div>
<div>学校:{{school}}</div>
</div>
<script type="text/javascript">
let data = {
name: 'zhima',
sex: '男',
age: 19,
hobbies: ['王者','原神','睡觉','干饭'],
school: {
name: '南京某高校',
address: '南京'
}
}
let vm = new Vue({
el: '#app',
data
});
</script>

我们先来回顾一下数据代理是什么

先输出一下vm对象

我们在data中配置的对象先经过Vue加工变成Observer对象

然后将其放到vm对象上的_data属性中

然后将_data中的数据全部代理到vm对象上

让我们可以直接使用{{}}读取vm身上的属性

这就是数据代理

我们修改了vm身上的属性实际上修改的是_data中的数据 , 然后页面就更新成了修改后的新的数据

或者直接修改_data中的数据,页面也会更新为修改后的数据

这样看来,响应式数据的原理是在_data中.还记得我之前提到的数据劫持吗?

Vue的响应式就是通过数据劫持来实现的,而数据劫持又是通过Object.defineProperty实现的,_data的每一个属性都有一个setter函数,当_data中任何一个数据

发生了改变,都会走这个setter方法,那么在这个时候,我就可以执行一遍重新渲染页面的方法,从而使得页面数据更新

监测数组

我们先来观察一下_data中的hobbies数组的样子,我们发现它没有给每一个元素设置一个getter/setter,那它是怎么实现响应式的呢

这里就不绕弯了

Vue里面对于数组的监测是

通过包装数组方法,先对页面进行更新,然后再调用原生的数组上的方法对数组进行更改

有以下这几种

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

Vue中的push不是原生的push

所以只有当你使用以上这些方法的时候,Vue才会监测到,然后对页面进行更新,这就解释了先前的那个案例,直接将数组第一个元素替换的时候,Vue并没有监测到.

但如果数组元素是对象,它一样还是会有getter/setter

过滤器

功能:先将数据作出处理再将数据进行显示

案例:显示格式化后的时间

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
<div id="app">
<div>
时间戳:{{nowTime}}
</div>
<div>
计算属性格式化后的时间是:{{fmtTime}}
</div>
<div>
方法格式化后的时间是:{{getFmtTime()}}
</div>
<div>
过滤器格式化后的时间:{{nowTime | timeFormatter}}
</div>
</div>
<script type="text/javascript">
new Vue({
el: '#app',
data(){
return {
nowTime: Date.now()
}
},
computed:{
fmtTime() {
return dayjs(this.nowTime).format('YYYY-MM-DD HH:mm:ss');
}
},
methods: {
getFmtTime(value) {
return dayjs(value).format('YYYY-MM-DD HH:mm:ss');
}
},
filters:{
timeFormatter(value){
return dayjs(value).format('YYYY-MM-DD HH:mm:ss')
}
}
})

</script>

以上这个过滤器叫局部过滤器,还有一种过滤器,叫全局过滤器

1
2
3
4
5
6
7
8
9
10
11
12
<div id="app">
<div>
<!--这里的过滤器第一个传入的参数永远是待处理的数据,这里传入的就直接是第二个参数第三个第四个参数-->
过滤器格式化后的时间:{{nowTime | globalTimeFormatter('YYYY-MM-DD')}}
</div>
</div>
<script type="text/javascript">
//全局过滤器
Vue.filter('globalTimeFormatter',(value,formatStr='YYYY-MM-DD HH:mm:ss')=>{
return dayjs(value).format(formatStr);
})
</script>

注意点:过滤器不是必要的,因为我们使用计算属性或者方法都可以实现,这只是一种方式

自定义指令

指令是什么?

指令就是v-on开头的一些语法,就是自己封装一些逻辑

需求1:定义一个v-big指令,会把绑定的数值放大10倍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<div id="app">
<h2>当前的n值是:<span v-text="n"></span> </h2>
<h2>放大10倍后的n值是:<span v-big="n"></span> </h2>
<button @click="n++">点我n+1</button>
</div>
<script type="text/javascript">
new Vue({
el:'#app',
data() {
return {
n: 1
}
},
directives:{
// big函数何时会被调用?
// 1.指令与元素成功绑定时。
// 2.指令所在的模板被重新解析时。
big(element,binding){
console.log('big',this) //注意此处的this是window
element.innerText = binding.value * 10
}
}
}
</script>

需求2:定义一个v-fbind指令,和v-bind功能类似,但可以让其所绑定的input元素默认获取焦点。

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
<div id="app">
<input type="text" v-fbind:value="n">

</div>
<script type="text/javascript">

new Vue({
el:'#root',
data() {
return {
n: 1
}
},
directives:{
fbind:{
//指令与元素成功绑定时(一上来)
bind(element,binding){
element.value = binding.value
},
//指令所在元素被插入页面时
inserted(element,binding){
element.focus()
},
//指令所在的模板被重新解析时
update(element,binding){
element.value = binding.value
}
}
}
}
</script>

这两个都是局部指令,我们来看看全局指令

1
2
3
4
5
6
7
8
9
10
11
12
 Vue.directive('fbind',{
bind(element,binding){
element.value = binding.value
},
inserted(element,binding){
element.focus()
},
//指令所在的模板被重新解析时
update(element,binding){
element.value = binding.value
}
})

注意点:

1
2
3
指令定义时不加v-,但使用时要加v-;

指令名如果是多个单词,要使用kebab-case命名方式,不要用camelCase命名。

生命周期

定义:Vue中每一个生命周期都对应着一个钩子(函数),当Vue走到该生命周期阶段的时候,就会自己调用该钩子。

黄色的标签是我加的注释,其他的流程都是从Vue官网查看的

最常用的就是mounteddestoryed

一般在mounted钩子中初始化数据(Ajax请求),在destoryed钩子中删除定时器,解绑自定义事件等收尾工作

组件

模块化的概念

如果所有的js代码都在一个文件中,那么后期维护和更新会非常吃力。所以就有了模块化的概念了,这里可以看看我之前写的JavaScript的模块化规范

组件的定义

用来实现局部功能效果的所有代码和资源的集合(HTML、CSS、JavaScript)

传统开发

我们先来看看传统开发的结构

其存在的问题是

1、代码复用率不高

2、依赖关系混乱,后期难以维护

组件开发

我们来看看官网给我们的图

最上层的就是vm对象,它管理所有的组件对象

在Vue中有两种使用组件化开发的思路

1、非单文件组件

2、单文件组件

非单文件组件

定义组件
1
2
3
4
5
6
7
8
9
10
11
12
const hello = Vue.extend({
template:`
<div>
<h2>你好啊!{{name}}</h2>
</div>
`,
data(){
return {
name:'Tom'
}
}
})

这里的data配置项我们说过可以写成两种形式

1、函数式

2、对象式

到了组件开发,这个data配置项就必须是函数式

避免组件被复用时,数据存在引用关系。

注册组件

局部注册

1
2
3
4
5
6
7
new Vue({
el:'#app',
//第二步:注册组件(局部注册)
components:{
hello
}
})

全局注册

1
Vue.component('hello',hello)
使用组件
1
2
3
<div id="app">
<hello></hello>
</div>

注意点:

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
1.关于组件名:
一个单词组成:
第一种写法(首字母小写):school
第二种写法(首字母大写):School
多个单词组成:
第一种写法(kebab-case命名):my-school
第二种写法(CamelCase命名):MySchool (需要Vue脚手架支持)
备注:
(1)组件名尽可能回避HTML中已有的元素名称
(2)可以使用name配置项指定组件在开发者工具中呈现的名字

2.关于组件标签:
第一种写法:<school></school>
第二种写法:<school/>不用使用脚手架时,<school/>会导致后续组件不能渲染。

3.关于VueComponent:
school组件本质是一个名为VueComponent的构造函数,且不是程序员定义的,是Vue.extend生成的。

我们只需要写<school/>或<school></school>,Vue解析时会帮我们创建school组件的实例对象,即Vue帮我们执行的:new VueComponent(options)。

4.关于this指向:
(1).组件配置中:
data函数、methods中的函数、watch中的函数、computed中的函数 它们的this均是【VueComponent实例对象】。
(2).new Vue(options)配置中:
data函数、methods中的函数、watch中的函数、computed中的函数 它们的this均是【Vue实例对象】。

5.VueComponent的实例对象,以后简称vc(也可称之为:组件实例对象)。
Vue的实例对象,以后简称vm。

6.Vc和Vm的区别
两者最大的差别就是vc没有el配置项,vc只能依附vm
其他的配置项可以说是一样的

7.重要的内置关系
函数上的prototype属性叫显示原型属性
对象上的__proto__属性叫隐式原型属性
两者指向的都是同一个原型对象
实例对象的隐式原型属性指向自己缔造者的原型对象
VueComponent.prototype.__proto__ === Vue.prototype
让组件实例对象(vc)可以访问到 Vue原型上的属性、方法。

单文件组件(脚手架)

这里我们就可以使用脚手架(Vue CLI)啦!

CLI(Command Line Interface)命令行接口,我们就叫它脚手架

首先,你必须要有node和gitbash的环境

在gitbash的命令行中输入以下命令

1
2
3
4
5
6
7
8
# 下载脚手架
npm i @vue/cli -g

# 创建Vue项目
vue create 项目名

# 启动Vue项目
npm run serve

注意

如果你是在idea中的终端运行的vue create 项目名报了以下错误,有以下两种解决方案

1、以管理员身份打开idea

2、使用命令行到当前目录下使用该命令

这是正常启动成功的界面

脚手架的HelloWorld案例界面

观察项目结构

main.js
1
2
3
4
5
6
7
8
9
10
// 引入Vue
import Vue from 'vue'
// 引入App根组件
import App from './App.vue'
// 关闭生产提示
Vue.config.productionTip = false
// 创建vm对象
new Vue({
render: h => h(App),
}).$mount('#app')

我们之前是这么写的

1
2
3
4
5
6
7
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
new Vue({
template: `<App></App>`,
components: {App}
}).$mount('#app')

我们运行,发现报错了,报错如下

大概意思是脚手架使用的运行时版本的Vue,它没有模板解析器

你可以将模板放入render函数中,或者使用完整版的Vue

来解决问题

使用完整版Vue
1
import Vue from 'vue/dist/vue'

发现不报错了,页面也正常显示了

使用render函数
1
2
3
4
5
6
7
8
9
10
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
render(createElement){
return createElement(App)
}
}).$mount('#app')

一个参数省略小括号

1
2
3
4
5
6
7
8
9
10
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
render: createElement=>{
return createElement(App)
}
}).$mount('#app')

直接写返回语句省略花括号和return

1
2
3
4
5
6
7
8
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
render: createElement => createElement(App)
}).$mount('#app')

换个参数名

1
2
3
4
5
6
7
8
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
render: h => h(App)
}).$mount('#app')

为什么要使用残缺版的Vue呢?

模板解析器占了Vuejs源码的1/3的体积,为了减少打包后的体积,所以就将模板解析器删去,用更小的render函数来完成该任务

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
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
</div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'

export default {
name: 'App',
components: {
HelloWorld
}
}
</script>

<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>

我们可以看到一个vue文件可以分为三部分

第一部分是用来写HTML结构的

第二部分是用来写JavaScript的

第三部分是用来写样式的

index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<!--针对IE,让IE浏览器最高级别渲染页面-->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<!--引入页面图标<%= BASE_URL %>可以拿到项目根路径,就是public文件夹-->
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<!--设置网站名字,拿着package.json中配置的name作为网站名-->
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<!--当网站不支持JavaScript的时候,显示noscript中的内容-->
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<!--容器-->
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
给作者买杯咖啡吧~~~