Vue与MVVM模式
后端中的 MVC 与 前端中的 MVVM 之间的区别
-
MVC 是后端的分层开发概念;
-
MVVM是前端视图层的概念,主要关注于 视图层分离,也就是说:MVVM把前端的视图层,分为了 三部分 Model, View , VM ViewModel
-
为什么有了MVC还要有MVVM
Vue.js 基本代码 和 MVVM 之间的对应关系
<body>
<!-- 将来 new 的Vue实例,会控制这个 元素中的所有内容 -->
<!-- Vue 实例所控制的这个元素区域,就是我们的 V -->
<div id="app">
<p>{{ msg }}</p>
</div>
<script>
// 2. 创建一个Vue的实例
// 当我们导入包之后,在浏览器的内存中,就多了一个 Vue 构造函数
// 注意:我们 new 出来的这个 vm 对象,就是我们 MVVM中的 VM调度者
var vm = new Vue({
el: '#app', // 表示,当前我们 new 的这个 Vue 实例,要控制页面上的哪个区域
// 这里的 data 就是 MVVM中的 M,专门用来保存 每个页面的数据的
data: { // data 属性中,存放的是 el 中要用到的数据
msg: '欢迎学习Vue' // 通过 Vue 提供的指令,很方便的就能把数据渲染到页面上,程序员不再手动操作DOM元素了【前端的Vue之类的框架,不提倡我们去手动操作DOM元素了】
}
})
</script>
</body>
声明式渲染
插值表达式{{ }}
插值表达式会出现闪烁问题,即在网速慢时会出现{{ msg }}
,数据加载好后才会显示数据
v-cloak、v-text、v-html
-
v-cloak
-
解决插值表达式闪烁的问题
<style> [v-cloak] { display: none; } </style> <p v-cloak>{{ msg }}</p>
-
-
v-text
- 没有闪烁问题
- v-text会覆盖元素中原本的内容,但是 插值表达式 只会替换自己的这个占位符,不会把 整个元素的内容清空
-
v-html
- 可以输出html标签,插值表达式和v-text只会输出普通文本
v-bind 绑定属性
-
直接使用指令
v-bind
-
使用简化指令
:
-
在绑定的时候,拼接绑定内容:
:title="btnTitle + '追加内容'"
<div id="app">
<input type="button" value="按钮" v-bind:title="mytitle + '123'">
</div>
<script>
var vm = new Vue({
el: '#app',
data: {
mytitle: '这是一个自己定义的title'
},
})
</script>
v-on 绑定事件
-
直接使用指令
v-on
-
使用简化指令
@
<div id="app">
<input type="button" value="按钮" v-on:click="show">
</div>
<script>
var vm = new Vue({
el: '#app',
methods: {
// 这个 methods属性中定义了当前Vue实例所有可用的方法
show: function () {
alert('Hello')
}
}
})
</script>
demo:跑马灯
分析:
- 给 【浪起来】 按钮,绑定一个点击事件 v-on @
- 在按钮的事件处理函数中,写相关的业务逻辑代码:拿到 msg 字符串,然后 调用 字符串的 substring 来进行字符串的截取操作,把 第一个字符截取出来,放到最后一个位置即可;
- 为了实现点击下按钮,自动截取的功能,需要把 2 步骤中的代码,放到一个定时器中去;
代码:
- HTML
<div id="app">
<input type="button" value="浪起来" @click="lang">
<input type="button" value="低调" @click="stop">
<h4>{{ msg }}</h4>
</div>
- Vue实例
<script>
// 在 VM 实例中,如果想要获取 data 上的数据,或者 想要调用 methods 中的 方法,
// 必须通过 this.数据属性名 或 this.方法名 来进行访问,
// this表示 new 出来的 VM 实例对象
var vm = new Vue({
el: '#app',
data: {
msg: '猥琐发育,别浪~~!',
intervalId: null // 在data上定义 定时器Id
},
methods: {
lang() {
if (this.intervalId != null) return; // 防止多次点击按钮,创建多个定时器
// =>箭头函数解决了this的指向问题,如果不加,则this指向的是setInterval方法
this.intervalId = setInterval(() => {
var start = this.msg.substring(0, 1)
// 获取到 后面的所有字符
var end = this.msg.substring(1)
// 重新拼接得到新的字符串,并赋值给 this.msg
this.msg = end + start
}, 400)
// VM实例监听自己身上 data 中的数据,只要数据改变,就会自动把最新的数据,同步到页面中去
// 【好处:程序员只需要关心数据,不需要考虑如何重新渲染DOM页面】
},
stop() { // 停止定时器
clearInterval(this.intervalId)
// 每当清除了定时器之后,需要重新把 intervalId 置为 null
this.intervalId = null;
}
}
})
</script>
事件修饰符
-
.stop 阻止冒泡
-
// 此时点击按钮,不会触发div1Handler事件 <div class="inner" @click="div1Handler"> <input type="button" value="戳他" @click.stop="btnHandler"> </div>
-
.prevent 阻止默认事件
-
// 此时点击a标签不弹网页 <a href="http://www.baidu.com" @click.prevent="linkClick">有问题,先去百度</a>
-
.capture 添加事件侦听器时使用事件捕获模式
-
// 此时点击按钮,先触发div1Handler再触发btnHandler,由外向里 <div class="inner" @click.capture="div1Handler"> <input type="button" value="戳他" @click="btnHandler"> </div>
-
.self 只当事件在该元素本身(比如不是子元素)触发时触发回调
-
// 此时点击按钮,不会触发div1Handler事件 <div class="inner" @click.self="div1Handler"> <input type="button" value="戳他" @click="btnHandler"> </div>
-
.once 事件只触发一次
-
// 点击第一次不跳转,第二次跳转,使prevent事件只触发一次 // 事件修饰符可以链式操作 <a href="http://www.baidu.com" @click.prevent.once="linkClick">有问题,先去百度</a>
stop和self在阻止冒泡时的区别:
- stop阻止了所有的冒泡
- self只会阻止自己身上冒泡行为的触发,并不会真正阻止冒泡的行为
v-model 双向数据绑定
- v-bind 只能实现数据的单向绑定,从 M 自动绑定到 V,无法实现数据的双向绑定
- v-model 指令可以实现 表单元素和 Model 中数据的双向数据绑定
- v-model 只能运用在 表单元素中 input/select/checkbox/textarea
demo:简易计算器
- HTML
<div id="app">
<input type="text" v-model="n1">
<select v-model="opt">
<option value="+">+</option>
<option value="-">-</option>
<option value="*">*</option>
<option value="/">/</option>
</select>
<input type="text" v-model="n2">
<input type="button" value="=" @click="calc">
<input type="text" v-model="result">
</div>
- Vue实例
<script>
// 创建 Vue 实例,得到 ViewModel
var vm = new Vue({
el: '#app',
data: {
n1: 0,
n2: 0,
result: 0,
opt: '+'
},
methods: {
calc() { // 计算器算数的方法
// 逻辑:
switch (this.opt) {
case '+':
this.result = parseInt(this.n1) + parseInt(this.n2)
break;
case '-':
this.result = parseInt(this.n1) - parseInt(this.n2)
break;
case '*':
this.result = parseInt(this.n1) * parseInt(this.n2)
break;
case '/':
this.result = parseInt(this.n1) / parseInt(this.n2)
break;
}
// 注意:这是投机取巧的方式,正式开发中,尽量少用
// var codeStr = 'parseInt(this.n1) ' + this.opt + ' parseInt(this.n2)'
// this.result = eval(codeStr)
}
}
});
</script>
在Vue中使用样式
使用class样式
- 数组
<h1 :class="['red', 'thin']">这是一个邪恶的H1</h1>
- 数组中使用三元表达式
<h1 :class="['red', 'thin', isactive?'active':'']">这是一个邪恶的H1</h1>
// 三元表达式isactive?'active':''
// 其中isactive是判断条件,如果为true,则执行?后面的否则执行:后面的
- 数组中嵌套对象
<h1 :class="['red', 'thin', {'active': isactive}]">这是一个邪恶的H1</h1>
// 'active'是style中的类名,isactive是vm实例中data中的变量
- 直接使用对象,对象的属性是类名,可不带引号
<h1 :class="{red:true, italic:true, active:false, thin:true}">这是一个邪恶的H1</h1>
使用内联样式
- 直接在元素上通过
:style
的形式,书写样式对象
<h1 :style="{color: 'red', 'font-size': '40px'}">这是一个善良的H1</h1>
// 属性中有-时必须加引号
- 将样式对象,定义到
data
中,并直接引用到:style
中
- 在data上定义样式:
data: {
h1StyleObj: { color: 'red', 'font-size': '40px', 'font-weight': '200' }
}
- 在元素中,通过属性绑定的形式,将样式对象应用到元素中:
<h1 :style="h1StyleObj">这是一个善良的H1</h1>
- 在
:style
中通过数组,引用多个data
上的样式对象
- 在data上定义样式:
data: {
h1StyleObj: { color: 'red', 'font-size': '40px', 'font-weight': '200' },
h1StyleObj2: { fontStyle: 'italic' }
}
- 在元素中,通过属性绑定的形式,将样式对象应用到元素中:
<h1 :style="[h1StyleObj, h1StyleObj2]">这是一个善良的H1</h1>
循环与条件
v-for和key属性
- 迭代数组
<ul>
<li v-for="(item, i) in list">索引:{{i}} --- 姓名:{{item.name}}</li>
</ul>
- 迭代对象中的键值对
<!-- 循环遍历对象身上的属性 -->
<div v-for="(val, key, i) in userInfo">{{val}} --- {{key}} --- {{i}}</div>
- 迭代数字,从1开始
<p v-for="i in 10">这是第 {{i}} 个P标签</p>
2.2.0+ 的版本里,当在组件中使用 v-for 时,key 现在是必须的。
当 Vue.js 用 v-for 正在更新已渲染过的元素列表时,它默认用 “就地复用” 策略。如果数据项的顺序被改变,Vue将不是移动 DOM 元素来匹配数据项的顺序, 而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。
为了给 Vue 一个提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,你需要为每项提供一个唯一 key 属性。
注意:
- v-for 循环时,key属性使用number或string
- key在使用时,必须使用v-bind属性绑定的形式指定key的值
:key="item.id"
v-if和v-show
- v-if
- 每次都会重新删除或创建元素
- 有较高的切换性能消耗,如果元素可能永远不会显示出来,推荐用v-if
- v-show
- 每次不会重新进行DOM的删除和创建操作,只是切换了元素的 display:none 样式
- 有较高的初始渲染消耗,需要频繁切换时用v-show
<h3 v-if="flag">这是用v-if控制的元素</h3>
<h3 v-show="flag">这是用v-show控制的元素</h3>
// flag等于true时标签显示,否则不显示
一般来说,v-if 有更高的切换消耗而 v-show 有更高的初始渲染消耗。因此,如果需要频繁切换 v-show 较好,如果在运行时条件不大可能改变 v-if 较好。
demo:品牌管理
HTML
<div id="app">
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">添加品牌</h3>
</div>
<div class="panel-body form-inline">
<label>
Id:
<input type="text" class="form-control" v-model="id">
</label>
<label>
Name:
<input type="text" class="form-control" v-model="name">
</label>
<!-- 在Vue中,使用事件绑定机制,为元素指定处理函数的时候,如果加了小括号,就可以给函数传参了 -->
<input type="button" value="添加" class="btn btn-primary" @click="add()">
<label>
搜索名称关键字:
<input type="text" class="form-control" v-model="keywords">
</label>
</div>
</div>
<table class="table table-bordered table-hover table-striped">
<thead>
<tr>
<th>Id</th>
<th>Name</th>
<th>Ctime</th>
<th>Operation</th>
</tr>
</thead>
<tbody>
<!-- 之前, v-for 中的数据,都是直接从 data 上的list中直接渲染过来的 -->
<!-- 现在, 自定义了一个 search 方法,同时,把搜索的关键字通过传参的形式,传递给了 search 方法 -->
<!-- 在 search 方法内部,通过 执行 for 循环, 把所有符合 搜索关键字的数据,保存到一个新数组中,再返回 -->
<tr v-for="item in search(keywords)" :key="item.id">
<td>{{ item.id }}</td>
<td v-text="item.name"></td>
<td>{{ item.ctime }}</td>
<td>
<a href="" @click.prevent="del(item.id)">删除</a>
</td>
</tr>
</tbody>
</table>
</div>
添加新品牌
var vm = new Vue({
el: '#app',
data: {
id: '',
name: '',
keywords: '', // 搜索的关键字
list: [
{id: 1, name: '奔驰', ctime: new Date()},
{id: 2, name: '宝马', ctime: new Date()}
]
},
methods: {
add() {
// 1. 从data中取出id和name并组成一个对象
// 2. 把这个对象,添加到 data 上的 list 中
// 注意:Vue已经实现了数据的双向绑定,每当修改了 data 中的数据,Vue会默认监听到数据的改动,自动把最新的数据,应用到页面上;
var car = {id: this.id, name: this.name, ctime: new Date()}
this.list.push(car)
// 将input标签内的内容清空
this.id = this.name = ''
},
删除品牌
del(id) { // 根据Id删除数据
// 分析:
// 1. 如何根据Id,找到要删除这一项的索引
// 2. 如果找到索引了,直接调用 数组的 splice 方法
// 方法一,使用数组的some方法
/* this.list.some((item, i) => {
if (item.id == id) {
this.list.splice(i, 1)
// 在 数组的 some 方法中,如果 return true,就会立即终止这个数组的后续循环
return true;
}
}) */
// 方法二,使用数组的findIndex方法
var index = this.list.findIndex(item => {
if (item.id == id) {
return true;
}
})
this.list.splice(index, 1)
},
根据条件筛选品牌
-
筛选框绑定到 VM 实例中的
keywords
属性 -
在使用
v-for
指令循环每一行数据的时候,不再直接item in list
,而是in
一个 过滤的methods 方法,同时,把过滤条件keywords
传递进去 -
search
过滤方法中,使用 数组的filter
方法进行过滤:
search(keywords) { // 根据关键字,进行数据的搜索
// 方法一
/* var newList = []
this.list.forEach(item => {
if (item.name.indexOf(keywords) != -1) {
newList.push(item)
}
})
return newList */
// 注意: forEach some filter findIndex 这些都属于数组的新方法,
// 都会对数组中的每一项,进行遍历,执行相关的操作;
// 方法二
return this.list.filter(item => {
// if(item.name.indexOf(keywords) != -1)
// 注意 : ES6中,为字符串提供了一个新方法,叫做 String.prototype.includes('要包含的字符串')
// 如果包含,则返回 true ,否则返回 false
if (item.name.includes(keywords)) {
return item
}
})
// return newList
}
调试工具vue-devtools
过滤器
概念:Vue.js 允许你自定义过滤器,可被用作一些常见的文本格式化。过滤器可以用在两个地方:mustache 插值和 v-bind 表达式。过滤器应该被添加在 JavaScript 表达式的尾部,由“管道”符指示;
私有过滤器
私有局部过滤器,只能在 当前 VM 对象所控制的 View 区域进行使用
HTML
<td>{{item.ctime | dataFormat('yyyy-mm-dd')}}</td>
Vue实例
filters: {
// 第一个参数接收过滤器前面的值,后面的参数接收过滤器的参数
dataFormat(input, pattern = "") { // 在参数列表中 通过 pattern="" 来指定形参默认值,防止报错
var dt = new Date(input);
// 获取年月日
var y = dt.getFullYear();
var m = (dt.getMonth() + 1).toString().padStart(2, '0');
// month从0开始,所以要+1
var d = dt.getDate().toString().padStart(2, '0');
// padStart(2, '0')把个位数转化为01,02的两位数
// 如果 传递进来的字符串类型,转为小写之后,等于 yyyy-mm-dd,那么就返回 年-月-日
// 否则,就返回 年-月-日 时:分:秒
if (pattern.toLowerCase() === 'yyyy-mm-dd') {
return `${y}-${m}-${d}`;
} else {
// 获取时分秒
var hh = dt.getHours().toString().padStart(2, '0');
var mm = dt.getMinutes().toString().padStart(2, '0');
var ss = dt.getSeconds().toString().padStart(2, '0');
return `${y}-${m}-${d} ${hh}:${mm}:${ss}`;
}
}
}
使用ES6中的字符串新方法 String.prototype.padStart(maxLength, fillString='') 或 String.prototype.padEnd(maxLength, fillString='')来填充字符串;
全局过滤器
HTML
<div id="app">
<p>{{ msg | msgFormat('疯狂', '123') | test }}</p>
</div>
vue实例
// 定义一个 Vue 全局的过滤器,名字叫做 msgFormat
Vue.filter('msgFormat', function (msg, arg, arg2) {
// 字符串的 replace 方法,第一个参数,除了可写一个 字符串之外,还可以定义一个正则
return msg.replace(/内容/g, arg + arg2)
})
Vue.filter('test', function (msg) {
return msg + '========'
})
// 创建 Vue 实例,得到 ViewModel
var vm = new Vue({
el: '#app',
data: {
msg: '一段内容'
},
methods: {}
});
注意:当有局部和全局两个名称相同的过滤器时候,会以就近原则进行调用,即:局部过滤器优先于全局过滤器被调用!
键盘修饰符以及自定义键盘修饰符
vue文档-按键修饰符
自定义键盘修饰符
- 通过
Vue.config.keyCodes.名称 = 按键值
来自定义案件修饰符的别名:
Vue.config.keyCodes.f2 = 113;
- 使用自定义的按键修饰符:
<input type="text" v-model="name" @keyup.f2="add">
自定义指令
- 自定义全局和局部的自定义指令:
注意: 在每个函数中,第一个参数永远是 el ,它表示被绑定了指令的那个元素。这个 el 参数,是一个原生的JS对象
// 自定义全局指令 v-focus,为绑定的元素自动获取焦点:
Vue.directive('focus', {
inserted: function (el) { // inserted 表示被绑定元素插入父节点时调用
el.focus();
}
});
// 自定义局部指令 v-color 和 v-font-weight,为绑定的元素设置指定的字体颜色 和 字体粗细:
directives: {
color: { // 为元素设置指定的字体颜色
bind(el, binding) {
el.style.color = binding.value;
}
},
'font-weight': function (el, binding2) { // 自定义指令的简写形式,等同于定义了 bind 和 update 两个钩子函数
el.style.fontWeight = binding2.value;
}
}
- 自定义指令的使用方式,必须v-名字
<input type="text" v-model="searchName" v-focus v-color="'red'" v-font-weight="900">
钩子函数 (均为可选)
bind
:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。每当指令绑定到元素上的时候,会立即执行这个 bind 函数,只执行一次- 和样式相关的操作,一般都可以在 bind 执行。样式只要通过指令绑定给了元素,不管这个元素有没有被插入到页面中去,这个元素肯定有了一个内联的样式。将来元素肯定会显示到页面中,这时候,浏览器的渲染引擎必然会解析样式,应用给这个元素
inserted
:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。inserted 表示元素 插入到DOM中的时候,会执行 inserted 函数【触发1次】。- 和JS行为有关的操作,最好在 inserted 中去执行,放置 JS行为不生效
update
:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。
钩子函数参数
el
:指令所绑定的元素,可以用来直接操作 DOM 。binding
:一个对象,包含以下属性:name
:指令名,不包括v-
前缀。value
:指令的绑定值,例如:v-my-directive="1 + 1"
中,绑定值为2
。oldValue
:指令绑定的前一个值,仅在update
和componentUpdated
钩子中可用。无论值是否改变都可用。expression
:字符串形式的指令表达式。例如v-my-directive="1 + 1"
中,表达式为"1 + 1"
。arg
:传给指令的参数,可选。例如v-my-directive:foo
中,参数为"foo"
。modifiers
:一个包含修饰符的对象。例如:v-my-directive.foo.bar
中,修饰符对象为{ foo: true, bar: true }
。
vnode
:Vue 编译生成的虚拟节点。移步 VNode API 来了解更多详情。oldVnode
:上一个虚拟节点,仅在update
和componentUpdated
钩子中可用。
methods、watch、computed
三者对比
computed
属性的结果会被缓存,除非依赖的响应式属性变化才会重新计算。主要当作属性来使用;必须returnmethods
方法表示一个具体的操作,主要书写业务逻辑;watch
属性的值是一个对象,键是需要观察的表达式,值是对应回调函数。主要用来监听某些特定数据的变化,从而进行某些具体的业务逻辑操作;可以看作是computed
和methods
的结合体;
通过一个demo,来介绍三者的区别和使用方法。
实现姓、名两个文本框的内容的改变,则全名的文本框中的值也跟着改变
methods方法
通过监听keyup事件,然后调用方法实现
<div id="app">
<input type="text" v-model="firstname" @keyup="getFullname"> +
<input type="text" v-model="lastname" @keyup="getFullname"> =
<input type="text" v-model="fullname">
</div>
var vm = new Vue({
el: '#app',
data: {
firstname: '',
lastname: '',
fullname: ''
},
methods: {
getFullname() {
this.fullname = this.firstname + '-' + this.lastname
}
}
});
watch属性
- 监听data中属性的变化
<div id="app">
<input type="text" v-model="firstname"> +
<input type="text" v-model="lastname"> =
<input type="text" v-model="fullname">
</div>
var vm = new Vue({
el: '#app',
data: {
firstname: '',
lastname: '',
fullname: ''
},
methods: {},
watch: { // 使用watch属性可以监视 data 中指定数据的变化,然后触发 watch 中对应的 function 处理函数
'firstname': function (newVal, oldVal) {
// newVal, oldVal分别是变化前的值和变化后的值
this.fullname = newVal + '-' + this.lastname
},
'lastname': function (newVal) {
this.fullname = this.firstname + '-' + newVal
}
}
});
computed计算属性的使用
- 默认只有
getter
的计算属性
<div id="app">
<input type="text" v-model="firstname"> +
<input type="text" v-model="lastname"> =
<input type="text" v-model="fullname">
</div>
var vm = new Vue({
el: '#app',
data: {
firstname: '',
lastname: ''
},
methods: {},
computed: { // 在 computed 中可以定义一些叫做 【计算属性】的属性,计算属性的本质就是一个方法,只不过在使用这些计算属性时,是把它们的名称直接当作属性来使用的;并不会把计算属性当作方法去调用;
// 注意1:在引用计算属性时,不要加()去调用,直接把它当作普通属性去使用;
// 注意2:只要计算属性的 function 内部,所用到的任何 data 中的数据发送了变化,就会立即重新计算这个计算属性的值
// 注意3:计算属性的结果会被缓存起来,方便下次直接使用;如果计算属性方法中的任何数据都没有发生过变化,则不会重新对计算属性求值;即不会重新执行这个方法
'fullname': function () {
console.log('ok')
return this.firstname + '-' + this.lastname
}
}
});
- 定义有
getter
和setter
的计算属性
<div id="app">
<input type="text" v-model="firstName">
<input type="text" v-model="lastName">
<!-- 点击按钮重新为 计算属性 fullName 赋值 -->
<input type="button" value="修改fullName" @click="changeName">
<span>{{fullName}}</span>
</div>
var vm = new Vue({
el: '#app',
data: {
firstName: 'jack',
lastName: 'chen'
},
methods: {
changeName() {
this.fullName = 'TOM - chen2';
}
},
computed: {
fullName: {
get: function () {
return this.firstName + ' - ' + this.lastName;
},
set: function (newVal) {
var parts = newVal.split(' - ');
this.firstName = parts[0];
this.lastName = parts[1];
}
}
}
});
nrm的安装使用
作用:提供了一些最常用的NPM包镜像地址,能够让我们快速的切换安装包时候的服务器地址;
什么是镜像:原来包刚一开始是只存在于国外的NPM服务器,但是由于网络原因,经常访问不到,这时候,我们可以在国内,创建一个和官网完全一样的NPM服务器,只不过,数据都是从人家那里拿过来的,除此之外,使用方式完全一样;
- 运行
npm i nrm -g
全局安装nrm
包; - 使用
nrm ls
查看当前所有可用的镜像源地址以及当前所使用的镜像源地址; - 使用
nrm use npm
或nrm use taobao
切换不同的镜像源地址;