简介
Vue是一套构建用户界面渐进式 javascritp 框架
- 框架 vs 库
库:方法的集合,避免相同功能重复定义,还具有一定兼容性(js高级封装)
框架:规范开发者(按照框架的设计进行编码),还提供了很多方法
- 渐进式
指物品具有核心功能和扩展功能的,核心功能是必备,而扩展功能是选配,例如:手机的核心功能是打电话,扩展功能是上网、看视频、听音乐、遥控等
vue的核心功能:声明式渲染
vue的扩展功能:组件、路由、统一状态管理、脚手架
- 构建用户界面
框架本身只关注视图层,也是最后展示的结果
MVC vs MVVM
-
MVC: 基于事件驱动来控制界面的显示
-
MVVC: 基于数据驱动来控制界面的显示,是MVC的优化升级版
特点
1、视图与数据解耦
2、响应式数据绑定
3、可复用的组件
4、前端路由技术
5、状态管理
6、虚拟Dom
模板(声明式)
插值
绑定内容( {{}} )
使用 “Mustache” 语法 (双大括号) 在模板中声明
<div id="app">
<p>{{name}}</p> // 与 v-text 一样
</div>
<script>
const app = new Vue({
el: '#app',
data: {
name: 'xxx'
}
})
</script>
上面这种方式,只能给标签添加文本,如果需要给标签添加HTML,要使用指令v-html,代码如下
<div id="app">
<p v-html="content"></p>
</div>
<script>
const app = new Vue({
el: '#app',
data: {
content: '<span>1234</span>'
}
})
</script>
注意点
- 插值可以理解为一个迷你的JS运行环境,它内部只支持
变量,方法调用,计算属性,表达式,不能是语句、流程控制 - 使用插值表达式去访问Vue实例对象不存在的属性数据时,
将会报错 - 使用插值表达式去访问Vue实例对象存在的属性数据下不存的属性(undefined)时,
不会报错 - 如果同时存在
v-text(v-html)和插值表达式{{}},v-text优先级高于插值表达式
指令
绑定内容(v-html、v-text)
HTML内容
<div id="app">
<p v-html="content"></p>
</div>
<script>
const app = new Vue({
el: '#app',
data: {
content: '<span>1234</span>'
}
})
</script>
v-html指令慎用,只能用于绑定自己确定安全的数据,千万别绑定第三方请求数据(代码注入攻击)
文本内容
<div id="app">
<p v-text="content"></p>
</div>
<script>
const app = new Vue({
el: '#app',
data: {
content: '123456'
}
})
</script>
v-text指令一般很少使用,都是直接使用插值取代
绑定属性(v-bind)
使用v-bind指令,简写:
<div id="app">
<a :href="url">跳转百度</a>
<a v-bind:href="qq">qq</a>
</div>
<script>
const app = new Vue({
el: '#app',
data: {
url: 'http://www.baidu.com',
qq: 'http://www.qq.com'
}
})
</script>
绑定事件(v-on)
使用v-on指令,简写@。虽然v-on中可以写JS代码,但由于事件处理逻辑比较复杂,直接写JS代码在v-on指令中不可行,所以v-on指令支持绑定事件处理函数,以下有两种事件绑定方式:
绑定函数名
<div id="app">
<button @click="openMsg">点击</button>
</div>
<script>
const app = new Vue({
el: '#app',
data: {
url: 'http://www.baidu.com',
qq: 'http://www.qq.com'
},
methods: {
openMsg: function(e){
console.log(e); // Dom事件对象
}
}
})
</script>
绑定函数调用 (推荐)
与上面方式相比,区别如下
1、函数名方式无法传递参数,函数调用方式可以传递参数
2、函数名方式默认传递原始Dom事件,函数调用方式必须显式指定$event
3、@click="openMsg"与@click="openMsg($event)"效果一样
<div id="app">
<button @click="openMsg('str', $event)">点击</button>
</div>
<script>
const app = new Vue({
el: '#app',
data: {
url: 'http://www.baidu.com',
qq: 'http://www.qq.com'
},
methods: {
openMsg: function(p,e){
console.log(p,e);
}
}
})
</script>
总结:
1、事件回调函数需要编写在methods对象中,最终会在Vue实例上
2、methods中编写的函数,不要使用箭头函数,否则this指向Vue实例
3、methods中编写的函数,都是被Vue管理的,所以this指向Vue实例或组件实例
条件渲染(v-if)
有两个指令 v-if 和 v-show
语法
// v-if
<div v-if="condition">111</div>
<div v-else-if="type === 'username'">222</div>
<div v-else>333</div>
// v-show
<div v-show="type === 'password'">xxxx</div>
v-show 与 v-if 区别
1、v-show是通过CSS来控制显示和隐藏,v-if是通过是否渲染来控制的
2、v-if可以使用在<template>元素上,v-show不可以使用在<template>元素上
经常需要频繁地切换,则使用
v-show较好;
很少切换,则使用v-if较好
v-if 元素复用
默认会自动复用元素,如果不需要复用,必须在元素上加上 key 属性
注意事项
1、v-if/v-else/v-else-if 必须连着使用,不能中间有起来元素
2、v-else/v-else-if 不能单独使用,必须前面要有v-if元素
列表渲染(v-for)
使用指令 v-for,遍历的数据类型 数组、对象,页面的代码结构一致都可以使用 v-for 指令,例如:ul>lli
语法
// 遍历数组
<ul id="example-2">
<li v-for="(item, index) in arr">
{{ index }} - {{ item }}
</li>
</ul>
// 遍历对象
<div v-for="(v, k, index) in obj">
{{ index }}. {{ k }}: {{ v }}
</div>
// 一般写法
<div v-for="(v, k) in obj">
{{ k }}: {{ v }}
</div>
* key
提高更新效率,减少不必要DOM操作,( key更像列表项唯一标识 )
<ul>
<li v-for="item in list" :key="item">{{item}}</li>
</ul>
<script>
const app = new Vue({
el: "#app",
data: {
list: ['vue', 'html', 'css']
}
});
</script>
总结:
1、如果用v-for指令,必须配合:key属性一起使用
2、:key的值是浏览器判断是否需要更新DOM的依据,如果这个依据发生变化,浏览器就会对这个值进行更新,否则不会更新(DOM更新就是先删除后重新创建)
3、:key不要使用数组索引作为值
双向数据绑定(v-model)
使用指令 v-model 对表单<input>、<select>和<textarea>元素进行双向数据绑定,并该指令只能与前面三种表单元素和自定义输入组件配合使用,其他元素无法使用
什么是双向数据绑定
用户操作视图层(view)导致模型层(model)数据发生变化,反过来模型层(model)数据发生变化又影响视图层(view)数据的展示
简单应用
<div id="app">
<input type="text" v-model="desc" />
<p>{{desc}}</p>
<select v-model="selVal">
<option value="bj">北京</option>
<option value="nj">南京</option>
<option value="hz">杭州</option>
</select>
<p>{{selVal}}</p>
</div>
<script>
const app = new Vue({
el: '#app',
data: {
desc: '',
selVal: ''
}
});
</script>
原样输出(v-pre)
跳过这个元素和它的子元素的解析,直接输出(说白就是不解析插值语法) 很少使用
<div v-pre>{{content}}</div> // {{content}}
延时隐藏(v-cloak)
这个指令保持在元素上直到关联实例结束编译。和 CSS 规则如 [v-cloak] { display: none }一起使用,一般配合webpack开发单页应用时,动态生成HTML结构,不需要用到此指令
[v-cloak] {
display: none;
}
<div v-cloak>
{{ message }}
</div>
一次渲染(v-once)
只渲染元素、组件及其子元素一次,随后的重新渲染将跳过
<!-- 单个元素 -->
<span v-once>This will never change: {{msg}}</span>
<!-- 有子元素 -->
<div v-once>
<h1>comment</h1>
<p>{{msg}}</p>
</div>
自定义指令
对原生Dom操作进行一次封装
应用场景
动态样式绑定
动态绑定样式是每个项目必备的技能,类名和style属性是唯一的两种绑定方式。
通过类名
静态就直接写,动态绑定必须在属性前加冒号(v-bind缩写)
字符串写法
适用:类个数确定,类名字不确定,需要动态绑定
// 推荐
<div class="base" :class="color"></div>
// 直接在模板绑定(写死,Vue无法管理)
<div class="base" :class="'main'"></div>
<script>
new Vue({
el: "#app",
data: {
color: "main"
}
});
</script>
数组写法
适用:类个数不确定,类名字也不确定,需要动态绑定
// 推荐
<div class="base" :class="classArr"></div>
// 直接在模板绑定(写死,Vue无法管理)
<div class="base" :class="['main', 'box', 'mt-10']"></div>
<script>
new Vue({
el: "#app",
data: {
classArr: ["main", "box", "mt-10"]
}
});
</script>
对象写法
适用:类个数确定,类名字也确定,需要动态j决定用不用
// 推荐
<div class="base" :class="classObj"></div>
// 直接在模板绑定(写死,Vue无法管理)
<div class="base" :class="{main:true,box:false}"></div>
<script>
new Vue({
el: "#app",
data: {
classObj: {
main: true,
box: false
}
}
});
</script>
通过style属性
对象写法
// 不推荐
<p style="color:red" :style="{fontSize: fontMin+'px'}">测试<p>
// 推荐
<p style="color:red" :style="styleObj">测试<p>
new Vue({
el: '#app',
data: {
fontMin: 16,
styleObj: {
fontSize: '36px',
backgroundColor: 'red'
}
}
})
数组写法(不推荐)
// 不推荐
<p style="color:red" :style="[styleObj]">测试<p>
// 推荐
<p style="color:red" :style="styleObj">测试<p>
new Vue({
el: '#app',
data: {
fontMin: 16,
styleObj: {
fontSize: '36px',
backgroundColor: 'red'
},
styleArr: [
{
fontSize: '36px',
backgroundColor: 'red'
}
]
}
})
样式对象的
Key要和样式名是一致,遇到样式名带横杠连接的,需要把横杠去掉后面单词首字母大写(例如:font-size => fontSize)
事件修饰符
有些HTML元素有默认行为,例如:<a>会自动跳转;为了阻止默认行为,我们需要使用到Vue提供的事件修饰符
语法
// 以前做法
<div id="app">
<a href="http://www.baidu.com" @click="send">发送</a>
</div>
<script>
new Vue({
el: '#app',
methods: {
send(e){
e.preventDefault()
alert('123');
}
}
});
</script>
// 事件修饰符做法
<div id="app">
<a href="http://www.baidu.com" @click.prevent="send">发送</a>
</div>
<script>
new Vue({
el: '#app',
methods: {
send(e){
alert('123');
}
}
});
</script>
种类
.stop // 阻止单击事件冒泡 (常用)
.prevent // 阻止默认行为 (常用)
.once // 事件只触发一次 (常用)
.capture // 捕获阶段触发事件
.self // 只有当event.target是当前元素时,才会触发事件处理函数
.passive // 元素默认行为立刻执行,不用等待事件处理函数执行完成触发 (移动端使用)
.stop与.self的区别
1、.stop是阻止事件冒泡,.self并没有阻止冒泡
2、.stop与.self看上去好像是相反的功能
串联顺序问题(
有些串联顺序不同效果也一样,没区别)
<div @click="show(3)">
<div @click.self.stop="show(2)">
<a href="http://www.baidu.com" @click.prevent="show(1)">百度</a>
</div>
</div>
methods: {
show(i){
console.log(i);
}
}
// 当前元素的点击事件如果是自身触发(非冒泡)就会阻止事件冒泡
@click.self.stop
// 当前元素的点击事件将阻止冒泡,如果是自身触发,将执行后面的事件处理函数
(无论是否是自身触发的点击事件,都会阻止事件冒泡)
@click.stop.self
键盘事件修饰符
键盘事件常用的有两个:keyup 和 keydown,keyup更为常用
用法
// 焦点在这个输入框时,按enter键时触发test函数
<input type="text" @keyup.enter="test" />
motheds:{
test(){
alert('已按');
}
}
为了兼容旧浏览器,Vue 提供了绝大多数常用的按键码的别名
.enter
.tab
.delete (捕获“删除”和“退格”键)
.esc
.space
.up
.down
.left
.right
.tab 比较特殊,只能配合keydown,不能配合keyup
Vue未提供别名的按键,可以使用按键原始的key去绑定(小写)
注意事项
1、不是所有元素都有键盘事件的,可绑键盘事件的元素如下:
<a>、<button>、<input>、<textarea>、<select>
2、键盘事件修饰符为按键名,用小写,遇到多个单词的按键名时,需要转换为kebab-case形式
例如:PageDown 转为 page-down @keyup.page-down="xxx"
3、每个按键都有键名和键值
键值: $event.keyCode
键名:$event.key
系统修饰键
.ctrl
.alt`
.shift
.meta
1、配合keyup使用,按下修饰键的同时,再按下其他键,然后释放其他键,事件才被触发 2、配合keydown使用,正常触发
数据绑定
是一种响应式的数据操作,摆脱传统的Dom操作
单向数据绑定
通过v-bind指令绑定,数据只能从data流向页面
双向数据绑定
通过v-model指令绑定,修改数据来让页面发生变化,还可以通过页面输入来让数据发生变化
限制:v-model只支持表单类(输入类)元素,例如
<input>、<textarea>、<select>
收集表单数据
单行与多行文本输入
使用 value 属性 和 input 事件;v-model收集的是value值,用户输入的也是value值
<input type="text" v-model="formData.username" />
<textarea v-model="formData.desc" />
new Vue({
el: "#app",
data: {
formData: {
username: '',
desc: ''
}
}
})
单选
使用 checked 属性 和 change 事件;v-model收集的是value值,并且必须给标签配置value值
<input type="radio" value="man" v-model="formData.sex" />男
<input type="radio" value="woman" v-model="formData.sex" />女
new Vue({
el: "#app",
data: {
formData: {
sex: ''
}
}
})
多选
使用 checked 属性 和 change 事件
单个复选框
当应用一个复选框时,无须指定value值,v-model绑定的是布尔值类型,选中就返回true,未选中就返回false
<input type="checkbox" v-model="formData.isAgree" /> 是否同意协议
new Vue({
el: "#app",
data: {
formData: {
isAgree: false
}
}
})
多个复选框
当应用一个复选框时,必须指定value值,v-model绑定的必须是数组类型,而且是同一个数据
<input type="checkbox" value="tw" v-model="formData.hobby" /> 跳舞
<input type="checkbox" value="cg" v-model="formData.hobby" /> 唱歌
new Vue({
el: "#app",
data: {
formData: {
hobby: []
}
}
})
总结
1、没有配置value属性,则v-model收集的是checked(选中就是true,未选中就是false,是布尔类型)
2、配置value属性
(1)如果v-model的初始值是非数组,则v-model收集的是checked(选中就是true,未选中就是false,是布尔类型)
(2)如果v-model的初始值是数组,则v-model收集的是value组成的数组
三大修饰符
- .lazy 失去焦点再收集数据
- .number 把输入字符转成数字类型
- .trim 去掉输入的首尾空格
下拉选择
使用 value 属性 和 change 事件
<select v-model="formData.birthplace">
<option disabled value="">请选择</option>
<option value="gz">广州</option>
<option value="zj">湛江</option>
<option value="sz">深圳</option>
</select>
new Vue({
el: "#app",
data: {
formData: {
birthplace: ''
}
}
})
生命周期
是什么
又名生命周期回调函数,Vue在特定时刻会自动调用一些特殊名称的函数,这些特殊名称的函数是Vue规定好的,我们不能更改,但函数内容是让我们自己编写的
流程图讲解
注意
1、生命周期函数必须使用function定义,不能使用箭头函数定义
2、生命周期函数体内this指向Vue实例对象或组件实例对象
3、destroyed函数体内,还是可以访问到销毁的Vue实例,哪怕beforeDestroy函数体内修改过Vue实例中的data的数据,都可以访问到
4、【重要】生命周期函数中,最重要的两个函数分别是:
计算属性与监听器
computed
什么叫计算属性?
拿着已有的属性进行加工和计算,然后生成全新的属性,这就是所谓计算属性
语法
computed是对象类型,而computed对象中的属性可以是函数(简化写法)或者对象(完整写法)
const app = new Vue({
el: "#app",
data: {
firstName: "张",
lastName: "三"
},
computed: {
// 简化写法(其实是get方法)
fullName(){
// 必须要有返回值,并且返回值就是fullName的值
return this.firstName + '-' + this.lastName
},
// 完整写法
fullName:{
get(){
// 必须要有返回值,并且返回值就是fullName的值
return this.firstName + '-' + this.lastName
},
set(val){
// 没有返回值
}
}
}
});
1、在计算属性的get和set方法中,this也是指向Vue实例
2、get方法什么时候调用?
(1) 初始读取计算属性时
(2) 所依赖的数据发生变化时
3、set方法什么时候调用?
(1)当计算属性给修改时
4、set方法作用
用于修改计算属性的依赖,并非修改计算属性
缓存
计算属性与方法的唯一区别就是:缓存,计算属性性能优于方法
<div id="app">
<p>{{test()}}</p>
<p>{{res}}</p>
<p>{{test()}}</p>
<p>{{res}}</p>
<p>{{test()}}</p>
<p>{{res}}</p>
<button @click="go">123</button>
</div>
<script>
const app = new Vue({
el: "#app",
data: {
num1: 1,
num2: 2
},
computed:{
res(){
console.log(1);
return this.num1 + this.num2;
}
},
methods:{
test(){
console.log(2);
return this.num1 + this.num2;
},
go(){
this.num1 = 12;
}
}
});
</script>
计算属性 和 方法 都具有
响应式,依赖一旦发生变化,两者都会自动重新调用,计算属性调用一次,而方法就会调用N次
watch
对已存在的数据进行监听
写法(2种)
第一种,写在Vue实例初始化配置中
<script>
const app = new Vue({
el: "#app",
data: {
num1: 1,
num2: 2
},
computed:{
res(){
console.log(1);
return this.num1 + this.num2;
}
},
methods:{},
watch:{
res: {
handler(val, oldVal){
alert(123);
}
}
}
});
</script>
第二种,调用Vue实例的$watch方法进行监听
<script>
const vm = new Vue({
el: "#app",
data: {
num1: 1,
num2: 2
},
computed:{
res(){
console.log(1);
return this.num1 + this.num2;
}
},
methods:{}
});
vm.$watch('res', {
handler(val, oldVal){
alert(123);
}
})
</script>
简写
如果不需要配置deep、immediate,可以使用简写
// 使用Vue初始配置
<script>
const app = new Vue({
el: "#app",
data: {
num1: 1,
num2: 2
},
computed:{
res(){
console.log(1);
return this.num1 + this.num2;
}
},
methods:{},
watch:{
res(val, oldVal){
alert(123);
}
}
});
</script>
// 使用Vue实例方法$watch
<script>
const vm = new Vue({
el: "#app",
data: {
num1: 1,
num2: 2
},
computed:{
res(){
console.log(1);
return this.num1 + this.num2;
}
},
methods:{}
});
vm.$watch('res', function(val, oldVal){
alert(123);
})
</script>
深度监听
// 监听多层级结构对象(数据属性)的某个属性的变化
const vm = new Vue({
el: "#app",
data: {
a: {
b: {
c: 12
}
}
},
watch: {
'a.b.c': {
handler(newVal,oldVal){
console.log('修改')
}
}
}
})
// 监听多层级结构对象的所有属性的变化
const vm = new Vue({
el: "#app",
data: {
a: {
b: {
c: 12
}
}
},
watch: {
'a': {
deep: true, // 不论其被嵌套多深,只要a对象里的属性值给改变就会触发监听函数
handler(newVal,oldVal){
console.log('修改')
}
}
}
})
1、第一种监听多层级数据的某个属性时,监听函数中的
newVal与oldVal的值,如果是基本数据类型就正常,否则不正常
2、第二种监听多层级数据的所有属性时,监听函数中的newVal与oldVal的值都是指向引用地址,所以oldVal失效了
watch与computed区别
1、computed能实现的功能,watch也能实现
2、watch可以实现的功能,computed不一定能实现,例如:watch能进行异步任务
3、computed的每个计算属性的get函数必须带有return,watch不需要带有return
总结
1、可监听的属性包含:数据属性、计算属性
2、当被监听的属性发生变化时,处理函数会自动调用,进行相关操作
3、监听的属性必须存在,才能够监听
4、如果监听属性的处理函数有修改本属性值的代码,将出现死循环,所以不推荐处理器中修改本属性
5、Vue默认监听对象内部属性值的变化,而Vue的watch默认不监听对象内部属性值的变化,要watch监听需要配置deep配置为true
6、使用watch监听数据时,要根据数据的具体结构来决定是否采取深度监听
过滤器
过滤器其实就是函数,用于对要显示的数据进行特定格式化后再显示(适合一些简单逻辑的处理),但计算属性、方法也同样可以实现类似功能
局部过滤器
局部过滤器只能使用在当前Vue实例中,不能使用在其他Vue实例中
// 注册
new Vue({
el: "#app",
data: {
nowday: 1222334444
},
filters: {
formatDay(value){
return value+"天";
}
}
});
// 使用
<p>{{ nowday | formatDay }}</p>
<div :title="nowday | formatDay">测试</div>
在多组件单页应用中,组件间的局部过滤器不能相互使用
全局过滤器
// 注册
Vue.filter('formatDay', function(value){
return value+"天";
});
new Vue({...})
注意:
必须在Vue实例创建前定义全局过滤器,否则无效
过滤器传参
// 使用
<p>{{ nowday | formatDay('yyds') }}</p>
// 注册
new Vue({
el: "#app",
data: {
nowday: 1222334444
},
filters: {
formatDay(value, str){
return value+"天"+str;
}
}
});
注意:
过滤器接收的第一个参数永远都是管道符前面的数据
过滤器串联
使用多个过滤器格式化数据时,后面过滤器接收的参数是前面过滤器返回的值
// 模板
<p>{{ nowday|formatDay('yyds')|formatNum }}</p>
// 语法
new Vue({
el: "#app",
data: {
nowday: 1222334444
},
filters: {
formatDay(value, str){
return value+"天"+str;
},
formatNum(value){
return "$"+value;
}
}
});
总结
1、注册过滤器2种方式:局部和全局
2、使用过滤器2种方式:插值表达式{{ xxx | 过滤器名称 }} 和 :属性名="xxx | 过滤器名称"
3、过滤器是无法改变原数据的
4、过滤器也能接收额外的参数,过滤器可以串联使用的
组件
组件化诞生历史
传统开发方式
一个页面就是:一个html文档+N个JS文件+N个CSS文件的集合
缺点:
1、依赖关系混乱(js模块化缺失),不好维护
2、代码复用率不高 (每次复用都要复制黏贴html结构,引入JS和CSS,复用很麻烦)
3、容易造成命名污染(JS还好,可以使用自执行的函数作用域来解决,但CSS就没有,只能通过定制命名规范来避免)
4、html缺少模块化
5、css缺少模块化
模块化出现
模块化最主要想体现的就是代码拆分,便于代码维护和复用
1、JS模块化出现,标志着依赖引入代码更合理,更好维护(无需顾虑引入依赖的顺序问题和命名污染问题)
2、html模块化依然没有
3、css模块化虽然语法有提供@import语法,但由于性能问题,不建议使用
组件化出现
模块化最主要想体现的就是代码封装,把页面上局部功能的代码(结构、样式、交互)和资源封装成集合,便于代码维护和复用
在模块化的基础上,组件化更进一步解决以上2个问题,把页面拆分成一个一个组件,每个组件都相互独立,互不影响,每个组件包含其独自的html、css、js代码,其中css和js都在组件私有作用域内,并不会命名污染全局
解决
1、出现css模块化(通过前端工程化工具解决)
2、出现html模块化(通过前端工程化工具解决)
3、3种语言史无前例出现大融合,出现了一个完整的整体,复用率更高,更好维护
4、引入了前端工程化的概念
第一步:创建组件
Vue组件分为2种:单文件组件和非单文件组件
非单文件组件
组件template选项的结构,最外层元素只能是一个
const school = Vue.extend({
template: `
<div>
<h1>{{name}}</h1>
</div>
`,
data(){
return {
name: "张三"
}
}
});
// 简写方式
const school = {
template: `
<div>
<h1>{{name}}</h1>
</div>
`,
data(){
return {
name: "张三"
}
}
};
单文件组件 [推荐]
创建一个以.vue为后缀的文件,该文件就是单文件组件,文件内部结构如下:
<template>
// 组件html结构,最外层元素只能是一个
</template>
<script>
// 组件数据交换js
</script>
<style>
// 组件样式
</style>
命名规范
1、单个单词使用小写(例如:school),多个单词使用横杠'-'连接小写单词(例如:my-school)
2、单个单词使用首字母大写(例如:School),多个单词使用大驼峰(例如:MySchool)
更推荐使用第二种
组件与Vue实例的区别
组件和Vue实例在创建时,都需要需要传一个配置对象,配置项基本相同,但存在以下区别:
1、组件没有el配置项,Vue实例有 (所有组件一切听从Vue实例管理与分配)
2、组件的data配置项必须是函数,Vue实例的data配置项可以是对象也可以是函数
单文件组件 vs 非单文件组件
非单文件组件缺点:
1、封装不完整,无法把css封装在组件当中
2、组件样式没有私有作用域,会产生污染
总结:实际开发必须使用单文件组件
第二步:注册组件
注册组件有2种方式:局部注册和全局注册
局部注册
// 非单文件组件创建
const a = Vue.extend({
template: `
<div>
<h1>{{name}}</h1>
</div>
`,
data(){
return {
name: "张三"
}
}
});
// 创建Vue实例来管理组件
new Vue({
el: "#app",
data: {},
// 注册组件
components: {
a // 对象属性简写,全写:a:a
}
})
全局注册
全局注册使用比较少,因为一般我们开发单页应用时,都是一个Vue实例管理所有组件的,哪怕组件再嵌套组件
// 非单文件组件创建
const a = Vue.extend({
template: `
<div>
<h1>{{name}}</h1>
</div>
`,
data(){
return {
name: "张三"
}
}
});
// 全局组件注册
Vue.component('组件名',a)
第三步:使用
和普通html标签一样写法
<div>
<student></student>
</div>
// 非单文件组件创建
const student = Vue.extend({
template: `
<div>
<h1>{{name}}</h1>
</div>
`,
data(){
return {
name: "张三"
}
}
});
// 创建Vue实例来管理组件
new Vue({
el: "#app",
data: {},
// 注册组件
components: {
student // 对象属性简写
}
})
Vue实例的模板有2种:
1、直接写在el指定的容器里
2、直接使用template配置项
Vue实例的模板只有一种:
1、单文件组件写在<template>标签内
2、非单文件组件写在template配置项
组件样式问题
命名冲突
Vue脚手架项目中,组件与组件之间存在着class命名冲突问题(后面导入的组件会覆盖前面导入的组件的相同class名的样式),所以vue-cli脚手架提供了scoped属性让样式只在组件内生效(组件局部样式)
<style scoped>
.test{
color: red;
}
</style>
CSS 预处理器
默认<style></style>标签内只能写css代码,如果需要用到Sass/Scss、Less等预编译语言,需要先安装sass-loader、less-loader等,然后书写代码如下:
<style scoped lang="scss">
</style>
插槽
让组件使用者可以向组件指定的位置插入自定义的html结构,让组件灵活性、复用性更强大,也是一种组件间的通信的方式,只适合 父组件 ==> 子组件
作用:不仅可以传数据,还可以传结构
组件插槽有以下3种:默认插槽、具名插槽、作用域插槽
默认插槽
最简单的插槽
// 父组件
<Category>
<div>xxxx</div>
</Category>
// 子组件
<template>
<div>
<slot>如果使用者没有定义插槽内容,默认就显示这句话</slot>
</div>
</template>
具名插槽
当一个组件有多个插槽时,需要使用具名插槽
// 父组件(使用者)
<Category>
<!-- 这种方式已废弃 -->
<template slot="header">
<div>xxxxx</div>
</template>
<!-- 推荐这种方式 -->
<template v-slot:footer>
<div>sssss</div>
</template>
</Category>
// 子组件
<template>
<div>
<slot name="header">如果使用者没有定义插槽内容,默认就显示这句话</slot>
<slot name="footer">如果使用者没有定义插槽内容,默认就显示这句话</slot>
</div>
</template>
推荐写法:v-slot
作用域插槽
数据在组件(定义插槽的组件)的自身,但根据数据生成的结构需要组件的使用者来决定。
// 父组件(使用者)
<Category>
<!-- 第一种方式已废弃 -->
<template scope="scopeData">
<ul>
<li v-for="item in scopeData.games" :key="item.id">{{item.name}}</li>
</ul>
</template>
<!-- 第二种方式已废弃 -->
<template slot-scope="scopeData">
<h2 v-for="item in scopeData.games" :key="item.id">{{item.title}}</h2>
</template>
<!-- 推荐这种方式 -->
<template v-slot="scopeData">
<h2 v-for="item in scopeData.games" :key="item.id">{{item.title}}</h2>
</template>
<!-- 推荐这种方式(指定插槽名称,缺省时默认是default) -->
<template v-slot:main="scopeData">
<h2 v-for="item in scopeData.books" :key="item.id">{{item.title}}</h2>
</template>
</Category>
// 子组件
<template>
<div>
<slot name="main" :books="books">如果使用者没有定义插槽内容,默认就显示这句话</slot>
<slot :games="games">如果使用者没有定义插槽内容,默认就显示这句话</slot>
</div>
</template>
作用域插槽
scope、slot-scope、'v-slot' 支持解构赋值
scope="{books}"
slot-scope="{books}"
v-slot:default="{books}"
特殊属性
$root
用于子组件获取根组件对象,可以使用$root属性,很少用,Vue单页应用根组件对象就是Vue对象(上面没数据和方法)
new Vue({
el: "#app",
data: {},
components: {
Book: {
name: 'Book',
methods: {
test(){
console.log(this.$root) // Vue对象
}
}
}
}
})
$parent
用于子组件获取父组件对象,可以使用$parent属性,很少用,因为这样操作组件间耦合度很高
new Vue({
el: "#app",
data: {},
components: {
Book: {
name: 'Book',
methods: {
test(){
console.log(this.$parent) // Vue对象
}
}
}
}
})
$children
用于父组件获取子组件对象,可以使用$children属性,很少用,因为这样操作组件间耦合度很高
(获取子组件的属性或调用子组件的方法)
数组类型,默认是空数组
new Vue({
el: "#app",
data: {},
methods: {
getSubComp(){
console.log(this.$children[0].name)
}
}
})
$refs
用于父组件获取子组件对象,使用比较多,比$children好
对象类型,默认是空对象
<div id="app">
<Header ref="aaa"></Header>
</div>
new Vue({
el: "#app",
data: {},
methods: {
getSubComp(){
console.log(this.$refs.aaa)
}
}
})
组件间通信 (重要)
组件间数据或事件的传递
父子组件
父 ==> 子
[推荐]通过props向子组件传递数据
给子组件配置props选项,有2种方式:数组方式与对象方式
// 第一种:数组方式
<script>
export default {
name: 'Test',
props: ['books']
}
</script>
/*
* 注意:
* 使用数组方式,编码简洁方便,但缺省:
* 1、数据类型校验
* 2、默认值指定
* 3、必填校验
*/
// 第二种:对象方式
<script>
export default {
name: 'Test',
props: {
// 仅限制类型写法
books: Array,
content: String,
// 完整限制写法(数据类型、默认值、是否必填),其中默认值和必填为互斥的,只能有一个
books: {
type: Array,
default: () => []
},
content: {
type: String,
required: true
},
user: {
type: Array,
default: () => ({})
}
}
}
</script>
父组件使用子组件并传值
<div>
<MyComp :books="books"></MyComp>
</div>
<script>
import MyComp from './components/MyComp.vue'
new Vue({
el: '#app',
data(){
return {
books: ['柯南','火影']
}
},
components: {
MyComp
}
})
</script>
注意
1、如果子组件props中定义的参数名是小驼峰命名的,那么父组件传值要使用短横线分隔命名,例如studentName ==> student-name
2、子组件不能直接修改props中父组件传过来的参数值,只能通知父组件,让父组件修改传过来的值
- 通过
$attrs向子组件传递数据
虽然能够实现,但$attrs更适合用于多层嵌套的祖孙组件数据通信(祖先传给子孙)
// 祖先组件
<template>
<div class="home">
<h1>{{msg}}</h1>
<HelloWorld :msg="msg" class="wt-container" style="font-size: 30px;"/>
</div>
</template>
// 中间组件
<template>
<div class="hello">
<Sub v-bind="$attrs"></Sub>
</div>
</template>
// 目标子孙组件
this.$attrs
注意:
1、父组件传过来,不作为props给识别的数据,(除开class与style) 2、v-bind="$attrs"中v-bind不能缩写
- 通过
$refs或者$children向子组件传递数据
原理:获取子组件对象,然后直接操作子组件对象上的data属性或直接调用motheds上的方法
这种方式实际开发中使用相对少点
子 ==> 父
[推荐]通过自定义事件向父组件传递数据
子组件定义并发射自定义事件
// 调用组件对象的$emit方法来发射自定义事件
this.$emit('custem-event', args)
父组件监听并处理自定义事件
<ChildComp @custem-event="add"></ChildComp>
import ChildComp from './components/ChildComp.vue'
<script>
new Vue({
methods: {
add(params){
console.log(params); // 子组件传过来的参数
}
},
components: {
ChildComp
}
})
</script>
注意
1、自定义事件名称,推荐使用"短横线分隔命名"进行命名
2、自定义事件是否会冒泡??? 答案:Vue 自定义事件没有冒泡机制,也无需使用事件修饰符.stop,只有在父组件中子组件标签上能监听自定义事件,因此也不会有命名冲突问题
- 通过
props向父组件传递数据
原理:利用props能传函数类型的特性
把父组件定义的函数传给子组件,父组件代码如下
<template>
<div class="home">
<h1>{{msg}}</h1>
<HelloWorld :goChange="goChange" />
</div>
</template>
<script>
import HelloWorld from '@/components/HelloWorld.vue'
export default {
name: 'Home',
data(){
return {
msg: '你好'
}
},
components: {
HelloWorld
},
methods:{
goChange(msg){
this.msg = msg.title;
}
}
}
</script>
子组件直接调用传过来的函数并带上数据
<template>
<div class="hello-world">
<button @click="send">调用父函数传参</button>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
goChange: Function
},
methods:{
send(){
this.goChange({
title: '我是标题'
})
}
}
}
</script>
- 通过
$parent向父组件传递数据
简单粗暴,直接修改父组件data中属性或者直接调用父组件mothed中方法
兄弟组件
[推荐]通过Vuex实现
Vuex是Vue的一个官方插件,专门用于任意组件间通信。
好处:使用简单,便于后期维护,几乎所有Vue单页应用必备
学习请看链接:前端学习日记 # Vuex
- 通过
EventBus(全局事件总线)实现
原理:借助Vue根实例的API:$on、$once、$off、$emit,通过自定义事件进行任意组件通信
不适合中大型项目中使用,造成难以维护
注意:
1、$on、$once在哪个生命周期都可以使用,但推荐在mounted()中使用
2、记得在beforeDestroy或destroyed生命周期函数中使用$off解绑自定义事件
组件A
mounted() {
this.$root.$on('go-come', function(d) {
console.log(d);
});
}
组件B
methods: {
send() {
this.$root.$emit('go-come', '我调用了')
}
}
待解决问题:
1、多人协助时,事件名命名冲突问题
2、当同一个组件在同一个界面被多次复用,那该组件上使用$on监听自定义事件重复注册同一个事件问题
[不推荐]通过消息订阅与发布实现
需要数据的人订阅消息,提供数据的人发布消息
原理:大致上和全局事件总线的实现原理大同小异,更推荐直接使用全局事件总线,因为vuedevTools能监控触发的自定义事件
现在已经有很多实现消息订阅与发送的第三方库,推荐 **pubsub-js **库(可用于Vue、react、anglerJs)
// 安装
npm i pubsub-js
祖先后代组件
[推荐]通过Vuex实现
可追溯,可调试,但只适合应用用,不适合开发组件库使用
- 通过
$attrs/$listeners实现
上面"父子组件"中,已经介绍过$attrs属性的用法,它可以把数据从父-->子-->孙
下面介绍$listeners属性的用法,它可以把事件从孙-->子-->父,顺序和$attrs刚好相反
由于自定义事件没有冒泡机制,所有要通过$listeners属性来实现
// 孙组件
<template>
<div class="sub">
<p>我是孙子</p>
<button @click="getAttrs">获取$attrs</button>
</div>
</template>
<script>
export default {
name: 'Sub',
methods: {
getAttrs() {
this.$emit('top-event', '来自Sub的事件');
}
}
}
</script>
// 子组件
<template>
<div class="hello">
<p>
For a guide and recipes on how to configure / customize this project,<br>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<h3>Installed CLI Plugins</h3>
<Sub v-on="$listeners"></Sub>
</div>
</template>
<script>
import Sub from './Sub.vue'
export default {
components:{ Sub },
name: 'HelloWorld'
}
</script>
// 父组件
<template>
<div class="home">
<h1>{{msg}}</h1>
<HelloWorld :msg="msg" class="wt-container" style="font-size: 30px;" @top-event="test"/>
</div>
</template>
<script>
import HelloWorld from '@/components/HelloWorld.vue'
export default {
name: 'Home',
data(){
return {
msg: '你好'
}
},
components: {
HelloWorld
},
methods:{
test(msg){
this.msg = msg;
}
}
}
</script>
总结:
1、v-on="$listeners"中v-on不能缩写
2、与props相比,写法简化了些,但如果组件层级比较多,还是很麻烦,所以不太适合组件层级过多的使用
- 通过
provide/inject实现
provide 可以在祖先组件中指定我们想要提供给后代组件的数据或方法,而在任何后代组件中,我们都可以使用 inject 来接收 provide 提供的数据或方法。
// 父组件或祖先组件
<script>
import HelloWorld from '@/components/HelloWorld.vue'
export default {
name: 'Home',
data(){
return {}
},
provide(){
return {
bookName: "我来自Home的provide"
}
},
components: {
HelloWorld
}
}
</script>
// 子组件或后代组件
<script>
export default {
name: 'Sub',
inject: ['bookName'],
methods: {
getAttrs() {
console.log(this,this.bookName);
}
}
}
</script>
注意:
1、provide和inject绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的。
2、编码方便优雅,只要在祖先组件上声明提供的数据,在任意后代组件上都可以获取,无需逐层传递的代码
使用 provide/inject 做全局状态管理
// 根组件提供将自身提供给后代组件
export default {
provide() {
return {
app: this
}
},
data() {
return {
text: 'bar'
}
}
}
// 后代组件注入 'app'
<template>
<div>{{this.app.text}}</div>
</template>
<script>
export default {
inject: ['app'],
created() {
this.app.text = 'baz' // 在模板中,显示 'baz'
}
}
</script>
也许有的同学会问:使用 $root 依然能够取到根节点,那么我们何必使用 provide/inject 呢?
在实际开发中,一个项目常常有多人开发,每个人有可能需要不同的全局变量,如果所有人的全局变量都统一定义在根组件,势必会引起变量冲突等问题。
使用 provide/inject 在不同模块的入口组件传给各自的后代组件可以完美的解决该问题。以最近的组件优先级最高
虽然
provide/inject能做全局状态管理,但慎用,因为最好做全局状态管理的是Vuex
provide/inject 最适合编写组件
使用 provide/inject 做组件开发,是 Vue 官方文档中提倡的一种做法。
以我比较熟悉的 elementUI 来举例:
在 elementUI 中有 Button(按钮)组件,当在 Form(表单)组件中使用时,它的尺寸会同时受到外层的 FormItem 组件以及更外层的 Form 组件中的 size 属性的影响。
如果是常规方案,我们可以通过 props 从 Form 开始,一层层往下传递属性值。看起来只需要传递传递两层即可,还可以接受。但是,Form 的下一层组件不一定是 FormItem,FormItem 的下一层组件不一定是 Button,它们之间还可以嵌套其他组件,也就是说,层级关系不确定。如果使用 props,我们写的组件会出现强耦合的情况。
provide/inject 可以完美的解决这个问题,只需要向后代注入组件本身(上下文),后代组件中可以无视层级任意访问祖先组件中的状态。
跨层级组件
-
[推荐]通过Vuex实现 -
通过
EventBus(全局事件总线)实现 -
通过
消息订阅与发布实现 -
通过
provide/inject实现
注意点
1、组件命名
1、一个单词
第一种写法(首字母小写):school
第二种写法(首字母大写):School (推荐)
2、多个单词
第一种写法(kebab-case命名法):my-school
第二种写法(大驼峰命名法):MySchool (推荐,需要vue脚手架支持)
注意:
1、组件名必须避开html标签名称
2、组件可以使用name配置项指定组件在调试工具上显示的名称
2、组件标签
1、第一写法:<school></school>
2、第二写法(自闭合):<school/> (需要vue脚手架支持)
特殊API
nextTick
用途:回调函数延迟到下次Dom更新之后执行
vm.$nextTick(fn) 与 Vue.nextTick(fn)区别
1、$nextTick是局部的(使用较多),nextTick是全局的(使用较小)
2、$nextTick的回调函数中this指向当前vm/vc,而nextTick的回调函数中this指向window
<div id="app">
<p ref="title">{{ msg }}</p>
<button @click="changeMsg">更改</button>
</div>
<script>
new Vue({
el: '#app',
data: {
msg: '你好呀'
},
methods:{
changeMsg(){
this.msg = "我改过自身";
alert(this.$refs.title.innerHTML) // '你好呀'
this.$nextTick(function(){
alert(this.$refs.title.innerHTML) // '我改过自身'
})
}
}
});
Vue.nextTick(function(){
console.log(this); // window
})
</script>
其实无论是否有Dom更新,最后都会延迟执行nextTick中的回调函数,所以
nextTick实现原理因该是setTimeout
过渡与动画
实现原理:Vue 在插入、更新或者移除 DOM 时,在合适的时机给元素添加样式类名
无论是v-if还是v-show,都可以使用Vue过渡与动画
过渡与动画的区别
动画是过渡的超集,动画更加强大,能实现更加炫酷的效果,而过渡相对动画就简单点,因为过渡只要指定2端的效果就可以了
图示
总共有6个Class类名
// 元素进入的类名
v-enter:进入的起点
v-enter-active:进入的过程
v-enter-to:进入的终点
// 元素离开的类名
v-leave:进入的起点
v-leave-active:进入的过程
v-leave-to:进入的终点
2个内置组件
transition
使用<transition>包裹要过渡或动画的元素,元素只能有一个,可以配置name属性
transition-group
使用<transition-group>包裹要过渡或动画的元素,元素可以是多个,但每个元素都要指定key值
注意事项
1、如果数据对象data是对象时,数据对象data中的属性,如果使用this,this指代是window
{{title}} // 'undefined'
{{testThis}} // '123'
var title = "123";
data: {
title: 'undefined',
testThis: this.title // 这里的this指代window
}
2、所有Vue管理的函数都不能用箭头函数定义,必须用function定义,否则this不是指向Vue实例,而是window,下面是Vue管理的函数
1、methods中定义的函数
2、事件处理函数
3、计算属性中get和set函数(包括简写)
4、watch中的监听函数(包括简写)
3、data、computed中的数据发生变化,只要有用到,Vue将重新渲染模板
4、开发单页应用时,需要修改第三方UI组件(例如:ElementUI组件)或自定义组件的 默认样式 时,何时直接修改,何时需要用到::v-deep深度作用选择器呢?
深度作用选择器有3种
1、>>>
2、/deep/
3、::v-deep (推荐)
当父组件中用到子组件,并且我要在作用域样式(<style scoped lang="scss">)中修改子元素的默认样式时,
子元素的跟元素会有2种属性标识,一种是父组件实例唯一标识,一种是本组件实例唯一标识
如果只需要修改子组件中根元素的样式,直接修改就可以
<style scoped lang="scss">
.parentClass{
.childClass{}
}
</style>
编译后
<style scoped lang="scss">
.parentClass[data-v-0945424]{}
.parentClass .childClass[data-v-0945424]{}
</style>
如果需要修改子组件中非根元素的样式,那必须使用深度选择器::v-deep
<style scoped lang="scss">
.parentClass{
::v-deep .childNotRootClass{}
}
</style>
编译后
<style scoped lang="scss">
.parentClass[data-v-0945424]{}
.parentClass[data-v-0945424] .childNotRootClass{}
</style>
原因:子组件的非根元素只有本组件唯一标识的属性,而没有携带父组件唯一标识的属性,所以要使用深度作用选择器