概述
组件是 Vue.js 最核心的功能之一,它提供了一种抽象,允许我们使用小型、独立和通常可复用的组件构建大型应用。组件可以扩展 HTML 元素,封装可重用的代码。在较高层面上,组件是自定义元素,Vue.js 的编译器为它添加特殊功能。
💡 组件精讲补充:一个再复杂的组件,都是由三部分组成的:prop、event、slot,它们构成了 Vue.js 组件的 API。如果你开发的是一个通用组件,一定要事先设计好这三部分,因为组件一旦发布,后面再修改 API 就很困难了。如果你阅读别人写的组件,也可以从这三个部分展开,它们可以帮助你快速了解一个组件的所有功能。
组件与 Vue 实例的关系
组件是可复用的 Vue 实例,它们与 new Vue 接收相同的选项,例如 data、computed、watch、methods 以及生命周期钩子等。仅有的例外是像 el 这样根实例特有的选项。
// 定义一个名为 button-counter 的新组件
Vue.component('button-counter', {
data: function () {
return {
count: 0
}
},
template: '<button v-on:click="count += 1">点击了 {{ count }} 次</button>'
})组件基础
组件的定义与使用
基本示例
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/vue@2"></script>
</head>
<body>
<div id="components-demo">
<button-counter></button-counter>
</div>
<script>
Vue.component('button-counter', {
data: function () {
return {
count: 0
}
},
template: '<button v-on:click="count += 1">点击了 {{ count }} 次</button>'
})
new Vue({ el: '#components-demo' })
</script>
</body>
</html>组件的复用
组件可以进行任意次数的复用,每使用一次组件,就会有一个新的实例被创建。
<div id="components-demo">
<button-counter></button-counter>
<button-counter></button-counter>
<button-counter></button-counter>
</div>注意:当点击按钮时,每个组件都会各自独立维护它的 count。因为每用一次组件,就会有一个新的实例被创建。
data 必须是一个函数
组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立拷贝:
// 正确 ✓
Vue.component('button-counter', {
data: function () {
return {
count: 0
}
}
})
// 错误 ✗ - 如果 data 是一个对象,所有实例将共享同一个数据对象
Vue.component('button-counter', {
data: {
count: 0 // 这会导致所有组件实例共享同一个 count
}
})单个根元素
每个组件必须只有一个根元素。当模板包含多个元素时,必须用一个父元素包裹它们:
<!-- 错误 ✗ -->
<h3>{{ title }}</h3>
<div v-html="content"></div>
<!-- 正确 ✓ -->
<div class="blog-post">
<h3>{{ title }}</h3>
<div v-html="content"></div>
</div>组件注册
在注册一个组件的时候,需要给它一个名字。定义组件名的方式有两种:
组件命名规范
kebab-case (短横线分隔命名)
Vue.component('my-component-name', { /* ... */ })当使用 kebab-case 定义一个组件时,在引用自定义元素时也要使用 kebab-case,例如 <my-component-name>。
PascalCase (首字母大写命名)
Vue.component('MyComponentName', { /* ... */ })使用 PascalCase 定义一个组件时,在引用这个自定义元素时两种命名法都可以使用。<my-component-name> 和 <MyComponentName> 都是可接受的。
注意:直接在 DOM (即非字符串的模板) 中使用时只有 kebab-case 是有效的。
全局注册
全局注册的组件可以用在任何新创建的 Vue 根实例 (new Vue) 的模板中,也可以在其所有子组件中使用。
Vue.component('component-a', { /* ... */ })
Vue.component('component-b', { /* ... */ })
Vue.component('component-c', { /* ... */ })
new Vue({ el: '#app' })<div id="app">
<component-a></component-a>
<component-b></component-b>
<component-c></component-c>
</div>注意:全局注册的所有组件即便不再使用,它仍然会被包含在最终的构建结果中,这可能造成用户下载的 JavaScript 的无谓增加。
局部注册
局部注册可以避免全局注册带来的问题。可以通过一个普通的 JavaScript 对象来定义组件:
var ComponentA = { /* ... */ }
var ComponentB = { /* ... */ }
var ComponentC = { /* ... */ }
new Vue({
el: '#app',
components: {
'component-a': ComponentA,
'component-b': ComponentB
}
})注意:局部注册的组件在其子组件中不可用。如果希望 ComponentA 在 ComponentB 中可用,需要这样写:
var ComponentA = { /* ... */ }
var ComponentB = {
components: {
'component-a': ComponentA
},
// ...
}ES2015+ 模块系统
在 ES2015+ 中,在对象中放一个类似 ComponentA 的变量名其实是 ComponentA: ComponentA 的缩写:
import ComponentA from './ComponentA.vue'
export default {
components: {
ComponentA // 等价于 ComponentA: ComponentA
},
// ...
}模块系统中的组件注册
推荐创建一个 components 目录,并将每个组件放置在其各自的文件中,然后在局部注册之前导入每个想使用的组件:
import ComponentA from './ComponentA'
import ComponentC from './ComponentC'
export default {
components: {
ComponentA,
ComponentC
},
// ...
}基础组件的自动化全局注册
如果使用 webpack (或在内部使用了 webpack 的 Vue CLI 3+),可以使用 require.context 只全局注册这些非常通用的基础组件:
import Vue from 'vue'
import upperFirst from 'lodash/upperFirst'
import camelCase from 'lodash/camelCase'
const requireComponent = require.context(
'./components',
false,
/Base[A-Z]\w+\.(vue|js)$/
)
requireComponent.keys().forEach(fileName => {
const componentConfig = requireComponent(fileName)
const componentName = upperFirst(
camelCase(
fileName.split('/').pop().replace(/\.\w+$/, '')
)
)
Vue.component(
componentName,
componentConfig.default || componentConfig
)
})注意:全局注册的行为必须在根 Vue 实例创建之前发生。
组件选项
数据选项
data
类型:Object | Function
限制:组件的定义只接受 function
Vue 实例的数据对象。Vue 会递归地把 data 的 property 转换为 getter/setter,从而让 data 的 property 能够响应数据变化。
// 直接创建一个实例
var data = { a: 1 }
var vm = new Vue({
data: data
})
vm.a // => 1
vm.$data === data // => true
// Vue.extend() 中 data 必须是函数
var Component = Vue.extend({
data: function () {
return { a: 1 }
}
})注意:
- 以
_或$开头的 property 不会被 Vue 实例代理,因为可能和 Vue 内置的 property、API 方法冲突 - 如果为
dataproperty 使用了箭头函数,则this不会指向组件实例
// 使用箭头函数时,可以通过第一个参数访问实例
data: vm => ({ a: vm.myProp })props
类型:Array<string> | Object
props 用于接收来自父组件的数据,可以是简单的数组,或者使用对象配置高级选项。
// 简单语法
Vue.component('props-demo-simple', {
props: ['size', 'myMessage']
})
// 对象语法,提供验证
Vue.component('props-demo-advanced', {
props: {
// 检测类型
height: Number,
// 检测类型 + 其他验证
age: {
type: Number,
default: 0,
required: true,
validator: function (value) {
return value >= 0
}
}
}
})验证选项:
type:可以是下列原生构造函数中的一种:String、Number、Boolean、Array、Object、Date、Function、Symbol、任何自定义构造函数、或上述内容组成的数组default:为该 prop 指定默认值。对象或数组的默认值必须从一个工厂函数返回required:定义该 prop 是否是必填项validator:自定义验证函数
computed
类型:{ [key: string]: Function | { get: Function, set: Function } }
计算属性将被混入到 Vue 实例中。所有 getter 和 setter 的 this 上下文自动地绑定为 Vue 实例。
var vm = new Vue({
data: { a: 1 },
computed: {
// 仅读取
aDouble: function () {
return this.a * 2
},
// 读取和设置
aPlus: {
get: function () {
return this.a + 1
},
set: function (v) {
this.a = v - 1
}
}
}
})
vm.aPlus // => 2
vm.aPlus = 3
vm.a // => 2
vm.aDouble // => 4计算属性的结果会被缓存,除非依赖的响应式 property 变化才会重新计算。
注意:如果为一个计算属性使用了箭头函数,则 this 不会指向组件实例:
computed: {
aDouble: vm => vm.a * 2
}methods
类型:{ [key: string]: Function }
methods 将被混入到 Vue 实例中,方法中的 this 自动绑定为 Vue 实例。
var vm = new Vue({
data: { a: 1 },
methods: {
plus: function () {
this.a++
}
}
})
vm.plus()
vm.a // 2注意:不应该使用箭头函数来定义 method 函数,因为箭头函数绑定了父级作用域的上下文,this 将不会指向 Vue 实例。
watch
类型:{ [key: string]: string | Function | Object | Array }
一个对象,键是需要观察的表达式,值是对应回调函数。
var vm = new Vue({
data: {
a: 1,
b: 2,
c: 3,
d: 4,
e: {
f: {
g: 5
}
}
},
watch: {
a: function (val, oldVal) {
console.log('new: %s, old: %s', val, oldVal)
},
// 方法名
b: 'someMethod',
// 深度监听
c: {
handler: function (val, oldVal) { /* ... */ },
deep: true
},
// 立即执行
d: {
handler: 'someMethod',
immediate: true
},
// 回调数组
e: [
'handle1',
function handle2 (val, oldVal) { /* ... */ },
{
handler: function handle3 (val, oldVal) { /* ... */ },
}
],
// 监听嵌套属性
'e.f': function (val, oldVal) { /* ... */ }
}
})watch 选项:
handler:回调函数deep:深度监听,当监听对象内部值的变化时使用immediate:立即执行回调,在侦听开始之后被立即调用
DOM 选项
el
类型:string | Element
限制:只在用 new 创建实例时生效
提供一个在页面上已存在的 DOM 元素作为 Vue 实例的挂载目标。
new Vue({
el: '#app'
})
// 或
new Vue({}).$mount('#app')template
类型:string
一个字符串模板作为 Vue 实例的标识使用。模板将会替换挂载的元素。
Vue.component('my-component', {
template: '<div>自定义组件</div>'
})
// 使用 script 标签
<script type="x-template" id="my-template">
<div>自定义组件</div>
</script>
Vue.component('my-component', {
template: '#my-template'
})render
类型:(createElement: () => VNode) => VNode
字符串模板的代替方案,允许你发挥 JavaScript 最大的编程能力。
Vue.component('my-component', {
render: function (createElement) {
return createElement('div', '自定义组件')
}
})资源选项
directives
类型:Object
包含 Vue 实例可用指令的哈希表。
new Vue({
directives: {
focus: {
inserted: function (el) {
el.focus()
}
}
}
})filters
类型:Object
包含 Vue 实例可用过滤器的哈希表。
new Vue({
filters: {
capitalize: function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
}
}
})components
类型:Object
包含 Vue 实例可用组件的哈希表。
new Vue({
components: {
'my-component': {
template: '<div>自定义组件</div>'
}
}
})组合选项
mixins
类型:Array<Object>
mixins 选项接收一个混入对象的数组。这些混入对象可以像正常的实例对象一样包含实例选项。
var mixin = {
created: function () {
console.log('混入对象的钩子被调用')
}
}
new Vue({
created: function () {
console.log('组件钩子被调用')
},
mixins: [mixin]
})
// => "混入对象的钩子被调用"
// => "组件钩子被调用"混入规则:
- Mixin 钩子按照传入顺序依次调用,并在调用组件自身的钩子之前被调用
- 值为对象的选项(如
methods、components等)将被合并,键冲突时取组件对象的键值
extends
类型:Object | Function
允许声明扩展另一个组件,而无需使用 Vue.extend。
var CompA = { /* ... */ }
var CompB = {
extends: CompA,
// ...
}provide / inject
类型:
- provide:
Object | () => Object - inject:
Array<string> | { [key: string]: string | Symbol | Object }
这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深。
// 父级组件提供
var Provider = {
provide: {
foo: 'bar'
}
}
// 子组件注入
var Child = {
inject: ['foo'],
created () {
console.log(this.foo) // => "bar"
}
}注意:provide 和 inject 绑定并不是可响应的,这是刻意为之的。然而,如果传入了一个可监听的对象,那么其对象的 property 还是可响应的。
// 使用注入的值作为 property 的默认值
const Child = {
inject: ['foo'],
props: {
bar: {
default () {
return this.foo
}
}
}
}
// 设置默认值使其变成可选项
const Child = {
inject: {
foo: { default: 'foo' }
}
}
// 从不同名字的 property 注入
const Child = {
inject: {
foo: {
from: 'bar',
default: 'foo'
}
}
}其他选项
name
类型:string
限制:只有作为组件选项时起作用
允许组件模板递归地调用自身,也便于调试。
Vue.component('recursive-component', {
name: 'recursive-component',
template: `
<div>
<recursive-component v-if="hasChildren"></recursive-component>
</div>
`
})model
类型:{ prop?: string, event?: string }
允许一个自定义组件在使用 v-model 时定制 prop 和 event。
Vue.component('my-checkbox', {
model: {
prop: 'checked',
event: 'change'
},
props: {
value: String,
checked: {
type: Number,
default: 0
}
}
})<!-- 使用 v-model -->
<my-checkbox v-model="foo" value="some value"></my-checkbox>
<!-- 等价于 -->
<my-checkbox :checked="foo" @change="val => { foo = val }" value="some value"></my-checkbox>inheritAttrs
类型:boolean
默认值:true
默认情况下父作用域的不被认作 props 的 attribute 绑定将会"回退"且作为普通的 HTML attribute 应用在子组件的根元素上。通过设置 inheritAttrs 到 false,可以去掉这些默认行为。
Vue.component('base-input', {
inheritAttrs: false,
props: ['label', 'value'],
template: `
<label>
{{ label }}
<input
v-bind="$attrs"
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
>
</label>
`
})注意:这个选项不影响 class 和 style 绑定。
自定义事件
自定义事件是子组件向父组件通信的核心机制。Vue 组件实例提供了 $emit/$on/$off/$once 四个事件方法,基于发布-订阅模式实现。
📖 详细文档:
- 完整 API 参考、命名规范、v-model 语法糖、.sync 修饰符、$listeners 透传 → 自定义事件
- 事件系统的源码级实现(
vm._events事件中心、$emit回调遍历、$once一次性包装)→ 事件处理 - 自定义事件系统原理
插槽
插槽(Slot)是 Vue 的内容分发 API,允许父组件向子组件注入模板内容。包括默认插槽、具名插槽、作用域插槽三种类型。
📖 详细文档:
- 完整插槽语法(v-slot、具名插槽、作用域插槽、动态插槽名、解构 prop)→ 插槽
- 插槽的编译原理(
$slotsvs$scopedSlots、render 函数中的 VNode 映射)→ 插槽 - 插槽原理与底层机制
动态组件
有时在不同组件之间进行动态切换是非常有用的,比如在一个多标签的界面里:
<!DOCTYPE html>
<html>
<head>
<title>Dynamic Components Example</title>
<script src="https://unpkg.com/vue@2"></script>
<style>
.tab-button {
padding: 6px 10px;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
border: 1px solid #ccc;
cursor: pointer;
background: #f0f0f0;
margin-bottom: -1px;
margin-right: -1px;
}
.tab-button:hover {
background: #e0e0e0;
}
.tab-button.active {
background: #e0e0e0;
}
.tab {
border: 1px solid #ccc;
padding: 10px;
}
</style>
</head>
<body>
<div id="dynamic-component-demo">
<button
v-for="tab in tabs"
v-bind:key="tab"
v-bind:class="['tab-button', { active: currentTab === tab }]"
v-on:click="currentTab = tab">
{{ tab }}
</button>
<component v-bind:is="currentTabComponent" class="tab"></component>
</div>
<script>
Vue.component("tab-home", {
template: "<div>Home component</div>"
})
Vue.component("tab-posts", {
template: "<div>Posts component</div>"
})
Vue.component("tab-archive", {
template: "<div>Archive component</div>"
})
new Vue({
el: "#dynamic-component-demo",
data: {
currentTab: "Home",
tabs: ["Home", "Posts", "Archive"]
},
computed: {
currentTabComponent: function () {
return "tab-" + this.currentTab.toLowerCase()
}
}
})
</script>
</body>
</html>解析 DOM 模板时的注意事项
有些 HTML 元素,诸如 <ul>、<ol>、<table> 和 <select>,对于哪些元素可以出现在其内部是有严格限制的。
<!-- 错误 -->
<table>
<blog-post-row></blog-post-row>
</table>
<!-- 正确 -->
<table>
<tr is="blog-post-row"></tr>
</table>注意:如果从以下来源使用模板的话,这条限制是不存在的:
- 字符串 (例如:
template: '...') - 单文件组件 (.vue)
最佳实践
组件设计原则
- 单一职责原则:每个组件应该只做一件事
- 可复用性:组件应该是独立的、可复用的
- 清晰的接口:通过 props 和 events 明确组件的输入输出
Props 最佳实践
- 定义清晰的类型验证
props: {
status: {
type: String,
required: true,
validator: value => ['active', 'inactive', 'pending'].includes(value)
}
}- 为对象和数组类型提供默认值
props: {
items: {
type: Array,
default: () => []
},
config: {
type: Object,
default: () => ({})
}
}- 避免修改 Props
// 错误 ✗
props: ['initialValue'],
methods: {
updateValue() {
this.initialValue = newValue // 不要直接修改
}
}
// 正确 ✓
props: ['initialValue'],
data() {
return {
value: this.initialValue
}
}事件命名最佳实践
- 使用 kebab-case
this.$emit('update-user', user) // ✓
this.$emit('updateUser', user) // ✗- 事件名称要清晰表达含义
this.$emit('user-selected', user) // ✓
this.$emit('select', user) // ✗ 不够明确组件通信最佳实践
- 父子组件通信:使用 props 和 events
- 兄弟组件通信:使用事件总线或 Vuex
- 跨层级组件通信:使用 provide/inject 或 Vuex
性能优化
- 使用计算属性代替 methods
// ✓ 使用计算属性(有缓存)
computed: {
filteredItems() {
return this.items.filter(item => item.active)
}
}
// ✗ 使用 methods(每次调用都重新计算)
methods: {
getFilteredItems() {
return this.items.filter(item => item.active)
}
}- 合理使用 v-if 和 v-show
v-if:条件很少改变时使用v-show:需要频繁切换时使用
- 使用 key 优化列表渲染
<div v-for="item in items" :key="item.id">
{{ item.name }}
</div>常见问题
1. 为什么组件的 data 必须是函数?
如果 data 是一个对象,那么所有组件实例将共享同一个数据对象。使用函数可以确保每个组件实例都有自己独立的数据副本。
2. 如何在组件中使用 v-model?
需要:
- 将
value属性绑定到 prop 上 - 在
input事件被触发时,通过自定义的input事件抛出新值
Vue.component('custom-input', {
props: ['value'],
template: `
<input
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
>
`
})3. 父组件如何监听子组件的事件?
使用 v-on 监听子组件实例的事件:
<child-component v-on:event-name="handler"></child-component>4. 如何实现组件的双向绑定?
使用 .sync 修饰符或自定义 v-model:
<!-- 使用 .sync -->
<text-document v-bind:title.sync="doc.title"></text-document>
<!-- 自定义 v-model -->
<my-checkbox v-model="checked"></my-checkbox>5. 如何访问子组件或父组件?
- 访问父组件:
this.$parent - 访问子组件:
this.$children(不推荐) - 推荐使用
ref访问子组件:
<child-component ref="child"></child-component>this.$refs.child.someMethod()6. 插槽内容访问不到子组件的数据怎么办?
使用作用域插槽:
<!-- 子组件 -->
<slot :user="user"></slot>
<!-- 父组件 -->
<child-component v-slot="{ user }">
{{ user.name }}
</child-component>7. 如何在组件上使用原生事件?
使用 .native 修饰符(已废弃,Vue 3 中移除),或者使用 $listeners:
<!-- Vue 2 方式(已废弃) -->
<my-component @click.native="handleClick"></my-component>
<!-- 推荐方式 -->
<my-component @click="handleClick"></my-component>在组件内部:
Vue.component('my-component', {
template: `
<div @click="$emit('click', $event)">
<!-- content -->
</div>
`
})8. 为什么 Props 的修改没有生效?
Props 是单向数据流,子组件不应该直接修改 props。如果需要修改,应该:
- 将 prop 作为本地数据的初始值
- 使用计算属性转换 prop
- 使用
.sync或自定义事件通知父组件修改
9. 如何动态加载组件?
使用异步组件:
Vue.component('async-component', function (resolve) {
setTimeout(() => {
resolve({
template: '<div>I am async!</div>'
})
}, 1000)
})
// 或者使用 import
Vue.component('async-component', () => import('./AsyncComponent.vue'))10. 组件命名有什么规范?
- 使用 kebab-case(短横线分隔):
my-component - 使用 PascalCase(首字母大写):
MyComponent - 避免 HTML 元素名称和 Vue 保留名称
- 遵循语义化命名原则
总结
Vue 组件是构建 Vue 应用的核心概念,理解和掌握组件的使用对于开发高质量的 Vue 应用至关重要。本文档详细介绍了:
- 组件基础:组件的定义、复用、data 必须是函数等
- 组件注册:全局注册和局部注册的区别及使用场景
- 组件选项:data、props、computed、methods、watch 等选项详解
- Props:类型验证、单向数据流、非 Prop 属性等
- 自定义事件:事件命名、v-model、.sync 修饰符等
- 插槽:具名插槽、作用域插槽等
- 动态组件:组件切换和 DOM 模板解析注意事项
- 最佳实践:组件设计、性能优化等
- 常见问题:开发中的常见问题和解决方案
通过合理使用这些特性,可以构建出高质量、可维护、可复用的 Vue 组件。