vue组件详解

组件

1.1组件与复用

1.2 组件用法

组件需要注册后方能使用,有下面两种注册方式:

  • 全局注册:注册后任何 Vue 实例都可以使用
  • 局部注册:使用 components 选项,注册后的组件只有在该实例作用域下有效。组件中也可使用 components 选项来注册组件,使得组件可以嵌套。

全局注册组件示例代码

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

局部注册组件示例代码

components: {
	'my-component': Child
}

在某些情况下,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 app = new Vue({
		el: '#app'
	}) 
</script>

tbody 在渲染时,会被替换为组件的内容。常见的限制元素还有 ul 、ol 、select.

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

Vue.component('my-component', {
	template: '<div>message</div>',
	data: function () {
		return {
			message: '组件内容'
		}
		
	}	
});

注意:

  • 如果使用的是字符串模板,是不受限制的,比如 .vue 单文件用法等。

JavaScript 对象是引用关系,所以如果 return 出的对象引用了外部的一个对象,则该对象是共享的,任何一方的修改都会同步。例如

1.2 使用 props传递数据

1.2.1 基本用法

组件的功能在于:

  • 复用模板的内容
  • 进行组件之间的通信

父组件向子组件传递数据或参数,子组件接收到后根据参数的不同来渲染不同的内容或执行操作。这个正向传递数据的过程是由 props 来实现的

在组件中,通过选项 props 来声明需要从父组件接收的数据,props的值可以是下面两种:

  • 字符串数组
  • 对象

props 中声明的数据与 组件 data 函数 return 的数据主要区别在于:

  • props 的数据来自父级,data 中的数据是组件自己的数据,作用域是组件本身;
  • 都可以在模板 template 以及 计算属性 computed 和方法 methods 中使用

由于 HTML 特性不区分大小写,当使用 DOM 模板时,驼峰命名(camelCase)的props 名称要转为短横分隔命名(kebab-case).

<div id="app">
	<my-component warning-text="提示信息"></my-component>
</div>
<script>
	vue.component('my-component', {
		props: ['warningText'],
		template: '<div>{{warningText}}</div>'
	});
	
	var app = new Vue({
		el: '#app'
	})
</script>

注意:

  • 如果使用的是 字符串模板,仍然可以忽略这些限制。

有时,传递的数据并不是直接写死的,而是来自父级的动态数据,此时可以使用指令 v-bind(:) 来动态绑定 props 的值,当父组件的数据变化时,也会传递给子组件。

<div id="app">
	<input type="text" v-model="parentMessge">
	<my-component :message="parentMessge"></my-component>
</div>
<script>
	vue.component('my-component', {
		props: ['message'],
		template: '<div>{{message}}</div>'
	});
	
	var app = new Vue({
		el: '#app',
		data: {
			parentMessage: ''
		}
	})
</script>

注意:

  • 如果你要直接传递数字、布尔值、数组、对象,而且不使用 v-bind,传递的仅仅是字符串

1.2.2 单向数据流

Vue 2.x 与 Vue 1.x 比较大的一个改变是:

Vue 2.x 通过 props 传递的数据是单向的,即父组件数据变化时会传递给子组件,但是反过来不行
Vue 1.x 中提供了 .sync 修饰符来支持双向绑定。
设计成单向传递数据的原因是:尽可能将父子组件解耦,避免子组件无意中修改了父组件的状态

实际业务中,会经常遇到两种需要改变 prop 的情况:

  • 父组件传递初始值进来,子组件将它作为初始值保存起来,在自己的作用域下可以随意使用和修改。这种情况可以在组件 data 内再声明一个数据,引用父组件的 prop
  • prop 作为需要被转变的原始值传入。这种情况使用计算属性就可以了。

第一种情况示例代码;

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

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

第二种情况示例代码:

<div id="app">
	<my-component :width="100"></my-component>
</div>
<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>

因为用 CSS 传递宽度要带单位(px),但是每次都写太麻烦,而且数值计算一般是不带单位的,所以统一在组件内使用计算属性就可以了。

1.2.3 数据验证

前面介绍的 props 选项的值都是一个数组,除了数组外,还可以是对象当 prop 需要验证时,就需要对象写法

一般当你的组件需要提供给别人使用,都推荐进行数据验证

比如某个数据必须是数字类型,如果传入字符串,就会在控制台弹出警告。

下面是几个 prop 的示例:

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

验证的 type 类型可以是:

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

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

