组件 (Component) 是 Vue.js 最强大的功能之一。组件可以扩展 HTML 元素,封装可重用的代码。在较高层面上,组件是自定义元素,Vue.js 的编译器为它添加特殊功能。在有些情况下,组件也可以表现为用 is 特性进行了扩展的原生 HTML 元素。
所有的 Vue 组件同时也都是 Vue 的实例,所以可接受相同的选项对象 (除了一些根级特有的选项) 并提供相同的生命周期钩子。
使用组件的原因
提高代码的复用性
组件的使用方法
- 全局注册
Vue.component('my-component', {
template: '<div>我是组件的内容</div>'
})
优点:所有的 Vue 实例都可以用。
缺点:权限太大,容错率降低。
- 局部注册
var app = new Vue({
el: '#app',
components: {
'my-component': {
template: '<div>我是组件的内容</div>'
}
}
})
- Vue 组件的模板在某些情况下会受到 html 标签的限制,比如
<table>
中只能含有<tr>
,<td>
,<tbody>
这些元素,所以直接在 table 中使用组件是无效的,此时可以使用 is 属性来挂载组件。
<table>
<tbody is="my-component"></tbody>
</table>
组件使用的技巧
- 必须使用小写字母,短横分隔命名( kebab-case )。
- template 中的元素必须被一个 DOM 元素包裹。
- 在组件的的定义中,除了 template 之外还可以有 data 、computed 、methods 等。
- 组件的 data 必须是一个方法。
使用 props 传递数据
- 在组件中使用 props 来从父组件接收参数。
- props 是来自父级的数据,而组件中 data return 的数据是组件自己的数据,它们的作用域都是组件本身,可以在 template ,computed ,methods 中直接使用。
- props 的值有两种,一种是字符串数组,一种是对象。
- 可以使用 v-bind 动态绑定父组件来的内容,不使用 v-bind 传递的是字符串,使用 v-bind 会按照 JS 语法来解析传递的内容。
单向数据流
通过 props 传递数据是单向的,也就是父组件数据变化时会传递给子组件,但是反过来不行。
这样可以避免子组件无意中修改了父组件的状态。
业务中经常会遇到两种需要改变 props 的情况:
一种是父组件传递初始值进来,子组件将它作为初始值保存起来,在自己的作用域下可以随意使用和修改。这种情况可以在组件 data 内再声明一个数据,引用父组件传来的 prop 。
另一种是 prop 作为需要被转变的原始值传入,这种情况用计算属性就可以了。
prop 数据验证
驼峰式命名(camelCase)与短横线命名(kebab-case)
- 在 html 中,不区分大小写,因此组件在 html 中使用必须使用短横线命名方式,不允许使用驼峰式!!!!!!
- 在组件中,父组件给子组件传递数据必须用短横线式命名。
- 在组件的 template 中,必须使用驼峰式命名方式。
- 在组件的 data 中,用 this.xxx 引用时,只能是驼峰命名方式。
验证的 type 类型可以是
- Number
- String
- Boolean
- Object
- Array
- Function
- Date
为了定制 prop 的验证方式,你可以为 props 中的值提供一个带有验证需求的对象,而不是一个字符串数组。例如:
Vue.component('my-component', {
props: {
// 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
propA: Number,
// 多个可能的类型
propB: [String, Number],
// 必填的字符串
propC: {
type: String,
required: true
},
// 带有默认值的数字
propD: {
type: Number,
default: 100
},
// 带有默认值的对象
propE: {
type: Object,
// 对象或数组默认值必须从一个工厂函数获取
default: function () {
return { message: 'hello' }
}
},
// 自定义验证函数
propF: {
validator: function (value) {
// 这个值必须匹配下列字符串中的一个
return ['success', 'warning', 'danger'].indexOf(value) !== -1
}
}
}
})
注意那些 prop 会在一个组件实例创建之前进行验证,所以实例的属性 (如 data、computed 等) 在 default 或 validator 函数中是不可用的。
组件通讯
自定义事件---子组件给父组件传递数据
使用 v-on 除了监听 DOM 事件外,还可以用于组件之间的自定义事件。
JavaScript 的设计模式——观察者模式,dispatchEvent
和 addEventListener
这两个方法。Vue 组件也有与之类似的一套模式,子组件用 $emit()
来触发事件,父组件用 v-on
来监听子组件的事件。
<div id="app">
{{number}}
<son-component @update="updateNumber"></son-component>
</div>
<script>
Vue.component('son-component', {
template: '<button @click="add1">+1</button>',
data: function(){
return {count: 0}
},
methods: {
add1(){
this.count += 1
this.$emit('update', this.count)
}
}
})
var app = new Vue({
el: '#app',
data: {
number: 0
},
methods: {
updateNumber(count){
this.number = count
}
}
})
</script>
在组件中使用 v-model
v-model 其实是一个语法糖,这背后做了两个操作:
- v-bind 绑定一个数据。
- v-on 指令给当前元素监听一个 input 事件,并将 input 事件传递过来的值赋给绑定的数据。
<div id="app">
{{number}}
<son-component v-model="number"></son-component>
</div>
<script>
Vue.component('son-component', {
template: '<button @click="add1">+1</button>',
data: function(){
return {count: 0}
},
methods: {
add1(){
this.count += 1
this.$emit('input', this.count)
}
}
})
var app = new Vue({
el: '#app',
data: {
number: 0
}
})
</script>
非父子组件之间的通信
有时候两个组件也需要通信(非父子关系)。在简单场景下,可以使用一个空的 Vue 实例作为中央时间总线。
var bus = new Vue()
用 bus.$emit()
触发事件,用 bus.$on()
监听事件。
<div id="app">
<a-component></a-component>
<b-component></b-component>
</div>
<script>
Vue.component('a-component', {
template: `
<div>
{{msg}}
<button @click="handle">点我传递数据到b组件</button>
</div>
`,
data: function(){
return { msg: '我是a中的数据' }
},
methods: {
handle(){
this.$root.bus.$emit('xxx', this.msg)
}
}
})
Vue.component('b-component', {
template: '<div>{{msg}}</div>',
data: function(){
return { msg: '我是b中的数据'}
},
created(){
this.$root.bus.$on('xxx', function(msg){
alert(msg)
})
}
})
var app = new Vue({
el: '#app',
data: {
bus: new Vue()
}
})
</script>
父链
this.$parent
Vue.component('child-component', {
template: '<button @click="setParentData">点我修改父亲的数据</button>',
methods: {
setParentData(){
console.log(this.msg);
this.$parent.msg = '数据已经修改了'
}
}
})
子链
this.$children
var app = new Vue({
el: '#app',
data: {
msg: '我是你爸爸'
},
methods: {
getChildData(){
console.log(this.$children);
this.msg = this.$children[0].msg
}
}
})
Vue 提供了为子组件提供索引的方法,用特殊属性 ref 为其增加一个索引。
<div id="app">
{{msg}}
<button @click="getChildData">点我拿到儿子的数据</button>
<a-component ref="a"></a-component>
<b-component ref="b"></b-component>
</div>
<script>
Vue.component('a-component', {
template: '<div>{{msg}}</div>',
data(){
return {
msg: '我是a'
}
}
})
Vue.component('b-component', {
template: '<div>{{msg}}</div>',
data() {
return {
msg: '我是b'
}
}
})
var app = new Vue({
el: '#app',
data: {
msg: '我是你爸爸'
},
methods: {
getChildData(){
this.msg = this.$refs.a.msg
}
}
})
</script>
使用 slot 分发内容
什么是 slot
为了让组件可以组合,我们需要一种方式来混合父组件的内容与子组件自己的模板。这个过程被称为内容分发。Vue.js 实现了一个内容分发 API ,使用特殊的 slot 元素作为原始内容的插槽。
编译的作用域
父组件模板的内容在父组件作用域内编译,子组件模板的内容在子组件作用域内编译。
单个插槽
template: `
<div>
<slot></slot>
</div>
`
具名插槽
<div id="app">
<a-component>
<h3 slot="header">我是标题</h3>
<p>我是正文内容</p>
<p>我是第二段正文内容</p>
<p slot="footer">我是底部内容</p>
</a-component>
</div>
<script>
Vue.component('a-component', {
template: `
<div>
<div class="header">
<slot name="header"></slot>
</div>
<div class="container">
<slot></slot>
</div>
<div class="footer">
<slot name="footer"></slot>
</div>
</div>
`
})
var app = new Vue({
el: '#app'
})
</script>
作用域插槽
作用域插槽是一种特殊的 slot ,可以从子组件获取数据。
slot 的 name 属性无法获取。
template 模板标签不会被渲染。
在 Vue.js 2.5.0 之后的版本不再需要 template 模板,可以直接写在任意标签里。
<div id="app">
<a-component>
<template slot="xxx" slot-scope="prop">
{{prop.text}}
{{prop.abc}}
</template>
</a-component>
</div>
<script>
Vue.component('a-component', {
template: `
<div>
<slot name="xxx" text="子组件中的数据" abc="123"></slot>
</div>
`
})
var app = new Vue({
el: '#app'
})
</script>
访问 slot
通过 this.$slots.name
访问具名插槽 、this.$slots.default
访问默认插槽。得到的是内容为 vnode 的数组。
mounted(){
console.log(this.$slots.header[0].elm.innerText)
console.log(this.$slots.header[0].elm.innerHTML)
}
动态组件
VUE 给我们提供了一个元素叫 component ,作用是用来挂载动态组件。
<div id="app">
<component :is="view"></component>
<button @click="handleView('a')">第一句</button>
<button @click="handleView('b')">第二句</button>
<button @click="handleView('c')">第三句</button>
<button @click="handleView('d')">第四句</button>
</div>
<script>
Vue.component('a-component', {
template: '<div>锄禾日当午</div>'
})
Vue.component('b-component', {
template: '<div>汗滴禾下土</div>'
})
Vue.component('c-component', {
template: '<div>谁知盘中餐</div>'
})
Vue.component('d-component', {
template: '<div>粒粒皆辛苦</div>'
})
var app = new Vue({
el: '#app',
data: {
view: 'a-component'
},
methods: {
handleView(tag){
this.view = tag + '-component'
}
}
})
</script>