组件最大的价值在于复用。一个组件可以创建多个实例,每个实例相互独立,互不干扰。理解组件复用的原理,是掌握 Vue 组件化的关键。
每次使用组件,都会创建一个新的实例。这些实例各自维护自己的状态。
<div id="app">
<counter></counter>
<counter></counter>
<counter></counter>
</div>
<script>
Vue.component('counter', {
data() {
return {
count: 0
}
},
template: `
<div>
<button @click="count++">点击</button>
<span>{{ count }}</span>
</div>
`
})
new Vue({ el: '#app' })
</script>
三个计数器各自独立,点击一个不会影响其他两个。
因为 data 是函数,每次创建组件实例时,都会调用这个函数,返回一个新的数据对象:
// 第一次使用 <counter>
data() { return { count: 0 } } // 返回对象 A
// 第二次使用 <counter>
data() { return { count: 0 } } // 返回对象 B
// 第三次使用 <counter>
data() { return { count: 0 } } // 返回对象 C
// A、B、C 是三个不同的对象
如果 data 不是函数,所有实例会共享同一个数据对象。
// ❌ 错误:data 是对象
var sharedData = {
count: 0
}
Vue.component('counter', {
data: sharedData,
template: '<button @click="count++">{{ count }}</button>'
})
所有 <counter> 实例共享 sharedData,点击任意一个,其他都会变化。
Vue 会检测组件的 data 是否为函数:
Vue.component('my-component', {
data: {
message: 'Hello'
}
})
// 控制台警告:
// [Vue warn]: The "data" option should be a function
// that returns a per-instance value in component definitions.
Vue 2.x 会警告,但仍会运行
Vue 2.x 只是警告,不会阻止运行。Vue 3.x 会直接报错。
<div id="app">
<product-item
v-for="product in products"
:key="product.id"
:product="product"
></product-item>
</div>
<script>
Vue.component('product-item', {
props: ['product'],
template: `
<div class="product">
<h3>{{ product.name }}</h3>
<p>价格:{{ product.price }}</p>
<button @click="addToCart">加入购物车</button>
</div>
`,
methods: {
addToCart() {
this.$emit('add', this.product)
}
}
})
new Vue({
el: '#app',
data: {
products: [
{ id: 1, name: '商品A', price: 99 },
{ id: 2, name: '商品B', price: 199 },
{ id: 3, name: '商品C', price: 299 }
]
}
})
</script>
每个 <product-item> 接收不同的 product,显示不同内容。
<div id="app">
<alert-box>这是一条成功消息</alert-box>
<alert-box>这是一条警告消息</alert-box>
<alert-box>这是一条错误消息</alert-box>
</div>
<script>
Vue.component('alert-box', {
template: `
<div class="alert">
<strong>提示:</strong>
<slot></slot>
</div>
`
})
</script>
同一个组件,通过插槽展示不同内容。
<div id="app">
<p>总数:{{ total }}</p>
<counter @increment="updateTotal"></counter>
<counter @increment="updateTotal"></counter>
<counter @increment="updateTotal"></counter>
</div>
<script>
Vue.component('counter', {
data() {
return {
count: 0
}
},
template: `
<button @click="increment">{{ count }}</button>
`,
methods: {
increment() {
this.count++
this.$emit('increment', this.count)
}
}
})
new Vue({
el: '#app',
data: {
total: 0
},
methods: {
updateTotal(count) {
this.total++
}
}
})
</script>
组件通过 $emit 向父组件发送事件,父组件更新共享状态。
有时需要组件全局共享状态,可以使用 Vue 实例作为事件总线:
// 创建事件总线
var bus = new Vue()
Vue.component('counter', {
data() {
return {
count: 0
}
},
template: '<button @click="increment">{{ count }}</button>',
methods: {
increment() {
this.count++
bus.$emit('count-changed', this.count)
}
},
created() {
bus.$on('reset', () => {
this.count = 0
})
}
})
// 在任何地方触发重置
bus.$emit('reset')
对于复杂应用,推荐使用 Vuex:
// store.js
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment(state) {
state.count++
}
}
})
// 组件中使用
Vue.component('counter', {
computed: {
count() {
return this.$store.state.count
}
},
template: '<button @click="increment">{{ count }}</button>',
methods: {
increment() {
this.$store.commit('increment')
}
}
})
<div id="app">
<button @click="showModal1 = true">打开模态框1</button>
<button @click="showModal2 = true">打开模态框2</button>
<modal v-if="showModal1" @close="showModal1 = false">
<h2>模态框1</h2>
<p>这是第一个模态框的内容</p>
</modal>
<modal v-if="showModal2" @close="showModal2 = false">
<h2>模态框2</h2>
<p>这是第二个模态框的内容</p>
</modal>
</div>
<script>
Vue.component('modal', {
template: `
<div class="modal-overlay" @click.self="$emit('close')">
<div class="modal-content">
<button class="close" @click="$emit('close')">×</button>
<slot></slot>
</div>
</div>
`
})
new Vue({
el: '#app',
data: {
showModal1: false,
showModal2: false
}
})
</script>
<div id="app">
<tabs>
<tab title="首页">
<h2>首页内容</h2>
<p>欢迎来到首页</p>
</tab>
<tab title="关于">
<h2>关于我们</h2>
<p>这是关于页面</p>
</tab>
<tab title="联系">
<h2>联系方式</h2>
<p>联系我们</p>
</tab>
</tabs>
</div>
<script>
Vue.component('tabs', {
template: `
<div class="tabs">
<ul class="tab-header">
<li
v-for="(tab, index) in tabs"
:key="index"
:class="{ active: index === activeIndex }"
@click="activeIndex = index"
>
{{ tab.title }}
</li>
</ul>
<div class="tab-content">
<slot></slot>
</div>
</div>
`,
data() {
return {
activeIndex: 0,
tabs: []
}
},
mounted() {
this.tabs = this.$children
}
})
Vue.component('tab', {
props: ['title'],
template: `
<div v-show="active">
<slot></slot>
</div>
`,
computed: {
active() {
return this.$parent.activeIndex === this.$parent.$children.indexOf(this)
}
}
})
</script>
<div id="app">
<form @submit.prevent="submit">
<form-input
v-model="form.username"
label="用户名"
:rules="[
{ required: true, message: '请输入用户名' },
{ min: 3, message: '用户名至少3个字符' }
]"
></form-input>
<form-input
v-model="form.email"
label="邮箱"
type="email"
:rules="[
{ required: true, message: '请输入邮箱' },
{ type: 'email', message: '邮箱格式不正确' }
]"
></form-input>
<button type="submit">提交</button>
</form>
</div>
<script>
Vue.component('form-input', {
props: ['value', 'label', 'type', 'rules'],
data() {
return {
error: ''
}
},
template: `
<div class="form-group">
<label>{{ label }}</label>
<input
:type="type || 'text'"
:value="value"
@input="$emit('input', $event.target.value)"
@blur="validate"
>
<span v-if="error" class="error">{{ error }}</span>
</div>
`,
methods: {
validate() {
this.error = ''
if (!this.rules) return
for (let rule of this.rules) {
if (rule.required && !this.value) {
this.error = rule.message
return
}
if (rule.min && this.value.length < rule.min) {
this.error = rule.message
return
}
if (rule.type === 'email') {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!re.test(this.value)) {
this.error = rule.message
return
}
}
}
}
}
})
new Vue({
el: '#app',
data: {
form: {
username: '',
email: ''
}
},
methods: {
submit() {
console.log('提交:', this.form)
}
}
})
</script>
不要在 data 中引用外部对象
// ❌ 错误:外部对象被所有实例共享
var sharedState = { count: 0 }
Vue.component('my-component', {
data() {
return sharedState
}
})
// ✅ 正确:每个实例返回新对象
Vue.component('my-component', {
data() {
return {
count: 0
}
}
})
props 的响应式
props 是响应式的,父组件数据变化会传递给子组件。但子组件不应该直接修改 props:
// ❌ 错误:直接修改 props
props: ['message'],
methods: {
updateMessage() {
this.message = 'new value' // 警告
}
}
// ✅ 正确:使用本地数据或计算属性
props: ['message'],
data() {
return {
localMessage: this.message
}
}
组件复用要点: