组件注册
组件(Component)是Vue.js的最核心功能,是可扩展的HTML元素(可看作自定义的HTML元素),是封装可重用的代码,同时也是Vue实例,可以接受与Vue相同的选项对象并提供相同的生命周期钩子。
为了能在UI模板中使用组件,必须先注册组件以便Vue识别。
有两种组件的注册类型:全局注册和局部注册。
全局注册
const app = Vue.createApp({
})
app.component('component-a', {
//选项
})
app.component('component-b', {
//选项
})
app.component的第一个参数component-a组件的名称(自定义标签),组件名称推荐全部小写包含连字符(即有多个单词),避免与HTML元素相冲突。 注册后任何Vue实例都可以使用这些组件
示例代码如下:
<div id="app">
<component-a></component-a>
<component-b></component-b>
<component-c></component-c>
</div>
案例
<template id="button-counter">
<button @click="count++">You clicked me {{ count }} times.</button>
</template>
<div id="components-demo">
<!-- 在模板中任意使用组件 -->
<!-- 每个组件都各自独立维护它的count。因为每用一次组件。就会有一个它的新实例被创建 -->
<button-counter></button-counter><br><br>
<button-counter></button-counter><br><br>
<button-counter></button-counter>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
// 创建一个Vue 应用
const app = Vue.createApp({})
// 定义一个名为 button-counter 的全局组件(注册)
app.component('button-counter', {
data() {
return {
count: 0
}
},
// 组件显示内容
template: '#button-counter'
})
app.mount('#components-demo')
</script>
局部注册
全局注册往往是不够理想的。
比如,使用webpack构建系统,全局注册的组件,即使不再使用,仍然被包含在最终的构建结果中,造成用户无意义的下载JavaScript。
局部注册的组件只在该组件作用域下有效。
例如,希望ComponentA在ComponentB中可用,需要在ComponentB中,使用components选项局部注册ComponentA。
const ComponentA = {
/* ... */
}
const ComponentB = {
components: { 'component-a': ComponentA
} //……
}
使用props传递数据
在组件中,使用选项props来声明从父级组件接收的数据,props的值可以是两种,一种是字符串数组,一种是对象。
案例
构造两个数组props,一个数组接收来自父级组件的数据message(实现静态传递),一个数组接收来自父级组件的数据id和title(实现动态传递),并将它们在组件模板中渲染。
<template id="parent">
<h4>{{ message }}</h4>
<children v-for="post in posts" :id="post.id" :title="post.title"></children>
<children v-for="post in posts" v-bind="post"></children>
</template>
<template id="children">
<h4>{{id}} : {{ title }}</h4>
</template>
<div id="demo">
<parent message="来自父组件的消息"></parent>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
const messageApp = Vue.createApp({})
messageApp.component('parent', {
data() {
return {
//posts是对象数组
posts: [
{ id: 1, title: 'A' },
{ id: 2, title: 'B' },
{ id: 3, title: 'C' }
]
}
},
props: ['message'],
components: {
'children':{
props: ['id','title'],
template: '#children'
}
},
template: '#parent'
})
messageApp.mount('#demo')
</script>
单项数据流
使用props实现数据传递都是单向的,即父组件数据变化时,子组件中所有的prop将刷新为最新的值,但是反过来不行。这样设计的原因是尽可能将父子组件解耦,避免子组件无意中修改父组件的状态。如果业务中,需要改变prop时,一种是父组件传递初始值进来,子组件将它作为初始值保存起来,在子组件自己的作用域下随意修改;一种是使用计算属性修改。
案例
在子组件中声明数据count保存来自父组件的mycount,count的改变不影响mycount。
<template id="child-app">
<button @click="count++">You clicked me {{ count }} times.</button>
</template>
<div id="app">
父组件的计件数:{{mycount}}<br>
<child-counter :parent-count="mycount"></child-counter>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
mycount: 100
}
}
})
app.component('child-counter', {
props: ['parentCount'],
data() {
return {
count: this.parentCount
}
},
template: '#child-app'
})
app.mount('#app')
</script>
数据验证
使用props实现数据传递的同时,还可以为props指定验证要求。一般当你的组件需要提供给别人使用时,最好进行数据验证。例如某个数据必须是数字类型,如果传入字符串,Vue将在浏览器控制台中弹出警告。
为了定制props的验证方式,可以为props的值提供带有验证需求的对象,而不是字符串数组。
案例
给组件的props提供带有验证需求的对象。
<template id="validate">
<div>
<h4>{{ num }}</h4>
<h4>{{ strnum }}</h4>
<h4>{{ isrequired }}</h4>
<h4>{{ numdefault }}</h4>
<h4>{{ objectdefault }}</h4>
<h4>{{ myfun }}</h4>
</div>
</template>
<div id="demo">
<validate-post
:num="200"
:strnum="'sdf'"
:isrequired="'abc'"
:numdefault="300"
:objectdefault="{a:'a'}"
:myfun="'aaa'"
></validate-post>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
const messageApp = Vue.createApp({})
messageApp.component('validate-post', {
props: {
num: Number,
strnum: [String, Number],
isrequired: {
type: String,
required: true
},
numdefault: {
type: Number,
default: 100
},
objectdefault: {
type: Object,
default: function () {
return { message: 'hello' }
}
},
myfun: {
validator: function (value) {
alert(['success', 'warning', 'danger'].indexOf(value) !== -1)
return ['success', 'warning', 'danger'].indexOf(value) !== -1
}
}
},
template: '#validate'
})
messageApp.mount('#demo')
</script>
组件通信
props可以实现父组件向子组件传递数据,即通信。但Vue组件通信的场景有多种,包括父子组件通信、兄弟组件通信、组件链通信。
使用自定义事件通信
可通过props从父组件向子组件传递数据,并且这种传递是单向的。当需要从子组件向父组件传递数据时,需要首先给子组件自定义事件并使用$emit(事件名, 要传递的数据)方法触发事件,然后父组件使用v-on或@监听子组件的事件。
案例
子组件触发两个事件,分别实现字体变大变小。
<template id="blog">
<h4>{{id}} : {{ title }}</h4>
<button @click="$emit('enlarge-text', 0.1)">变大</button>
<button @click="$emit('ensmall-text', 0.1)">变小</button>
</template>
<div id="demo">
<div v-bind:style="{ fontSize: postFontSize + 'em' }">
<blog-post v-for="post in posts" v-bind="post"
@ensmall-text="postFontSize -= $event"
@enlarge-text="onEnlargeText"></blog-post>
</div>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
const blogApp = Vue.createApp({
data() {
return {
//posts是对象数组
posts: [
{ id: 1, title: 'A' },
{ id: 2, title: 'B' },
{ id: 3, title: 'C' }
],
postFontSize: 1
}
},
methods: {
onEnlargeText(enlargeAmount) {
this.postFontSize += enlargeAmount
}
}
})
blogApp.component('blog-post', {
props: ['id','title'],
template: '#blog'
})
blogApp.mount('#demo')
</script>
使用v-model通信
除了自定义事件实现子组件向父组件传值外,还可以在子组件上使用v-model向父组件传值,实现双向绑定。
案例
使用v-model实现子组件向父组件传值,并实现双向绑定。
<template id="custom">
<input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)">
</template>
<div id="demo">
{{searchText}}<br><br>
<custom-input v-model="text"></custom-input><br><br>
<custom-input :model-value="text" @update:model-value="searchText = $event"></custom-input>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
const blogApp = Vue.createApp({
data() {
return {
text: '张三'
}
}
})
blogApp.component('custom-input', {
props: ['modelValue'],
template: '#custom'
})
blogApp.mount('#demo')
</script>
使用mitt实现非父子组件通信
在Vue.js中,推荐使用一个空的Vue实例作为媒介(中央事件总线)实现父子组件、兄弟组件及组件链通信。比如我们生活中的例子:买房卖房中介帮忙,买卖双方通过房产中介(中央事件总线)实现需求对接。
在Vue 2.x中,Vue实例可通过事件触发API($on、$off 和 $once)实现中央事件总线,但是在Vue 3.x中,移除了$on、$off 和 $once 实例方法,推荐使用外部库mitt来代替$on、$emit和$off实例方法。
案例
首先使用mitt新建一个中央事件总线bus,然后分别创建两个Vue实例buyer(买方)和seller(卖方),买卖双方互相通信。
mitt.js
/**
* @param 入参为 EventHandlerMap 对象
* @returns 返回一个对象,对象包含属性 all,方法 on,off,emit
*/
function mitt(all) {
/*
此处实参可传一个EventHandlerMap对象,实现多个 mitt 的合并。例如:
const m1 = mitt();
m1.on('hi', () => { console.log('Hi, I am belongs to m1.'); });
const m2 = mitt(m1.all);
m2.emit('hi') // Hi, I am belongs to m1.
m2.on('hello', () => { console.log('Hello, I am belongs to m2.'); });
m1.emit('hello'); // Hello, I am belongs to m2.
m1.all === m2.all // true
*/
all = all || new Map();
return {
// 事件键值对映射对象
all,
/**
* 注册一个命名的事件处理
* @param type 事件名,官方表示事件名如 *,用来标记为通用事件,调用任何事件,都会触发命名为 * 的事件
* @param handler 事件处理函数
*/
on(type, handler) {
// 根据type去查找事件
const handlers = all.get(type);
// 如果找到有相同的事件,则继续添加,Array.prototype.push 返回值为添加后的新长度,
const added = handlers && handlers.push(handler);
// 如果已添加了type事件,则不再执行set操作
if (!added) {
all.set(type, [handler]); // 注意此处值是数组类型,可以添加多个相同的事件
}
},
/**
* 移除指定的事件处理
* @param type 事件名,和第二个参数一起用来移除指定的事件,
* @param handler 事件处理函数
*/
off(type, handler) {
// 根据type去查找事件
const handlers = all.get(type);
// 如果找到则进行删除操作
if (handlers) {
handlers.splice(handlers.indexOf(handler) >>> 0, 1);
}
},
/**
* 触发所有 type 事件,如果有type为 * 的事件,则最后执行。
* @param type 事件名
* @param evt 传递给处理函数的参数
*/
emit(type, evt) {
// 找到type的事件循环执行
(all.get(type) || []).slice().map((handler) => { handler(evt); });
// 然后找到所有为*的事件,循环执行
(all.get('*') || []).slice().map((handler) => { handler(type, evt); });
}
};
}
<div id="buyer">
<h1>显示卖方消息:{{ message1 }}</h1>
<button @click="transferb">我是买方,向卖方传递信息</button>
</div>
<div id="seller">
<h1>显示买方消息:{{ message2 }}</h1>
<button @click="transfers">我是卖方,向买方传递信息</button>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
//此处需引用mitt.js文件!!!
<script>
const bus = mitt()
//买方
const buyer = Vue.createApp({
data() {
return {
message1: ''
}
},
methods: {
transferb() {
//用emit触发事件传值
bus.emit('on-message1', '来自买方的信息')
}
},
mounted(){
//监听
bus.on('on-message2', (msg) => {//(msg)相当于function (msg)
this.message1 = msg
})
}
})
buyer.mount('#buyer')
//卖方
const seller = Vue.createApp({
data() {
return {
message2: ''
}
},
methods: {
transfers() {
//用emit触发事件传值
bus.emit('on-message2', '来自卖方的信息')
}
},
mounted(){
//监听
bus.on('on-message1', (msg) => {//(msg)相当于function (msg)
this.message2 = msg
})
}
})
seller.mount('#seller')
</script>