前言
Vue 组件是可复用的 Vue 实例,其拥有一个名字,可直接当作 Dom 元素使用,使用次数也没有限制。与函数封装等思想一样,本质都是对重复出现的功能进行复用。但 Vue 组件是对结构、样式、功能的整体封装。利用 Vue 组件,可以实现代码复用与高效开发。以下是一个 Vue 组件的基本定义:
// 定义一个名为 button-counter 的新组件
Vue.component('button-counter', {
data: function () {
return {
count: 0
}
},
template: ''
})
template 里的字符串模板可以理解为要复用的结构部分。可以看到,组件里也有 data,但这里的 data 不同于Vue实例身上的 data,这里的 data 是一个函数。这是因为函数拥有作用域,可以对 data里的数据进行私有限制,防止其数据影响全局实例的数据。因为组件是可复用的 Vue 实例,所以它们与 new Vue 接收相同的选项,例如 data、computed、watch、methods 以及生命周期钩子等。仅有的例外是像 el 这样根实例特有的选项。
当使用 PascalCase 形式命名一个组件时,你在引用这个自定义元素时两种命名法都可以使用。也就是说
全局组件与局部组件
全局组件就是组件的注册位置与 Vue 的根实例在同一作用域下,这些全局组件在任何地方都可以使用,在彼此的内部也可以使用彼此。
Vue.component('component-a', { /* ... */ })
Vue.component('component-b', { /* ... */ })
Vue.component('component-c', { /* ... */ })
new Vue({ el: '#app' })
局部组件就是组件的注册作用域在 Vue 的根实例内,可以在根实例外部声明一个组件对象,然后在 Vue 根实例内的components属性内注册该组件:
var ComponentA = { /* ... */ }
var ComponentB = { /* ... */ }
var ComponentC = { /* ... */ }
new Vue({
el: '#app',
components: {
'component-a': ComponentA,
'component-b': ComponentB
}
})
因为作用域的限制,局部组件不能在彼此内部使用,但因为 Vue 组件本身就是 Vue 实例,其也有一个 components 属性,想要在其内部使用其他局部组件,可以在局部组件内部注册:
// 一般使用
var ComponentA = { /* ... */ }
var ComponentB = {
components: {
'component-a': ComponentA
},
// ...
}
// 模块系统使用
import ComponentA from './ComponentA.vue'
export default {
components: {
ComponentA
},
// ...
}
Prop
prop 是组件的属性,与 data、methods 等属性一样。其名字可以是驼峰命名或 kebab-case 形式,在非字符串模板中使用时,如果在 Dom 中使用组件 prop 则其必须是 kebab-case 形式。prop 可以声明其类型:
props: {
title: String,
likes: Number,
isPublished: Boolean,
commentIds: Array,
author: Object,
callback: Function,
contactsPromise: Promise // or any other constructor
}
prop 可以是静态,也可以是动态,所谓动态就是变量,Vue 通过 v-bind 指令绑定 prop 变量,使得prop 变得灵活、强大起来:
单项数据流
所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。额外的,每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。
也就是说,prop 的作用就是父组件向子组件传递数据。这个数据传递的过程是不可逆的,子组件向父组件传递数据是不允许通过 prop 的。同时,这个 prop 也不能随意为子组件所用,不然会造成各种难题、例如数据混乱、数据污染等。且组件本身已经有了维护数据的 data ,再有一个 prop 做同样的事就显得多余。如果确实需要某个 prop,可以将其在 computed 属性里进行转换储存:
props: ['size'],
computed: {
normalizedSize: function () {
return this.size.trim().toLowerCase()
}
}
prop 验证
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 会在一个组件实例创建之前进行验证,所以实例的 property (如 data、computed 等) 在 default 或 validator 函数中是不可用的。
自定义事件
自定义组件的 v-model
一个组件上的 v-model
默认会利用名为 value
的 prop 和名为 input
的事件,但是像单选框、复选框等类型的输入控件可能会将 value
attribute 用于不同的目的。model
选项可以用来避免这样的冲突:
Vue.component('base-checkbox', {
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: Boolean
},
template: `
`
})
现在在这个组件上使用 v-model 的时候:
这里的 lovingVue 的值将会传入这个名为 checked 的 prop。同时当
关于事件的两个重要 API
父子组件的通信可以通过 props(父传子)以及 $emit
(子传父)来实现,但如果涉及到父传孙时,使用 vm.$attrs
和vm.$listeners
会有用得多。
vm.$attrs
主要是在孙组件上使用,通过 v-bind 命令实现绑定,这个 API 主要用于收集父级组件作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件(包括子组件与孙组件)——在创建高级别的组件时非常有用。
vm.$listeners
主要是在孙组件上使用,通过 v-on 命令实现父组件的事件监听与收集,包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。以下案例是子组件(A组件是父,B组件是子,C组件是孙):
in child1
props: {{ pchild1 }}
$attrs: {{ $attrs }}
在上面代码中出现了一个组件属性:inheritAttrs: false。Vue 官方的解释是:
“ 默认情况下父作用域的不被认作 props 的 attribute 绑定 (attribute bindings) 将会“回退”且作为普通的 HTML attribute 应用在子组件的根元素上。如果你不希望组件的根元素继承特性,你可以在组件的选项中设置 inheritAttrs: false。”
也就是说,父组件的非 props 属性默认会被当做 Dom 元素的 attribute 那样,直接让子组件的根元素继承。
// 父组件
// 子组件
代码中的 aaa 便是这个非 props 属性,尽管 $attrs 值与 inheritAttrs 无关,但当inheritAttrs为true时,在最终渲染后,这个 aaa 会被添加为 子组件根元素的一个属性:
// 渲染后的 element 树显示:
子组件