1.Vue基本概念
1.1Vue的介绍
Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。
1.2Vuejs安装方式
1.2.1直接编写一个HTML页面,引用vue.js文件
项目结构如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
{{message}}
</div>
<script src="../js/vue.js"></script>
<script>
const vue = new Vue({
el: '#app',
data: {
message: 'Hello World!'
}
})
</script>
</body>
</html>
1.3Vue中的MVVM
M----Model 模型
V----View 视图
VM------ViewModel
ViewModel是Vue.js的核心,它是一个Vue实例。Vue实例是作用于某一个HTML元素上的,这个元素可以是HTML的body元素,也可以是指定了id的某个元素。
当创建了ViewModel后,双向绑定是如何达成的呢?
首先,我们将上图中的DOM Listeners和Data Bindings看作两个工具,它们是实现双向绑定的关键。
从View侧看,ViewModel中的DOM Listeners工具会帮我们监测页面上DOM元素的变化,如果有变化,则更改Model中的数据;
从Model侧看,当我们更新Model中的数据时,Data Bindings工具会帮我们更新页面中的DOM元素。
使用Vue的过程就是定义MVVM各个组成部分的过程的过程。
定义View
定义Model
创建一个Vue实例或”ViewModel”,它用于连接View和Model
在创建Vue实例时,需要传入一个选项对象,选项对象可以包含数据、挂载元素、方法、模生命周期钩子等等。
在上面的示例中,选项对象的el属性指向View,el: ‘#app’表示该Vue实例将挂载到<div id="app">...</div>
这个元素;
data属性指向Model,data: exampleData表示我们的Model是exampleData对象。
Vue.js有多种数据绑定的语法,最基础的形式是文本插值,使用一对大括号语法,在运行时{{ message }}会被数据对象的message属性替换,所以页面上会输出”Hello World!”。
1.4Vue的生命周期
- new Vue()实例化一个vue实例,然后init初始化event 和 lifecycle, 其实这个过程中分别调用了3个初始化函数(initLifecycle(), initEvents(), initRender()),分别初始化了生命周期,事件以及定义createElement函数,初始化生命周期时,定义了一些属性,比如表示当前状态生命周期状态得_isMounted ,_isDestroyed ,_isBeingDestroyed,表示keep-alive中组件状态的_inactive,而初始化event时,实际上就是定义了 o n c e 、 once、 once、off、 e m i t 、 emit、 emit、on几个函数。而createElement函数是在初始化render时定义的(调用了initRender函数)
- 执行beforeCreate生命周期函数
- beforeCreate执行完后,会开始进行数据初始化,这个过程,会定义data数据,方法以及事件,并且完成数据劫持observe以及给组件实例配置watcher观察者实例。这样,后续当数据发生变化时,才能感知到数据的变化并完成页面的渲染
- 执行created生命周期函数,所以,当这个函数执行的时候,我们已经可以拿到data下的数据以及methods下的方法了,所以在这里,我们可以开始调用方法进行数据请求了
- created执行完后,我们可以看到,这里有个判断,判断当前是否有el参数(这里为什么需要判断,是因为我们后面的操作是会依赖这个el的,后面会详细说),如果有,我们再看是否有template参数。如果没有el,那么我们会等待调用$mount(el)方法(后面会详细说)。
- 确保有了el后,继续往下走,判断当有template参数时,我们会选择去将template模板转换成render函数(其实在这前面是还有一个判断的,判断当前是否有render函数,如果有的话,则会直接去渲染当前的render函数,如果没有那么我们才开始去查找是否有template模板),如果没有template,那么我们就会直接将获取到的el(也就是我们常见的#app,#app里面可能还会有其他标签)编译成templae, 然后在将这个template转换成render函数。
- 之后再调用beforMount, 也就是说实际从creted到beforeMount之间,最主要的工作就是将模板或者el转换为render函数。并且我们可以看出一点,就是你不管是用el,还是用template, 或者是用我们最常用的.vue文件(如果是.vue文件,他其实是会先编译成为template),最终他都是会被转换为render函数的。
- beforeMount调用后,我们是不是要开始渲染render函数了,首先我们会先生产一个虚拟dom(用于后续数据发生变化时,新老虚拟dom对比计算),进行保存,然后再开始将render渲染成为真实的dom。渲染成真实dom后,会将渲染出来的真实dom替换掉原来的vm. e l ( 这 一 步 我 们 可 能 不 理 解 , 请 耐 心 往 下 看 , 后 面 我 会 举 例 说 明 ) , 然 后 再 将 替 换 后 的 el(这一步我们可能不理解,请耐心往下看,后面我会举例说明),然后再将替换后的 el(这一步我们可能不理解,请耐心往下看,后面我会举例说明),然后再将替换后的el append到我们的页面内。整个初步流程就算是走完了
- 之后再调用mounted,并将标识生命周期的一个属性_isMounted 置为true。所以mounted函数内,我们是可以操作dom的,因为这个时候dom已经渲染完成了。
- 再之后,只有当我们状态数据发生变化时,我们在触发beforeUpdate,要开始将我们变化后的数据渲染到页面上了(实际上这里是有个判断的,判断当前的_isMounted是不是为ture并且_isDestroyed是不是为false,也就是说,保证dom已经被挂载的情况下,且当前组件并未被销毁,才会走update流程)
- beforeUpdate调用之后,我们又会重新生成一个新的虚拟dom(Vnode),然后会拿这个最新的Vnode和原来的Vnode去做一个diff算,这里就涉及到一系列的计算,算出最小的更新范围,从而更新render函数中的最新数据,再将更新后的render函数渲染成真实dom。也就完成了我们的数据更新
- 然后再执行updated,所以updated里面也可以操作dom,并拿到最新更新后的dom。不过这里我要插一句话了,mouted和updated的执行,并不会等待所有子组件都被挂载完成后再执行,所以如果你希望所有视图都更新完毕后再做些什么事情,那么你最好在mouted或者updated中加一个 n e x t T i c k ( ) , 然 后 把 要 做 的 事 情 放 在 nextTick(),然后把要做的事情放在 nextTick(),然后把要做的事情放在netTick()中去做(至于为什么,以后讲到$nextTick再说吧)
- 再之后beforeDestroy没啥说的,实例销毁前,也就是说在这个函数内,你还是可以操作实例的
- 之后会做一系列的销毁动作,解除各种数据引用,移除事件监听,删除组件_watcher,删除子实例,删除自身self等。同时将实例属性_isDestroyed置为true
- 销毁完成后,再执行destroyed
大致过程就是这样,下面我们来通过例子来看一看
<body>
<div id="app">
<p>{{message}}</p>
<button @click="changeMsg">改变</button>
</div>
</body>
<script>
var vm = new Vue({
el: '#app',
data: {
message: 'hello world'
},
methods: {
changeMsg () {
this.message = 'goodbye world'
}
},
beforeCreate() {
console.log('------初始化前------')
console.log(this.message)
console.log(this.$el)
},
created () {
console.log('------初始化完成------')
console.log(this.message)
console.log(this.$el)
},
beforeMount () {
console.log('------挂载前---------')
console.log(this.message)
console.log(this.$el)
},
mounted () {
console.log('------挂载完成---------')
console.log(this.message)
console.log(this.$el)
},
beforeUpdate () {
console.log('------更新前---------')
console.log(this.message)
console.log(this.$el)
},
updated() {
console.log('------更新后---------')
console.log(this.message)
console.log(this.$el)
}
})
</script>
2.Vue基本语法
2.1插值操作mustache语法
数据绑定最常见的形式就是使用“Mustache”语法 (双大括号) 的文本插值:
<span>Message: {{ msg }}</span>
Mustache 标签将会被替代为对应数据对象上 msg
property 的值。无论何时,绑定的数据对象上 msg
property 发生了改变,插值处的内容都会更新。
2.1.1v-once
通过使用 v-once 指令,你也能执行一次性地插值,当数据改变时,插值处的内容不会更新。但请留心这会影响到该节点上的其它数据绑定:
<span v-once>这个将不会改变: {{ msg }}</span>
2.1.2v-html
双大括号会将数据解释为普通文本,而非 HTML 代码。为了输出真正的 HTML,你需要使用 v-html
指令:
<p>Using mustaches: {{ rawHtml }}</p>
<p>Using v-html directive: <span v-html="rawHtml"></span></p>
2.2v-bind
Mustache 语法不能在 HTML attribute (属性)中使用,然而,可以使用 v-bind
指令:
2.2.1v-bind的基本使用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<h2>{{message}}</h2>
<!-- 错误做法:这里不可以使用mustache语法-->
<!-- <img src="{{imgURL}}">-->
<!-- 正确做法-->
<img v-bind:src="imgURL">
</div>
<script src="../js/vue.js"></script>
<script>
const vue = new Vue({
el: '#app',
data: {
message: 'helloworld!',
imgURL: 'https://gitee.com/tuxianshengxiansheng/typora-picture/raw/master/微信图片_20210816145537.jpg'
}
})
</script>
</body>
</html>
2.2.2v-bind动态绑定class(对象语法)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
.active{
color:red;
}
</style>
</head>
<body>
<div id="app">
<!-- v-bind绑定对象-->
<!-- <h2 v-bind:class="{key1: value1, key2: value2}">{{message}}</h2>-->
<!-- <h2 v-bind:class="{类名1: true, 类名2: false}">{{message}}</h2>-->
<h2 v-bind:class="{'active': isActive, 'line': isLine}">{{message}}</h2>
</div>
<script src="../js/vue.js"></script>
<script>
const vue = new Vue({
el: '#app',
data: {
message: '你好啊',
isActive: true,
isLine: false
}
})
</script>
</body>
</html>
2.2.3v-bind动态绑定class(数组语法)
v-bind里绑定的元素不用加单引号
加上单引号表示是个字符串
不加单引号表示是变量 才会通过data进行动态渲染
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
.active{
color:red;
}
</style>
</head>
<body>
<div id="app">
<!-- v-bind绑定对象-->
<!-- <h2 v-bind:class="{key1: value1, key2: value2}">{{message}}</h2>-->
<!-- <h2 v-bind:class="{类名1: true, 类名2: false}">{{message}}</h2>-->
<!-- <h2 v-bind:class="{active: isActive, line: isLine}">{{message}}</h2>-->
<h2 v-bind:class="[active,line]">{{message}}</h2>
<h2 v-bind:class="['active','line']">{{message}}</h2>
</div>
<script src="../js/vue.js"></script>
<script>
const vue = new Vue({
el: '#app',
data: {
message: '你好啊',
isActive: true,
isLine: false,
active: 'aaaa',
line: 'bbb'
}
})
</script>
</body>
</html>
2.3语法糖
语法糖是指在不影响功能的情况下, 添加某种方法实现同样的效果, 从而方便程序开发。 Vue扣 的 v-bind 和 v-on 指令都提供了语法糖, 也可以说是缩写, 比如 v-bind , 可以省略 v-bind,直接写一个冒号 “ :”
<!-- 完整语法 -->
<a v-bind:href="url"> ... </a>
<!-- 缩写 -->
<a :href="url"> ... </a>
<!-- 动态参数的缩写 -->
<a :[key]="url"> ... </a>
v-on 可以直接用 “@” 来缩写:
<!-- 完整语法 -->
<a v-on:click="doSomething"> ... </a>
<!-- 缩写 -->
<a @click="doSomething"> ... </a>
<!-- 动态参数的缩写 (2.6.0+) -->
<a @[event]="doSomething"> ... </a>
2.4计算属性
模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的。在模板中放入太多的逻辑会让模板过重且难以维护。例如:
<div id="example">
{{ message.split('').reverse().join('') }}
</div>
在这个地方,模板不再是简单的声明式逻辑。你必须看一段时间才能意识到,这里是想要显示变量 message
的翻转字符串。当你想要在模板中的多处包含此翻转字符串时,就会更加难以处理。
所以,对于任何复杂逻辑,你都应当使用计算属性。
2.4.1基础例子
<div id="example">
<p>Original message: "{{ message }}"</p>
<p>Computed reversed message: "{{ reversedMessage }}"</p>
</div>
var vm = new Vue({
el: '#example',
data: {
message: 'Hello'
},
computed: {
// 计算属性的 getter
reversedMessage: function () {
// `this` 指向 vm 实例
return this.message.split('').reverse().join('')
}
}
})
结果:
Original message: “Hello”
Computed reversed message: “olleH”
这里我们声明了一个计算属性 reversedMessage
。我们提供的函数将用作 property vm.reversedMessage
的 getter 函数:
console.log(vm.reversedMessage) // => 'olleH'
vm.message = 'Goodbye'
console.log(vm.reversedMessage) // => 'eybdooG'
你可以打开浏览器的控制台,自行修改例子中的 vm。vm.reversedMessage
的值始终取决于 vm.message
的值。
你可以像绑定普通 property 一样在模板中绑定计算属性。Vue 知道 vm.reversedMessage
依赖于 vm.message
,因此当 vm.message
发生改变时,所有依赖 vm.reversedMessage
的绑定也会更新。而且最妙的是我们已经以声明的方式创建了这种依赖关系:计算属性的 getter 函数是没有副作用 (side effect) 的,这使它更易于测试和理解。
2.4.2计算属性缓存 vs 方法
你可能已经注意到我们可以通过在表达式中调用方法来达到同样的效果:
<p>Reversed message: "{{ reversedMessage() }}"</p>
// 在组件中
methods: {
reversedMessage: function () {
return this.message.split('').reverse().join('')
}
}
我们可以将同一函数定义为一个方法而不是一个计算属性。两种方式的最终结果确实是完全相同的。然而,不同的是计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。这就意味着只要 message
还没有发生改变,多次访问 reversedMessage
计算属性会立即返回之前的计算结果,而不必再次执行函数。
这也同样意味着下面的计算属性将不再更新,因为 Date.now()
不是响应式依赖:
computed: {
now: function () {
return Date.now()
}
}
相比之下,每当触发重新渲染时,调用方法将总会再次执行函数。
我们为什么需要缓存?假设我们有一个性能开销比较大的计算属性 A,它需要遍历一个巨大的数组并做大量的计算。然后我们可能有其他的计算属性依赖于 A。如果没有缓存,我们将不可避免的多次执行 A 的 getter!如果你不希望有缓存,请用方法来替代。
2.4.3计算属性的 Setter
计算属性默认只有 getter,不过在需要时你也可以提供一个 setter:
// ...
computed: {
fullName: {
// getter
get() {
return this.firstName + ' ' + this.lastName
},
// setter
set(newValue) {
const names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[names.length - 1]
}
}
}
// ...
现在再运行 vm.fullName = 'John Doe'
时,setter 会被调用,vm.firstName
和 vm.lastName
也会相应地被更新。
2.5v-on
可以用 v-on
指令监听 DOM 事件,并在触发时运行一些 JavaScript 代码。
示例:
<div id="example-1">
<button v-on:click="counter += 1">Add 1</button>
<p>The button above has been clicked {{ counter }} times.</p>
</div>
var example1 = new Vue({
el: '#example-1',
data: {
counter: 0
}
})
2.5.1ES6对象字面增强写法
2.5.1.1属性增强写法
// 1.属性的增强写法
const name = 'why';
const age = 18;
const height = 1.88;
// ES5写法
// const obj = {
// name : name,
// age : age,
// height: height
// }
const obj = {
name,
age,
height
}
2.5.1.2方法增强写法
// 2.函数增强写法
// ES5的写法
// const obj = {
// run : function (){},
// eat : function (){}
// }
const obj = {
run(){
},
eat(){
}
}
2.5.2事件处理方法
然而许多事件处理逻辑会更为复杂,所以直接把 JavaScript 代码写在 v-on
指令中是不可行的。因此 v-on
还可以接收一个需要调用的方法名称。
示例:
<div id="example-2">
<!-- `greet` 是在下面定义的方法名 -->
<button v-on:click="greet">Greet</button>
</div>
var example2 = new Vue({
el: '#example-2',
data: {
name: 'Vue.js'
},
// 在 `methods` 对象中定义方法
methods: {
greet: function (event) {
// `this` 在方法里指向当前 Vue 实例
alert('Hello ' + this.name + '!')
// `event` 是原生 DOM 事件
if (event) {
alert(event.target.tagName)
}
}
}
})
// 也可以用 JavaScript 直接调用方法
example2.greet() // => 'Hello Vue.js!'
2.5.3内联处理器中的方法
除了直接绑定到一个方法,也可以在内联 JavaScript 语句中调用方法:
<div id="example-3">
<button v-on:click="say('hi')">Say hi</button>
<button v-on:click="say('what')">Say what</button>
</div>
new Vue({
el: '#example-3',
methods: {
say: function (message) {
alert(message)
}
}
})
有时也需要在内联语句处理器中访问原始的 DOM 事件。可以用特殊变量 $event
把它传入方法:
<button v-on:click="warn('Form cannot be submitted yet.', $event)">
Submit
</button>
// ...
methods: {
warn: function (message, event) {
// 现在我们可以访问原生事件对象
if (event) {
event.preventDefault()
}
alert(message)
}
}
2.5.4事件修饰符
在事件处理程序中调用 event.preventDefault()
或 event.stopPropagation()
是非常常见的需求。尽管我们可以在方法中轻松实现这点,但更好的方式是:方法只有纯粹的数据逻辑,而不是去处理 DOM 事件细节。
为了解决这个问题,Vue.js 为 v-on
提供了事件修饰符。之前提过,修饰符是由点开头的指令后缀来表示的。
.stop
.prevent
.capture
.self
.once
.passive
<!-- 阻止单击事件继续传播 -->
<a v-on:click.stop="doThis"></a>
<!-- 提交事件不再重载页面 -->
<form v-on:submit.prevent="onSubmit"></form>
<!-- 修饰符可以串联 -->
<a v-on:click.stop.prevent="doThat"></a>
<!-- 只有修饰符 -->
<form v-on:submit.prevent></form>
<!-- 添加事件监听器时使用事件捕获模式 -->
<!-- 即内部元素触发的事件先在此处理,然后才交由内部元素进行处理 -->
<div v-on:click.capture="doThis">...</div>
<!-- 只当在 event.target 是当前元素自身时触发处理函数 -->
<!-- 即事件不是从内部元素触发的 -->
<div v-on:click.self="doThat">...</div>
使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用
v-on:click.prevent.self
会阻止所有的点击,而v-on:click.self.prevent
只会阻止对元素自身的点击。
2.6条件渲染
2.6.1v-if
v-if
指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回 truthy 值的时候被渲染。
<h1 v-if="awesome">Vue is awesome!</h1>
也可以用 v-else
添加一个“else 块”:
<h1 v-if="awesome">Vue is awesome!</h1>
<h1 v-else>Oh no 😢</h1>
2.6.1.1在 <template>
元素上使用 v-if
条件渲染分组
因为 v-if
是一个指令,所以必须将它添加到一个元素上。但是如果想切换多个元素呢?此时可以把一个 <template>
元素当做不可见的包裹元素,并在上面使用 v-if
。最终的渲染结果将不包含 <template>
元素。
<template v-if="ok">
<h1>Title</h1>
<p>Paragraph 1</p>
<p>Paragraph 2</p>
</template>
2.6.2v-else
你可以使用 v-else
指令来表示 v-if
的“else 块”:
<div v-if="Math.random() > 0.5">
Now you see me
</div>
<div v-else>
Now you don't
</div>
v-else
元素必须紧跟在带 v-if
或者 v-else-if
的元素的后面,否则它将不会被识别。
2.6.3v-else-if
v-else-if
,顾名思义,充当 v-if
的“else-if 块”,并且可以连续使用:
<div v-if="type === 'A'">
A
</div>
<div v-else-if="type === 'B'">
B
</div>
<div v-else-if="type === 'C'">
C
</div>
<div v-else>
Not A/B/C
</div>
与 v-else
的用法类似,v-else-if
也必须紧跟在带 v-if
或者 v-else-if
的元素之后。
2.6.4v-show
另一个用于条件性展示元素的选项是 v-show
指令。用法大致一样:
<h1 v-show="ok">Hello!</h1>
不同的是带有 v-show
的元素始终会被渲染并保留在 DOM 中。v-show
只是简单地切换元素的 CSS property display
。
注意,v-show
不支持 <template>
元素,也不支持 v-else
。
2.6.5v-if
vs v-show
v-if
是“真正”的条件渲染,因为它会确保在切换过程中,条件块内的事件监听器和子组件适当地被销毁和重建。
v-if
也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
相比之下,v-show
就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。
一般来说,v-if
有更高的切换开销,而 v-show
有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 v-show
较好;如果在运行时条件很少改变,则使用 v-if
较好。
2.7列表渲染
2.7.1用 v-for
把一个数组对应为一组元素
我们可以用 v-for
指令基于一个数组来渲染一个列表。v-for
指令需要使用 item in items
形式的特殊语法,其中 items
是源数据数组,而 item
则是被迭代的数组元素的别名。
<ul id="example-1">
<li v-for="item in items" :key="item.message">
{{ item.message }}
</li>
</ul>
var example1 = new Vue({
el: '#example-1',
data: {
items: [
{ message: 'Foo' },
{ message: 'Bar' }
]
}
})
结果:
- Foo
- Bar
在 v-for
块中,我们可以访问所有父作用域的 property。v-for
还支持一个可选的第二个参数,即当前项的索引。
<ul id="example-2">
<li v-for="(item, index) in items">
{{ parentMessage }} - {{ index }} - {{ item.message }}
</li>
</ul>
var example2 = new Vue({
el: '#example-2',
data: {
parentMessage: 'Parent',
items: [
{ message: 'Foo' },
{ message: 'Bar' }
]
}
})
结果:
- Parent - 0 - Foo
- Parent - 1 - Bar
你也可以用 of
替代 in
作为分隔符,因为它更接近 JavaScript 迭代器的语法:
<div v-for="item of items"></div>
2.7.2在 v-for
里使用对象
你也可以用 v-for
来遍历一个对象的 property。
<ul id="v-for-object" class="demo">
<li v-for="value in object">
{{ value }}
</li>
</ul>
new Vue({
el: '#v-for-object',
data: {
object: {
title: 'How to do lists in Vue',
author: 'Jane Doe',
publishedAt: '2016-04-10'
}
}
})
结果:
- How to do lists in Vue
- Jane Doe
- 2016-04-10
你也可以提供第二个的参数为 property 名称 (也就是键名):
<div v-for="(value, name) in object">
{{ name }}: {{ value }}
</div>
title: How to do lists in Vue
author: Jane Doe
publishedAt: 2016-04-10
还可以用第三个参数作为索引:
<div v-for="(value, name, index) in object">
{{ index }}. {{ name }}: {{ value }}
</div>
\0. title: How to do lists in Vue
\1. author: Jane Doe
\2. publishedAt: 2016-04-10
在遍历对象时,会按 Object.keys()
的结果遍历,但是不能保证它的结果在不同的 JavaScript 引擎下都一致。
2.7.3维护状态
当 Vue 正在更新使用 v-for
渲染的元素列表时,它默认使用“就地更新”的策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是就地更新每个元素,并且确保它们在每个索引位置正确渲染。这个类似 Vue 1.x 的 track-by="$index"
。
这个默认的模式是高效的,但是只适用于不依赖子组件状态或临时 DOM 状态 (例如:表单输入值) 的列表渲染输出。
为了给 Vue 一个提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,你需要为每项提供一个唯一 key
attribute:
<div v-for="item in items" v-bind:key="item.id">
<!-- 内容 -->
</div>
建议尽可能在使用 v-for
时提供 key
attribute,除非遍历输出的 DOM 内容非常简单,或者是刻意依赖默认行为以获取性能上的提升。
因为它是 Vue 识别节点的一个通用机制,key
并不仅与 v-for
特别关联。后面我们将在指南中看到,它还具有其它用途。
不要使用对象或数组之类的非基本类型值作为 v-for
的 key
。请用字符串或数值类型的值。
2.7.4数组更新检测
2.7.4.1变更方法
Vue 将被侦听的数组的变更方法进行了包裹,所以它们也将会触发视图更新。这些被包裹过的方法包括:
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
你可以打开控制台,然后对前面例子的 items
数组尝试调用变更方法。比如 example1.items.push({ message: 'Baz' })
。
2.7.4.2替换数组
变更方法,顾名思义,会变更调用了这些方法的原始数组。相比之下,也有非变更方法,例如 filter()
、concat()
和 slice()
。它们不会变更原始数组,而总是返回一个新数组。当使用非变更方法时,可以用新数组替换旧数组:
example1.items = example1.items.filter(function (item) {
return item.message.match(/Foo/)
})
你可能认为这将导致 Vue 丢弃现有 DOM 并重新渲染整个列表。幸运的是,事实并非如此。Vue 为了使得 DOM 元素得到最大范围的重用而实现了一些智能的启发式方法,所以用一个含有相同元素的数组去替换原来的数组是非常高效的操作。
2.7.4.3注意事项
由于 JavaScript 的限制,Vue 不能检测数组和对象的变化。深入响应式原理中有相关的讨论。
2.7.5显示过滤/排序后的结果
有时,我们想要显示一个数组经过过滤或排序后的版本,而不实际变更或重置原始数据。在这种情况下,可以创建一个计算属性,来返回过滤或排序后的数组。
例如:
<li v-for="n in evenNumbers">{{ n }}</li>
data: {
numbers: [ 1, 2, 3, 4, 5 ]
},
computed: {
evenNumbers: function () {
return this.numbers.filter(function (number) {
return number % 2 === 0
})
}
}
在计算属性不适用的情况下 (例如,在嵌套 v-for
循环中) 你可以使用一个方法:
<ul v-for="set in sets">
<li v-for="n in even(set)">{{ n }}</li>
</ul>
data: {
sets: [[ 1, 2, 3, 4, 5 ], [6, 7, 8, 9, 10]]
},
methods: {
even: function (numbers) {
return numbers.filter(function (number) {
return number % 2 === 0
})
}
}
2.7.6在 v-for
里使用值范围
v-for
也可以接受整数。在这种情况下,它会把模板重复对应次数。
<div>
<span v-for="n in 10">{{ n }} </span>
</div>
结果:
1 2 3 4 5 6 7 8 9 10
2.7.8在template上使用v-for
类似于 v-if
,你也可以利用带有 v-for
的 <template>
来循环渲染一段包含多个元素的内容。比如:
<ul>
<template v-for="item in items">
<li>{{ item.msg }}</li>
<li class="divider" role="presentation"></li>
</template>
</ul>
2.7.9v-for
与 v-if
一同使用
注意我们不推荐在同一元素上使用 v-if
和 v-for
。更多细节可查阅风格指南。
当它们处于同一节点,v-for
的优先级比 v-if
更高,这意味着 v-if
将分别重复运行于每个 v-for
循环中。当你只想为部分项渲染节点时,这种优先级的机制会十分有用,如下:
<li v-for="todo in todos" v-if="!todo.isComplete">
{{ todo }}
</li>
上面的代码将只渲染未完成的 todo。
而如果你的目的是有条件地跳过循环的执行,那么可以将 v-if
置于外层元素 (或 [`) 上。如:
<ul v-if="todos.length">
<li v-for="todo in todos">
{{ todo }}
</li>
</ul>
<p v-else>No todos left!</p>
2.8购物车案例
vue基本语法的综合运用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
table{
border: 1px solid #f6abd2;
border-collapse: collapse;
border-spacing: 0;
}
th, td{
padding: 8px 16px;
border: 1px solid #f6abd2;
text-align: left;
}
th{
background-color: #f6abd2;
color: black;
font-weight: 600;
}
button {
background-color: #f6abd2;
border: none;
color: white;
padding: 15px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
transition-duration: 0.4s;
cursor: pointer;
}
button:hover {
background-color: #f2fa02;
color: white;
}
.disabled {
opacity: 0.6;
cursor: not-allowed;
}
</style>
</head>
<body>
<script src="../js/vue.js"></script>
<div id="app">
<div v-if="books.length">
<table>
<thead>
<tr>
<th></th>
<th>书籍名称</th>
<th>出版日期</th>
<th>价格</th>
<th>购买数量</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(item,index) in books">
<td>{{item.id}}</td>
<td>{{item.name}}</td>
<td>{{item.date}}</td>
<td>{{item.price | showPrice}}</td>
<td>
<button @click="decrement(index)" :class="{'disabled':item.count <= 1}" :disabled="item.count <= 1">-</button>
{{item.count}}
<button @click="increment(index)">+</button>
</td>
<td>
<button @click="removeHandle(index)">移除</button>
</td>
</tr>
</tbody>
</table>
<h2>总价格:{{totalPrice | showPrice}}</h2>
</div>
<h2 v-else>购物车为空!</h2>
</div>
<script>
const app = new Vue({
el: '#app',
data: {
books: [
{
id: 1,
name: '《算法导论》',
date: '2021年08月20日',
price: 80.00,
count: 1
},
{
id: 2,
name: '《数据库》',
date: '2021年08月20日',
price: 80.00,
count: 1
},
{
id: 3,
name: '《springCloud》',
date: '2021年08月20日',
price: 80.00,
count: 1
},
{
id: 4,
name: '《springBoot》',
date: '2021年08月20日',
price: 80.00,
count: 1
}
]
},
methods: {
increment(index){
this.books[index].count++
},
decrement(index){
this.books[index].count--
},
removeHandle(index){
this.books.splice(index, 1)
}
},
computed: {
totalPrice() {
let totalPrice =0;
for(let i = 0; i <this.books.length;i++ ){
totalPrice += this.books[i].count * this.books[i].price;
}
return totalPrice;
}
},
filters: {
showPrice(price){
return '¥' + price.toFixed(2)
}
}
})
</script>
</body>
</html>
2.9v-model
2.9.1基础使用
你可以用 v-model
指令在表单 <input>
、<textarea>
及 <select>
元素上创建双向数据绑定。它会根据控件类型自动选取正确的方法来更新元素。尽管有些神奇,但 v-model
本质上不过是语法糖。它负责监听用户的输入事件以更新数据,并对一些极端场景进行一些特殊处理。
v-model
在内部为不同的输入元素使用不同的 property 并抛出不同的事件:
- text 和 textarea 元素使用
value
property 和input
事件; - checkbox 和 radio 使用
checked
property 和change
事件; - select 字段将
value
作为 prop 并将change
作为事件。
text类型
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<input type="text" v-model="message">
<input type="text" :value="message" @input="valueChange">
<input type="text" :value="message" @input="message = $event.target.value">
<h2>{{message}}</h2>
</div>
<script src="../js/vue.js"></script>
<script>
const vue = new Vue({
el: '#app',
data: {
message: 'helloworld!',
},
methods: {
valueChange(event){
this.message = event.target.value
}
}
})
</script>
</body>
</html>
radio类型
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<label for="male">
<input type="radio" id="male" value="男" v-model="sex">男
</label>
<label for="female">
<input type="radio" id="female" value="女" v-model="sex">女
</label>
<h2>您选择的性别是:{{sex}}</h2>
</div>
<script src="../js/vue.js"></script>
<script>
const vue = new Vue({
el: '#app',
data: {
sex: ''
}
})
</script>
</body>
</html>
checkbox类型
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<!-- 1.checkbox单选框-->
<label for="agree">
<input type="checkbox" id="agree" v-model="isAgree">同意协议
</label>
<h2>您选择的是:{{isAgree}}</h2>
<button :disabled="!isAgree">下一步</button>
<br>
<!-- 2.checkbox多选框-->
<input type="checkbox" value="篮球" v-model="hobbies">篮球
<input type="checkbox" value="足球" v-model="hobbies">足球
<input type="checkbox" value="网球" v-model="hobbies">网球
<input type="checkbox" value="桌球" v-model="hobbies">桌球
<h2>您的爱好是{{hobbies}}</h2>
</div>
<script src="../js/vue.js"></script>
<script>
const vue = new Vue({
el: '#app',
data: {
isAgree: false,
hobbies: []
}
})
</script>
</body>
</html>
select类型
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<!-- 1.选择一个-->
<select name="abc" v-model="fruit">
<option value="苹果">苹果</option>
<option value="香蕉">香蕉</option>
<option value="榴莲">榴莲</option>
<option value="梨子">梨子</option>
</select>
<h2>您选择的水果是:{{fruit}}</h2>
<!-- 2.选择多个-->
<select name="abc" v-model="fruits" multiple>
<option value="苹果">苹果</option>
<option value="香蕉">香蕉</option>
<option value="榴莲">榴莲</option>
<option value="梨子">梨子</option>
</select>
<h2>您选择的水果是:{{fruits}}</h2>
</div>
<script src="../js/vue.js"></script>
<script>
const vue = new Vue({
el: '#app',
data: {
fruit: '香蕉',
fruits: []
}
})
</script>
</body>
</html>
2.9.2input中的值绑定
即多选框的值可以通过后台来获取
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<!-- 1.checkbox单选框-->
<label for="agree">
<input type="checkbox" id="agree" v-model="isAgree">同意协议
</label>
<h2>您选择的是:{{isAgree}}</h2>
<button :disabled="!isAgree">下一步</button>
<br>
<!-- 2.checkbox多选框-->
<input type="checkbox" value="篮球" v-model="hobbies">篮球
<input type="checkbox" value="足球" v-model="hobbies">足球
<input type="checkbox" value="网球" v-model="hobbies">网球
<input type="checkbox" value="桌球" v-model="hobbies">桌球
<h2>您的爱好是{{hobbies}}</h2>
<label v-for="item in originHobbies" :for="item">
<input type="checkbox" :value="item" :id="item" v-model="hobbies">{{item}}
</label>
</div>
<script src="../js/vue.js"></script>
<script>
const vue = new Vue({
el: '#app',
data: {
isAgree: false,
hobbies: [],
originHobbies: ['篮球','足球','网球','桌球','羽毛球']
}
})
</script>
</body>
</html>
2.9.3v-model修饰符的使用
.lazy
在默认情况下,v-model
在每次 input
事件触发后将输入框的值与数据进行同步 (除了上述输入法组合文字时)。你可以添加 lazy
修饰符,从而转为在 change
事件_之后_进行同步:
<!-- 在“change”时而非“input”时更新 -->
<input v-model.lazy="msg">
.number
如果想自动将用户的输入值转为数值类型,可以给 v-model
添加 number
修饰符:
<input v-model.number="age" type="number">
这通常很有用,因为即使在 type="number"
时,HTML 输入元素的值也总会返回字符串。如果这个值无法被 parseFloat()
解析,则会返回原始的值。
.trim
如果要自动过滤用户输入的首尾空白字符,可以给 v-model
添加 trim
修饰符:
<input v-model.trim="msg">
3.组件
3.1注册组件的基本步骤
组件的使用分成三个步骤:
- 创建组件构造器
- 注册组件
- 使用组件
- 调用Vue.extend()方法 创建组件构造器
- 调用Vue.component()方法 注册组件
- 在Vue实例的作用范围内 使用组件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<!-- 3.使用组件-->
<my-cpn></my-cpn>
<my-cpn></my-cpn>
<my-cpn></my-cpn>
</div>
<script src="../js/vue.js"></script>
<script>
//1.创建组件构造器
const cpnC = Vue.extend({
template: `
<div>
<h2>我是标题</h2>
<h2>我是内容1</h2>
<h2>我是内容2</h2>
</div>`
})
//2.注册组件
Vue.component('my-cpn',cpnC)
const vue = new Vue({
el: '#app',
data: {
message: 'helloworld!'
}
})
</script>
</body>
</html>
Vue.extend() 事实上,这种写法在Vue2.x的文档中几乎已经看不到了,它会直接使用下面的语法糖
// 定义一个名为 button-counter 的新组件
Vue.component('button-counter', {
data: function () {
return {
count: 0
}
},
template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})
全局注册
到目前为止,我们只用过 Vue.component
来创建组件:
Vue.component('my-component-name', {
// ... 选项 ...
})
这些组件是全局注册的。也就是说它们在注册之后可以用在任何新创建的 Vue 根实例 (new Vue
) 的模板中。比如:
Vue.component('component-a', { /* ... */ })
Vue.component('component-b', { /* ... */ })
Vue.component('component-c', { /* ... */ })
new Vue({ el: '#app' })
<div id="app">
<component-a></component-a>
<component-b></component-b>
<component-c></component-c>
</div>
在所有子组件中也是如此,也就是说这三个组件在各自内部也都可以相互使用。
局部注册
全局注册往往是不够理想的。比如,如果你使用一个像 webpack 这样的构建系统,全局注册所有的组件意味着即便你已经不再使用一个组件了,它仍然会被包含在你最终的构建结果中。这造成了用户下载的 JavaScript 的无谓的增加。
在这些情况下,你可以通过一个普通的 JavaScript 对象来定义组件:
var ComponentA = { /* ... */ }
var ComponentB = { /* ... */ }
var ComponentC = { /* ... */ }
然后在 components
选项中定义你想要使用的组件:
new Vue({
el: '#app',
components: {
'component-a': ComponentA,
'component-b': ComponentB
}
})
对于 components
对象中的每个 property 来说,其 property 名就是自定义元素的名字,其 property 值就是这个组件的选项对象。
注意局部注册的组件在其子组件中*不可用*。例如,如果你希望 ComponentA
在 ComponentB
中可用,则你需要这样写:
var ComponentA = { /* ... */ }
var ComponentB = {
components: {
'component-a': ComponentA
},
// ...
}
或者如果你通过 Babel 和 webpack 使用 ES2015 模块,那么代码看起来更像:
import ComponentA from './ComponentA.vue'
export default {
components: {
ComponentA
},
// ...
}
注意在 ES2015+ 中,在对象中放一个类似 ComponentA
的变量名其实是 ComponentA: ComponentA
的缩写,即这个变量名同时是:
- 用在模板中的自定义元素的名称
- 包含了这个组件选项的变量名
组件模板的分离写法
- 使用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<cpn></cpn>
<cpn></cpn>
</div>
<!--1.第一种写法-->
<!--通过script标签 类型必须为text/x-template-->
<!--<script type="text/x-template" id="cpn">-->
<!--<div>-->
<!-- <h2>我是标题</h2>-->
<!-- <p>我是内容</p>-->
<!--</div>-->
<!--</script>-->
<!--2.template标签-->
<template id="cpn">
<div>
<h2>我是标题</h2>
<p>我是内容</p>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
//1.注册一个全局组件
Vue.component('cpn',{
template: '#cpn'
})
const vue = new Vue({
el: '#app',
data: {
message: 'helloworld!'
}
})
</script>
</body>
</html>
组件中的数据存放问题
- 组件是一个单独功能模块的封装:
- 这个模块有属于自己的数据data。
data必须是一个函数
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<cpn></cpn>
<cpn></cpn>
</div>
<template id="cpn">
<div>
<h2>{{title}}</h2>
<p>我是内容</p>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
//1.注册一个全局组件
Vue.component('cpn',{
template: '#cpn',
data(){
return {
title: 'abc'
}
}
})
const vue = new Vue({
el: '#app',
data: {
message: 'helloworld!'
}
})
</script>
</body>
</html>
当我们定义这个 <button-counter>
组件时,你可能会发现它的 data
并不是像这样直接提供一个对象:
data: {
count: 0
}
取而代之的是,一个组件的 data
选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝:
data: function () {
return {
count: 0
}
}
如果 Vue 没有这条规则,点击一个按钮就可能会像如下代码一样影响到其它所有实例:详情见官方文档
3.2父组件和子组件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<cpn2></cpn2>
</div>
<script src="../js/vue.js"></script>
<script>
//1.创建第一个组件构造器(子组件)
const cpnC1 = Vue.extend({
template: `
<div>
<h2>我是标题1</h2>
<h2>我是内容1</h2>
</div>`
})
//2.创建第二个组件构造器(父组件)
const cpnC2 = Vue.extend({
template: `
<div>
<h2>我是标题2</h2>
<h2>我是内容2</h2>
<cpn1></cpn1>
</div>`,
components: {
cpn1: cpnC1
}
})
//root组件
const app = new Vue({
el: '#app',
data: {
message: 'helloworld!'
},
components: {
cpn2: cpnC2
}
})
</script>
</body>
</html>
3.2.1父子组件通信
3.2.1.1通过props向子组件传递数据
在组件中,使用选项 props 来声明需要从父级接收的数据, props 的值可以是两种, 一种是字 符串数组,一种是对象, 本小节先介绍数组的用法。比如我们构造一个数组,接收一个来自父级的 数据 message,并把它在组件模板中渲染,示例代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<my-component message="来自父组件的数据"></my-component>
</div>
<script src="../js/vue.js"></script>
<script>
Vue.component('my-component', {
props: ['message'],
template: '<div>{{message}}</div>'
});
var app = new Vue({
el: '#app'
})
</script>
</body>
</html>
渲染后的结果为:
<div id="app">
<div>来自父组件的数据</div>
</div>
props 中声明的数据与组件 data 函数 return 的数据主要区别就是 props 的来自父级,而 data 中 的是组件自己的数据,作用域是组件本身,这两种数据都可以在模板 template 及计算属性 computed 和方法 methods 中使用。上例的数据 message 就是通过 props 从父级传递过来的,在组件的自定义 标签上直接写该 props 的名称,如果要传递多个数据,在 props 数组中添加项即可。 由于 HTML特性不区分大小写,当使用 DOM 模板时,驼峰命名 (camelCase)的 props 名称 要转为短横分隔命名 (kebab-case),例如:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<my-component warning-text="提示信息"></my-component>
</div>
<script src="../js/vue.js"></script>
<script>
Vue.component('my-component', {
props: ['warningText'],
template: '<div>{{warningText}}</div>'
});
var app = new Vue({
el: '#app'
})
</script>
</body>
</html>
有时候,传递的数据并不是直接写死的,而是来自父级的动态数据,这时可以使用指令 v-bind 来动态绑定 props 的值,当父组件的数据变化时,也会传递给子组件。示例代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<input type="text" v-model="parentMessage">
<my-component :message="parentMessage"></my-component>
</div>
<script src="../js/vue.js"></script>
<script>
Vue.component('my-component', {
props: ['message'],
template: '<div>{{message}}</div>'
});
var app = new Vue({
el: '#app',
data: {
parentMessage:''
}
})
</script>
</body>
</html>
这里用 v-model 绑定了父级的数据 parentMessage,当通过输入框任意输入时,子组件接收到的 props “message”也会实时响应,并更新组件模板。
注意,如果你要直接传递数字、布尔值、数组、对象,而且不使用 v-bind,传递的仅 仅是字符串,尝试下面的示例来对比:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<my-component message="[1,2,3]"></my-component>
<my-component :message="[1,2,3]"></my-component>
</div>
<script src="../js/vue.js"></script>
<script>
Vue.component('my-component', {
props: ['message'],
template: '<div>{{message.length}}</div>'
});
var app = new Vue({
el: '#app'
})
</script>
</body>
</html>
同一个组件使用了两次,区别仅仅是第二个使用的是v-bind。
渲染后的结果,第一个是7,第二个才是数组的长度3
业务中会经常遇到两种需要改变 prop 的情况,一种是父组件传递初始值进来,子组件将它作 为初始值保存起来,在自己的作用域下可以随意使用和修改。这种情况可以在组件 data 内再声明 一个数据,引用父组件的 prop,示例代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<my-component :init-count="1"></my-component>
</div>
<script src="../js/vue.js"></script>
<script>
Vue.component('my-component', {
props: ['initCount'],
template: '<div>{{count}}</div>',
data: function (){
return{
count: this.initCount
}
}
});
var app = new Vue({
el: '#app'
})
</script>
</body>
</html>
组件中声明了数据 count, 它在组件初始化时会获取来自父组件的 initCount , 之后就与之无关了,只用维护 count, 这样就可以避免直接操作 initCount。
另一种情况就是 prop 作为需要被转变的原始值传入。这种情况用计算属性就可以了 , 示例代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<my-component :width="100"></my-component>
</div>
<script src="../js/vue.js"></script>
<script>
Vue.component('my-component', {
props: ['width'],
template: '<div :style="style">组件内容</div>',
computed: {
style: function (){
return{
width: this.width + 'px'
}
}
}
});
var app = new Vue({
el: '#app'
})
</script>
</body>
</html>
因为用 css 传递宽度要带单位(px),但是每次都写太麻烦,而且数值计算一般是不带单位 的,所以统一在组件内使用计算属性就可以了。
注意,在 JavaScript中的对象和数组是引用类型,指向同一个内存空间,所以 props 是对 象和数组时,在子组件内改变是会影响父组件的。
数据验证
我们上面所介绍的 props 选项的值都是一个数组,一开始也介绍过,除了数组外,还可以是对象,当 prop 需要验证时,就需要对象写法。 一般当你的组件需要提供给别人使用时,推荐都进行数据验证,比如某个数据必须是数字类 型,如果传入字符串,就会在控制台弹出警告。 以下是几个 prop 的示例:
Vue.component('my-component', {
props: {
//必须是数字类型
propA: Number,
//必须是字符串或数字类型
propB: [String,Number],
//布尔值,如果没有定义,默认值就是true
propC: {
type: Boolean,
default: true
},
//数字,而且是必传
propD: {
type: Number,
required: true
},
//如果是数组或对象,默认值必须是一个函数来返回
propE: {
type: Array,
default: function (){
return [];
}
},
//自定义一个验证函数
propF: {
validator: function (value) {
return value > 10;
}
}
}
});
验证的type类型可以是:
- String
- Number
- Boolean
- Object
- Array
- Function
type 也可以是一个自定义构造器,使用 instanceof 检测。 当 prop 验证失败时,在开发版本下会在控制台抛出一条警告。
3.2.1.2通过事件向父组件发送消息
当子组件需要向父组件传递数据时,就要用到自定义事件。我们在介绍指令 v-on 时有提到,v-on 除了监昕 DOM 事件外,还可以用于组件之间的自定义事件。
如果你了解过 JavaScript 的设计模式一一观察者模式,一定知道 dispatchEvent和 addEventListener这两个方法。
Vue 组件也有与之类似的一套模式,子组件用
e
m
i
t
(
)
来
触
发
事
件
,
父
组
件
用
emit()来触发事件,父组件用
emit()来触发事件,父组件用on()来 监听子组件的事件。
父组件也可以直接在子组件的自定义标签上使用 v-on 来监听子组件触发的自定义事件,示例 代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<p>总数: {{total}}</p>
<my-component @increase="handleGetTotal"
@reduce="handleGetTotal"></my-component>
</div>
<script src="../js/vue.js"></script>
<script>
Vue.component('my-component', {
props: ['message'],
template: '<div><button @click="handleIncrease">+1</button>' +
'<button @click="handleReduce">-1</button> </div>',
data: function (){
return{
counter: 0
}
},
methods: {
handleIncrease(){
this.counter++;
this.$emit('increase',this.counter);
},
handleReduce(){
this.counter--;
this.$emit('reduce',this.counter);
}
}
});
var app = new Vue({
el: '#app',
data: {
total: 0
},
methods: {
handleGetTotal(total){
this.total = total;
}
}
})
</script>
</body>
</html>
上面示例中,子组件有两个按钮,分别实现加 1 和减 l 的效果, 在改变组件的 data “ counter"后,通过$emit()再把它传递给父组件, 父组件用 v-on:increase 和 v-on:reduce(示例使用的是语法糖)。 $emit()方法的第一个参数是自定义事件的名称, 例如示例的 increase 和 reduce 后面的参数都是要传递的数据,可以不填或填写多个。
除了用 v-on 在组件上监听自定义事件外,也可以监昕 DOM 事件,这时可以用.native 修饰符表示监听的是一个原生事件,监听的是该组件的根元素,示例代码如下: <my-component v-on:click .native=” handleClick” ></my- component>
3.2.1.3父子组件通信-结合双向绑定
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<cpn :number1="num1"
:number2="num2"
@num1change="num1change" @num2change="num2change"
/>
</div>
<script src="../js/vue.js"></script>
<template id="cpn">
<div>
<h2>props:{{number1}}</h2>
<h2>data:{{dnumber1}}</h2>
<!-- <input type="text" v-model="dnumber1" >-->
<input type="text" :value="dnumber1" @input="num1Input">
<h2>props:{{number2}}</h2>
<h2>data:{{dnumber2}}</h2>
<!-- <input type="text" v-model="dnumber2">-->
<input type="text" :value="dnumber2" @input="num2Input">
</div>
</template>
<script>
const app = new Vue({
el: '#app',
data: {
num1:0,
num2:1
},
components: {
cpn: {
template: '#cpn',
props: {
number1: Number,
number2: Number
},
data(){
return{
dnumber1: this.number1,
dnumber2: this.number2
}
},
methods: {
num1Input(event){
this.dnumber1 = event.target.value;
this.$emit('num1change',this.dnumber1);
this.dnumber2 = this.dnumber1 * 100;
this.$emit('num2change',this.dnumber2)
},
num2Input(event){
this.dnumber2 = event.target.value;
this.$emit('num2change',this.dnumber2)
this.dnumber1 = this.dnumber2 / 100;
this.$emit('num1change',this.dnumber1)
}
}
}
},
methods: {
num1change(value){
this.num1=parseFloat(value);
},
num2change(value){
this.num2=parseFloat(value);
}
}
})
</script>
</body>
</html>
3.2.1.4父链和子组件索引
父链
在子组件中,使用 this. p a r e n t 可 以 直 接 访 问 该 组 件 的 父 实 例 或 组 件 , 父 组 件 也 可 以 通 过 t h i s . parent 可以直接访问该组件的父实例或组件,父组件也可以通过this. parent可以直接访问该组件的父实例或组件,父组件也可以通过this.children 访问它所有的子组件,而且可以递归向上或向下无线访问, 直到根实例或最内层的组 件。示例代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
{{message}}
<component-a></component-a>
</div>
<script src="../js/vue.js"></script>
<script>
Vue.component('component-a', {
template: '<button @click="handleEvent">通过父链直接修改数据</button>',
methods: {
handleEvent() {
this.$parent.message = '来自组件component-a的内容';
}
}
});
var app = new Vue({
el: '#app',
data: {
message:''
}
})
</script>
</body>
</html>
尽管 Vue 允许这样操作,但在业务中 , 子组件应该尽可能地避免依赖父组件的数据,更不应 该去主动修改它的数据,因为这样使得父子组件紧藕合,只看父组件,很难理解父组件的状态,因 为它可能被任意组件修改, 理想情况下,只有组件自己能修改它的状态。父子组件最好还是通过 props 和$emit 来通信。
子组件索引
当子组件较多时, 通过 this.$children 来一一遍历出我们需要的一个组件实例是比较困难的, 尤其是组件动态渲染时,它们的序列是不固定的。 Vue 提供了子组件索引的方法,用特殊的属性 ref 来为子组件指定一个索引名称,示例代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<button @click="handleRef">通过ref获取子组件实例</button>
<component-a ref="comA"></component-a>
</div>
<script src="../js/vue.js"></script>
<script>
Vue.component('component-a', {
template: '<div>子组件</div>',
data(){
return{
message: '子组件内容'
}
}
});
var app = new Vue({
el: '#app',
methods: {
handleRef(){
var msg = this.$refs.comA.message;
console.log(msg);
}
}
})
</script>
</body>
</html>
在父组件模板中,子组件标签上使用 ref指定一个名称,井在父组件内通过 this.$refs 来访问指 定名称的子组件。
r e f s 只 在 组 件 渲 染 完 成 后 才 填 充 , 并 且 它 是 非 响 应 式 的 . 它 仅 仅 作 为 一 个 直 接 访 问 子 组 件 的 应 急 方 案 , 应 当 避 免 在 模 板 或 计 算 属 性 中 使 用 refs 只在组件渲染完成后才填充,并且它是非响应式的. 它仅仅作为一个直接访问子 组件的应急方案,应当避免在模板或计算属性中使用 refs只在组件渲染完成后才填充,并且它是非响应式的.它仅仅作为一个直接访问子组件的应急方案,应当避免在模板或计算属性中使用refs
3.3插槽
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!--1.插槽的基本使用 <slot></slot>-->
<!--2.插槽的默认值 <slot><button></button></slot>-->
<!--3.如果有多个值可以同时放入组件中进行替换,一起作为替换元素-->
<div id="app">
<cpn></cpn>
<cpn>哈哈哈</cpn>
<cpn>呵呵呵</cpn>
<cpn>
<i>哈哈哈</i>
<div>我是元素</div>
<p>lalala</p>
</cpn>
<cpn></cpn>
</div>
<template id="cpn">
<div>
<h2>我是组件</h2>
<p>我是组件,哈哈哈</p>
<slot><button>按钮</button></slot>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
var app = new Vue({
el: '#app',
data: {
message:'你好啊'
},
components:{
cpn: {
template: '#cpn'
}
}
})
</script>
</body>
</html>
具名插槽的使用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<cpn><span>标题</span></cpn>
<cpn><span slot="center">中间标题</span></cpn>
</div>
<template id="cpn">
<div>
<slot name="left"><span>左边</span></slot>
<slot name="center"><span>中间</span></slot>
<slot name="right"><span>右边</span></slot>
<slot>哈哈哈</slot>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
var app = new Vue({
el: '#app',
data: {
message:'你好啊'
},
components:{
cpn: {
template: '#cpn'
}
}
})
</script>
</body>
</html>
编译作用域
<child-component>
{{message}}
</child-component>
这里的 message 就是一个 slot,但是它绑定的是父组件的数据,而不是组件<child-component>的数据。 父组件模板的内容是在父组件作用域内编译,子组件模板的内容是在子组件作用域内编译。 例如下面的代码示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<child-component v-show="showChild"></child-component>
</div>
<script src="../js/vue.js"></script>
<script>
Vue.component('child-component', {
template: '<div>子组件</div>'
});
var app = new Vue({
el: '#app',
data: {
showChild: true
}
})
</script>
</body>
</html>
这里的状态 showChild 绑定的是父组件的数据,如果想在子组件上绑定,那应该是:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<child-component></child-component>
</div>
<script src="../js/vue.js"></script>
<script>
Vue.component('child-component', {
template: '<div v-show="showChild">子组件</div>',
data(){
return{
showChild: true
}
}
});
var app = new Vue({
el: '#app'
})
</script>
</body>
</html>
作用域插槽
父组件替换插槽的标签,但是内容有子组件来提供。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<cpn></cpn>
<cpn>
<span>Javascript-</span>
<span>C++</span>
</cpn>
<cpn>
<!-- 目的是获取子组件中的pLanguages-->
<div slot-scope="slot">
<span v-for="item in slot.data"> {{item}} -</span>
</div>
</cpn>
</div>
<template id="cpn">
<div>
<slot :data="pLanguages">
<ul>
<li v-for="item in pLanguages">{{item}}</li>
</ul>
</slot>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
message: '你好啊'
},
components: {
cpn: {
template:"#cpn",
data(){
return{
pLanguages: ['Javascript','c++','Java','Go','Python']
}
}
}
}
})
</script>
</body>
</html>