1.3 组件通信

父组件向子组件通信,通过 props 传递数据即可;
子组件和父组件通信,通过 $emit 即可。

但是Vue 组件通信的场景很多,归纳起来,组件关系分为:

  • 父子组件通信
  • 兄弟组件通信
  • 跨级组件通信

1.3.1 自定义事件

指令 v-on 的作用:

  • 监听 DOM 事件
  • 用于组件之间的自定义事件

子组件需要向父组件传递数据时,就要用自定义事件

若你了解过 JavaScript 的设计模式—观察者模式,一定知道 dispatchEventaddEventListener 这两个方法。 Vue 组件也有与之类似的一套模式,

子组件用 $emit() 来触发事件,父组件用 $on() 来监听子组件的事件。

注: emit 含义是 发出、发射,在这里即触发事件

父组件也可以直接在子组件的自定义标签上使用 v-on 来监听子组件触发的自定义事件。

示例代码如下:

<div id="app">
	<p>总数: {{ total }}</p>
	<my-component 
		@increase="handleGetTotal"
		@reduce="handleGetTotal"></my-component>
</div>
<script>
	// 全局注册组件
	vue.component('my-component', {
		template: '\
		<div>\
			<button @click="handleIncrease">+1</button>
			<button @click="handleReduce">11</button>\
		</div>',
		data: function () {
			return {
				counter: 0
			}
		},
		methods: {
			handleIncrease: function () {
				this.counter++;
				this.$emit('increase', this.counter);
			},
			handleReduce: function () {
				this.counter--;
				this.$emit('reduce', this.counter);
			}
		}
	});
	
	var app = new Vue({
		el: '#app',
		data: {
			total: 0
		},
		methods: {
			handleGetTotal: function (total) {
				this.total = total;
			}
		}
	})
</script>

分析:

  • 子组件有两个按钮,分别实现加1 和减一的效果,在改变组件的 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>

1.3.2 使用 v-model

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

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

这里组件 $emit() 的事件名是特殊的 input, 在使用组件的父级,并没有在my-component上使用 @input=“handler”,而是直接用了 v-model 绑定的一个数据 total. 这也可以称作是一个语法糖,因为上面的示例可以间接地用自定义事件来实现:

<div id="app">
	<p>总数: {{ total }}</p>
	<my-component @input="handleGetTotal"></my-component>
</div>
<script>
	//组件代码
	vue.component('my-component', {
		template: '<button @click="handleClick">+1</button>',
		data: function () {
			return {
				counter: 0
			}
		},
		methods: {
			handleClick: function () {
				this.counter++;
				this.$emit('input', this.counter);
			}
		}
	});
	var app = new Vue({
		el: '#app',
		data: {
			total: 0
		},
		methods: {
			handleGetTotal: function (total) {
				this.total = total;
			}
		}
	})
</script>

v-model 还可用来创建自定义的表单输入组件,进行数据双向绑定
实现一个具有双向绑定的 v-model 组件要满足下面两个要求:

  • 接收一个 value 属性
  • 在有新的 value 时触发 input 事件

1.3.3 非父子组件通信

实际业务中,除了父子组件通信外,还有很多非父子组件通信的场景,非父子组件一般有两种:兄弟组件和跨多级组件

vue.js 1.x 中,除了 $emit() 方法外,还提供了 $dispatch() 和 $broadcast() 这两个方法。

  • $dispatch() 用于向上级派发事件,只要是它的父级(一级或多级以上),都可以在 Vue 实例的 events 选项内接收,示例代码如下:
<!-- 注意:该示例需使用 Vue.js 1.x 版本 -->
<div id="app">
	{{ message }}
	<my-component></my-component>
</div>
<script>
	//组件代码
	vue.component('my-component', {
		template: '<button @click="handleDispatch">派发事件</button>',
		methods: {
			handleDispatch: function () {
				this.$dispatch('on-message', '来自内部组件的数据');
			}
		}
	});
	var app = new Vue({
		el: '#app',
		data: {
			message: ''
		},
		events: {
			'on-message': function (msg) {
				this.message = msg;
			}
		}
	})
</script>

同理,$broadcast() 是由上级向下级广播事件的,用法完全一致,只是方向相反。

这两种方法一旦发出事件后,任何组件都是可以接收到的,就近原则,而且会在第一次接收到后停止冒泡,除非返回true.

