Vue2 - 核心
1. 初始Vue
引入Vue.js:你可以下载Vue.js到本地,也可以使用Vue的CDN,不过我还是推荐下载到本地。
下载完毕之后,使用script
标签引入即可。
在引入完Vue之后,这时候只是允许你使用Vue,但是页面并没有什么变化,所以,你要使用Vue就需要创建一个Vue实例,并且给出一些配置。
例如,我现在有这么一段代码:
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="../Vue_js/vue.js"></script>
</head>
<body>
<div id="root">
<h1>hello world</h1>
</div>
<script type="text/javascript">
Vue.config.productionTip = false; //阻止Vue在启动时生成生产提示。
</script>
</body>
我现在的要求是我的h1中的内容不能写死了,必须根据实际得到的数据来展示。
这个时候就需要用到Vue:
创建Vue实例,并且传入配置对象:
new Vue({
key:value
});
学过axios的我们知道,在使用axios发送请求的时候,也是需要传入配置对象,例如发送的url是什么,请求的方式是什么;这里也是一样,需要传入一个配置对象:
new Vue({
el:'#root' //el表示element,value为CSS选择器,这里是id选择器。
});
这样就将Vue和上面的div容器进行了关联,现在,你就可以继续设置配置对象,来达到需求:
<div id="root">
<h1>{{keyword}} world</h1>
</div>
<script type="text/javascript">
Vue.config.productionTip = false; //阻止Vue在启动时生成生产提示。
//创建Vue实例
new Vue({
el:'#root',//el用于指定当前Vue实例为哪个容器服务,值通常为css选择器字符串
data:{
keyword:'hello' //data中用于存储数据,数据供e1所指定的容器去使用,值我们暂时先写成一个对象。
}
});
</script>
对于上述代码中{{}}
表示插值语法,先记住,我通过data配置属性中keyword属性来渲染数据,最终显示到页面上还是hello world。
注意:上述代码中的Vue实例只是通过el配置属性绑定了id为root的div,对于其他的div是没有联系的。容器和实例之间的关系只能是一对一。
例如:
<div id="root">
<h1>{{keyword}} world</h1>
</div>
<div id="child">
<h1>{{keyword}} world</h1>
</div>
可见第二个div并没有渲染出数据。
总结:
- 想让Vue工作,就必须创建一个Vue实例,且要传入一个配置对象
- root容器里的代码依然符合html规范,只不过混入了一些特殊的Vue语法
- root容器里的代码被称为【Vue模板】:也就是上面的
{{}}
- Vue实例和容器是一一对应的,且只能是一一对应的
- 真实开发中只有一个Vue实例,并且会配合着组件一起使用
- 中的xxx要写js**表达式**,且xxx可以自动读取到data中的所有属性,一旦data中的数据发生改变,那么页面中用到该数据的地方也会自动更新
2. 模板语法
Vue中的模板语法有两大类:
插值语法:
{{}}
插值语法用于解析标签体内容,例如:
<h1>{{keyword}} world</h1>
,其中h1中的内容就是标签体。语法:
{{xxx}}
,xxxx 会作为 js 表达式解,例如:<h1>{{Date.now()}}</h1>
,其中{{}}
中的内容就是一个js表达式。js表达式:一个表达式会产生一个值,可以放在任何一个需要值的地方,例如
a、a+b、demo(1)、x===y?'a':'b'
。js语句(代码):
if(){}、for(){}
指令语法:
v-xxx
用于解析标签(包括:标签属性、标签体内容、绑定事件.···.)
例如我现在有这么一段代码:
<div id="root"> <h1>{{keyword}} world</h1> <a href="www.baidu.com">点我去百度</a> </div> <script type="text/javascript"> Vue.config.productionTip = false; //阻止Vue在启动时生成生产提示。 new Vue({ el:'#root',//el表示element,value为CSS选择器,这里是id选择器。 data:{ keyword:'hello' } }); </script>
其中的a标签中,href写了跳转的地址,现在我同时要求使用Vue来渲染这个跳转地址,不能写死了,可能出现下面几种情况:
<div id="root"> <h1>{{keyword}} world</h1> <a href="url">点我去百度</a> </div> <script type="text/javascript"> Vue.config.productionTip = false; //阻止Vue在启动时生成生产提示。 new Vue({ el:'#root',//el表示element,value为CSS选择器,这里是id选择器。 data:{ keyword:'hello', url:'www.baidu.com' } }); </script>
或者:
<div id="root"> <h1>{{keyword}} world</h1> <a href="{{url}}">点我去百度</a> </div> <script type="text/javascript"> Vue.config.productionTip = false; //阻止Vue在启动时生成生产提示。 new Vue({ el:'#root',//el表示element,value为CSS选择器,这里是id选择器。 data:{ keyword:'hello', url:'www.baidu.com' } }); </script>
可见都没有渲染成功,所以,对于标签相关的内容,我们一般使用指令语法来渲染:
<div id="root"> <h1>{{keyword}} world</h1> <a v-bind:href="url">点我去百度</a> </div> <script type="text/javascript"> Vue.config.productionTip = false; //阻止Vue在启动时生成生产提示。 new Vue({ el:'#root',//el表示element,value为CSS选择器,这里是id选择器。 data:{ keyword:'hello', url:'www.baidu.com' } }); </script>
可见这下成功渲染出来了。
注意:
v-bind:href="xxx"
或简写为:href="xxx"
,xxx同样要写js表达式,且可以直接读取到data中的所有属性。并且在实际开发中,一般简写使用的较多。Vue中有很多的指令,且形式都是:
v-xxx
,此处我们只是拿v-bind举个例子。
3. 数据绑定
数据绑定分为两种:
单项数据绑定:
v-bind
数据只能从data流向页面,例如如下代码:
<div id="root"> 单项数据绑定: <input type="text" :value="desc"> </div> <script type="text/javascript"> Vue.config.productionTip = false; //阻止Vue在启动时生成生产提示。 new Vue({ el:'#root',//el表示element,value为CSS选择器,这里是id选择器。 data:{ desc:'我是单项数据绑定' } }); </script>
双向数据绑定:v-model
数据不仅能从 data 流向页面,还能从页面流向data,例如如下代码:
双项数据绑定: <input type="text" v-model:value="desc">
将
v-bind
换为v-model
即可。注意:双向绑定一般都应用在表单类元素上(如:input、select等) ;v-model:value可以简写为v-model,因为v-model默认收集的就是value值。
4. data和el的两种写法
el的两种写法:
写法一:
new Vue({
el:'#root'
});
写法二:
const x = new Vue();
x.$mount('#root');
这里有些人可能会疑惑,我将x对象输出给大家看看:
看了上图,并没有看见$mount
,这是为什么呢?因为$mount
是存放在了原型对象上:
在原型对象上,由于x是Vue的实例,所以x够看见Vue身上的原型对象。
这里的mount就相当于挂载的意思,可以理解为将Vue实例挂载到了容器身上,使二者产生了关系,所以就能通过Vue来渲染数据了。
在实际工作做,el的写法一和写法二都可以,任选一个几个。
data的两种写法:
写法一:对象式
new Vue({
el:'#root',
data:{
desc:'数据'
}
});
写法一是data后面直接跟上一个对象。
写法二:函数式
new Vue({
el:'#root',
data:function (){
return {
//数据对象
}
}
});
写法二是跟一个函数,不过这个函数必须要有返回值,返回值一般就是一个数据对象
简写:
new Vue({
el:'#root',
data(){
return {
//数据对象
}
}
});
一般我们使用简写形式居多。
注意: 如果考虑到this的问题,那你就要考虑清楚,你的this指向想要指向谁;如果想要指向Vue,那么这里就只能写为普通函数,不能写为箭头函数,如果想要指向window,那么这里就可以使用箭头函数。
在实际开发中,如果设计到了组件,那么你就必须使用函数式,否则就会报错
还有一个重要原则:由Vue管理的函数,一定不要写箭头函数,一旦写了箭头函数,this就不再是Vue实例了。这个其实就是我上面提到的this指向的问题。
5. MVVM模型
MVVM实际上是3部分:
- M:模型(Model) :对应 data 中的数据
- V:视图(View) :模板
- VM:视图模型(ViewModel) : Vue 实例对象
在Vue(VM)中,存在一个DOM监听器和Data绑定,当视图(View/DOM)改变的时候,会被Vue给监听到,从而将改变的数据放入model中,当model中的数据改变的时候,也会将数据给Binding到View中,所以Vue相当于一个桥梁,后面我们常将Vue的实例对象命名为vm
,表示视图模型。
对于上图中,data中所有的属性都会出现在Vue上,例如desc,并且上诉的所有属性在Vue模板中可以直接使用,不用再使用对象点属性的方式来调用。
6. Object.defineProperty方法
我现在有这样一段代码:
let person = {
name:'念心卓'
}
我现在要给person对象添加一个age属性,可能以前是person.age=value
的形式,不过现在我们有更加高级的方式:Object.defineProperty
Object.defineProperty
方法是更高级的给对象添加属性方式。比原来的person.age这种方法高级的多,可以配置很多详细信息:
let person = {
name:'念心卓'
}
//参数1:要改变的对象;参数2:要添加的属性;参数3:配置对象
Object.defineProperties(person,age,{
value: 18
//还有一些其他属性...
})
注意:这种方式添加的属性默认是不可枚举的,也就是不能遍历到这个属性。
常用属性:
enumerable,布尔类型,默认值是false。如果想让该字段可枚举,就要显式设置为true。
configurable,布尔类型,默认值是false。控制字段是否可以被删除
writable,布尔类型,默认值是false。控制字段是否可以被修改
value,就是赋值,赋字段一个值。
还有两个重要的常用属性:get和set
来看下面这种场景:
let number = 18
let person = {
name: "大吉",
sex: "男",
age: number
}
console.log(person.age)//输出18
number = 22//将number这个变量修改为22
console.log(person.age)//输出仍然是18,而不是22
如果我们想让第二个console.log输出的是22呢?应该怎么操作?
这里就引入了我们强大的get和set功能了!!
let number = 18
let person = {
name: "大吉",
sex: "男",
age: number
}
console.log(person.age) //输出18
Object.defineProperty(person, "age", {
get() {
console.log('有人读取了age属性')
return number; //return值很重要
}
})
number = 22 //将number这个变量修改为22
//输出的是22。只要有人读取age属性,就会调用get方法,get方法return的值就是number这个变量
console.log(person.age)//22
所以我们通过get,实现了只要有人读取age属性,就可以执行get这个方法,至于get这个方法你里面想塞什么都可以。反正只要有人读取age这个属性,就执行get。
我们再来看看set:
let number = 18
let person = {
name: "大吉",
sex: "男",
age: number
}
console.log("number的值是:" + number) //18
Object.defineProperty(person, "age", {
//只要有人修改age属性
//就会调用set方法,且会收到修改age属性的具体值
set(value) {
console.log('有人修改了age属性,age:' + value)
//神奇的操作来了,我们让number这个变量修改
number = value;
}
})
person.age = 22 //修改age属性,从而调用set方法
//非常神奇,我们明明修改了age,却能对number做修改
console.log("number的值是:" + number) //22
总结:通过get和set,使得操作对象更加灵活了
为什么要学这个Object.defineProperty方法呢?是为了下一节数据代理做准备!
7. 数据代理(数据劫持)
概念:数据代理就是通过一个对象对另一个对象中属性的操作(读/写)
有如下代码:
let obj1 = {x:100};
let obj2 = {y:100};
Object.defineProperty(obj2,'x',{
get(){
return obj1.x;
},
set(value){
obj1.x = value;
}
})
可见上图中如果有人动了obj2,相当于通过obj2动了obj1。 这就是最简单的数据代理实例
Vue如何应用数据代理呢?
无法理解可以查看视频:尚硅谷Vue2.0+Vue3.0全套教程丨vuejs从入门到精通第13个视频。
通过数据代理,我们本应该操作vm._data.属性
,现在我们直接操作vm.属性
即可。
总结:
- Vue中的数据代理:通过vm对象来代理data对象中属性的操作(读/写)
- Vue中数据代理的好处:更加方便的操作data中的数据
- 基本原理:通过Object.defineProperty()把data对象中所有属性添加到vm上。为每一个添加到vm上的属性,都指定一个getter/setter。在getter/setter内部去操作(读/写)data中对应的属性。
8. 事件处理
事件的基本使用:
- 使用
v-on:x
或@xxx
绑定事件,其中xxx是事件名; - 事件的回调需要配置在
methods
对象中,最终会在vm上 - methods中配置的函数,不要用箭头函数!否则this就不是vm了
- methods中配置的函数,都是被Vue所管理的函数,this的指向是vm或组件实例对象;
@click="demo"
和@c1ick="demo($event)"
效果一致,但后者可以传参
例如我有下面一部分代码:
<div id="root">
<button>点我输出信息</button>
</div>
我现在想要点击这个按钮就输出信息:
<div id="root">
<button v-on:click="showInfo">点我输出信息</button>
<button @click="showInfo">点我输出信息(简写形式)</button>
</div>
<script type="text/javascript">
new Vue({
el:'#root',
methods:{
showInfo(ev){
console.log(ev); //输出事件对象
alert('点击成功');
}
}
})
</script>
自行测试即可。
可见,绑定事件使用v-on:事件类型
,例如上面我的是点击事件,所以写的是v-on:click
,后面跟上回调的函数名称,并且函数名称写在methods配置对象属性中;v-on:click = @click
,后者在实际开发中用的较多。并且如果没有传参,就无需写小括号,里面会自动带有一个参数,参数为事件对象。
如果想要传参,则使用@click="showInfo(参数)"
,这样你写方法的时候也可以接收到参数:
<div id="root">
<button @click="showInfo2(66)">我是事件传参</button>
</div>
<script type="text/javascript">
new Vue({
el:'#root',
methods:{
showInfo2(number){
console.log(number) //输出66
}
}
})
</script>
但是这样虽然可以传递参数,但是会将事件对象给弄丢,所以我们一般会使用$event
来占位,这样事件对象也不会丢:
<div id="root">
<button @click="showInfo2($event,66)">给事件对象占一个位</button>
</div>
<script type="text/javascript">
new Vue({
el:'#root',
methods:{
showInfo2(e,number){
console.log(e,number) //输出事件对象和66
}
}
})
</script>
注意:并不是都需要事件对象,如果不需要,你也可以不占位,一切按照实际开发为准。
9. 事件修饰符
Vue中的事件修饰符:
prevent:阻止默认事件(常用)
<div id="root"> <a href="http://www.baidu.com" @click.prevent="showInfo">点我去百度</a> </div> <script type="text/javascript"> new Vue({ el:'#root', methods:{ showInfo(){ console.log('阻止事件的默认行为'); }, } }) </script>
当我点击链接的时候,事件触发了,但是并没有发送跳转,阻止了a标签的默认行为。
stop:阻止事件冒泡(常用)
<div id="root"> <div @click="showInfo"> <button @click="showInfo">点我冒泡</button> </div> </div> <script type="text/javascript"> new Vue({ el:'#root', methods:{ showInfo(){ console.log(123); }, } }) </script>
这里会输出两次,这就是事件冒泡导致的,当我点击button的时候,由于外层的div绑定的事件和button一致,所以就会发生事件冒泡,所以输出两次
不了解事件冒泡的可以去看JavaScript中相关内容,里面有详细介绍。
现在要阻止冒泡:
<div id="root"> <div @click="showInfo"> <button @click.stop="showInfo">点我冒泡</button> </div> </div>
给事件后面加上
.stop
即可。once:事件只触发一次(常用)
<div id="root"> <button @click="showInfo">点我触发</button> </div>
正常情况是你点击一次这个案例,这个事件的回调就会执行一次,现在我要求它只有第一次点击的时候执行回调,后面无论点击多少次都不会执行:
<div id="root"> <button @click.once="showInfo">点我触发</button> </div>
capture:使用事件的捕获模式
同理,不明表什么是捕获的去看JavaScript。
self:只有event.target是当前操作的元素是才触发事件
passive:事件的默认行为立即执行,无需等待事件回调执行完毕
10. 键盘事件
键盘事件一般用两个:keydown
、keyup
;常用keydown
。
对于键盘事件,也有按键修饰符,以下是Vue为我们提供的一些案件别名
.enter
:回车Enter.tab
:换行Tab.delete
:(捕获“Delete”和“Backspace”两个按键).esc
:退出Esc.space
:空格Space.up
:箭头上.down
:箭头下.left
:箭头左.right
:箭头右
例如:
<div id="root">
<input type="text" @keydown.enter="showInfo">
</div>
<script type="text/javascript">
new Vue({
el:'#root',
methods:{
showInfo(){
console.log(123);
},
}
})
</script>
上诉代码中,当我按下enter的时候,就会触发事件。
其他按键自行实现。
注意:
Vue未提供别名的按键,可以使用按键原始的key值去绑定,但注意要转为kebab-case(短横线命名) ,例如
CapsLock
键,对应的事件修饰符:
caps-lock
系统修饰键(用法特殊):
ctrl
、alt
、shift
、meta
- 配合keyup使用:按下修饰键的同时,再按下其他键,随后释放其他键,事件才被触发。
- 配合keydown使用:正常触发事件。
也可以使用keyCode去指定具体的按键(不推荐)
Vue.config.keyCodes.自定义键名=键码
,可以去定制按键别名例如:
<div id="root"> <input type="text" @keydown.huiche="showInfo"> </div> <script type="text/javascript"> Vue.config.keyCodes.huiche = 13 //回车Enter对应的键码为13 </script>
依然触发成功。
对于按键修饰符还可以连着写,例如:
@keydown.ctrl.y
;表示同时按下ctrl和y的时候才能触发,同理,之前的事件修饰符也可以连着写:click.stop.prevent
;表示先阻止冒泡,在阻止默认行为。
11. 计算属性
模板中的表达式虽然方便,但也只能用来做简单的操作。如果在模板中写太多逻辑,会让模板变得臃肿,难以维护。比如说,我们有这样一个包含嵌套数组的对象:
new Vue({
el:'#root',
data: {
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
},
})
然后我想将books拿到,并且要获取第一本书,然后所有字母大写,之后再获取书名:
<div id="root">
<span>获取到第一本书:{{books[0].toUpperCase().split('-')[0]}}</span>
</div>
可见模板中的表达式十分的臃肿,且难以维护。
计算属性要解决的问题是:插值语法{{}}
中,越来越长的JS表达式,不利于阅读和组件化的问题。我们希望的是在插值语法中就写一个简单的词,不要是一些复杂的表达式。
可能你会想,那么我们是否可以用函数来做呢?其实当然可以:
<div id="root">
<!--注意:这里调用函数与之前的事件回调函数的调用方法有区别:
事件回调:调用函数不用写小括号(无参情况)
模板语法:调用函数必须写小括号,否则就是输出函数体了-->
<span>获取到第一本书:{{findFirstBookName()}}</span><br/>
<span>获取到第一本书:{{findFirstBookName()}}</span><br/>
<span>获取到第一本书:{{findFirstBookName()}}</span><br/>
<span>获取到第一本书:{{findFirstBookName()}}</span><br/>
<span>获取到第一本书:{{findFirstBookName()}}</span>
</div>
<script type="text/javascript">
Vue.config.keyCodes.huiche = 13 //回车Enter对应的键码为13
new Vue({
el:'#root',
data: {
books: 'Vue 2 - Advanced Guide'
},
methods:{
findFirstBookName(){
console.log('我被调用了');
return this.books[0].toUpperCase().split('-')[0];
}
}
})
</script>
可见使用函数来做也是可以的,不过这个函数只要有一个地方使用,就会被调用一次。
使用计算属性:
<div id="root">
<!--注意:这里调用函数与之前的事件回调函数的调用方法有区别:
事件回调:调用函数不用写小括号(无参情况)
模板语法:调用函数必须写小括号,否则就是输出函数体了,但是对于计算属性来说,不用写小括号,计算属性中,它本来就是一个属性-->
<span>获取到第一本书:{{book}}</span><br/>
<span>获取到第一本书:{{book}}</span><br/>
<span>获取到第一本书:{{book}}</span><br/>
<span>获取到第一本书:{{book}}</span><br/>
<span>获取到第一本书:{{book}}</span>
</div>
<script type="text/javascript">
const vm = new Vue({
el:'#root',
data: {
books: 'Vue 2 - Advanced Guide '
},
// methods:{
// findFirstBookName(){
// console.log('我被调用了');
// return this.books[0].toUpperCase().split('-')[0];
// }
// },
computed:{//computed配置对象
book:{ //具体的某个配置对象
//获取属性时自动被Vue调用
get(){ //相当于Object.defineProperty中的get
console.log(this) //这个this仍然是vue实例对象,因为最终是通过vue来调用这个get
console.log('我被调用了');
return this.books.split('-')[0];
},
//修改属性时自动被Vue调用
set(value){相当于Object.defineProperty中的set
this.books = value;
console.log(`当前的books对象的值:${this.books}`); //注意,我这里其实是调用的get方法
}
}
}
})
二者对比:计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。这就意味着只要
books
还没有发生改变,多次访问book
计算属性会立即返回之前的计算结果,而不必再次执行函数。而使用函数则不一样,它没有缓存,无论你的值是否改变,只要有多处使用到了,那么他就会渲染多次。
补充:
- get有什么作用:当有人读取
book
时,get就会被调用,且返回值就作为book的值。 - get什么时候调用:
- 初次读取
book
时。 - 所依赖的数据发生变化时(此处依赖的数据为
books
)。
- 初次读取
- set什么时候调用:当
book
被改变时。
计算属性还有简写形式:
computed:{
book(){
console.log(this)
console.log('我被调用了');
return this.books.split('-')[0];
}
}
注意:只有当只读不改的情况下才能使用简写形式,并且此处的book()就相当于之前的get(),由于不能修改,所以就没有set();
12. 监视属性
案例:点击按钮,切换天气显示
<body>
<div id="root">
<h2>今天天气很{{weather}}</h2>
<button @click="changeWeather">点我切换天气</button>
</div>
<script type="text/javascript">
Vue.config.keyCodes.huiche = 13 //回车Enter对应的键码为13
const vm = new Vue({
el: '#root',
data: {
isHot: true
},
methods: {
changeWeather() {
this.isHot = !this.isHot;
}
},
computed: {
weather() {
return this.isHot ? '炎热' : '凉爽';
}
}
})
</script>
</body>
自行测试即可。
可以发现,我上面的代码是用以前学过的知识来完成,现在我我们要学习一个新的属性:监视属性。
要求:我改变天气的时候,它能够检测到我的改变,并且能够把我改变之前的值输出。
新增Vue配置属性:
watch:{
isHot:{ //配置对象:需要检测的属性,这里我选择检测isHot,如果检测weather,那么你改为weather即可
handler(newValue,oldValue){ //这里要写一个handler函数
console.log('weather被修改了',newValue,oldValue);
}
}
}
自行测试即可。
注意:计算属性也可以被监视。
当然,还有另外一种写法,当你再配置Vue对象的时候,你还不知道需要检测哪一个值,这时候,你可以在Vue配置完毕之后,用实例对象来配置也是可以的:
const vm = new Vue({
el: '#root',
data: {
isHot: true
},
methods: {
changeWeather() {
this.isHot = !this.isHot;
}
},
computed: {
weather() {
return this.isHot ? '炎热' : '凉爽';
}
},
/*watch:{
isHot:{
handler(newValue,oldValue){
console.log('weather被修改了',newValue,oldValue);
}
}
}*/
});
vm.$watch('isHot',{
handler(newValue,oldValue){
console.log('isHot被修改了',newValue,oldValue);
}
})
这样,使用$watch追加也是可以的。
适用场景:当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。
注意:监视属性中的配置对象中,除了handler配置,还有其他很多配置。
总结:
- 当被监视的属性变化时,回调函数自动调用,进行相关操作
- 监视的属性必须存在,才能进行监视!
- 监视的两种写法:
- new Vuel时传入watch配置
- 通过vm.$watch监视
明白了监视属性是怎么一回事,现在我们看看监视属性中的一个属性:deep;它表示深度监视。
我现在的代码如下:
<div id="root">
<h1>a的值为{{number.a}}</h1>
<button @click="number.a++">点我a加1</button>
</div>
<script type="text/javascript">
Vue.config.keyCodes.huiche = 13 //回车Enter对应的键码为13
const vm = new Vue({
el: '#root',
data: {
number: {
a: 1,
b: 1
}
}
});
</script>
</body>
现在我点击按钮,要求a+1之后,能够监视到a的属性的变化:
new Vue({
el: '#root',
data: {
number: {
a: 1,
b: 1
}
},
watch:{
'number.a':{
handler(){
console.log('a的值被改变了');
}
}
}
});
测试发现成功,注意,我监视属性中配置对象写的是'number.a'
,这是因为直接写number.a
回报错,事实上,所有的key-value形式中,key真正的写法都是用单引号包起来的,只不过如果不涉及到多层级结构的,我们一般是直接写key,没有用单引号包起来。
在上面的代码的基础上,我又想监视number中,b值改变,你可能会想到,仿照'number.a'
的写法来监视b,这么做是不合适,因为如果我number对象中,还有其他的很多的key-value要被监视,你这样一直写下去就很繁琐了,所以我们这时候用到watch中的deep属性,来实现深度监视。
watch:{
number:{
deep:true, //表示开启深度监视,默认是false
handler(){
console.log('a的值被改变了');
}
}
}
这时候,只要number中任意的key改变了,都能够被监视到。
监视属性也有简写形式,例如下面的代码:
<div id="root">
<h1>选择的宠物为{{pet}}</h1>
</div>
<script type="text/javascript">
const vm = new Vue({
el: '#root',
data: {
pet: 'cat'
},
watch: {
pet(newValue,oldValue) { //简写形式
console.log(`pet的值被改变了原来为${oldValue},现在为${newValue}`);
}
}
});
</script>
注意:在简写形式中,不可以再写其他的配置属性了(deep、immediate),因为这里根本就没有了配置对象,而是直接的一个函数(pet)。
使用vm.$watch
的形式:
vm.$watch('pet',function (newValue,oldValue) {
console.log(`pet的值被改变了原来为${oldValue},现在为${newValue}`);
})
13. 监视属性和计算属性的区别
computed和watch之间的区别:
- computed能完成的功能,watch都可以完成。
- watch能完成的功能,computed不一定能完成,例如:watch可以进行异步操作。
**两个重要的小原则: **
- 所被Vue管理的函数,最好写成普通函数,这样this的指向才是vm或组件实例对象。
- 所有不被Vue所管理的函数(定时器的回调函数、ajax的回调函数等),最好写成箭头函数,这样this的指向才是vm或组件实例对象。
其最终目的就是为了让this的指向为vm(Vue实例)。
例如之前的监视属性中的代码:
watch: {
pet(newValue,oldValue) {
console.log(`pet的值被改变了原来为${oldValue},现在为${newValue}`);
setTimeout(()=>{ //过1秒之后再执行,相当于异步任务
console.log(this); //这里的this就是vue实例vm
},1000)
}
}
14. 样式绑定
Vue中样式绑定一共有两种方式:
- class样式绑定
- style样式绑定
14.1 class样式绑定
我现在有如下样式代码:
.basic{
width: 400px;
height: 100px;
border: 1px solid black;
}
.happy{
border: 4px solid red;;
background-color: rgba(255, 255, 0, 0.644);
background: linear-gradient(30deg,yellow,pink,orange,yellow);
}
.sad{
border: 4px dashed rgb(2, 197, 2);
background-color: gray;
}
.normal{
background-color: skyblue;
}
.atguigu1{
background-color: yellowgreen;
}
.atguigu2{
font-size: 30px;
text-shadow:2px 2px 10px red;
}
.atguigu3{
border-radius: 20px;
}
基本的html代码:
<div id="root">
<div class="basic">{{name}}</div>
</div>
我现在的基本要求:点击div模块,要求将happy、sad、normal任意一个样式加在这个div模块上:
<div id="root">
<!-- 绑定class样式--字符串写法,适用于:样式的类名不确定,需要动态指定 -->
<div class="basic" :class="mood" @click="changeMood">{{name}}</div> <br/>
</div>
<script type="text/javascript">
new Vue({
el: '#root',
data: {
name: 'class样式选择',
mood:'normal'
},
methods:{
changeMood(){
const arr = ['happy','sad','normal']
const index = Math.floor(Math.random()*3)
this.mood = arr[index]
console.log(this.mood)
}
}
});
</script>
以上方法适用于:个数确认(1个),样式类名不确定。
现在我要求换了:要求将atguigu1、atguigu2、atguigu3任意样式加在这个div模块上,可以是多个样式,也可以是单个样式等:
<div id="root">
<!-- 绑定class样式--数组写法,适用于:要绑定的样式个数不确定、名字也不确定 -->
<div class="basic" :class="classArr">{{name}}</div> <br/><br/>
</div>
<script type="text/javascript">
const vm = new Vue({
el: '#root',
data: {
name: 'class样式选择',
classArr:['atguigu1','atguigu2','atguigu3']
}
});
</script>
以上方法适用于:个数不确定,样式类名也不确定。
现在我要求换了:要求有atguigu1、atguigu2两个样式在这个div模块上,但是我自己来决定使用1还是用2还是两个都用或都不用:
<div id="root">
<!-- 绑定class样式--对象写法,适用于:要绑定的样式个数确定、名字也确定,但要动态决定用不用 -->
<div class="basic" :class="classObj">{{name}}</div> <br/><br/>
</div>
<script type="text/javascript">
const vm = new Vue({
el: '#root',
data: {
name: 'class样式选择',
classObj:{
atguigu1:true,
atguigu2:false
}
}
});
</script>
以上方法适用于:个数确定,样式类名也确定,但是决定是否使用,或者使用那几个是由自己动态决定的。
14.2 style样式绑定
<div id="root">
<!-- 绑定style样式--对象写法 -->
<div class="basic" :style="styleObj">{{name}}</div> <br/><br/>
<!-- 绑定style样式--数组写法 -->
<div class="basic" :style="styleArr">{{name}}</div>
</div>
<script type="text/javascript">
const vm = new Vue({
el: '#root',
data: {
name: 'class样式选择',
styleObj:{
fontSize: '40px', //注意:这里的key可不能乱写,因为css中由font-size,所以这里为fontSize
color:'red',
},
styleArr:[
{
fontSize: '40px',
color:'blue',
},
{
backgroundColor:'gray' //因为css中由background-color,所以这里为backgroundColor
}
]
}
});
</script>
style样式绑定用的比较少。
15. 条件渲染
15.1 v-show
<div id="root">
<!-- 使用v-show做条件渲染 -->
<h2 v-show="false">欢迎{{name}}</h2>
<h2 v-show="1 === 1">欢迎{{name}}</h2>
</div>
可见v-show底层是通过控制display属性来实现显示与隐藏的。
15.2 v-if
<div id="root">
<!-- 使用v-if做条件渲染 -->
<h2 v-if="false">欢迎{{name}}</h2>
<h2 v-if="1 === 1">欢迎{{name}}</h2>
</div>
可见v-if底层是直接将DOM都给干掉了。
既然说了v-if,那么就会有v-else-if,这和if、else是一致的:
<div id="root">
<button @click="n++">点我n+1</button>
<!-- v-else和v-else-if -->
<div v-if="n === 1">Angular</div>
<div v-else-if="n === 2">React</div>
<div v-else-if="n === 3">Vue</div>
<div v-else>哈哈</div>
<div>此处没有v-if</div>
</div>
<script type="text/javascript">
const vm = new Vue({
el: '#root',
data: {
name: '念心卓',
n:0
}
});
</script>
结果自行测试即可。
这里简单说一下,当data配置对象中的数据发生变化时,Vue会重新渲染整个页面(DOM),所以,最开始页面显示的是:哈哈,此处没有v-if,当n=1的时候,页面上就会显示Angular,当Vue发现已经有一个条件满足的时候,后面的条件就都不会再看了,就跳过了后面的渲染,直接来到了最后一个div。
注意:当你使用v-if、v-else-if的时候,中间不能够被打断,例如:
<div v-if="n === 1">Angular</div> <div v-else-if="n === 2">React</div> <div>此处没有v-if</div> <div v-else-if="n === 3">Vue</div> <div v-else>哈哈</div>
这样是会报错的。
有时还会遇到这么一种情况:
<h2>你好</h2>
<h2>念心卓</h2>
<h2>哈哈</h2>
我现在的代码如上,我要求当n等于1的时候,上面3个h2同时显示,可能你会这样做:
<h2 v-if="n === 1">你好</h2>
<h2 v-if="n === 1">念心卓</h2>
<h2 v-if="n === 1">哈哈</h2>
但是这样做略显麻烦,那么你可能又会这样做:
<div v-if="n===1">
<h2>你好</h2>
<h2>念心卓</h2>
<h2>哈哈</h2>
</div>
给外面包上一层div,但是这样也会有隐藏的问题,比如当你所有样式都配置好了之后(使用JavaScript配置的),如果你再给外面加上一层div,会导致样式出错,找不到,所以这样这种方法也不是很好。
为了解决这样一种问题,你可以使用新的标签:template
:
<template v-if="n===1">
<h2>你好</h2>
<h2>念心卓</h2>
<h2>哈哈</h2>
</template>
可见使用template
标签之后,源码上并没有template
,这样既方便,也不会影响既有的样式。
总结:
v-if
适用于:切换频率较低的场景。
特点:不展示的DOM元素直接被移除。
注意:v-if可以和:v-else-if、v-else一起使用,但要求结构不能被“打断”。v-show
写法:v-show=”表达式”
适用于:切换频率较高的场景。
特点:不展示的DOM元素未被移除,仅仅是使用样式隐藏掉
备注:使用v-if的时,元素可能无法获取到(因为DOM有可能都被干掉),而使用v-show一定可以获取到。
16. 列表渲染
16.1 基本列表
在列表渲染中,我们一般使用v-for指令来进行渲染。
我们可以用 v-for
指令基于一个数组来渲染一个列表。v-for
指令需要使用 item in items
形式的特殊语法,其中 items
是源数据数组,而 item
则是被迭代的数组元素的别名。
数组渲染代码如下:
<div id="root">
<ul>
<li v-for="person in persons">
{{person.name}}--{{person.age}}
</li>
</ul>
</div>
<script type="text/javascript">
new Vue({
el: '#root',
data: {
persons:[
{id:'001',name:'张三',age:18},
{id:'002',name:'李四',age:19},
{id:'003',name:'王五',age:20},
]
}
});
</script>
在 v-for
块中,我们可以访问所有父作用域的 property。v-for
还支持一个可选的第二个参数,即当前项的索引。
例如:
<div id="root">
<ul>
<li v-for="(person,index) in persons">
{{person.name}}--{{person.age}}--{{index}}
</li>
</ul>
</div>
当然,在遍历的时候,你也可以用
of
替代in
作为分隔符,因为它更接近 JavaScript迭代器的语法:<div id="root"> <ul> <li v-for="(person,index) of persons"> {{person.name}}--{{person.age}}--{{index}} </li> </ul> </div>
效果也是一样的。
当然除了能够遍历数组,v-for也能够遍历对象:
<div id="root">
<!-- 遍历对象 -->
<h2>汽车信息(遍历对象)</h2>
<ul>
<li v-for="(value,key) of car">
{{key}}-{{value}}
</li>
</ul>
</div>
<script type="text/javascript">
new Vue({
el: '#root',
data: {
car: {
name: '奥迪A8',
price: '70万',
color: '黑色'
}
}
});
</script>
注意:
遍历对象的时候,第一个参数是value,第二个参数才是key,还可以用第三个参数作为索引:
<div id="root"> <!-- 遍历对象 --> <h2>汽车信息(遍历对象)</h2> <ul> <li v-for="(value,key,index) of car"> {{key}}-{{value}}--{{index}} </li> </ul> </div>
除了遍历对象之外,还能够遍历字符串:
<div id="root">
<!-- 遍历字符串 -->
<h2>测试遍历字符串(用得少)</h2>
<ul>
<li v-for="(char,index) of str">
{{char}}-{{index}}
</li>
</ul>
</div>
<script type="text/javascript">
new Vue({
el: '#root',
data: {
str: 'Vue'
}
});
</script>
注意:
- 上述v-for指令中,我都没有写
:key
,但是一般我们再写v-for的时候,:key
是少不了的,:key
指令是十分重要的,在下一小节给出。- v-for可以遍历数组、对象,这两个用的多一点,遍历字符串用的较少
16.2 Key的基本原理
对于上一小节的代码中,我使用v-for的时候并没有使用:key
,但是在实际开发中,最好还是写上,否则可能会出现问题,下面我来演示一下如何出现问题。
我现在有这么一段代码:
<div id="root">
<ul>
<li v-for="person in persons">
{{person.name}}--{{person.age}} <input type="text">
</li>
</ul>
</div>
<script>
new Vue({
el: '#root',
data: {
persons: [
{id: '001', name: '张三', age: 18},
{id: '002', name: '李四', age: 19},
{id: '003', name: '王五', age: 20}
]
}
});
</script>
现在我的需求是,也要在张三前面添加一个人:
<div id="root">
<ul>
<li v-for="person in persons">
{{person.name}}--{{person.age}} <input type="text">
</li>
</ul>
<button @click="addPerson">点我添加成员</button>
</div>
<script>
new Vue({
el: '#root',
data: {
persons: [
{id: '001', name: '张三', age: 18},
{id: '002', name: '李四', age: 19},
{id: '003', name: '王五', age: 20}
]
},
methods:{
addPerson(){
const person = {id: '004', name: '赵六', age: 22}
this.persons.unshift(person);
}
}
});
</script>
当输入框中没有东西的时候,点击添加毫无问题,现在,我在输入框中加入东西:
当我点击添加的时候,注意观察:
可见发生了错位,那么这是为什么呢?难道是我没加:key
吗,加上看看:
<li v-for="person in persons" :key="index">
测试发现,还是一样的问题,那我换个试试:
<li v-for="person in persons" :key="person.id">
可见,当:key的值为索引或者不加的时候,都会出差,但是使用person中的唯一表示id就不会报错,这是什么原因呢?
详细解释可看:禹神Vue视频中P30节。(最好理解了)
总结:
面试题:react、vue中的key有什么作用?(key的内部原理)
虚拟DOM中key的作用:
key是虚拟DOM对象的标识,当数据发生变化时,Vue会根据【新数据】生成【新的虚拟DOM】,随后Vue进行【新虚拟DOM】与【旧虚拟DOM】的差异比较,比较规则如下:对比规则:
旧虚拟DOM中找到了与新虚拟DOM相同的key:- 若虚拟DOM中内容没变,直接使用之前的真实DOM!
- 若虚拟DOM中内容变了,则生成新的真实DOM,随后替换掉页面中之前的真实DOM。
旧虚拟DOM中未找到与新虚拟DOM相同的Key:
- 创建新的真实DOM,随后渲染到到页面。
用index作为key可能会引发的问题:
若对数据进行:逆序添加、逆序删除等破坏顺序操作:会产生没有必要的真实DOM更新==>界面效果没问题,但效率低。
如果结构中还包含输入类的DOM:会产生错误DOM更新=>界面有问题。开发中如何选择key?:
最好使用每条数据的唯一标识作为key,比如id、手机号、身份证号、学号等唯一值。
如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用index作为key是没有问题的。
16.3 列表过滤
我现在有如下代码:
<div id="root">
<input type="text" placeholder='请输入筛选的关键字'>
<ul>
<li v-for="person in persons" :key="person.id">
{{person.name}}--{{person.age}}--{{person.sex}}
</li>
</ul>
</div>
<script>
new Vue({
el: '#root',
data: {
persons: [
{id: '001', name: '马冬梅', age: 19, sex: '女'},
{id: '002', name: '周冬雨', age: 20, sex: '女'},
{id: '003', name: '周杰伦', age: 21, sex: '男'},
{id: '004', name: '温兆伦', age: 22, sex: '男'}
]
}
});
</script>
页面效果:
现在要求我输入什么,就筛选出符合条件的即可。
思路:监视输入的值,然后使用数组过滤
一说到监视,可能就会想到使用vue中的监视属性watch来做:
<div id="root">
<input type="text" placeholder='请输入筛选的关键字' v-model="keyWords">
<ul>
<li v-for="person in filterPersons" :key="person.id">
{{person.name}}--{{person.age}}--{{person.sex}}
</li>
</ul>
</div>
<script>
new Vue({
el: '#root',
data: {
keyWords: '',
persons: [
{id: '001', name: '马冬梅', age: 19, sex: '女'},
{id: '002', name: '周冬雨', age: 20, sex: '女'},
{id: '003', name: '周杰伦', age: 21, sex: '男'},
{id: '004', name: '温兆伦', age: 22, sex: '男'}
],
filterPersons: []
},
watch: {
//这种简写形式初始的时候页面没有数据,可以自行测试
/*keyWords(value) {
this.filterPersons = this.persons.filter((person) => {
return person.name.indexOf(value) !== -1;
})
}*/
keyWords: {
immediate: true, //立即以表达式的当前值触发回调,也就是初始的时候value='',但是person.name.indexOf(value)却不等于-1
handler(value) {
this.filterPersons = this.persons.filter((person) => {
return person.name.indexOf(value) !== -1;
})
}
}
}
});
</script>
测试成功。
但是,最为标准的写法还是计算属性来做:
<div id="root">
<input type="text" placeholder='请输入筛选的关键字' v-model="keyWords">
<ul>
<li v-for="person in filterPersons" :key="person.id">
{{person.name}}--{{person.age}}--{{person.sex}}
</li>
</ul>
</div>
<script>
new Vue({
el: '#root',
data: {
keyWords: '',
persons: [
{id: '001', name: '马冬梅', age: 19, sex: '女'},
{id: '002', name: '周冬雨', age: 20, sex: '女'},
{id: '003', name: '周杰伦', age: 21, sex: '男'},
{id: '004', name: '温兆伦', age: 22, sex: '男'}
]
},
computed:{
filterPersons(){
return this.persons.filter((person)=>{
//注意这里的this是Vue实例,因为不被Vue管理的函数最好写为箭头函数才是Vue实例
return person.name.indexOf(this.keyWords) !== -1;
})
}
}
});
</script>
可见计算属性完成更加简洁。
16.4 列表排序
在之前列表过滤的基础上,我想要在加上排序,代码实现:
<div id="root">
<input type="text" placeholder='请输入筛选的关键字' v-model="keyWords">
<button @click="sortType = 1">升序</button>
<button @click="sortType = 2">降序</button>
<button @click="sortType = 0">原序</button>
<ul>
<li v-for="person in filterPersons" :key="person.id">
{{person.name}}--{{person.age}}--{{person.sex}}
</li>
</ul>
</div>
<script>
new Vue({
el: '#root',
data: {
keyWords: '',
persons: [
{id: '001', name: '马冬梅', age: 19, sex: '女'},
{id: '002', name: '周冬雨', age: 20, sex: '女'},
{id: '003', name: '周杰伦', age: 21, sex: '男'},
{id: '004', name: '温兆伦', age: 22, sex: '男'}
],
sortType: 0 //0表示原序,1表示升序,2表示降序
},
computed: {
filterPersons() {
const arr = this.persons.filter((person) => {
//注意这里的this是Vue实例,因为不被Vue管理的函数最好写为箭头函数才是Vue实例
return person.name.indexOf(this.keyWords) !== -1;
});
if (this.sortType) {
arr.sort((p1, p2) => {
return this.sortType === 1 ? p1.age - p2.age : p2.age - p1.age;
})
}
return arr;
}
}
});
</script>
17. 监测改变
这里说的监测数据改变和我们之前学的watch是不一样的,watch他是提供我们程序员使用的表层的一个监测,我这里所说的监测是指的Vue底层的数据监测,就是当你改变数据的时候,Vue能够监测到。
17.1 更新时问题
例如如下代码就不能够监测到:
<div id="root">
<button @click="updateMeiInfo">点我更新马冬梅信息</button>
<ul>
<li v-for="person in persons" :key="person.id">
{{person.name}}--{{person.age}}--{{person.sex}}
</li>
</ul>
</div>
<script>
const vm = new Vue({
el: '#root',
data: {
persons: [
{id: '001', name: '马冬梅', age: 19, sex: '女'},
{id: '002', name: '周冬雨', age: 20, sex: '女'},
{id: '003', name: '周杰伦', age: 21, sex: '男'},
{id: '004', name: '温兆伦', age: 22, sex: '男'}
],
},
methods:{
updateMeiInfo(){
// this.persons[0].name = '马老师';// 更新奏效
// this.persons[0].age = 50;// 更新奏效
// this.persons[0].sex = '男';// 更新奏效
this.persons[0] = {id: '001', name: '马老师', age: 50, sex: '男'};
}
}
});
</script>
对于上面的代码,可见,单个修改每个对象的属性能够成功,但是修改整个对象数组中的某一项整体修改的时候,却无法被Vue监测到,这是怎么回事呢?
在解释这个现象之前,我们先来聊聊Vue是如何监视一个对象的改变的?
17.2 Vue监视对象
在前面的时候,我们学习了数据代理,粗略的知道了vm中的data其实和vm中的_data是一回事,执行vm._data === vm._data
输出true
,
但是现在,我要说的是,这并不是一回事,用下图讲解:
在图中,黄色的线属于数据代理,在代理的时候,Vue会按照下面的步骤来完成:
- 加工数据data
- 加工完data之后,才执行
vm._data = vm.data
- 最后使用
Object.defineProperty
来添加属性,提供getter/setter
现在我们来模拟一下对象的数据监测:
<script type="text/javascript">
//定义一份数据,用来模拟vm中的data
let data = {
name: '念心卓',
age: 18,
}
//声明一个观察者构造函数
function Observer(obj) {
//汇总对象中所有的属性形成一个数组
const keys = Object.keys(obj)
//遍历
keys.forEach((k) => {
//使用这个来提供getter和setter
Object.defineProperty(this, k, {
get() {
console.log(`${obj[k]}被访问了`)
return obj[k]
},
set(val) {
console.log(`${k}被改了,我要去解析模板,生成虚拟DOM.....我要开始忙了`)
obj[k] = val
}
})
})
}
//创建一个监视的实例对象,用于监视data中属性的变化
const obs = new Observer(data)
console.log(obs)
//准备一个vm实例对象
let vm = {}
vm._data = data = obs //这里就是Vue中vm._data === vm.data的原因了
</script>
可见这里面提供了get和set,当我访问数据的时候,get就会别调用,当我修改数据的时候,set就会被调用。
可见当我访问和修改都能够被vm给监测到了。
17.3 Vue.set()
知道了Vue中对象底层的监视原理,那么我们现在有新的需求了,我现在有如下代码:
<div id="root">
<h1>学校信息</h1>
<h2>学校名称:{{school.name}}</h2>
<h2>学校地址:{{school.address}}</h2>
<hr/>
<h1>学生信息</h1>
<h2>姓名:{{student.name}}</h2>
<h2>年龄:真实{{student.age.rAge}},对外{{student.age.sAge}}</h2>
<h2>朋友们</h2>
<ul>
<li v-for="(f,index) in student.friends" :key="index">
{{f.name}}--{{f.age}}
</li>
</ul>
</div>
<script type="text/javascript">
Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。
const vm = new Vue({
el:'#root',
data:{
school:{
name:'尚硅谷',
address:'北京',
},
student:{
name:'tom',
age:{
rAge:40,
sAge:29,
},
friends:[
{name:'jerry',age:35},
{name:'tony',age:36}
]
}
}
})
</script>
最新的需求如下:我要给学生信息新增一个属性为性别(sex),要求点击按钮实现性别的添加,你可能会这样做,关键代码:
<h1>学生信息</h1>
<button @click="addSex">添加一个性别属性,默认值是男</button>
<h2>姓名:{{student.name}}</h2>
<h2 v-if="student.sex">性别:{{student.sex}}</h2>
<script type="text/javascript">
const vm = new Vue({
el:'#root',
data:{
school:{
name:'尚硅谷',
address:'北京',
},
student:{
name:'tom',
age:{
rAge:40,
sAge:29,
},
friends:[
{name:'jerry',age:35},
{name:'tony',age:36}
]
}
},
methods: {
addSex(){
this.student.sex = '男' //这样写
}
}
})
</script>
测试的时候发现点击按钮之后,页面并没有展示出来,但是内存中的数据却是发生了改变的:
可见数据是成功添加进去了,但是并未被Vue监测到,在上一小节,我们明白了Vue要想监测到数据的改变,必须要有对应的get和set方法,通过上图我们可以发现,student对象的其他属性都有对应的get和set,但是sex属性却是干巴巴的显示出来了,并且没有对应的get和set,这就是为什么不能够被Vue监测到的原因,对应的,既然无法被Vue监测到,那么就不可能去重现渲染页面了,自然页面也就没有展示。
那么如何让Vue监测到呢?使用Vue.set()
API即可监测。
查看官方文档解释如下:
解释:响应式对象就是data中的数据,并且当数据改变的时候,页面要跟着变,这就是响应式
现在我们使用这个API来试试:
methods: {
addSex(){
Vue.set(this.student,'sex','男')
//this.$set(this.student,'sex','男') //另外一种写法
}
}
注意:还有一种写法:
vm.$set(target,propertyName/index,value)
测试发现当我点击的时候,页面重新渲染了,并且展示成功了,这时候我们再来看看vm上的数据:
这时你发现sex都有对应的get和set了,所以能够被Vue监测到,当数据发生变化的时候,页面也就被重新渲染了。
重新回头看官方文档你会发现有一个注意事项:注意对象不能是 Vue 实例,或者 Vue 实例的根数据对象。
表达的意思就是如果你直接在data根数据上添加数据是不可以,例如如下代码:
data:{
school:{
name:'尚硅谷',
address:'北京',
},
student:{
name:'tom',
age:{
rAge:40,
sAge:29,
},
friends:[
{name:'jerry',age:35},
{name:'tony',age:36}
]
}
}
现在你想在school和student同级添加一个其他属性,是不会成功的,会报错的。这里的就是根数据。
好了,现在你知道了对于对象来说,如何添加一个属性能够被Vue监测到(使用Vue.set()API),那么我们再来看看数组的情况
17.4 Vue监视数组
还是之前的代码,我给用户信息中添加一个hobby数组数据:
<!-- 准备好一个容器-->
<div id="root">
<h1>学生信息</h1>
<h2>姓名:{{student.name}}</h2>
<h2>年龄:真实{{student.age.rAge}},对外{{student.age.sAge}}</h2>
<h2>爱好</h2>
<ul>
<li v-for="(h,index) in student.hobby" :key="index">
{{h}}
</li>
</ul>
</div>
<script type="text/javascript">
const vm = new Vue({
el: '#root',
data: {
student: {
name: 'tom',
age: {
rAge: 40,
sAge: 29,
},
hobby: ['抽烟', '喝酒', '烫头'], //准备好数组数据
}
}
})
</script>
可见我的hobby数组中并没有提供get和set,这时候你去改变数组其中的一个元素是不生效的,试试:
同样的,内存中的数据改动了,但是Vue并没有监测到他的改变,也就没有重新渲染页面,从而页面也就没有展示出来,这就和我们最开始说的更新时的问题很相似了。
我们使用之前的API - Vue.set()来看看:
可见,这样更改能够被Vue监测到,并且重新渲染页面。
除了这种方式来更改,还有其他方式,官方文档给出:
官方文档的意思就是,数组本身是存在一些API的,这些API会导致原数组更改,只要调用数组本身API导致原数组了,那么就会被Vue监测到,对应的API如上图。
那么对于这种情况Vue又是怎样实现的呢?其实Vue底层做了包装,当我们在Vue中想要更改数组,并且重新渲染,我们调用了使原数组更改的API的时候,例如arr.push()
,其实是调用了Vue底层的push()
方法,在Vue的这个方法中,首先会调用Array.prototype.push()
,之后再去做渲染相关的动作。
同样的,凡是不能够是原数组改变的API,生成新数组的,例如:
如果你不用原来的数组对象去接收,那么也是不会被监测到的。
总结:
Vue监视数据的原理:
vue会监视data中所有层次的数据。
如何监测对象中的数据?
通过setter实现监视,且要在new Vue时就传入要监测的数据,即使说写data配置的时候就要定义好。
- 对象中后追加的属性,Vue默认不做响应式处理
- 如需给后添加的属性做响应式,请使用如下API:
Vue.set(target,propertyName/index,value)
或者vm.$set(target,propertyName/index,value)
如何监测数组中的数据?
通过包裹数组更新元素的方法实现,本质就是做了两件事:
- 调用原生对应的方法对数组进行更新。
- 重新解析模板,进而更新页面。
在Vue修改数组中的某个元素一定要用如下方法:
- 使用这些API:
push()、pop()、shift()、unshift()、splice()、sort()、reverse()
- 使用
Vue.set()
或者vm.$set()
- 使用这些API:
特别注意:Vue.set() 和 vm.$set() 不能给vm 或 vm的根数据对象 添加属性!!!
18. 收集表单数据
我现在拥有的表单如下:
现在我想要通过Vue来收集表单中的数据,结果代码:
<!-- 准备好一个容器-->
<div id="root">
<form @submit.prevent="demo">
账号:<input type="text" v-model.trim="userInfo.account"> <br/><br/>
密码:<input type="password" v-model="userInfo.password"> <br/><br/>
年龄:<input type="number" v-model.number="userInfo.age"> <br/><br/>
性别:
男<input type="radio" name="sex" v-model="userInfo.sex" value="male">
女<input type="radio" name="sex" v-model="userInfo.sex" value="female"> <br/><br/>
爱好:
学习<input type="checkbox" v-model="userInfo.hobby" value="study">
打游戏<input type="checkbox" v-model="userInfo.hobby" value="game">
吃饭<input type="checkbox" v-model="userInfo.hobby" value="eat">
<br/><br/>
所属校区
<select v-model="userInfo.city">
<option value="">请选择校区</option>
<option value="beijing">北京</option>
<option value="shanghai">上海</option>
<option value="shenzhen">深圳</option>
<option value="wuhan">武汉</option>
</select>
<br/><br/>
其他信息:
<textarea v-model.lazy="userInfo.other"></textarea> <br/><br/>
<input type="checkbox" v-model="userInfo.agree">阅读并接受<a href="http://www.atguigu.com">《用户协议》</a>
<button>提交</button>
</form>
</div>
<script type="text/javascript">
Vue.config.productionTip = false
new Vue({
el:'#root',
data:{
userInfo:{
account:'',
password:'',
age:'',
sex:'female',
hobby:[],
city:'beijing',
other:'',
agree:''
}
},
methods: {
demo(){
console.log(JSON.stringify(this.userInfo))
}
}
})
</script>
解释:
首先我们都知道,收集input
框中的数据使用v-model
是没有错的,但是,这里面有一些例外,例如:
收集单选框
收集单选框中的value值的时候,例如上图中的男或者女,必须给input框指定value属性,否则无法收集,例如没有value属性的时候:
vue初始化数据:
new Vue({ el:'#root', data:{ userInfo:{ sex:'' //用于接收数据,初始化为空 } } })
男<input type="radio" name="sex" v-model="userInfo.sex"> 女<input type="radio" name="sex" v-model="userInfo.sex"> <br/><br/>
可见点击的时候就为null了,收集不到你选择的值,所以我们需要指定value属性:
男<input type="radio" name="sex" v-model="userInfo.sex" value="男"> 女<input type="radio" name="sex" v-model="userInfo.sex" value="女"> <br/><br/>
可见收集成功。
收集复选框
对于复选框,也会存在同样的问题:
Vue初始化数据:
new Vue({ el:'#root', data:{ userInfo:{ hobby:'' } } })
爱好: 学习<input type="checkbox" v-model="userInfo.hobby" > 打游戏<input type="checkbox" v-model="userInfo.hobby"> 吃饭<input type="checkbox" v-model="userInfo.hobby">
可见,勾选的时候hobby的值是一个布尔值,并不是学习、打游戏、吃饭中的一种,现在我们按照之前的方法,指定value属性看看:
爱好: 学习<input type="checkbox" v-model="userInfo.hobby" value="study"> 打游戏<input type="checkbox" v-model="userInfo.hobby" value="game"> 吃饭<input type="checkbox" v-model="userInfo.hobby" value="eat">
经过测试,仍然是同样的问题。
解决办法:将初始化数据设置为数组即可解决:
new Vue({ el:'#root', data:{ userInfo:{ hobby:[] } } })
可见现在正常了。
对于最开始的结果代码处,你可能还会看见这部分:
<form @submit.prevent="demo">....</form>
这里有一个@submit.prevent
,这里表示表单提交事件,并且阻止了表单点击提交跳转页面的默认行为,这样,你就可以再demo出发出你的Ajax请求来做一些操作了。
同时,你可能还会看见这几处代码:
v-model.trim
:作用是当你再输入框中输入的内容首尾有空格的时候,Vue是不会帮你收集首尾的空格的。v-model.number
:作用是当你再输入框输入数组的时候,Vue默认会收集起来,类型为字符串,但是加上这个之后,就会按照number类型来收集。使用它的时候一般会将输入框的类型改为number,限制只能输入数子,不能输入字符:<input type="number" v-model.number="userInfo.age">
v-model.lazy
:作用是Vue不会实时监测你的输入,只会再你输入完毕之后,该输入框失去焦点的时候,你输入的内容才会被收集起来。
总结:
- 若:
<input type="text"/>
,则v-model收集的是value值,用户输入的就是value值。 - 若:
<input type="radio"/>
,则v-model收集的是value值,且要给标签配置value值。 - 若:
<input type="checkbox"/>
- 没有配置input的value属性,那么收集的就是
checked
(勾选 or 未勾选,是布尔值) - 配置input的value属性:
- v-model的初始值是非数组,那么收集的就是checked(勾选 or 未勾选,是布尔值)
- v-model的初始值是数组,那么收集的的就是value组成的数组
- 没有配置input的value属性,那么收集的就是
- 备注:v-model的三个修饰符:
- lazy:失去焦点再收集数据
- number:输入字符串转为有效的数字
- trim:输入首尾空格过滤
19. 过滤器
我目前有一个需求,有一个时间戳,要求生成指定格式的时间:
<div id="root">
<h2>时间戳:{{timestamp}}</h2>
<h2>格式化时间戳:{{formatByMethod()}}</h2>
<h2>格式化时间戳:{{formatByComputed}}</h2>
</div>
<script type="text/javascript">
Vue.config.productionTip = false
new Vue({
el:'#root',
data:{
timestamp: 1704374101424
},
methods:{
formatByMethod(){
return dayjs(this.timestamp).format('YYYY-MM-DD HH:mm:ss');
}
},
computed:{
formatByComputed(){
return dayjs(this.timestamp).format('YYYY-MM-DD HH:mm:ss');
}
}
})
</script>
注意:其中dayjs我是用的第三方库。
对于上面的代码,我是使用的函数,以及计算属性来完成的,现在我要求使用第三种方法来完成,要求使用过滤器来做:
<div id="root">
<h2>时间戳:{{timestamp}}</h2>
<h2>格式化时间戳:{{timestamp | timefFormater}}</h2>
</div>
<script type="text/javascript">
Vue.config.productionTip = false
new Vue({
el:'#root',
data:{
timestamp: 1704374101424
},
filters:{
timefFormater(valule){
return dayjs(valule).format('YYYY-MM-DD HH:mm:ss');
}
}
})
</script>
可见同样能够实现效果,现在我来解释一下这个代码的意思:
首先对于模板语中:{{timestamp | timefFormater}}
,第一个timestamp 是data中的属性,|
表示管道符,timefFormater
就代表一个过滤器方法,但是你在使用timefFormater的时候,要确保vm中有过滤器这个配置,也就是filters
配置属性,里面可以写多个过滤器方法。当执行
{{timestamp | timefFormater}}
的时候,会将前面的数据(timestamp
)传入过滤器方法(timefFormater
)中,并且,如果有多个过滤器方法,例如:
{{value| function1 | function2 | function3}}
,传值也是从左到右依次传递,先将value传递给function1,之后用function1的执行结果传递给function2,依次类推。对于过滤器方法:
timefFormater(valule){
return dayjs(valule).format('YYYY-MM-DD HH:mm:ss');
}
这里的value其实就是前面传递过来的值,并且第一个参数永远是前一个的值,后面才是你自定义的值,例如:
<h2>格式化时间戳:{{timestamp | timefFormater('YYYY年MM月DD日')}}</h2>
对应的过滤器方法为:
timefFormater(valule,str){
return dayjs(valule).format(str);
}
第一个参数是雷打不动的管道符前面的值,后面的参数才是你自定义的,并且过滤器方法的返回值就代表整个插值语法。
以上过滤器是局部过滤器,即是说多个vm实例之间是不共享的,如果要多个vm实例之间共享,那么你要注册全局过滤器:
// 注册
Vue.filter('my-filter', function (value) {
// 返回处理后的值
})
参数1为过滤器方法名称,参数2就是具体的方法体了。
总结:
过滤器就是对要显示的数据进行特定格式化后再显示(适用于一些简单逻辑的处理)。
语法:
1. 注册过滤器:Vue.filter(name,callback)
或 new Vue{filters:{}}
2. 使用过滤器:{{ xxx | 过滤器名}}
或 v-bind:属性 = "xxx | 过滤器名"
备注:
- 过滤器也可以接收额外参数、多个过滤器也可以串联
- 并没有改变原本的数据, 是产生新的对应的数据
20. 内置指令
我们已经知道的内置指令:
v-bind
:单向绑定解析表达式, 可简写为:xxx
,不过只能用于标签属性v-model
: 双向数据绑定,只能用于有value属性的input标签上v-for
:遍历数组/对象/字符串v-on
:绑定事件监听, 可简写为@v-if
:条件渲染(动态控制节点是否存存在,即DOM是否存在)v-else
:条件渲染(动态控制节点是否存存在),与v-if连用v-show
:条件渲染 (动态控制节点是否展示,DOM仍然存在)
现在又要讲解一些新的内置指令:
v-text
作用:向其所在的节点中渲染文本内容。例如:
<div id="root"> <div>我的名字:{{name}}</div> <div v-text="name">彭于晏</div> <div v-text="content"></div> </div> <script type="text/javascript"> Vue.config.productionTip = false new Vue({ el:'#root', data:{ name: '念心卓', content:'<h3>大帅哥</h3>' } }) </script>
可见结果中,v-text会直接拿到name的值替换掉div标签中的内容,并且如果它渲染的数据里面包含html标签,他是不会渲染html标签内容的,这一点和JavaScript中
innerHTML
和innerText
有点像。与插值语法的区别:v-text会替换掉节点中的内容,则不会
v-html
作用:向指定节点中渲染包含html结构的内容。例如:
<div id="root"> <div>我的名字:{{name}}</div> <div v-text="name">彭于晏</div> <div v-html="content"></div> </div> <script type="text/javascript"> Vue.config.productionTip = false new Vue({ el:'#root', data:{ name: '念心卓', content:'<h3>大帅哥</h3>' } }) </script>
可见v-html可以解析标签,这也是直观上和v-text的区别,同理,v-html也会替换掉标签体内的内容,和v-text一致。
严重注意:v-html有安全性问题!!!!
- 在网站上动态渲染任意HTML是非常危险的,容易导致XSS攻击
- 一定要在可信的内容上使用v-html,永不要用在用户提交的内容上!
v-cloak
看下面一段代码:
看到上面的这种情况,这时候你可能会想到这样来做:
对于上面这种情况,貌似可以行通,但是仍然有很大的问题,就是你先解析的标签体,那么,标签体里面所有用到Vue模板语法的地方都会出现
{{xxx}}
的情况,例如上面{{name}}
,这样给用户直接展示出来是不友好的,我们希望直接展示的就是:我的名字:念心卓
,而不是:我的名字:{{name}}
,所以,最好的解决办法是当Vue还没有渲染完毕的时候,不显示所有涉及到vue解析的模块,我们要将他们隐藏起来,可以使用css来控制,这时候,使用v-cloak来配合css是最好的:
注意:v-cloak指令(没有值),本质是一个特殊属性,Vue实例创建完毕并接管容器后,会删掉v-cloak属性,使用css配合v-cloak可以解决网速慢时页面展示出的问题
v-once
这个指令和之前学的事件修饰符有点像:
@事件类型.once
例如下面的代码:
<div id="root"> <h2>初始化的n值是:{{n}}</h2> <h2>当前的n值是:{{n}}</h2> <button @click="n++">点我n+1</button> </div> <script src="../Vue_js/vue.js"></script> <script type="text/javascript"> Vue.config.productionTip = false new Vue({ el:'#root', data:{ n:1 } }) </script>
可见当我点击按钮的时候,初始化的n值也跟着变了,我们想要的结果是当前n值改变,而初始化的n值不变。
这时候你可能会想到,直接使用静态的不行吗:
<h2>初始化的n值是:1</h2>
,这里行是行,不过为了讲解v-once的作用,这里我们不这样做。我们给初始化n值那行标签上加个v-once指令看看:
<h2 v-once>初始化的n值是:{{n}}</h2>
可见这次达到目的了。
作用:v-once所在节点在初次动态渲染后,就视为静态内容了,以后数据的改变不会引起v-once所在结构的更新,可以用于优化性能。
v-pre
在我们的代码中,经常有很多代码是写死了的,即没有使用vue的模板语法,绑定事件等等,也就是最纯粹的html的写法,这时候,对于这些代码,你可以给他加上v-pre指令,来提高页面的性能:
作用:可利用它跳过:没有使用指令语法、没有使用插值语法的节点,会加快编译
21. 自定义指令
前面我们讲解了内置指令,现在再详细说说自定义指令。注意:这里可能会涉及到操作DOM,你可能会有疑惑,我们都学习了Vue了,还需要自己来操作DOM,那学习Vue又有什么意义呢?其实Vue给的内置指令中,也有很多操作了DOM,不过这些都被Vue的指令来做了,所以,你一般是直接使用指令,而没有直接去操作DOM。
并且自定义指令有两种定义方式:
- 函数式:相当于简写形式。
- 对象式:最完整的写法。
21.1 函数式
对于一般简单的需求,不需要涉及到一些细节的时候,我们一般使用函数式就能够解决问题。
现在有一个需求:定义一个v-big指令,和v-text功能类似,但会把绑定的数值放大10倍
如何定义指令:我们仍然参考Vue,使用v-xxx
,这个xxx就是自定义指令。
定义规则:
- 对于自定义指令的xxx,你需要再Vue的配置对象中
directives
来定义。 - 对于多个单词的,不建议使用小驼峰,应该使用短横线分割。
参考代码:
<div id="root">
<h2>当前的n值是:<span v-text="n"></span> </h2>
<h2>放大10倍后的n值是:<span v-big="n"></span> </h2>
<button @click="n++">点我n+1</button>
<hr/>
</div>
<script type="text/javascript">
Vue.config.productionTip = false
new Vue({
el:'#root',
data:{
n:1
},
directives:{
//自定义指令这里接收两个参数:1.正式的元素DOM,2一些绑定的信息
//big函数何时会被调用?1.指令与元素成功绑定时(一上来)。2.指令所在的模板被重新解析时。
big(element,binding){ //因为自定义指令是v-big,所以这里要写big
console.log(element);
console.log(binding);
element.innerHTML = binding.value * 10;//操作DOM
}
}
})
</script>
注意上诉代码中说明的自定义指令调用的时机。
注意:其实自定义指令中,参数还有一些,这里我就只打印出来了最常用的两个。
具体可参考官网:https://v2.cn.vuejs.org/v2/guide/custom-directive.html#ad
21.2 对象式
有时候,对于一些细节的东西,函数式是无法搞定的,这时候我们就要用对象式
我现在有这样一个需求:定义一个v-fbind指令,和v-bind功能类似,但可以让其所绑定的input元素默认获取焦点。
我先用函数式实现一遍,看看有什么问题:
<div id="root">
<!--之前的代码省略-->
<input type="text" v-fbind:value="n">
</div>
<script type="text/javascript">
Vue.config.productionTip = false
new Vue({
el:'#root',
data:{
n:1
},
directives:{
//之前的代码省略
fbind(el,binding){
console.log(el);
console.log(binding);
el.value = binding.value;
el.focus(); //获取焦点
}
}
})
</script>
对于执行结果可以看出,效果并没有实现,这是为什么呢?难道是最后获取焦点的时候代码没有执行吗?可以肯定的告诉你,代码是执行了的,那为什么没有成功呢?这里就涉及到一个执行时机的问题,看看下面的代码,我现在有个要求是点击按钮创建一个输入框,要求这个输入框一创建的时候,就要获取焦点:
<button id="btn">点我创建一个输入框</button>
<script type="text/javascript" >
const btn = document.querySelector('#btn')
btn.addEventListener('click',function (){
const input = document.createElement('input')
input.value = '99';
input.focus();
document.body.appendChild(input);
})
</script>
执行结果:
可见并未成功获取焦点。
现在改动一下代码的执行顺序,先追加到父元素上了之后,再获取焦点:
document.body.appendChild(input);
input.focus();
这下就成功了。
解释:因为你虽然把input元素创建出来了,但是如果你还未将它放到页面的时候就获取焦点,显然是不可以的,你只有将input放入了页面,再获取焦点才会有效果,这就是为什么获取焦点在document.body.appendChild(input);
代码执行之后执行就可以了的原因。
所以,对于最开始的代码,我们如何改造呢?如何获取到放到页面时这个时间节点呢?
这时候就要使用对象式来解决了:
new Vue({
el:'#root',
data:{
n:1
},
directives:{
//省略之前的代码
fbind:{
bind(){},
inserted(){},
update(){}
}
}
})
可见,使用对象式之后,里面又写了3个函数,不过这3个函数可不能随便乱写,这是Vue规定好了的钩子函数,可以在特定的时机帮你调用对应的函数,无需自己调用。
三个函数的执行时机:
- bind:指令与元素成功绑定时(一上来)
- inserted:指令所在元素被插入页面时
- update:指令所在的模板被重新解析时
这样,我们就可以通过inserted钩子函数来获取到元素放入页面的这个时间节点:
fbind:{
//指令与元素成功绑定时(一上来)
bind(element,binding){
element.value = binding.value
},
//指令所在元素被插入页面时
inserted(element,binding){
element.focus()
},
//指令所在的模板被重新解析时
update(element,binding){
element.value = binding.value
}
}
可见现在成功了。
细心的你可能会发现,一般bind和update中执行的逻辑是一样的,这就是为什么有函数式写法的原因,因为函数式写法只能掌握bing和update两个阶段。
其实自定义指令上面两种写法都是属于局部的自定义指令,那全局的自定义指令怎么写呢?和之前过滤器一样:
Vue.directive('自定义指令名(不要v-)',回调函数/配置对象)
总结:
局部指令
new Vue({ directives:{指令名:配置对象} })
或者:
new Vue({ directives{指令名:回调函数} })
全局指令
Vue.directive(指令名,配置对象)或Vue.directive(指令名,回调函数)
配置对象中常用的3个回调:
bind
:指令与元素成功绑定时调用。inserted
:指令所在元素被插入页面时调用。update
:指令所在模板结构被重新解析时调用。
指令定义时不加
v-
,但使用时要加v-
指令名如果是多个单词,要使用
kebab-case
命名方式,不要用camelCase
命名
22. 生命周期
Vue生命周期整个流程图,非常重要!!!,请仔细观看:
上面所有红框的都是生命周期函数。
22.1 引出生命周期
需求:页面上有一句话,要求实现淡入淡出的效果
<div id="root">
<h1 style="opacity: 1">学习Vue中</h1>
</div>
<script type="text/javascript">
const h1 = document.querySelector('h1');
setInterval(() => {
h1.style.opacity = Number(h1.style.opacity) - 0.01 + "";
if (parseFloat(h1.style.opacity) <= 0) h1.style.opacity = '1';
}, 16);
</script>
自行执行上面的代码查看效果
现在我们再用Vue来做一遍:
<div id="root">
<h1 :style={opacity}>学习Vue中</h1>
</div>
<script type="text/javascript">
Vue.config.productionTip = false
const vm = new Vue({
el: '#root',
data: {
n: 1,
opacity: 1
}
})
setInterval(()=>{
vm.opacity -= 0.01;
if (vm.opacity <= 0) vm.opacity = 1;
},16)
</script>
这样做也能实现,但是不推荐,因为你在用定时器操作Vue里面的属性,但是这个定时器,你又没有放入到Vue中去,其实定时器应该放到Vue中,但是二者却割裂开了。
那怎么来解决呢?是否可以将定时器放入Vue的配置对象中呢?我们来看看:
<div id="root">
<h1 :style={opacity}>学习Vue中</h1>
{{change()}}
</div>
<script type="text/javascript">
Vue.config.productionTip = false
new Vue({
el: '#root',
data: {
n: 1,
opacity: 1
},
methods:{
change(){
setInterval(()=>{
this.opacity -= 0.01;
if (this.opacity <= 0) this.opacity = 1;
},16)
}
}
})
</script>
你可能会想到写出上面的代码,但是上面的代码虽然定时器的问题解决了,但是又会有新的问题,不停闪烁。
上诉代码带价特别大。
分析:想要将定时器放入Vue配置的对象中,那么你就要配置到methods配置项中,但是配置了之后,没有人来调用change函数,就不会有效果,所以你就想到了在页面中直接使用插值语法{{change()}}
的方式,因为change()函数没有返回值,插值语法处就是undefined
,undefined
在Vue中是不显示到页面上的,所以你就用这样写了,但是如果你执行了代码你就会发现,页面是很鬼畜的。我们之前学过,当vm实例上的属性发生改变的时候,就会重新解析模板。所以,上诉代码执行过程:**解析模板–>开启定时器,执行定时器改变opacity–>Vue检测到opacity改变,重新解析模板–>又开启定时器,改变opacity->…**,只要opacity改变了,页面就会重新解析,所以就会导致页面一直在重新解析,一直循环往复,所以就一直在闪烁,定时器也在不断开启,而且每个定时器里面,又是重复执行某个代码,循环将指数上升,想想就很恐怖。
那有没有办法控制定时器呢?不要让他指数增长?
这时候就要引出我们的生命周期函数了,期望只在初始化的真实DOM的时候开启定时器,之后就不再重复开启了:
<div id="root">
<h1 :style={opacity}>学习Vue中</h1>
</div>
<script type="text/javascript">
Vue.config.productionTip = false
new Vue({
el: '#root',
data: {
n: 1,
opacity: 1
},
//Vue完成模板的解析并把初始的真实DOM元素放入页面后(挂载完毕)调用mounted
mounted(){
setInterval(()=>{
this.opacity -= 0.01;
if (this.opacity <= 0) this.opacity = 1;
},16)
}
})
</script>
上诉代码即可解决问题。
可见新出现了一个函数,其实它也属于配置项,mounted()
的执行时机:Vue完成模板的解析并把初始的真实DOM元素放入页面后(挂载完毕)调用mounted。并且它在Vue的生命周期中只会执行一次。
22.2 生命周期详解
通用代码:
<div id="root">
<h1>当前的n值为:{{n}}</h1>
</div>
beforeCreate
此时无法通过vm访问到data中的数据以及methods中的方法。
<script type="text/javascript"> Vue.config.productionTip = false new Vue({ el: '#root', data: { n: 1 }, beforeCreate(){ console.log(this); debugger } }) </script>
created
此时可以通过vm访问到data中的数据以及methods中配置的方法
<script type="text/javascript"> Vue.config.productionTip = false new Vue({ el: '#root', data: { n: 1 }, created(){ console.log(this); debugger } }) </script>
beforeMount
到beforeMount阶段时,此时页面呈现的是未经Vue编译的DOM结构(其实前几个阶段也一样),所有对DOM的操作,最终都不奏效。
<script type="text/javascript"> Vue.config.productionTip = false new Vue({ el: '#root', data: { n: 1 }, beforeMount(){ console.log(this); debugger } }) </script>
一放行,之前操作的DOM都会被Vue替换为Vue解析的内容:
所以在这个阶段以及这个阶段之前,操作DOM都是不奏效的。因为在这阶段已过,就执行了这个环节:
因为在这个阶段之前,Vue的虚拟DOM已经生成了,并且已经放入到了内存中:
mounted
查看上图mounted阶段挺长的,里面还包括了
beforeUpdate
、updated
,这两个阶段后面讲。在mounted阶段,此时页面中呈现的是经过Vue编译的DOM,并且对DOM的操作均有效(尽可能避免)至此初始化过程结束。
一般在此进行:开启定时器、发送网络请求、订阅消息、绑定自定义事件、等初始化操作。
<script type="text/javascript"> Vue.config.productionTip = false new Vue({ el: '#root', data: { n: 1 }, mounted(){ console.log(this); debugger } }) </script>
可见成功操作了DOM,不过一般不要这样做。
beforeUpdate
、updated
当数据改变(when data changes)的时候,这两个钩子函数触发。
beforeUpdate阶段:此时数据是新的,但是页面仍然是旧的,即页面尚未和数据保持同步。
<div id="root"> <h1>当前的n值为:{{n}}</h1> <button @click="updateValue">点我更新n值</button> </div> <script type="text/javascript"> Vue.config.productionTip = false new Vue({ el: '#root', data: { n: 1 }, methods:{ updateValue(){ this.n = 100; } }, beforeUpdate(){ console.log(this); debugger } }) </script>
updated阶段:此时数据是最新的,页面也是最新的,即数据与页面保持同步。
将上述代码beforeUpdate改为updated查看:
beforeDestroy
、destroyed
在beforeDestroy阶段:此时vm中所有的:data、methods、指令等等,都处于可用状态,马上要执行销毁过程,一般在此阶段:关闭定时器、取消订阅消息、解绑自定义事件等收尾操作。
注意:在此阶段你不要再更新数据了,因为就算你再这个阶段以及后面的destroyed阶段更新数据都不会再呈现再页面上了。
最后destroyed阶段是被忽略得最严重的一个阶段,不用过度关心它。
使用代码总结一下流程:
<div id="root">
<h1>当前的n值为:{{n}}</h1>
<button @click="updateValue">点我更新n值</button>
<button @click="destroyVm">点我销毁vm</button>
</div>
<script type="text/javascript">
Vue.config.productionTip = false
new Vue({
el: '#root',
data: {
n: 1
},
methods:{
updateValue(){
this.n = 100;
console.log('updateValue被执行了');
},
destroyVm(){
this.$destroy();
}
},
beforeCreate() {
console.log('beforeCreate')
},
created() {
console.log('created')
},
beforeMount() {
console.log('beforeMount')
},
mounted() {
console.log('mounted')
},
beforeUpdate() {
console.log('beforeUpdate')
console.log(`当前n值为${this.n}`)
},
updated() {
console.log('updated')
console.log(`当前n值为${this.n}`)
},
beforeDestroy() {
console.log('beforeDestroy')
console.log(`当前n值为${this.n}`)
},
destroyed() {
console.log('destroyed')
console.log(`当前n值为${this.n}`)
},
})
</script>
为什么销毁过后,再次点击更新没用了呢?我们来看看官方文档:
总结:
常用的生命周期钩子:
mounted
: 发送ajax请求、启动定时器、绑定自定义事件、订阅消息等【初始化操作】。beforeDestroy
: 清除定时器、解绑自定义事件、取消订阅消息等【收尾工作】。
关于销毁Vue实例
- 销毁后借助Vue开发者工具看不到任何信息。
- 销毁后自定义事件会失效,但原生DOM事件依然有效。
- 一般不会在
beforeDestroy
操作数据,因为即便操作数据,也不会再触发更新流程了。