Vue.js 笔记06

发布于 2020-05-26  318 次阅读


关键词:组件、slot

组件是Vue.js最核心的功能,组件应注重复用性

组件用法

创建组件与创建Vue实例的方法类似,都需要先注册后才可以使用。注册分为全局注册和局部注册两种方式。全局注册后,任何Vue实例都可以使用:

<script>
	Vue.component('my-component',{
		//选项
	})
</script>

my-component就是注册的组件的自定义标签名称

要在父实例中使用这个组件,必须在实例创建前注册,之后便可用
<my-component></my-component>来使用组件

<div id="app">
	<my-component></my-component>
</div>
<script>
	Vue.component('my-component',{
		//选项
	});
	new Vue({
		el:'#app'
	})
</script>

在上面的代码实例中,显示出来的内容是一片空白,因为组件中没用任何内容,我们需要在组件中添加template来显示组件内容:

	Vue.component('my-component',{
		template:'<div>这里是组件的内容</div>'
	});

template 的 DOM 结构必须被一个元素包含,如果在这里去掉<div>,只保留“这里是组件的内容”,这样是无法渲染的。

在Vue实例中,可以用components选项进行局部注册组件,该组件只有在实例作用域下有效。组件中也可以使用components选项来注册组件,使组件嵌套:

<div id="app">
	<my-component></my-component>
</div>
<script>
	var Child = {
		template:'<div>局部注册组件的内容</div>'
	}
	var vm = new Vue({
		el:'#app',
		components:{
			'my-component':Child
		}
	})
</script>

Vue组件的模板在某些情况下会受到HTML的限制,比如<table>内规定只允许是<tr>、<td>、<th>等表格元素,所以无法直接在table标签内使用组件。
这种情况,可以使用特殊的 is 属性来挂载组件:

<div id="app">
	<table>
		<tbody is="my-component"></tbody>
	</table>
</div>
<script>
	Vue.component('my-component',{
		template:'<div>这里是组件的内容</div>'
	})
	var vm = new Vue({
		el:'#app'
	})

tbody在渲染时,会被替换为组件的内容。类似的受限制元素还有<ul> <ol> <select>

除了template选项外,组件中还可以像Vue实例那样使用其他的选项,比如data、computed、methods 等。但是在使用data时必须是函数,然后将数据return 出去:

<div id="app">
	<my-component></my-component>
</div>
<script>
	Vue.component('my-component',{
		template:'<div>{{message}}</div>',
		data(){
			return {
				message:'组件内容'
			}
		}
	})
	var vm = new Vue({
		el:'#app'
	})
</script>

JavaScript对象是引用关系,如果return出的对象引用了外部的一个对象,那这个对象就是共享的,任何一方修改都会同步。
如果你希望每个组件独立,不被其他同类型的组件影响,那就让组件返回一个新的data对象:

	Vue.component('my-component',{
		template:'<button @click="counter++">{{counter}}</button>',
		data(){
			return {
				counter:0
			}
		}
	})

这样,每个<my-component></my-component>就是独立的,互相不会影响显示的counter数

使用props传递数据

基本用法

组件的作用不仅仅是把模板的内容进行复用,更重要的是组件间需要进行通信。
在组件中,使用选项props来声明需要从父级接收的数据,props的值可以是两种,一种是字符串数组,一种是对象。
例如构造一个数组,接收一个来自父级的数据message,并将它在组件模板中渲染:

<div id="app">
	<my-component message="来自父级组件的数据"></my-component>
</div>
<script>
	Vue.component('my-component',{
		props:['message'],
		template:'<div>{{message}}</div>'
	})
	var vm = new Vue({
		el:'#app'
	})
</script>

props中声明的数据与组件data函数return的数据主要区别就是,props来自父级,而data中的是组件自己的数据,作用域是组件本身,这两种数据都可以在模板template及计算属性computed和方法methods中使用。
上述代码中组件的message就是通过props从父级传递过来的,在组件的自定义标签上直接写该props的名称,如果要传递多个数据,在props数组中添加项即可。
ps:由于HTML特性不区分大小写,当使用DOM模板时,驼峰命名的props名称要转为短横分隔命名

有时,需要接收来自父级的动态数据,这时可以通过v-bind来动态绑定props的值,当父组件的数据变化时,也会传递给子组件:

<div id="app">
	在父组件上输入:<input type="text" v-model="parentMessage" />
	<my-component :message="parentMessage"></my-component>
</div>
<script>
	Vue.component('my-component',{
		props:['message'],
		template:'<div>这里是子组件接收的值:{{message}}</div>'
	})
	var vm = new Vue({
		el:'#app',
		data:{
			parentMessage:''
		}
	})