虽然上面两个方法看起来很好用,但是 Vue.js 2.x 中都废弃了,原因是:

  • 基于组件树结构的事件流的方式让人难以理解,并且在组件结构扩展的过程中会变得越来越脆弱
  • 无法解决兄弟组件通信的问题。

在vue.js 2.x 中,推荐使用一个空的 Vue 实例 作为中央事件总线(bus),也就是一个中介。为更好理解它,举一个生活中的例子。

比如你需要租房子,你可能会找房产中介来登记你的需求,然后中介把你的信息发给满足要求的出租者,出租者再把报价和看房时间告诉中介,由中介再转达给你,整个过程中,买家和卖家没有任何交流,都是通过中间人传话的。

或者你最近可能要换房了,你会找中介登记你的信息,订阅与你找房需求相关的资讯,一旦有符合你的房子出现时,中介会通知你,并传达你房子的具体信息。
这两个例子,你和出租者就是两个跨级的组件,而房地产中介就是这个 中央事件总线(bus)。

示例代码如下:

<div id="app">
	{{ message }}
	<component-a></component-a>
</div>
<script>
	var bus = new Vue();

	Vue.component('component-a', {
		template: '<button @click="handleEvent">传递事件</button>',
		methods: {
			handleEvent: function () {
				bus.$emit('on-message', '来自组件 component-a 的内容');	
			}	
		}
	});

	var app = new Vue({
		el: '#app',
		data: {
			message: ''	
		},
		mounted: function () {
			var _this = this;
			//在实例初始化时,监听来自bus实例的事件
			bus.$on('on-message', function (msg) {
				_this.message = msg;
			});	
		}	
	})
</script>

分析:

  • 首先创建了一个名为 bus 的空 Vue 实例,里面没有任何内容;
  • 然后全局定义了组件 component-a;
  • 最后创建 Vue 实例app, 在 app 初始化时,也就是在声明周期 mounted 钩子函数里监听了来自 bus 的事件 on-message, 而在组件 component-a 中,点击按钮会通过 bus 把事件 on-message 发出去,此时 app 就会接收到来自 bus 的事件,进而在回调里完成自己的业务逻辑

这种方法巧妙而轻量地实现了任何组件间的通信,包含父子、兄弟、跨级,而且 Vue 1.x 和 Vue 2.x 都适用。如果深入使用,可以扩展 bus 实例,给它添加 data、methods、computed等选项,这些都是可以公用的。

在业务中,尤其是协同开发时非常有用,因为经常需要共享一些通用的信息,比如用户登录的昵称、性别、邮箱等,还有用户的授权 token 等。
只需要在初始化时让 bus 获取一次,任何时间、任何组件就可以从中直接使用了,在单页面富应用(SPA)中会很实用,进阶篇会介绍。

当你的项目比较大,有更多的人参与开发时,也可以选择更好的状态管理解决方案 vuex。

父链

在子组件中,使用 this.parent(中间少了一个符号,输入无法识别,见下面代码) 可以直接访问该组件的父实例或组件,父组件也可以通过 this.children(中间少了一个符号,输入无法识别,见下面代码) 访问它所有的子组件,而且可以递归向上或向下无限访问,直到根实例或最内层的组件。
示例代码如下:

<div id="app">
	{{ message }}
	<component-a></component-a>
</div>
<script>
	Vue.component('component-a', {
		template: '<button @click="handleEvent">通过父链直接修改数据</button>',
		methods: {
			handleEvent: function () {
				//访问到父链后,可以做任何操作,比如直接修改数据
				this.$parent.message = '来自组件 component-a 的内容';	
			}	
		}
	});

	var app = new Vue({
		el: '#app',
		data: {
			message: ''
		}
	})
</script>

实际业务中,子组件应尽可能避免依赖父组件的数据,更不应该去主动修改它的数据,因为这样使得父子组件紧耦合,只看父组件,很难理解父组件的状态,因为它可能被任意组件修改理想情况下,只有组件自己能修改它的状态。

父子组件最好还是通过 props 和 $emit 来通信。

子组件索引

子组件较多时,通过 this.$children 来遍历出我们需要的一个组件实例是比较困难的,尤其是组件动态渲染时,它们的序列是不固定的

Vue提供了子组件索引的方法,用特殊的属性 ref 来为子组件指定一个索引名称,示例代码如下:

<div id="app">
	<button @click="handleRef"></button>
	<component-a ref="comA"></component-a>
</div>
<script>
	Vue.component('component-a', {
		template: '<div>子组件</div>',
		data: function () {
			return {
				message: '子组件内容'
			}	
		}
	});

	var app = new Vue({
		el: '#app',
		message: {
			handleRef: function () {
				//通过 $refs 来访问指定的实例
				var msg = this.$refs.comA.message;
				console.log(msg);
			}
		}
	})
</script>

在父组件模板中,子组件标签上使用 ref 指定一个名称,并在父类组件内通过 this.$refs 来访问指定名称的子组件。

提示:

  • $refs 只在组件渲染完成后才填充,并且它是非响应式的。它仅仅作为一个直接访问子组件的应急方案,应当避免在模板或计算属性中使用 $refs

1.4 使用 slot 分发内容

1.4.1 什么是 slot

当需要让组件混合使用,混合父组件的内容与子组件的模板时,就会用到 slot,这个过程叫做内容分发。以 app组件 为例,它有两个特点:

  • app 组件不知道它的挂载点会有什么内容。挂载点的内容是由 app 的父组件决定的。
  • app 组件很可能有它自己的模板。

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

1.4.2 作用域

正式介绍slot 前,需先知道一个概念:编译的作用域。比如父组件中有如下模板:

<child-component>
	{{ message }}
</child-component>

此处的 message 就是一个 slot,但是它绑定的是父组件的数据,而不是组件 child-component 的数据。

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

例如下面的示例代码:

<div id="app">
	<child-component v-show="showChild"></child-component>
</div>
<script>
	Vue.component('child-component', {
		template: '<div>子组件</div>'
	});
	var app = new Vue(
		el: '#app',
		data: {
			showChild: true
		}
	)
</script>

这里的状态 showChild 绑定的是 父组件的数据,如果想在子组件上绑定,那应该是:

<div id="app">
	<child-component ></child-component>
</div>
<script>
	Vue.component('child-component', {
		template: '<div v-show="showChild">子组件</div>',
		data: {
			showChild: true
		}
	});
	var app = new Vue(
		el: '#app'
	)
</script>

因此,slot 分发的内容,作用域是在父组件上的

1.4.3 slot 用法

单个slot

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

<div id="app">
	<child-component>
		<p>分发的内容</p>
		<p>更多分发的内容</p>
	</child-component>
</div>
<script>
	Vue.component('child-component', {
		template: '\
		<div>\
			<slot>\
				<p>如果父组件没有插入内容,我将作为默认出现</p>\
			</sot>\
		</div>',
	});
	
	var app = new Vue(
		el: '#app'
	)
</script>

注意:

  • 子组件 slot 内的备用内容,它的作用域是子组件本身。

具名slot

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

<div id="app">
	<child-component>
		<h2 slot="header">标题</h2>
		<p>正文内容</p>
		<p>更多的正文内容</p>
		<div slot="footer">底部信息</div>
	</child-component>
</div>
<script>
	Vue.component('child-component', {
		template: '\
		<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>',
	});
	
	var app = new Vue(
		el: '#app'
	)
</script>

子组件内声明了 3 slot 元素,其中在 div class=“main” 内的 slot 没有使用 name 特性,它将作为默认 slot 出现,父组件没有使用 slot 特性的元素与内容都将出现在这里。

如果没有指定默认的匿名 slot,父组件内多余的内容片段都将抛弃。

上面例子最终渲染后的结果为:

<div id="app">
	<div class="header">
		<h2>标题</h2>			
	</div>
	<div class="main">
		<p>正文内容</p>
		<p>更多的正文内容</p>		
	</div>
	<div class="footer">
		<div>底部信息</div>		
	</div>
</div>

在组合使用组件时,内容分发 API 至关重要。

1.4.4 作用域插槽

作用域插槽是一种特殊的 slot, 使用一个可以复用的模板 替换 已渲染元素。这个概念比较难理解,先放着。

1.4.5 访问 slot

在 Vue.js 1.x 中,想要获取某个 slot 是比较麻烦的,需要用 v-el 间接获取。而在 2.x 提供了用来访问被 slot 分发的内容的方法 $slots.

通过 $slots 可以访问某个具名 slot, a(含义见下) 包括了所有没有被包含在具名 slot 中的节点。

a 表示 this.$slots.default

$slots 在业务中几乎用不到,在用 render 函数创建组件时会比较有用,但主要还是用于独立组件开发中。

参考资料:vue.js 实战

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值