前奏:
关于vue中,vscode实用的插件: vetur和vue vscode snippets
Vue的设计思想
Vue的设计思想有以下:
-
数据驱动应用
-
MVVM模式的践行者
MVVM框架的三要素:响应式、模板引擎及其渲染
响应式:vue如何监听数据变化?
模版:vue的模版如何编写和解析?
渲染:vue如何将模板转换为html?
安装
引入vue
使用
每个 Vue 应用都是通过用 Vue
函数创建一个新的 Vue 实例
开始的。
创建第一个vue程序:显示hello world
// html中
<!-- 根节点(宿主容器) -->
<div id="app">
<!-- Mustache语法不能作用在属性上,应该使用v-bind指令 -->
<h2 v-bind:title='title'> // 可以直接简写为:title='xxx'
<!-- 插值文本: Mustache语法(双大括号)来进行数据绑定 -->
{{title}} // 使用的是data中返回的数据对象中的title属性
</h2>
</div>
<script src="vueJs所在路径"></script>
<script>
// 创建vue实例
const vm = new Vue({
el: '#app', // 提供一个在页面上已存在的DOM元素作为Vue实例的挂载目标。可以是CSS选择器,也可以是HTMLElement实例。(挂载到上面html中id为app的容器中)
data() { // Vue实例的数据对象
return {
title: 'hello, vue!'
}
},
});
// 使用实例对象vm
setTimeout(() => {
vm.title = '看,我变了'
}, 1000);
</script>
Mustache表达式
-
语法:
{{ 表达式 }} // 表达式:变量或能够输出唯一结果的。 注意:if else之类的不是
-
作用: 把动态数据直接输出渲染到html中
-
用法:
// html中 <!-- 根节点(宿主容器) --> <div id="app"> <h2> <!-- 插值文本: Mustache语法(双大括号)来进行数据绑定 --> {{title}} // 使用的是data中返回的数据对象中的title属性 </h2> </div> <script src="vueJs所在路径"></script> <script> // 创建vue实例 new Vue({ el: '#app', // 提供一个在页面上已存在的DOM元素作为Vue实例的挂载目标。可以是CSS选择器,也可以是HTMLElement实例。(挂载到上面html中id为app的容器中) data() { // Vue实例的数据对象 return { title: 'hello, vue!' } }, }); </script>
指令
概念: 写在标签上的一种,以 “v-” 开头的自定义属性。一些指令能够接收一个“参数”,在指令名称之后以冒号表示。
作用: 帮助我们操作html,动态的获取或者渲染数据。
v- bind
v-bind
指令可以用于响应式地更新HTMLattribute。
HTML特性不能用Mustache 语法,应该使用v-bind指令。
// html中
<div id="app">
<!-- Mustache语法不能作用在特性、属性值上,应该使用v-bind指令 -->
<h2 v-bind:title='title'> // 可以直接简写为:title='xxx'
{{title}}
</h2>
</div>
<script src="vueJs所在路径"></script>
<script>
new Vue({
el: '#app',
data() {
return {
title: 'hello, vue!'
}
},
});
</script>
class与style绑定
操作元素的 class 列表和内联样式是数据绑定的一个常见需求。因为它们都是属性,所以我们可以用 v-bind 处理它们:只需要通过表达式计算出字符串结果即可。不过,字符串拼接麻烦且易错。因此,在将 v-bind用于 class 和 style 时,Vue.js 做了专门的增强。表达式结果的类型除了字符串之外,还可以是对象或数组。
使用:
<style> .active { background-color: red; } </style>
<ul>
<!-- class绑定 -->
<li v-for="item in goodList"
:class="{active: (selected === item)}"
@click="selected = item">{{item}}</li>
<!-- style绑定 -->
<!-- <li v-for="item in goodList"
:style="{backgroundColor: (selected === item)?'#ddd':'transparent'}" @click="selectedCourse = item">{{item}}</li> -->
</ul>
<script>
new Vue({
data: {
// 保存选中项
selected: '',
goodList: ['花生','瓜子','啤酒']
},
}) </script>
v-for
作用: 用于循环渲染,我们可以用 v-for 指令循环一个数组或者对象。
语法:
<标签 v-for="(值,索引) in 要循环的源数据数组"></标签>
<标签 v-for="(值,键名, 索引) in 要循环的源数据对象"></标签>
用法:
// html中
<div id="app">
<ul>
<li v-for="item in goodList">{{item}}</li>
</ul>
</div>
<script src="vueJs所在路径"></script>
<script>
new Vue({
el: '#app',
data() {
return {
goodList: ['花生','瓜子','啤酒']
}
},
});
</script>
注意:在js中for...in
循环,只能获得对象的键名(数组中就是索引),不能直接获取键值。但是在vue中使用v-for... in
和v-for... of
去循环数组,第一个参数都是获取的数组元素。但是官方有一句话:你也可以用 of
替代 in
作为分隔符,因为它更接近 JavaScript 迭代器的语法。
v-model
v-model 本质上是语法糖。它将转换为输入事件以更新数据,并对一些极端场景进行一些特殊处理。也就是说可以让data中的数据,和表单的数据双向绑定。但是需要注意的是只能用于表单元素。
<!-- 表单元素输入绑定 -->
<input v-model="good" type="text"/>
<span>{{good}}</span>
<script src="vueJs所在路径"></script>
<script>
new Vue({
el: '#app',
data() {
return {
goodList: ['花生','瓜子','啤酒'],
good: ''
}
},
});
</script>
v-on
我们可以使用 v-on 指令监听 DOM 事件,并在触发时运行一些 JavaScript 代码。
语法:
<标签 v-on:事件类型="表达式或函数名或函数名()"></标签>
例如:
<标签 v-on:click="count++"></标签>
<标签 v-on:click="increase"></标签>
<标签 v-on:click="increase(实参)"></标签>
// v-on可以简写为@
<标签 @事件类型="表达式或函数名或函数名()"></标签>
使用:
<!-- 表单元素输入绑定 -->
<input v-model="good" type="text" v-on:keydown.enter="addGood"/>
<button @click="addGood">添加商品</button>
<script src="vueJs所在路径"></script>
<script>
new Vue({
el: '#app',
data() {
return {
goodList: ['花生','瓜子','啤酒'],
good: ''
}
},
methods: {
addGood() {
this.goodList.push(this.good);
}
},
});
</script>
v-if
v-if
指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回 truthy 值的时候被渲染。
作用: 控制元素的显示渲染逻辑。
概念: 逻辑和JS是一样的,从上往下,找到第一个满足条件的渲染,其他的不渲染。
<div v-if="Math.random() > 0.5">
Now you see me
</div>
<div v-else-if="Math.random() > 0.3"> // v-else-if 必须紧跟在带v-if或者v-else-if的元素之后。
Now you can see me
</div>
<div v-else> // v-else元素必须紧跟在带v-if或者v-else-if的元素的后面,否则它将不会被识别。
Now you don't
</div>
因为 v-if
是一个指令,所以必须将它添加到一个元素上。但是如果想切换多个元素呢?此时可以把一个<template>
元素当做不可见的包裹元素,并在上面使用 v-if
。最终的渲染结果将不包含 <template>
元素。
<template v-if="ok">
<h1>Title</h1>
<p>Paragraph 1</p>
<p>Paragraph 2</p>
</template>
v-show
另一个用于根据条件展示元素的选项是 v-show
指令。用法大致一样:
<h1 v-show="ok">Hello!</h1>
注意,v-show
不支持 <template>
元素。
v-if
vs v-show
v-if
也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
v-show
就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。(v-show
只是简单地切换元素的display,所以 v-show
的元素始终会被渲染并保留在 DOM 中)。
一般来说,v-if
有更高的切换开销,而 v-show
有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 v-show
较好;如果在运行时条件很少改变,则使用 v-if
较好。
v-if
vs v-for
当 v-if
与 v-for
一起使用时,v-for
具有比 v-if
更高的优先级。所以不推荐在同一元素上同时使用 v-if
和 v-for
。
当它们处于同一节点,v-for
的优先级比 v-if
更高,这意味着 v-if
将分别重复运行于每个 v-for
循环中。
当你只想为部分项渲染节点时,这种优先级的机制会十分有用。
// 只渲染未完成的 todo。
<li v-for="todo in todos" v-if="!todo.isComplete">
{{ todo }}
</li>
如果你的目的是有条件地跳过循环的执行,那么可以将 v-if
置于外层元素上 (如果没有父级,那么可以在外层加一个 template
标签。同样地,template也不会渲染出来) 。
<ul v-if="todos.length">
<li v-for="todo in todos">
{{ todo }}
</li>
</ul>
计算属性和监听器
模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的。在模板中放入太多的逻辑会让模板过重且难以维护,此时就可以考虑计算属性和监听器。
计算属性computed
概念:computed的写法和methods一样,也是一个函数,但是computed中的函数,必须有一个返回值。而且这个函数的名字,就可以拿到返回值。
<p>
<!-- 计算属性 -->
商品总数:{{total}}
</p>
<script src="vueJs所在路径"></script>
<script>
new Vue({
el: '#app',
data() {
return {
goodList: ['花生','瓜子','啤酒'],
}
},
computed: {
total() {// 和上面使用计算属性total,名字保存一致
// 一般把比较复杂的逻辑计算写在这里 最终返回结果
return this.goodList.length + '种'
}
}
});
</script>
计算属性computed,有依赖缓存,只要所依赖的数据发生变化,才会重新计算,否则,直接使用缓存,页面不会重新渲染,性能之高,令人发指。
监听器(侦听器)
<p>
<!-- 监听器 -->
课程总数:{{totalCount}}
</p>
<script src="vueJs所在路径"></script>
<script>
new Vue({
el: '#app',
data() {
return {
goodList: ['花生','瓜子','啤酒'],
totalCount: 0
}
},
// 这种不能生效,因为初始化时不会触发
// watch: {
// goodList(newValue, oldValue) {
// this.totalCount = newValue.length + '种'
// }
// },
watch: {
goodList: { // 监控goodList,值发生变化时,才执行
immediate: true, // 立即执行一次
// deep: true, // 深层次监听,例如数组对象
handler(newValue, oldValue) {
this.totalCount = newValue.length + '种'
}
}
},
});
</script>
计算属性 vs 监听器
监听器更通用,理论上计算属性能实现的监听器也能实现。
处理数据的场景不同,监听器适合一个数据影响多个数据,计算属性适合一个数据受多个数
据影响。
计算属性有缓存性,计算所得的值如果没有变化不会重复执行。
监听器适合执行异步操作或较大开销操作的情况。
组件
定义:组件是可复用的 Vue 实例,准确讲它们是VueComponent的实例,继承自Vue。
优点:组件化可以增加代码的复用性、可维护性和可测试性。
使用场景:
-
通用组件:实现最基本的功能,具有通用性、复用性,例如按钮组件、输入框组件、布局组件等。
-
业务组件:它们完成具体业务,具有一定的复用性,例如登录组件、轮播图组件。
-
页面组件:组织应用各部分独立内容,需要时在不同页面组件间切换,例如列表页、详情页组件
如何使用组件:
- 定义:Vue.component(),components选项,sfc
- 分类:有状态组件,functional,abstract
- 通信:props,$emit()/$on(),provide/inject,$children/$parent/$root/$attrs/$listeners
- 内容分发:
<slot>
,<template>
,v-slot
- 使用及优化:is,keep-alive,异步组件
组件的本质:
vue中的组件经历如下过程: 组件配置 => VueComponent实例 => render() => Virtual DOM=> DOM
所以组件的本质是产生虚拟DOM。
需要注意的是,组件名、prop和自定义事件时,尽量使用‘羊肉串’的格式。特别是自定义事件,v-on
事件监听器在 DOM 模板中会被自动转换为全小写 (因为 HTML 是大小写不敏感的)。所以例如 v-on:myEvent
将会变成 v-on:myevent
,所以如果你使用 myEvent
事件句柄就不可能被监听到。因此推荐使用羊肉串格式:@my-event
事件名。
组件组册
之前的代码是这样的:
<div id="app">
<ul>
<li v-for="item in goodList"
:class="{active: (selected === item)}"
@click="selected = item">{{item}}</li>
</ul>
</div>
<script src="vue.js"></script>
<script>
new Vue({
el: '#app',
data() {
return {
selected: '',
goodList: ['花生','瓜子','啤酒'],
}
},
});
</script>
我们想把列表渲染,提出去成一个组件,那么我们怎么做呢。
<div id="app">
<!-- 组件列表 -->
<!-- goodList是父组件的数据对象中的属性, goodlist是子组件中用于接收数据的字段,在props中去设置类型和默认值和接收 -->
<good-list :goodlist='goodList'></good-list>
</div>
<script src="vue.js"></script>
<script>
Vue.component('good-list',{
data(){
return {
selected: '', // 这个属性是这个组件自己的,所以就从父组件中提到这个组件来维护
}
},
props: {
// 一定要注意,如果上面写:goodList='goodList',这儿用goodList接收不到,还是要使用goodlist
// 因为 HTML 是大小写不敏感的,会被自动转换为全小写
'goodlist': {
type: Array,
default: []
}
},
template: `
<ul>
<!-- class绑定 -->
<li v-for="item in goodlist"
:class="{active: (selected === item)}"
@click="selected = item">{{item}}</li>
</ul>
`
});
new Vue({
el: '#app',
data() {
return {
goodList: ['花生','瓜子','啤酒'],
}
},
})
</script>
自定义事件及其监听
新增时,原本的代码是:
<input v-model="good" type="text" v-on:keydown.enter="addGood"/>
<button @click="addGood">添加商品</button>
<ul>
<li v-for="item in goodList">{{item}}</li>
</ul>
<script src="vue.js"></script>
<script>
new Vue({
el: '#app',
data() {
return {
goodList: ['花生','瓜子','啤酒'],
good: ''
}
},
methods: {
addGood() {
this.goodList.push(this.good);
}
},
});
</script>
把新增功能提出去成组件之后:
<div id="app">
<good-add @good-add-handle='addGood'></good-add>
<ul>
<li v-for="item in goodList">{{item}}</li>
</ul>
</div>
<script src="vue.js"></script>
<script>
Vue.component('good-add',{
data(){
return {
good: '', // 将good从父组件提取到自己去维护
}
},
template: `
<div>
<input v-model="good" type="text" v-on:keydown.enter="add"/>
<button @click="add">添加商品</button>
</div>
`,
methods: {
add(){
// 发送自定义事件通知父组件新增商品(派发事件)
// 注意事件名称定义时不要有大写字母出现
this.$emit('good-add-handle', this.good);
}
}
})
new Vue({
el: '#app',
data() {
return {
goodList: ['花生','瓜子','啤酒'],
}
},
methods: {
addGood(good) { // 接收子组件传递过来的参数 然后维护goodList
this.goodList.push(good);
}
},
});
</script>
组件实现v-model
v-model默认转换是:value和@input,如果想要修改这个行为,可以通过定义model选项。
Vue.component('good-add',{
model: {
prop: 'value',
event: 'change'
},
})
代码如下:
<div id="app">
<!-- 子组件v-model的是父组件中的good状态 -->
<!-- 组件支持v-model需要实现内部input的:value和@input -->
<good-add v-model='good' @good-add-handle='addGood'></good-add>
<ul>
<li v-for="item in goodList">{{item}}</li>
</ul>
</div>
<script src="vue.js"></script>
<script>
Vue.component('good-add',{
model: {
prop: 'value',
event: 'change'
},
// 接收父组件传递value,不需要额外维护good了
props: {
value: String,
default: ''
},
template: `
<div>
<input
@change="$emit('change', $event.target.value)"
:value=value
type="text"
@keydown.enter='add'
/>
<button @click="add">添加商品</button>
</div>
`,
methods: {
add(){
// 派发事件不再需要传递数据
this.$emit('good-add-handle');
}
}
})
new Vue({
el: '#app',
data() {
return {
goodList: ['花生','瓜子','啤酒'],
good: '' // good是父组件来维护
}
},
methods: {
addGood() {
this.goodList.push(this.good); // 拿自己的good,因为子组件绑定的就是自己的这个属性
}
},
});
</script>
slot
通过插槽分发内容,也就是说子组件预留位置,让父组件在使用子组件的时候,可以把数据插入进去。
通过使用vue提供的 slot
可以给组件传递内容.
<div id="app">
<!-- 只是传递true测试时,需要加上v-bind代表表达式,否则会当成字符串 -->
<!-- <message :show="true">新增成功!!</message> -->
<!--
方式1: 声明一个closeMessage方法去修改状态
<message @close='closeMessage' :show="isShowMessgae">新增成功!!</message>
-->
<!-- 方式2: 直接修改isShowMessgae状态 -->
<message @close='isShowMessgae=$event' :show="isShowMessgae">新增成功!!</message>
<good-add v-model='good' @good-add-handle='addGood'></good-add>
<good-list :goodlist='goodList'></good-list>
</div>
<script src="vue.js"></script>
<script>
// 弹窗
Vue.component('message',{
props: {
show: {
type: Boolean,
default: false
}
},
template: `
<div class='message-box' v-if='show'>
<!--通过slot获取传入内容-->
<!--slot作为占位符(占坑位)-->
<slot></slot>
<span class='message-box-close' @click='$emit("close",false)'>X</span>
</div>
`
})
// 新增商品
Vue.component('good-add',{
model: {
prop: 'value',
event: 'change'
},
// 接收父组件传递value,不需要额外维护good了
props: {
value: String,
default: ''
},
template: `
<div>
<input
@change="$emit('change', $event.target.value)"
:value=value
type="text"
@keydown.enter='add'
/>
<button @click="add">添加商品</button>
</div>
`,
methods: {
add(){
// 派发事件不再需要传递数据
this.$emit('good-add-handle');
}
}
})
//商品列表
Vue.component('good-list',{
data(){
return {
selected: '', // 这个属性是这个组件自己的,所以就从父组件中提到这个组件来维护
}
},
props: {
// 一定要注意,如果上面写:goodList='goodList',这儿用goodList接收不到,还是要使用goodlist
// 因为 HTML 是大小写不敏感的,会被自动转换为全小写
'goodlist': {
type: Array,
default: []
}
},
template: `
<ul>
<!-- class绑定 -->
<li v-for="item in goodlist"
:class="{active: (selected === item)}"
@click="selected = item">{{item}}</li>
</ul>
`
});
new Vue({
el: '#app',
data() {
return {
goodList: ['花生','瓜子','啤酒'],
good: '',
isShowMessgae: false, // 控制弹窗是否显示
}
},
methods: {
addGood() {
this.goodList.push(this.good);
this.isShowMessgae = true; // 新增成功时,打开弹窗
},
closeMessage($evnet){
this.isShowMessgae = $evnet; // 点击弹窗组件中的X时,父组件修改状态为false
}
},
});
</script>
.sync 修饰符
双向绑定会带来维护上的问题,因为子组件可以变更父组件,且在父组件和子组件都没有明显的变更来源。因此官方推荐使用update:xxxx
的模式触发事件取而代之。
因此,上面的代码修改两处:
<message @update:show='isShowMessgae=$event' :show="isShowMessgae">新增成功!!</message>
// js 弹框组件message -> template中(弹框的X)
<span class='message-box-close' @click='$emit("update:show",false)'>X</span>
官方为了方便起见,为这种模式提供一个缩写,即 .sync
修饰符。
在上面的基础上,修改html中message组件:
<message :show.sync="isShowMessgae">新增成功!!</message>
注意: js 弹框组件message -> template中,依然且必须是@click='$emit("update:show",false)
<span class='message-box-close' @click='$emit("update:show",false)'>X</span>
具名slot
如果存在多个独立内容要分发,可以使用具名插槽v-slot:name
(可以简写为#name)
// html中 修改组件message
<message :show.sync="isShowMessgae">
<template v-slot:title>
<strong>
恭喜你,
</strong>
</template>
<!-- 当不写v-slot时,其实默认v-slot值为default <template v-slot:default>新增成功!!</template> -->
<template>新增成功!!</template>
</message>
// js中 message组件 -> template
template: `
<div class='message-box' v-if='show'>
<!--通过slot获取传入内容-->
<!--具名插槽-->
<slot name="title"></slot>
<!--slot作为占位符(占坑位)-->
<slot></slot>
<span class='message-box-close' @click='$emit("update:show",false)'>X</span>
</div>
`
插槽总结
匿名插槽:
// 父组件 【使用插槽的时候 父组件必须是双标签】
<子组件标签>我是要插入插槽的内容</子组件标签>
// 子组件
<template>
<div>
<h1>我是子组件</h1>
<div class="content">
<slot><slot /> // 插槽: 预留位置 将来父组件把内容插入此位置。
</div>
</div>
</template>
具名插槽:
// 父组件
<Son>
<p slot="header">我是头部内容 aaa</p>
<p slot="main">我是主体内容 bbb</p>
<p slot="footer">我是尾部内容 ccc</p>
</Son>
// 子组件
<!-- 具名插槽 -->
<div class="header">
<slot name="header" />
</div>
<div class="main">
<slot name="main" />
</div>
<div class="footer">
<slot name="footer" />
</div>
作用域插槽:
// 父组件
<p slot="box" slot-scope="scope">
我是外部数据:
<br />
{{ scope.msg }}
{{ scope.news }}
</p>
// 子组件
<div class="box">
<slot name="box" :msg="sonMsg" :news="news" />
</div>
export default {
data() {
return {
sonMsg: "我是son数据",
news: "我是新闻",
};
},
};
v-slot:
// 父组件
<!-- 指令v-slot -->
<template #header>
<p>header 哈哈哈</p>
</template>
<template #footer="scope">
<p>footer 嘻嘻嘻 : {{ scope.news }}</p>
</template>
// 子组件
<!-- v-slot -->
<div class="header">
<slot name="header" />
</div>
<div class="footer">
<slot name="footer" :news="news" />
</div>
组件通信总结
概念: 组件通信就是组件之间相互传递数据
常见的方式:
-
父传子
-
子传父
-
bus
-
vuex状态机
父传子
// 父组件
<子组件标签 属性名1="属性值" :属性名2="动态数据"></子组件标签>
// 子组件
export default {
props: ['属性名1', '属性名2'] // 数组格式
// 封装组件 使用对象写法 更严谨
props: {
属性名1: {
type: String, // 限制类型 Number Array Object Boolean
default: 'a', // 默认值
required: true, // 必填
},
属性名2: {
}
}
}
子传父
// 父组件
<子组件标签 @自定义事件="函数名"></子组件标签>
export default {
methods: {
函数名(data) {
// data就是子组件传递过来的数据
}
}
}
// 子组件
export default {
methods: {
toFa() {
// 把数据传递给父组件
this.$emit('自定义事件', 数据)
}
}
}
bus
// 1. 在main.js入口文件中, 创建一个Vue的空的实例对象,挂载在Vue的原型上
Vue.prototype.$bus = new Vue()
// 2. 在 A 组件把数据传递出去
export default { // new Vue({})
methods: {
sendMsg() {
this.$bus.$emit('自定义事件', 数据)
}
}
}
// 3. 在 B 组件 接收数据
export default {
created() {
this.$bus.$on('自定义事件', (data) => {
// data就是接收到的数据
})
}
}
必会API
数据相关API
Vue.set
向响应式对象中添加一个属性,并确保这个新属性同样是响应式的,且触发视图更新。
使用方法: Vue.set(target, propertyName/index, value)
在之前例子上改造,增加一个批量修改价格功能:
<div id="app">
<good-add @good-add-handle='addGood'></good-add>
<!-- 批量修改价格功能 -->
<div>
<!-- 加上.number修饰符,得到的值就是number类型,否则默认得到字符串类型 -->
<input v-model.number="price">
<button @click="batchUpdatePrice">批量更新价格</button>
</div>
<ul>
<li v-for="item in goodList">{{item.name}} - ¥{{item.price}}</li>
</ul>
</div>
<script src="vue.js"></script>
<script>
Vue.component('good-add',{
data(){
return {
good: '', // 将good从父组件提取到自己去维护
}
},
template: `
<div>
<input v-model="good" type="text" v-on:keydown.enter="add"/>
<button @click="add">添加商品</button>
</div>
`,
methods: {
add(){
// 发送自定义事件通知父组件新增商品(派发事件)
// 注意事件名称定义时不要有大写字母出现
this.$emit('good-add-handle', this.good);
}
}
})
new Vue({
el: '#app',
data() {
return {
goodList: [{name: '花生'},{name: '瓜子'},{name: '啤酒'}],
price: 0
}
},
methods: {
addGood(good) { // 接收子组件传递过来的参数 然后维护goodList
// this指向的是实例对象
// 需要注意 数组需要变异方法,才能被检测更新
this.goodList.push({name: good});
// Vue 不能检测以下数组的变动: 1. 当你利用索引直接设置一个数组项时 2.当你修改数组的长度时
// this.goodList[this.goodList.length] = this.good; // 不会更新视图
// console.log(this.goodList); // 数据是变化了的
},
// 添加批量更新价格方法
batchUpdatePrice(){
this.goodList.forEach(item => {
// item.price = this.price; // 不能更新视图,但是数据是修改了的
// 方式1
// Vue.set(item, 'price', this.price); // 全局方法 效果都是一样的
// 方式2
this.$set(item, 'price', this.price); // 推荐使用
});
}
},
});
</script>
Vue.delete
删除对象的属性。如果对象是响应式的,确保删除能触发更新视图。
使用方法: Vue.delete(target, propertyName/index)
Vue.nextTick
事件相关API
vm.$on
监听当前实例上的自定义事件。事件可以由 vm.$emit 触发。回调函数会接收所有传入事件触发函数的
额外参数。
vm.$on('事件名', function (payload) { console.log(payload) })
// 我们常在template中,以v-on(简写为@)指令去监听事件
vm.$emit
触发当前实例上的事件。附加参数都会传给监听器回调。
vm.$emit('事件名', 参数)
事件总线bus
通过在Vue原型上添加一个Vue实例作为事件总线,实现组件间相互通信,而且不受组件间关系的影响(无论层级有多深,不再局限于父子组件之间通信)。
Vue.prototype.$bus = new Vue();
// 我们可以在任意组件中使用this.$bus访问到该Vue实例
在具名slot上修改:
-
style上把弹框的样式,分为成功和失败
-
新增一个message组件,添加class为error(之前那个添加为success),把里面文本内容修改成错误提示
-
添加事件总线,在第一行添加
Vue.prototype.$bus = new Vue();
-
在弹窗组件message中,监听关闭事件
mounted () { this.$bus.$on('close-message', () => { this.$emit('update:show', false) }); },
-
新增商品good-add组件中,新增一个按钮,监听点击事件。
removeAllMessage(){ this.$bus.$emit('close-message'); // 名字和上面,$on中事件句柄名close-message一致 }
<style>
.active {
background-color: red;
}
.message-box {
padding: 10px 20px;
}
.success{
background: #4fc08d;
border: 1px solid #42b983;
}
.error{
background: red;
border: 1px solid red;
}
.message-box-close {
float: right;
}
</style>
<div id="app">
<!-- 错误时的消息提示 -->
<message :show.sync="isShowMessgaeError" class="error">
<template #title>
<strong>
输入有误!
</strong>
</template>
<template>内容不能为空</template>
</message>
<!-- 成功的消息提示 -->
<message :show.sync="isShowMessgae" class="success">
<!-- v-slot:可以简写为# -->
<template #title>
<strong>
恭喜你,
</strong>
</template>
<template>新增成功!!</template>
</message>
<good-add v-model='good' @good-add-handle='addGood'></good-add>
<good-list :goodlist='goodList'></good-list>
</div>
<script src="vue.js"></script>
<script>
// 放到第一行
Vue.prototype.$bus = new Vue();
// 弹窗
Vue.component('message',{
props: {
show: {
type: Boolean,
default: false
}
},
template: `
<div class='message-box' v-if='show'>
<!--通过slot获取传入内容-->
<!--具名插槽-->
<slot name="title"></slot>
<!--slot作为占位符(占坑位)-->
<slot></slot>
<span class='message-box-close' @click='$emit("update:show",false)'>X</span>
</div>
`,
// 监听关闭事件
mounted () {
this.$bus.$on('close-message', () => {
this.$emit('update:show', false)
});
},
})
// 新增商品
Vue.component('good-add',{
model: {
prop: 'value',
event: 'change'
},
props: {
value: String,
default: ''
},
template: `
<div>
<input
@change="$emit('change', $event.target.value)"
:value=value
type="text"
@keydown.enter='add'
/>
<button @click="add">添加商品</button>
<button @click="removeAllMessage">清空提示框</button>
</div>
`,
methods: {
add(){
this.$emit('good-add-handle');
},
removeAllMessage(){
this.$bus.$emit('close-message');
}
},
})
//商品列表
Vue.component('good-list',{
data(){
return {
selected: '',
}
},
props: {
'goodlist': {
type: Array,
default: []
}
},
template: `
<ul>
<!-- class绑定 -->
<li v-for="item in goodlist"
:class="{active: (selected === item)}"
@click="selected = item">{{item}}</li>
</ul>
`
});
new Vue({
el: '#app',
data() {
return {
goodList: ['花生','瓜子','啤酒'],
good: '',
isShowMessgae: false,
isShowMessgaeError: false,
}
},
methods: {
addGood() {
if(this.good){ // 如果输入有内容
this.goodList.push(this.good);
this.isShowMessgae = true;
} else {
this.isShowMessgaeError = true;
}
},
closeMessage($evnet){
this.isShowMessgae = $evnet;
}
},
});
</script>
vm.$once
监听一个自定义事件,但是只触发一次。一旦触发之后,监听器就会被移除。
vm.$once('事件名', function (payload) { console.log(payload) })
vm.$off
移除自定义事件监听器。
-
如果没有提供参数,则移除所有的事件监听器;
-
如果只提供了事件,则移除该事件所有的监听器;
-
如果同时提供了事件与回调,则只移除这个回调的监听器。
vm.$off() // 移除所有的事件监听器
vm.$off('事件名') // 移除该事件所有的监听器
vm.$off('事件名', callback) // 只移除这个回调的监听器
元素引用
ref和vm.$refs
ref 被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs 对象上。如果在普通的DOM元素上使用,引用指向的就是DOM元素;如果用在子组件上,引用就指向组件实例。
例如:
<div id="app">
<!-- 错误时的消息提示 -->
<message ref='error-message' class="error">
<template #title>
<strong>
输入有误!
</strong>
</template>
<template>内容不能为空</template>
</message>
<!-- 成功的消息提示 -->
<message ref='success-message' class="success">
<!-- v-slot:可以简写为# -->
<template #title>
<strong>
恭喜你,
</strong>
</template>
<template>新增成功!!</template>
</message>
<good-add @good-add-handle='addGood'></good-add>
<ul>
<li v-for="item in goodList">{{item}}</li>
</ul>
</div>
<script src="vue.js"></script>
<script>
// 弹窗
Vue.component('message',{
data() {
return {
show: false
}
},
template: `
<div class='message-box' v-if='show'>
<!--通过slot获取传入内容-->
<!--具名插槽-->
<slot name="title"></slot>
<!--slot作为占位符(占坑位)-->
<slot></slot>
<span class='message-box-close' @click='changeShow'>X</span>
</div>
`,
methods: {
changeShow() {
this.show = !this.show;
}
},
})
Vue.component('good-add',{
data(){
return {
good: '',
}
},
template: `
<div>
<input ref='addInput' v-model="good" type="text" v-on:keydown.enter="add"/>
<button @click="add">添加商品</button>
</div>
`,
mounted () {
// mounted及之后才能访问到ref
// 在普通的dom元素上使用,指向dom元素
this.$refs.addInput.focus();
},
methods: {
add(){
// 发送自定义事件通知父组件新增商品(派发事件)
// 注意事件名称定义时不要有大写字母出现
this.$emit('good-add-handle', this.good);
}
}
})
new Vue({
el: '#app',
data() {
return {
goodList: ['花生','瓜子','啤酒'],
}
},
methods: {
addGood(good) {
if(good){
this.goodList.push(good);
// 在子组件上使用,指向组件实例
this.$refs['success-message'].changeShow();
} else {
this.$refs['error-message'].changeShow();
}
}
},
});
</script>
注意:
-
ref 是作为渲染结果被创建的,在初始渲染时不能访问它们
-
$refs 不是响应式的,不要试图用它在模板中做数据绑定
-
当 v-for 用于元素或组件时添加ref,引用信息将是包含 DOM 节点或组件实例的数组。
进阶知识
过滤器filters
作用: 过滤处理数据的格式,可被用于一些常见的文本格式化。
使用场景:过滤器可以用在两个地方:双花括号插值和v-bind表达式,注意过滤器要被添加在表达式的尾部,由“管道”符号|
表示。
语法:
<!-- 在双花括号中 -->
<div>{{ msg | 函数名 }}</div>
<!-- 在 `v-bind` 中 -->
<div v-bind:id="msg | 函数名"></div>
// 过滤器
filters: {
函数名(msg) {
return 过滤结果
}
}
例如:
<div id="app">
<ul>
<li v-for='item of goodList'>
<!-- 不使用过滤器时这么写,但是货币符号是固定的 -->
<!-- {{item.name}} - ${{item.price}} -->
{{item.name}} - {{item.price | symbol}}
</li>
</ul>
</div>
<script src="vue.js"></script>
<script>
new Vue({
el: '#app',
data() {
return {
goodList: [{name: '花生', price: 10},{name: '瓜子', price: 40},{name: '啤酒', price: 90}],
}
},
filters: {
symbol: function(value) {
return '$' + value;
}
}
});
把上面的例子稍微改造一下,符号可以动态传递而不是写死的。
<div id="app">
<ul>
<li v-for='item of goodList'>
<!-- 和方法一样调用,传递参数 -->
{{item.name}} - {{item.price | symbol('¥')}}
</li>
</ul>
</div>
<script src="vue.js"></script>
<script>
new Vue({
el: '#app',
data() {
return {
goodList: [{name: '花生', price: 10},{name: '瓜子', price: 40},{name: '啤酒', price: 90}],
}
},
filters: {
symbol: function(value, sym = '$') { // 第一个参数,理解为上面的item.price 第二个参数,就是('¥')中传递过来的符号, 为了容错处理 默认值给个$
return sym + value;
}
}
});
</script>
自定义指令
除了核心功能默认内置的指令 ,Vue 允许注册自定义指令。有的情况下,仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。
例如官方输入框自动获取焦点例子:
<div id="app">
<input type="text" v-focus>
</div>
<script src="vue.js"></script>
<script>
// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
// 当被绑定的元素插入到 DOM 中时……
// binding很重要,详细信息参看官网
inserted: function (el, binding) {
// 聚焦元素
el.focus()
}
});
new Vue({
el: '#app',
});
</script>
然后我们再来自定义做一个,根据当前登陆用户级别,来做权限设置。
<div id="app">
<input type="text" v-focus>
<!-- 特别需要注意: 指令里,""中是表达式,如果需要传递字符串,则需要加上字符串 -->
<button v-permission="'superAdmin'">删除</button>
</div>
<script src="vue.js"></script>
<script>
// 假设当前登陆用户是会员
const user = 'member';
// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
// 当被绑定的元素插入到 DOM 中时……
// binding很重要,详细信息参看官网
inserted: function (el, binding) {
// 聚焦元素
el.focus()
}
});
// 第一个参数: 指令名,注意使用时要加上v-
// 第二个参数: 配置项
Vue.directive('permission', {
inserted: function (el, binding) {
console.log(binding);
// 若指定用户角色和当前用户角色不匹配则删除当前指令绑定的元素
if (user !== binding.value) {
el.parentElement.removeChild(el)
}
}
});
new Vue({
el: '#app',
});
</script>
渲染函数
Vue 推荐在绝大多数情况下使用模板来创建HTML。然而在一些场景中,真的需要 JavaScript 的完全编程的能力。这时你可以用渲染函数,它比模板更接近编译器。
基础:
render: function (createElement) {
// createElement函数返回结果是VNode(虚拟DOM)
return createElement( // 接收三个参数
tagname, // 标签名称
data, // 传递数据
children // 子节点数组
)
}
基于官网的例子:
<div id="app">
<!-- 用render实现一个组件 : 实现标题 -->
<!-- level是指需要生成h1-6哪一个标签 -->
<my-head :level='1' :title='title'>{{title}}</my-head>
<my-head :level='3' :title='title'>我是另一个我</my-head>
<!-- <h2 :title='title'>
{{title}}
</h2> -->
</div>
<script src="vue.js"></script>
<script>
Vue.component('my-head',{
props: ['level', 'title'],
// render函数接收一个 createElement参数,我们一般简写为h h === createElement
// 因为Vdom底层的算法是snabbdom算法,这个算法里面生成虚拟dom的方法名就叫h
render(h){
// 注意这儿一定要有return, return出createElement返回的Vnode。
return h(
'h'+this.level, // 参数1:标签名字
{attr: { title: this.title }},// 参数2
this.$slots.default, // 参数3: 子节点数组(虚拟节点) 标签之间的内容,需要使用默认插槽来获取
)
}
});
new Vue({
el: '#app',
data() {
return {
title: 'hello, vue!'
}
},
});
</script>
然后我们再来进阶来试一试:
当用户使用组件时,
<my-head :level='1' :title='title' icon='Food'>{{title}}</my-head>
我们希望渲染成:
<!-- 阿里矢量图使用方式 -->
<h1 :title='title'>
<svg class="icon"><use xlink:href="#icon-iconfinder_Food_C_"></use></svg>
{{title}}
</h1>
最终代码为:
<div id="app">
<my-head :level='1' :title='title' icon='Food'>{{title}}</my-head>
<!-- <h3 :title='title'>
<svg class="icon"><use xlink:href="#icon-iconfinder_Food_C_"></use></svg>
{{title}}
</h3> -->
</div>
<script src="./iconfont.js"></script>
<script src="vue.js"></script>
<script>
Vue.component('my-head',{
props: ['level', 'title', 'icon'],
render(h){
let children = [];
// 思路: 第一步,把用户传入的icon,生成<svg class="icon"><use xlink:href="#icon-icon名"></use></svg>添加到children数组中
// 第二步: 把默认插槽内容this.$slots.default放到children数组中
// 第三步:h函数参数3就替换为children数组
// 第一步: 生成svg,添加图标 同样是调用h函数生成
const svgVnode = h(
'svg',
{ class: 'icon' }, // 添加固定类名为icon 详见官网createElement参数2
[h('use',{attrs: {"xlink:href": `#icon-iconfinder_${this.icon}_C_`}})] // 参数3: 子节点数组(虚拟节点),svg还有个子级use,所以再调用h方法生成use,需要注意的是需要是数组,所以将返回的vnode放到一个数组中
);
children = [svgVnode, ...this.$slots.default];
return h(
'h'+this.level, // 参数1:标签名字
{attrs: { title: this.title }},// 参数2
children, // 参数3: 子节点数组(虚拟节点) 标签之间的内容,需要使用默认插槽来获取
)
}
});
new Vue({
el: '#app',
data() {
return {
title: 'hello, vue!'
}
},
});
</script>
模板语法是如何实现的
在底层的实现上,Vue 将模板编译成虚拟 DOM 渲染函数。结合响应系统,Vue 能够智能地计算出最少需要重新渲染多少组件,并把 DOM 操作次数减到最少。
之前的例子中原本代码如下:
<!-- 宿主容器(根节点) -->
<div id="app">
<ul>
<!-- class绑定 -->
<li v-for="item in goodList"
:class="{active: (selected === item)}"
@click="selected = item">{{item}}</li>
<!-- style绑定 -->
<!-- <li v-for="item in goodList"
:style="{backgroundColor: (selected === item)?'#ddd':'transparent'}" @click="selectedCourse = item">{{item}}</li> -->
</ul>
</div>
<script src="vueJs所在路径"></script>
<script>
const vm = new Vue({
el: '#app',
data() {
return {
goodList: ['花生','瓜子','啤酒'],
selected: ''
}
},
});
</script>
然后我们去输出vue替我们生成的渲染函数 :
执行代码: console.log(vm.$options.render)
我们看到输出信息:
(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"app"}},[_c('ul',_l((goodList),function(item){return _c('li',{class:{active: (selected === item)},on:{"click":function($event){selected = item}}},[_v(_s(item))])}),0)])}
})
然后我们基于这一个点,改写为渲染函数版本。
<!-- 宿主容器(根节点) -->
<div id="app"></div>
// 创建vue实例
new Vue({
el: '#app',
data() {
return {
goodList: ['花生','瓜子','啤酒'],
selected: ''
}
},
methods: {},
render() {
with(this){
return _c('div',{attrs:{"id":"app"}},[_c('ul',_l((goodList),function(item){return _c('li',{class:{active: (selected === item)},on:{"click":function($event){selected = item}}},[_v(_s(item))])}),0)])}
}
})
我们可以看到,结果是一样的。
结论:Vue通过它的编译器将模板编译成渲染函数,在数据发生变化的时候再次执行渲染函数,通过对比两次执行结果得出要做的dom操作,模板中的神奇魔法得以实现。
函数式组件
没有管理任何状态,也没有监听任何传递给它的状态,也没有生命周期方法。实际上,它只是一个接受一些 prop 的函数。在这样的场景下,我们可以将组件标记为 functional
,这意味它无状态 (没有响应式数据),也没有实例 (没有 this
上下文)。
修改上一个例子为函数式组件:
<div id="app">
<my-head :level='1' :title='title' icon='Food'>{{title}}</my-head>
</div>
<script src="./iconfont.js"></script>
<script src="vue.js"></script>
<script>
Vue.component('my-head',{
functional: true, // 1. functional设置为true,标示是函数式组件
props: ['level', 'title', 'icon'],
// 在函数式组件中,没有this
// 所以render函数,提供第二个参数作为上下文
render(h, context){
// 之前从this上拿取'level', 'title', 'icon',就要变化了
// 2. 从context.props上去拿取
const { level, title, icon } = context.props;
let children = [];
const svgVnode = h(
'svg',
{ class: 'icon' },
[h('use',{attrs: {"xlink:href": `#icon-iconfinder_${icon}_C_`}})]
);
// 3. 子元素获取: 增加context参数,并将this.$slots.default更新为context.children,然后将this.level更新为context.props.level。
children = [svgVnode, ...context.children];
return h(
'h'+level,
{attrs: { title: title }},
children,
)
}
});
new Vue({
el: '#app',
data() {
return {
title: 'hello, vue!'
}
},
});
</script>
混入
混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。
// 定义一个混入对象
let myMixin = {
created: function () {
this.hello()
},
methods: {
hello: function () {
console.log('hello from mixin!')
}
}
}
// 定义一个使用混入对象的组件
Vue.component('mycomponent', { mixins: [myMixin] })
插件
Vue.js 的插件应该暴露一个 install 方法。这个方法的第一个参数是 Vue 构造器,第二个参数是一
个可选的选项对象.
const MyPlugin = {
install (Vue, options) {
Vue.component('my-head', {...})
}
}
if (typeof window !== 'undefined' && window.Vue) {
window.Vue.use(MyPlugin)
}
例如把上面的标题组件,封装成插件。
首先新建个js文件,存放插件代码:
const MyPlugin = {
// 插件需要install方法
install (Vue, options) {
Vue.component('my-head',{
functional: true, // 1. functional设置为true,标示是函数式组件
props: ['level', 'title', 'icon'],
// 在函数式组件中,没有this
// 所以render函数,提供第二个参数作为上下文
render(h, context){
// 之前从this上拿取'level', 'title', 'icon',就要变化了
// 2. 从context.props上去拿取
const { level, title, icon } = context.props;
let children = [];
const svgVnode = h(
'svg',
{ class: 'icon' },
[h('use',{attrs: {"xlink:href": `#icon-iconfinder_${icon}_C_`}})]
);
// 3. 子元素获取: 增加context参数,并将this.$slots.default更新为context.children,然后将this.level更新为context.props.level。
children = [svgVnode, ...context.children];
return h(
'h'+level,
{attrs: { title: title }},
children,
)
}
});
}
}
// 判断当前环境 并且判断是否已经存在Vue
if (typeof window !== 'undefined' && window.Vue) {
window.Vue.use(MyPlugin);
}
然后在页面上,直接使用插件即可。
<div id="app">
<my-head :level='1' :title='title' icon='Food'>{{title}}</my-head>
</div>
<script src="./iconfont.js"></script>
<script src="vue.js"></script>
<script src="./plugins/head.js"></script>
<script>
new Vue({
el: '#app',
data() {
return {
title: 'hello, vue!'
}
},
});
</script>