</script>

如果想向组件传递数组、对象、布尔值、数字,则必须用v-bind进行绑定,否则这些内容将会被当成字符串进行传递。

单向数据流

Vue 2.x 通过props传递数据都是单向的了,父组件数据变化时会传递给子组件,但反过来不行。这样的目的是将父子组件解耦,防止互相干扰出现不必要的情况。

业务中会经常遇到两种需要改变prop的情况,一种是父组件传递初始值进来,子组件将其作为初始值保存起来,在自己的作用域下任意使用和修改:

<div id="app">
	<my-component :init-count="1"></my-component>
</div>
<script>
	Vue.component('my-component',{
		props:['initCount'],
		template:'<div>{{count}}</div>',
		data(){
			return {
				count:this.initCount
			}
		}
	})
	var vm = new Vue({
		el:'#app'
	})
</script>

组件中声明了数据count,它在组件初始化时会获取来自父组件的 initCount ,之后就与之无关了,我们秩序维护count,这样就可以避免直接操作 initCount

另一种情况就是prop作为需要被转变的原始值传入,用计算属性即可:

<div id="app">
	<my-component :width="100"></my-component>
</div>
<script>
	Vue.component('my-component',{
		props:['width'],
		template:'<div :style="style">组件内容</div>',
		computed:{
			style(){
				return this.width+'px'
			}
		}
	})
	var vm = new Vue({
		el:'#app'
	})
</script>

数据验证

当prop需要验证时,需要采用对象写法:

	Vue.component('my-component',{
		props:{
			propA:Number, //必须是数字类型
			propB:[String,Number], //必须是字符串或数字类型
			propC:{
				type:Boolean, //布尔值,若没定义则默认值是true
				default:true
			},
			propD:{
				trye:Number, //数字,而且是必传
				required:true
			},
			propE:{
				type:Array, //如果是数组或对象,默认值必须是一个函数来返回
				default(){
					return [];
				}
			},
			propF:{        //自定义一个验证函数
				validator(value){
					return value>10
				}
			}
		}
	})

校验的type类型可以是:

  • String
  • Number
  • Boolean
  • Object
  • Array
  • Function

type也可以是一个自定义构造器,使用instanceof检测
当prop验证失败时,在开发版本下会向控制台抛出一条警告

组件通信

父组件要向子组件通信,通过props传值即可,但Vue组件通信场景不止只有这样一种,归纳起来可用下图表示:

组件关系可分为父子组件通信、兄弟组件通信、跨级组件通信

自定义事件

当子组件需要向父组件传递数据时,就需要用到自定义事件。
v-on除了监听DOM事件外,还可以用于组件之间的自定义事件。
Vue组件中,子组件用$emit( )来触发事件,父组件用$on( )来监听子组件的事件
父组件也可以直接在子组件的自定义标签上使用v-on来监听子组件触发的自定义事件:

<div id="app">
	{{total}}
	<my-component @increase="handleGetTotal" @reduce="handleGetTotal"></my-component>
</div>
	<template id="example">
		<div>
			<button @click="handleIncrease">+1</button>
			<button @click="handleReduce">-1</button>
		</div>
	</template>
<script>
	Vue.component('my-component',{
		template:example,
		data(){
			return {
				counter:0
			}
		},
		methods:{
			handleIncrease(){
				this.counter++;
				this.$emit('increase',this.counter);
			},
			handleReduce(){
				this.counter--;
				this.$emit('reduce',this.counter);
			}
		}
	})
	var vm = new Vue({
		el:'#app',
		data:{
			total:0
		},
		methods:{
			handleGetTotal(total){
				this.total = total;
			}
		}
	})
</script>

上述例子中,子组件里定义了两个分别具有+1和-1功能的按钮,在调用对应的方法时,通过$emit( )再将数据传递给父组件,父组件用@increase 和@reduce。
$emit( )方法的第一个参数是自定义事件的名称,后面的参数是用来传递的数据

除了用v-on在组件上监听自定义事件外,还可以监听DOM事件,这时可用.nativ修饰符表示监听的是一个原生事件,监听的是该组件的根元素:

<my-component @click.native="handleClick"></my-component>

使用v-model

Vue 2.x 可在自定义组件上使用v-model指令:

<div id="app">
	{{total}}
	<my-component v-model="total"></my-component>
</div>
	<template id="example">
		<div>
			<button @click="handleIncrease">+1</button>
			<button @click="handleReduce">-1</button>
		</div>
	</template>
