组件是什么?
组件是可复用的 Vue 实例,且带有一个名字。
组件的注册与使用
组件与vue实例一样,需要注册,才可以使用,注册有全局注册和局部注册俩种方式
1、全局注册
<div id="app">
<my-component></my-component> // 使用组件
</div>
<script>
//全局注册组件,必须在实例创建前注册,注册后,任何vue实例都可以使用
Vue.component('my-component',{ // 'my-component' 就是组件的名字,推荐写法为小写加减号分隔
template: '<p>我是组件的内容</p>' // 组件的具体内容,外层必须用一个标签包裹一下
})
new Vue({
el: '#app'
})
</script>
2、局部注册
<div id="app">
<my-component></my-component>
</div>
<script>
new Vue({
el: '#app',
components: { // 在实例中,使用components选项局部注册,注册后只有在该实例下有效
'my-component': {
template: '<p>这里是组件的内容</p>'
}
}
})
</script>
以上代码渲染后的结果都为:
<div id="app">
<p>我是组件的内容</p>
</div>
3、组件的嵌套
<div id="app">
<my-component></my-component>
</div>
<script>
new Vue({
el: '#app',
components: {
'my-component': {
template: '<p>hello <my-component2></my-component2></p>',
components: { // 在这里,可以继续使用components选项注册组件
'my-component2': {
template: '<span>tom</span>'
}
}
}
}
})
</script>
渲染后的结果为:
<p>
hello
<span>tom</span>
</p>
4、组件中定义数据
组件中,也可以使用vue实例中的那些选项,只是这里data的定义方式有一点点不同。
<div id="app">
<my-component></my-component>
</div>
<script>
var app = new Vue({
el: '#app',
components: {
'my-component': {
template: '<p>{{message}}</p>',
data: function () { // 定义一个函数,内部返回一个对象,对象中定义数据
return {
message: 'hello'
}
}
}
}
})
</script>
组件的通信
组件不仅仅是要把模板的内容进行复用,更重要的是组件间要进行通信。
1.1、props——父组件向子组件传递数据
通常父组件的模板中包含子组件,父组件要正向的向子组件传递数据或者参数,子组件接收到后根据参数的不同来渲染不同的内容或者执行操作,这个正向传递数据的过程就是通过props来实现的。
<div id="app">
<my-component :message="message"></my-component> // 使用 ':message'的形式传递父组件中的data,不加冒号,就是传递字符串message
</div>
<script>
var app = new Vue({
el: '#app',
data: {
message: 'hello~'
},
components: {
'my-component': {
template: '<p>{{message}}</p>',
props: ['message'] // 使用props选项用数组(也可对象)的方式接收传递过来的数据,在template,computed,method中都可使用
}
}
})
</script>
1.2、props的实际使用方式
通过props传递数据是单向的,也就是说,父组件的数据变化了,子组件跟着变化,这没问题,但是呢,反过来,则不可以,vue不推荐直接修改prop的值(尽管可以实现子组件数据的变化),以下俩种使用方式比较常见。
1.2.1、使用data保存
第一种就是父组件传递初始值进来,子组件将它作为初始值保存起来,在自己的作用域下可以随意使用和修改,这时可以在组件data内再声明一个数据,引用父组件的prop。
<div id="app">
<my-component :message-par="message"></my-component>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
message: 'hello~'
},
components: {
'my-component': {
template: '<p><button @click="messageSon=\'hi\'">change</button>{{messageSon}}</p>', // 直接修改没问题
props: ['messagePar'],
data: function () {
return {
messageSon: this.messagePar // 在data中定义一个数据引用prop
}
}
}
}
})
</script>
1.2.2、使用computed保存
这种情况就是prop作为需要被转变的原始值传入,可以用计算属性。
<div id="app">
<my-component :width="100"></my-component>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
message: 'hello~'
},
components: {
'my-component': {
template: '<p :style="style">123</p>',
props: ['width'],
computed: {
style: function () {
return {
width: this.width + 'px'
}
}
}
}
}
})
</script>
1.3 prop的验证
上面所说的props选项的值都是一个数组,其实当prop需要验证时,就需要使用对象写法。一般,当你的组件需要提供给别人使用时,推荐都使用数据验证,比如某个数据必须是数字类型,否则在控制台弹出警告(必须引入的是vue包为开发版本)。
Vue.component('my-component',{
...,
props: {
propA: Number, // 必须是数字类型
propB: [String, Number], // 必须是字符串或者数字类型
propC: { // 必须是布尔值,如果没传的话默认为true
type: Boolean,
default: true
},
propD: { // 必须是数字类型,且必传
type: Number,
required: true
},
propE: { // 如果是数组或者对象,必须以一个函数来返回
type: Array,
default: function () {
return [];
}
},
propF: { // 自定义一个验证函数
validator: function (value) {
return value > 10;
}
}
}
})
例子:
<div id="app">
<my-component :message="message"></my-component>
</div>
<script>
var vm = new Vue({
el: '#app',
data: {
message: 'hi~'
},
components: {
'my-component': {
template: '<p>{{message}}</p>',
props: {
message: Number // 定义了number类型
}
}
}
})
</script>
以上代码虽然可以渲染,但会在控制台报错。
2.1、$emit 子组件向父组件传递数据
props是不能把数据传递给父组件的,这里有一种方法,就是,子组件用on()来监听子组件的事件。
<div id="app">
<p>{{total}}</p>
<my-component @increment="changeTotal" @reduce="changeTotal"></my-component> // 2、父组件根据子组件的事件名来接收,并做出响应
</div>
<script>
Vue.component('my-component',{
template: '\
<div>\
<button @click="handleIncrement">增加</button>\
<button @click="handleReduce">减少</button>\
</div>',
data: function(){
return {
counter: 0
}
},
methods: {
handleIncrement:function () {
this.counter ++;
this.$emit('increment', this.counter); // 1、触发事件,并传递值 'increment'是定义事件的名称
},
handleReduce:function () {
this.counter --;
this.$emit('reduce', this.counter);
}
}
})
var vm = new Vue({
el: '#app',
data: {
total: 0
},
methods: {
changeTotal: function (value) { // 3、具体响应,将子组件传递过来的值进行处理
this.total = value;
}
}
})
</script>
效果:
2.2、$emit的语法糖
<div id="app">
<p>{{total}}</p>
<my-component v-model="total"></my-component> // 直接用v-model绑定一个父组件的数据
</div>
<script>
Vue.component('my-component',{
template: '\
<div>\
<button @click="handleReduce">减少</button>\
</div>',
data: function(){
return {
counter: 0
}
},
methods: {
handleReduce:function () {
this.counter --;
this.$emit('input', this.counter); // $emit()的事件名改为'input'
}
}
})
var vm = new Vue({
el: '#app',
data: {
total: 0
}
})
</script>
仍然是点击减一的效果,这里简洁了许多。
2.3、v-model实现自定义的双向绑定的表单输入组件
<div id="app">
<p>{{total}}</p>
<my-component v-model="total"></my-component>
<button @click="total--">-1</button>
</div>
<script>
Vue.component('my-component',{
template: '<input type="text" :value="value" @input="handleInput" />',
props: ['value'],
methods: {
handleInput:function (event) {
this.$emit('input',event.target.value);
}
}
})
var vm = new Vue({
el: '#app',
data: {
total: 0
}
})
</script>
效果:
按钮点击,和子组件文本框输入都可以使父子组件的数据同时变化。
3.1、中央事件总线(bus) —— 非父子组件的通信
这种方式巧妙而轻量的实现了任何组件间的通信,包括父子,兄弟,跨级。比如兄弟组件间通信:
<div id="app">
<component-aaa></component-aaa>
<component-bbb></component-bbb>
</div>
<script>
var bus = new Vue(); // 1、首先创建一个空的vue实例
var vm = new Vue({
el: '#app',
components: {
'component-aaa': {
template: '<button @click="handleMessage">click me</button>',
data: function () {
return {
message: '我是来自子组件aaa的内容~'
}
},
methods: {
handleMessage:function () {
bus.$emit('on-message',this.message); // 3、组件aaa中按钮点击把事件'on-message'发出去,同时携带相应数据
}
}
},
'component-bbb':{
template: '<p>{{message}}</p>',
data: function(){
return {
message: '我是来自子组件bbb的内容~'
}
},
mounted: function () {
var that = this;
bus.$on('on-message', function (value) { // 2、在组件bbb中监听来自bus的事件'on-message'
that.message = value;
})
}
}
}
})
</script>
4.1、父链和子组件索引
除了中央事件总线bus外,还有俩种方法可以实现组件间通信:父链和子组件索引。
4.2、父链
在子组件中,使用this.parent可以直接访问该组件的父实例或组件,父组件也可以通过this.children访问它所有的子组件,而且可以递归向上或向下无限访问。
<div id="app">
<p>{{message}}</p>
<my-component></my-component>
</div>
<script>
var vm = new Vue({
el: '#app',
data: {
message: 123
},
components: {
'my-component': {
template: '<button @click="handleMessage">click me</button>',
methods: {
handleMessage: function () {
this.$parent.message = 456; // 通过父链直接修改父组件的数据
}
}
}
}
})
</script>
还是推荐使用props或者$emit()的方式通信。
4.3、子组件索引
当子组件较多时,通过this.$children来一一遍历出我们需要的一个组件实例是比较困难的,Vue提供了子组件索引的方法,用特殊的属性ref来为子组件指定一个索引名称:
<div id="app">
<button @click="handleMessage">click me</button>
<my-component ref="comA"></my-component> // 在父组件模板中子组件标签上,使用ref指定一个名称
</div>
<script>
var vm = new Vue({
el: '#app',
methods: {
handleMessage: function () {
this.$refs.comA.message = 456; // 父组件内部,通过 this.$refs来访问指定名称的子组件的
}
},
components: {
'my-component': {
template: '<p>{{message}}</p>',
data: function () {
return {
message: 123
}
}
}
}
})
</script>
slot —— 分发内容
1、单个slot
在子组件内使用特殊的<slot>元素就可以为这个子组件开启一个slot(插槽),在父组件模板里,插入在子组件标签内的所有内容将替代子组件的<slot>标签及它的内容。
<div id="app">
<my-component>
<p>hello</p>
<p>world</p>
</my-component>
</div>
<script>
Vue.component('my-component',{
template: '<div><slot><p>如果没有内容填充,我将作为默认的内容出现。</p></slot></div>'
})
var vm = new Vue({
el: '#app'
})
</script>
// 渲染后
<div>
<p>hello</p>
<p>world</p>
</div>
子组件my-component的模板内定义了一个slot元素,并且用一个<p>作为默认的内容,在父组件没有使用slot时,会渲染这段默认的文本,如果写入了slot,那就会替换整个<slot>。
2、具名slot
给slot元素指定一个name后可以分发多个内容,具名slot可以与单个slot共存:
<div id="app">
<my-component>
<p slot="header">我是头部内容</p>
<p>我是导航内容</p>
<p>我是主体内容</p>
<p slot="footer">我是底部内容</p>
</my-component>
</div>
<script>
Vue.component('my-component',{
template: '\
<div>\
<div class="header"><slot name="header"></slot></div>\
<div class="main"><slot></slot></div>\
<div class="footer"><slot name="footer"></slot></div>\
</div>'
})
var vm = new Vue({
el: '#app'
})
</script>
// 渲染后
<div>
<div class="header">
<p>我是头部内容</p>
</div>
<div class="main">
<p>我是导航内容</p>
<p>我是主体内容</p>
</div>
<div class="footer">
<p>我是底部内容</p>
</div>
</div>
子组件内声明了3个slot元素,其中在<div class="main"></div>内的slot没有使用name特性,它将作为默认slot出现,父组件没有slot特性的元素与内容都将出现在这里。如果没有指定默认的匿名slot元素,父组件内多余的内容片断都将被抛弃。
3、作用域插槽
作用域插槽是一种特殊的slot,使用一个可以复用的模板替换已渲染元素。基本用法:
<div id="app">
<my-component>
<template scope="props">
<p>{{prop.message}}</p>
</template>
</my-component>
</div>
<script>
var vm = new Vue({
el: '#app',
components: {
'my-component': {
template: '<div><slot :message="message"></slot></div>',
data: function () {
return {
message: 123
}
}
}
}
})
</script>
// 渲染后
<div>
<p>123</p>
</div>
作用域插槽最具代表性的例子就是列表组件,比如:
<div id="app">
<my-component :fruits="fruits">
<template scope="props">
<li :style="{backgroundColor:(props.index%2===0?'skyblue':'pink')}">{{props.fruitName}}</li>
</template>
</my-component>
</div>
<script>
var vm = new Vue({
el: '#app',
data: {
fruits: ['香蕉','苹果','梨子','西瓜','菠萝','水蜜桃']
},
components: {
'my-component': {
template: '<ul><slot v-for="(item, index) in fruits" :fruitName="item" :index="index"></slot></ul>',
props: ['fruits']
}
}
})
</script>
效果:
作用域插槽的使用场景是既可以复用子组件的slot,又可以使用slot内容不一致。
其他
1、$nextTick —— 异步更新队列
现在有一个需求,有一个div ,默认用v-if 将它隐藏,点击一个按钮后,改变v-if 的
值,让它显示出来,同时拿到这个div 的文本内容。如果v-if 的值是false ,直接去获取div 的内容
是获取不到的, 因为此时div 还没有被创建出来,那么应该在点击按钮后,改变v -if 的值为true,
div 才会被创建,此时再去获取,示例代码如下:
<div id="app">
<div id="con" v-if="flag">这是一段文本内容</div>
<button @click="fn">click me</button>
</div>
<script>
var vm = new Vue({
el: '#app',
data: {
flag: false
},
methods: {
fn: function () {
this.flag = true;
var conText = document.getElementById("con").innerHTML;
console.log(conText);
}
}
})
</script>
然而,在点击按钮之后,控制台却报错了,Cannot read property 'innerHTML' of null,意思就是获取不到div元素,这里就涉及Vue一个重要的概念,异步更新队列。
Vue 在观察到数据变化时并不是直接更新DOM,而是开启一个队列,并缓冲在同一事件循环
中发生的所有数据改变。在缓冲时会去除重复数据,从而避免不必要的计算和DOM 操作。然后,
在下一个事件循环tick 中, Vue 刷新队列井执行实际(己去重的)工作。所以如果你用一个for 循
环来动态改变数据100次,其实它只会应用最后一次改变,如果没有这种机制, DOM 就要重绘100
次,这固然是一个很大的开销。
知道了Vue 异步更新DOM 的原理,上面示例的报错也就不难理解了。事实上,在执行
this.flag = true时, div 仍然还是没有被创建出来,直到下一个Vue 事件循环时,才开始创建。
$nextTick 就是用来知道什么时候DOM 更新完成的,所以上面的示例代码需要修改为:
<div id="app">
<div id="con" v-if="flag">这是一段文本内容</div>
<button @click="fn">click me</button>
</div>
<script>
var vm = new Vue({
el: '#app',
data: {
flag: false
},
methods: {
fn: function () {
this.flag = true;
this.$nextTick(function () {
var conText = document.getElementById("con").innerHTML;
console.log(conText);
})
}
}
})
</script>
2、x-Templates
这个功能就是如果组件模板内容太过于复杂,不想拼接字符串,所以提供了一种方法,具体使用如下:
<div id="app">
<my-components></my-components>
</div>
<script type="text/x-template" id="my-components">
<div>
<p>123</p>
<p>456</p>
<p>789</p>
</div>
</script>
<script>
var vm = new Vue({
el: '#app',
components: {
'my-components': {
template: '#my-components'
}
}
})
</script>
// 渲染后
<div id="app">
<div>
<p>123</p>
<p>456</p>
<p>789</p>
</div>
</div>
3、watch —— 监听
watch选项用来监听某个prop或者data的改变,当它们发生改变时,就会触发watch配置的函数,从而完成我们的业务逻辑。
<div id="app">
<p>{{message}}</p>
<button @click="fn">click me</button>
</div>
<script>
var vm = new Vue({
el: '#app',
data: {
message: 123
},
methods: {
fn: function () {
this.message = 456;
}
},
watch: {
// 监听了message,如果发生改变,就会触发这个函数,参数1为改变后的值,参数2为改变前的值
message: function (newValue,oddValue) {
console.log(newValue); // 456
console.log(oddValue); // 123
}
}
})
</script>
实战
1、数字输入框组件
<div id="app">
<input-number :max="10" :min="0" v-model="value"></input-number>
</div>
<script>
function isValueNumber(value){
return (/(^-?[0-9]+\.{1}\d+$)|(^-?[1-9][0-9]*$)|(^-?0{1}$)/).test(value);
}
Vue.component('input-number', {
template: '\
<div>\
<input type="text" :value="currentValue" @change="handleChange" />\
<button @click="handleDown">-1</button>\
<button @click="handleUp">+1</button>\
</div>',
props: {
max: {
type: Number,
default: Infinity
},
min: {
type: Number,
default: -Infinity
},
value: {
type: Number,
default: 0
}
},
data: function () {
return {
currentValue: this.value
}
},
methods: {
handleDown: function () {
if(this.currentValue<=this.min) return;
this.currentValue --;
},
handleUp: function () {
if(this.currentValue>=this.max) return;
this.currentValue ++;
},
handleChange: function (event) {
var val = event.target.value.trim();
if(isValueNumber(val)){
if(val>this.max){
this.currentValue = this.max;
}else if(val<this.min){
this.currentValue = this.min;
}else{
this.currentValue = val;
}
}else{
event.target.value = this.currentValue;
}
},
updateValue: function (val) {
if(val > this.max){
this.currentValue = this.max;
}else if(val < this.min){
this.currentValue = this.min;
}else{
this.currentValue = val;
}
}
},
watch: {
currentValue: function(val){
this.$emit('input',val);
},
value: function (val) {
this.updateValue(val);
}
},
mounted: function () {
this.updateValue(this.value);
}
})
var vm = new Vue({
el: '#app',
data: {
value: 5
},
methods: {
changeValue: function () {
this.value ++;
}
}
})
</script>