<script>
	Vue.component('my-component',{
		template:example,
		data(){
			return {
				counter:0
			}
		},
		methods:{
			handleIncrease(){
				this.counter++;
				this.$emit('input',this.counter);
			},
			handleReduce(){
				this.counter--;
				this.$emit('input',this.counter);
			}
		}
	})
	var vm = new Vue({
		el:'#app',
		data:{
			total:0
		}
	})
</script>

这里和上面用自定义事件实现的效果一样,只不过这里子组件中的$emit( )中返回的事件名是一个特殊的 input ,这样也算是v-model的一个语法糖,简化了写法。

非父子组件通信

非父子组件一般有两种,兄弟组件和跨多级组件。
在Vue.js 2.x中,一般使用一个空的Vue实例作为中央事件总线(bus),也就是作为数据传递的中介:

<div id="app">
	{{message}}
	<my-component></my-component>
</div>
	<template id="example">
		<div>
			<button @click="handleEvent">传递事件</button>
		</div>
	</template>
<script>
	var bus = new Vue(); //定义一个空Vue实例作为bus
	Vue.component('my-component',{
		template:example,
		methods:{
			handleEvent(){
				//向bus发送事件
				bus.$emit('on-message','来自组件 my-component 的内容')
			}
		}
	})
	var vm = new Vue({
		el:'#app',
		data:{
			message:''
		},
		mounted() {
			var _this = this;
			//在实例初始化时,监听来自bus实例的事件
			bus.$on('on-message',function(msg){
				_this.message = msg;
			})
		}
	})
</script>

上述代码中,首先创建了一个名为bus的空Vue实例,我们将其作为中介使用。然后全局定义了一个组件my-component,最后创建Vue实例vm,在vm初始化时,通过钩子函数mounted开启了对bus事件 on-message 的监听,而在组件
my-component 中,点击按钮会通过bus将事件 on-message 发送出去,此时vm就会接收到来自bus的事件,进而在回调中完成自己的业务逻辑。

使用Slot分发内容

当需要让组件组合使用,混合父组件的内容与子组件的模板时,就会用到slot,这个过程叫做内容分发。

props 传递数据、events 触发事件和slot 内容分发就构成了Vue组件的3个API来源,再复杂的组件也是由这三部分构成。

作用域

例如,在父组件中有如下模板:

<div id="app">
	<my-component>{{message}}</my-component>
</div>

如上,message 实际上就是一个slot,但它绑定的是父组件data中的message数据,而不是组件<my-component>的数据

父组件模板的内容是在父组件的作用域内编译,子组件模板的内容是在子组件作用域内编译:

<div id="app">
	<my-component></my-component>
</div>
	<template id="example">
		<div v-show="isShow"></div>
	</template>
<script>
	Vue.component('my-component',{
		template:example,
		data(){
			return {
				isShow:true
			}
		}
	})
	var vm = new Vue({
		el:'#app'
	})
</script>

如上,此时,写在组件模板中的isShow绑定的便是对应组件内的data

slot用法

在子组件内使用特殊的<slot>元素就可以为这个子组件开启一个slot(插槽),在父组件模板里,插入在子组件标签内的所有内容将替代子组件的<slot> 标签及它的内容:

<div id="app">
	<my-component>
		<p>插入的内容</p>
		<p>更多插入的内容</p>
	</my-component>
</div>
	<template id="example">
	<div>
		<slot>
			<p>如果没插入内容,则会显示这行内容</p>
		</slot>
	</div>
	</template>
<script>
	Vue.component('my-component',{
		template:example
	})
	var vm = new Vue({
		el:'#app'
	})
</script>

子组件my-component的模板内定义了一个slot元素,并用一个<p>作为默认的内容,且此处的默认内容作用域是子组件本身;如果写入了slot,则会替换整个<slot>,故渲染后的结果是:

<div id="app">
	<div>
		<p>插入的内容</p>
		<p>更多插入的内容</p>
	<div>
</div>

具名Slot

给<slot>元素指定一个name后可以分发多个内容,具名Slot可以与单个Slot共存:

<div id="app">
	<my-component>
		<h2 slot="header">标题</h2>
		<p>正文内容</p>
		<p>更多的正文内容</p>
		<div slot="footer">底部信息</div>
	</my-component>
</div>
<template id="example">
	<div class="container">
		<div class="header">
			<slot name="header"></slot>
		</div>
		<div class="main">
			<slot></slot>
		</div>
		<div class="footer">
			<slot name="footer"></slot>
		</div>
	</div>
</template>

上述代码中,组件模板内声明了3个<slot>元素,其中<div class="main">内的<slot>没有使用name特性,故它将作为默认slot使用,父组件没有使用slot特性的元素和内容都将出现在这里。
如果没有指定默认的匿名slot,父组件内多余的内容片段都将被抛弃。


何为BS?B-big,S-shui。