Vue2 组件
组件注册
全局注册
js
import Vue from 'vue'
import MyComponent from './MyComponent.vue'
Vue.component('my-component', MyComponent)import Vue from 'vue'
import MyComponent from './MyComponent.vue'
Vue.component('my-component', MyComponent)全局注册的组件在任何实例中都能使用。
局部注册
vue
<template>
<div>
<child-component></child-component>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue'
export default {
components: {
ChildComponent
}
}
</script><template>
<div>
<child-component></child-component>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue'
export default {
components: {
ChildComponent
}
}
</script>局部注册的组件只在当前组件中可用。
Props — 父传子
基础用法
vue
<!-- 父组件 -->
<template>
<child-component
:title="title"
:count="count"
:list="list"
:user-info="userInfo"
/>
</template>
<!-- 子组件 ChildComponent.vue -->
<template>
<div>
<h3>{{ title }}</h3>
<p>数量:{{ count }}</p>
<p>用户:{{ userInfo.name }}</p>
</div>
</template>
<script>
export default {
// 数组声明(简单)
props: ['title', 'count', 'list', 'userInfo']
}
</script><!-- 父组件 -->
<template>
<child-component
:title="title"
:count="count"
:list="list"
:user-info="userInfo"
/>
</template>
<!-- 子组件 ChildComponent.vue -->
<template>
<div>
<h3>{{ title }}</h3>
<p>数量:{{ count }}</p>
<p>用户:{{ userInfo.name }}</p>
</div>
</template>
<script>
export default {
// 数组声明(简单)
props: ['title', 'count', 'list', 'userInfo']
}
</script>类型校验
js
export default {
props: {
title: String,
count: {
type: Number,
required: true,
default: 0
},
userInfo: {
type: Object,
default() {
return { name: '默认用户' }
}
},
status: {
type: String,
validator(value) {
return ['success', 'warning', 'error'].includes(value)
}
}
}
}export default {
props: {
title: String,
count: {
type: Number,
required: true,
default: 0
},
userInfo: {
type: Object,
default() {
return { name: '默认用户' }
}
},
status: {
type: String,
validator(value) {
return ['success', 'warning', 'error'].includes(value)
}
}
}
}单向数据流: props 不可在子组件中直接修改,需要通过 $emit 通知父组件修改。
$emit — 子传父
vue
<!-- 父组件 -->
<template>
<child-component @update-count="handleUpdate" @submit="handleSubmit" />
</template>
<script>
export default {
methods: {
handleUpdate(val) {
this.count = val
},
handleSubmit(data) {
console.log('提交数据:', data)
}
}
}
</script><!-- 父组件 -->
<template>
<child-component @update-count="handleUpdate" @submit="handleSubmit" />
</template>
<script>
export default {
methods: {
handleUpdate(val) {
this.count = val
},
handleSubmit(data) {
console.log('提交数据:', data)
}
}
}
</script>vue
<!-- 子组件 ChildComponent.vue -->
<template>
<div>
<button @click="$emit('update-count', count + 1)">增加</button>
<button @click="submitForm">提交</button>
</div>
</template>
<script>
export default {
data() {
return { count: 0 }
},
methods: {
submitForm() {
this.$emit('submit', { name: 'test', age: 18 })
}
}
}
</script><!-- 子组件 ChildComponent.vue -->
<template>
<div>
<button @click="$emit('update-count', count + 1)">增加</button>
<button @click="submitForm">提交</button>
</div>
</template>
<script>
export default {
data() {
return { count: 0 }
},
methods: {
submitForm() {
this.$emit('submit', { name: 'test', age: 18 })
}
}
}
</script>.sync 修饰符
vue
<!-- 父组件 -->
<template>
<child :title.sync="title" />
<!-- 等价于:<child :title="title" @update:title="val => title = val" /> -->
</template><!-- 父组件 -->
<template>
<child :title.sync="title" />
<!-- 等价于:<child :title="title" @update:title="val => title = val" /> -->
</template>vue
<!-- 子组件 -->
<script>
export default {
props: ['title'],
methods: {
updateTitle() {
this.$emit('update:title', '新标题')
}
}
}
</script><!-- 子组件 -->
<script>
export default {
props: ['title'],
methods: {
updateTitle() {
this.$emit('update:title', '新标题')
}
}
}
</script>组件通信 8 种方式
1. props / $emit — 父子通信
父子组件之间最常用的方式。
2. $refs — 父访问子
vue
<template>
<child-component ref="child" />
</template>
<script>
export default {
mounted() {
// 访问子组件实例
this.$refs.child.focus()
console.log(this.$refs.child.title)
}
}
</script><template>
<child-component ref="child" />
</template>
<script>
export default {
mounted() {
// 访问子组件实例
this.$refs.child.focus()
console.log(this.$refs.child.title)
}
}
</script>3. $parent / $children — 实例访问
js
// 子组件访问父组件
this.$parent.xxx
// 父组件访问所有子组件
this.$children // 数组,不保证顺序// 子组件访问父组件
this.$parent.xxx
// 父组件访问所有子组件
this.$children // 数组,不保证顺序注意: $parent 和 $children 应节制使用,推荐 props 和 events 进行通信。
4. provide / inject — 跨级通信
js
// 祖先组件
export default {
provide: {
theme: 'dark',
user: { name: '张三' }
}
}
// 后代组件
export default {
inject: ['theme', 'user'],
mounted() {
console.log(this.theme) // 'dark'
}
}// 祖先组件
export default {
provide: {
theme: 'dark',
user: { name: '张三' }
}
}
// 后代组件
export default {
inject: ['theme', 'user'],
mounted() {
console.log(this.theme) // 'dark'
}
}provide 默认不是响应式的,若要响应式需传对象或函数:
js
// 祖先组件 — 传整个实例以实现响应式
export default {
data() {
return { theme: 'dark' }
},
provide() {
return {
theme: this.theme, // 非响应式
updateTheme: this.updateTheme // 方法可以传递
}
},
methods: {
updateTheme(val) {
this.theme = val
}
}
}// 祖先组件 — 传整个实例以实现响应式
export default {
data() {
return { theme: 'dark' }
},
provide() {
return {
theme: this.theme, // 非响应式
updateTheme: this.updateTheme // 方法可以传递
}
},
methods: {
updateTheme(val) {
this.theme = val
}
}
}5. eventBus — 兄弟/任意组件通信
js
// main.js
Vue.prototype.$bus = new Vue()// main.js
Vue.prototype.$bus = new Vue()js
// 组件 A — 发送
export default {
methods: {
sendMessage() {
this.$bus.$emit('message', 'Hello from A')
}
}
}
// 组件 B — 接收
export default {
created() {
this.$bus.$on('message', (msg) => {
console.log(msg)
})
},
beforeDestroy() {
// 必须销毁,防止内存泄漏
this.$bus.$off('message')
}
}// 组件 A — 发送
export default {
methods: {
sendMessage() {
this.$bus.$emit('message', 'Hello from A')
}
}
}
// 组件 B — 接收
export default {
created() {
this.$bus.$on('message', (msg) => {
console.log(msg)
})
},
beforeDestroy() {
// 必须销毁,防止内存泄漏
this.$bus.$off('message')
}
}注意: 项目较大时 eventBus 难以维护,推荐 Vuex。
6. $attrs / $listeners — 透传属性和事件
主要用于二次封装组件时透传 props 和事件。
vue
<!-- 父组件 -->
<template>
<base-input
placeholder="请输入"
type="text"
@focus="onFocus"
@blur="onBlur"
/>
</template><!-- 父组件 -->
<template>
<base-input
placeholder="请输入"
type="text"
@focus="onFocus"
@blur="onBlur"
/>
</template>vue
<!-- BaseInput.vue — 二次封装 -->
<template>
<div>
<input
v-bind="$attrs" <!-- 透传所有未被 props 接收的属性 -->
v-on="$listeners" <!-- 透传所有事件 -->
/>
</div>
</template>
<script>
export default {
inheritAttrs: false // 阻止根元素继承未识别的属性
}
</script><!-- BaseInput.vue — 二次封装 -->
<template>
<div>
<input
v-bind="$attrs" <!-- 透传所有未被 props 接收的属性 -->
v-on="$listeners" <!-- 透传所有事件 -->
/>
</div>
</template>
<script>
export default {
inheritAttrs: false // 阻止根元素继承未识别的属性
}
</script>7. Vuex — 全局状态管理
详见 Vue-状态管理。
8. localStorage / sessionStorage
适合持久化非敏感数据。
js
// 存储
localStorage.setItem('token', 'xxx')
// 读取
const token = localStorage.getItem('token')
// 注意:数据变化需要手动更新视图// 存储
localStorage.setItem('token', 'xxx')
// 读取
const token = localStorage.getItem('token')
// 注意:数据变化需要手动更新视图插槽 (Slot)
默认插槽
vue
<!-- 子组件 Child.vue -->
<template>
<div class="card">
<slot>默认内容(没传内容时显示)</slot>
</div>
</template>
<!-- 父组件 -->
<template>
<child>
<p>这里替换插槽内容</p>
</child>
</template><!-- 子组件 Child.vue -->
<template>
<div class="card">
<slot>默认内容(没传内容时显示)</slot>
</div>
</template>
<!-- 父组件 -->
<template>
<child>
<p>这里替换插槽内容</p>
</child>
</template>具名插槽
vue
<!-- 子组件 Layout.vue -->
<template>
<div class="layout">
<header>
<slot name="header" />
</header>
<main>
<slot />
</main>
<footer>
<slot name="footer" />
</footer>
</div>
</template>
<!-- 父组件 -->
<template>
<layout>
<template #header>
<h1>标题</h1>
</template>
<p>默认插槽内容</p>
<template #footer>
<p>底部信息</p>
</template>
</layout>
</template><!-- 子组件 Layout.vue -->
<template>
<div class="layout">
<header>
<slot name="header" />
</header>
<main>
<slot />
</main>
<footer>
<slot name="footer" />
</footer>
</div>
</template>
<!-- 父组件 -->
<template>
<layout>
<template #header>
<h1>标题</h1>
</template>
<p>默认插槽内容</p>
<template #footer>
<p>底部信息</p>
</template>
</layout>
</template>作用域插槽
子组件向父组件传递插槽数据。
vue
<!-- 子组件 List.vue -->
<template>
<ul>
<li v-for="item in items" :key="item.id">
<slot :item="item" :index="index">
{{ item.name }} <!-- 默认显示 -->
</slot>
</li>
</ul>
</template>
<script>
export default {
data() {
return {
items: [
{ id: 1, name: '张三', age: 18 },
{ id: 2, name: '李四', age: 20 }
]
}
}
}
</script><!-- 子组件 List.vue -->
<template>
<ul>
<li v-for="item in items" :key="item.id">
<slot :item="item" :index="index">
{{ item.name }} <!-- 默认显示 -->
</slot>
</li>
</ul>
</template>
<script>
export default {
data() {
return {
items: [
{ id: 1, name: '张三', age: 18 },
{ id: 2, name: '李四', age: 20 }
]
}
}
}
</script>vue
<!-- 父组件 — 使用插槽定制渲染 -->
<template>
<list>
<template #default="{ item, index }">
<span>{{ index }} - {{ item.name }}({{ item.age }}岁)</span>
</template>
</list>
</template><!-- 父组件 — 使用插槽定制渲染 -->
<template>
<list>
<template #default="{ item, index }">
<span>{{ index }} - {{ item.name }}({{ item.age }}岁)</span>
</template>
</list>
</template>作用域插槽解构
vue
<template>
<!-- 解构 prop -->
<list v-slot="{ item }">
<span>{{ item.name }}</span>
</list>
<!-- 解构 + 重命名 -->
<list v-slot="{ item: user }">
<span>{{ user.name }}</span>
</list>
<!-- 具名 + 作用域 -->
<template #header="{ title }">
<h1>{{ title }}</h1>
</template>
</template><template>
<!-- 解构 prop -->
<list v-slot="{ item }">
<span>{{ item.name }}</span>
</list>
<!-- 解构 + 重命名 -->
<list v-slot="{ item: user }">
<span>{{ user.name }}</span>
</list>
<!-- 具名 + 作用域 -->
<template #header="{ title }">
<h1>{{ title }}</h1>
</template>
</template>生命周期
生命周期图示
new Vue()
│
beforeCreate data 和 methods 还未初始化
│
created data 和 methods 已初始化,可访问,但未挂载 DOM
│
beforeMount 模板已编译,虚拟 DOM 已创建,尚未渲染
│
mounted DOM 已挂载,可操作 DOM,适合发请求
│
│ (数据变化时)
│
beforeUpdate 数据已变化,DOM 尚未更新
│
updated DOM 已更新
│
│ (组件销毁时)
│
beforeDestroy 组件销毁前,适合清理定时器/事件监听
│
destroyed 组件已销毁new Vue()
│
beforeCreate data 和 methods 还未初始化
│
created data 和 methods 已初始化,可访问,但未挂载 DOM
│
beforeMount 模板已编译,虚拟 DOM 已创建,尚未渲染
│
mounted DOM 已挂载,可操作 DOM,适合发请求
│
│ (数据变化时)
│
beforeUpdate 数据已变化,DOM 尚未更新
│
updated DOM 已更新
│
│ (组件销毁时)
│
beforeDestroy 组件销毁前,适合清理定时器/事件监听
│
destroyed 组件已销毁生命周期用途总结
| 钩子 | 用途 |
|---|---|
beforeCreate | 极少使用,实例初始化前 |
created | 可访问 data/methods,适合初始化数据 |
beforeMount | 极少使用,编译完成未渲染 |
mounted | 最常用,发请求、操作 DOM、加事件监听 |
beforeUpdate | 数据更新前,可获取旧 DOM |
updated | 数据更新后,避免在此改数据(死循环) |
beforeDestroy | 清理:清除定时器、移除事件监听、取消订阅 |
destroyed | 实例已销毁 |
activated | keep-alive 组件激活时 |
deactivated | keep-alive 组件停用时 |
errorCaptured | 捕获后代组件错误 |
js
export default {
// 创建
created() {
this.fetchInitData()
},
// 挂载
mounted() {
window.addEventListener('resize', this.handleResize)
this.getList()
},
// 更新
beforeUpdate() {
console.log('数据变化,DOM 未更新')
},
updated() {
console.log('DOM 已更新')
},
// 销毁
beforeDestroy() {
window.removeEventListener('resize', this.handleResize)
clearInterval(this.timer)
},
// keep-alive
activated() {
console.log('组件激活')
},
deactivated() {
console.log('组件停用')
},
// 错误捕获
errorCaptured(err, vm, info) {
console.error('捕获错误:', err, info)
return false // 阻止错误向上传播
}
}export default {
// 创建
created() {
this.fetchInitData()
},
// 挂载
mounted() {
window.addEventListener('resize', this.handleResize)
this.getList()
},
// 更新
beforeUpdate() {
console.log('数据变化,DOM 未更新')
},
updated() {
console.log('DOM 已更新')
},
// 销毁
beforeDestroy() {
window.removeEventListener('resize', this.handleResize)
clearInterval(this.timer)
},
// keep-alive
activated() {
console.log('组件激活')
},
deactivated() {
console.log('组件停用')
},
// 错误捕获
errorCaptured(err, vm, info) {
console.error('捕获错误:', err, info)
return false // 阻止错误向上传播
}
}父组件和子组件生命周期顺序
父 beforeCreate → 父 created → 父 beforeMount
→ 子 beforeCreate → 子 created → 子 beforeMount → 子 mounted
→ 父 mounted父 beforeCreate → 父 created → 父 beforeMount
→ 子 beforeCreate → 子 created → 子 beforeMount → 子 mounted
→ 父 mounted监听子组件生命周期
vue
<template>
<!-- 在父组件中监听子组件的生命周期钩子 -->
<child @hook:mounted="onChildMounted" @hook:updated="onChildUpdated" />
</template>
<script>
export default {
methods: {
onChildMounted() {
console.log('子组件已挂载')
}
}
}
</script><template>
<!-- 在父组件中监听子组件的生命周期钩子 -->
<child @hook:mounted="onChildMounted" @hook:updated="onChildUpdated" />
</template>
<script>
export default {
methods: {
onChildMounted() {
console.log('子组件已挂载')
}
}
}
</script>动态组件
基本用法
vue
<template>
<div>
<button @click="currentComponent = 'ComponentA'">组件A</button>
<button @click="currentComponent = 'ComponentB'">组件B</button>
<component :is="currentComponent" />
</div>
</template>
<script>
import ComponentA from './ComponentA.vue'
import ComponentB from './ComponentB.vue'
export default {
components: { ComponentA, ComponentB },
data() {
return { currentComponent: 'ComponentA' }
}
}
</script><template>
<div>
<button @click="currentComponent = 'ComponentA'">组件A</button>
<button @click="currentComponent = 'ComponentB'">组件B</button>
<component :is="currentComponent" />
</div>
</template>
<script>
import ComponentA from './ComponentA.vue'
import ComponentB from './ComponentB.vue'
export default {
components: { ComponentA, ComponentB },
data() {
return { currentComponent: 'ComponentA' }
}
}
</script>keep-alive 缓存组件
vue
<template>
<div>
<!-- 动态组件切换时,默认会销毁/重建 -->
<!-- 使用 keep-alive 可以缓存组件状态 -->
<keep-alive>
<component :is="currentComponent" />
</keep-alive>
<!-- 条件缓存:include/exclude 匹配组件 name -->
<keep-alive :include="['ComponentA', 'ComponentC']">
<component :is="currentComponent" />
</keep-alive>
<!-- 最大缓存数 -->
<keep-alive :max="5">
<component :is="currentComponent" />
</keep-alive>
</div>
</template><template>
<div>
<!-- 动态组件切换时,默认会销毁/重建 -->
<!-- 使用 keep-alive 可以缓存组件状态 -->
<keep-alive>
<component :is="currentComponent" />
</keep-alive>
<!-- 条件缓存:include/exclude 匹配组件 name -->
<keep-alive :include="['ComponentA', 'ComponentC']">
<component :is="currentComponent" />
</keep-alive>
<!-- 最大缓存数 -->
<keep-alive :max="5">
<component :is="currentComponent" />
</keep-alive>
</div>
</template>关于组件 name
js
export default {
// name 的作用
name: 'MyComponent'
// 1. keep-alive 的 include/exclude 匹配
// 2. Vue DevTools 显示
// 3. 递归组件调用自身
}export default {
// name 的作用
name: 'MyComponent'
// 1. keep-alive 的 include/exclude 匹配
// 2. Vue DevTools 显示
// 3. 递归组件调用自身
}函数式组件
函数式组件无状态、无实例、无 this,渲染开销低。
js
// 全局注册
Vue.component('smart-list', {
functional: true,
props: {
items: Array,
isOrdered: Boolean
},
render(h, context) {
const children = context.props.items.map(item => h('li', item.name))
return context.props.isOrdered ? h('ol', children) : h('ul', children)
}
})// 全局注册
Vue.component('smart-list', {
functional: true,
props: {
items: Array,
isOrdered: Boolean
},
render(h, context) {
const children = context.props.items.map(item => h('li', item.name))
return context.props.isOrdered ? h('ol', children) : h('ul', children)
}
})vue
<!-- 单文件函数式组件 -->
<template functional>
<div>
<h3>{{ props.title }}</h3>
<slot />
</div>
</template><!-- 单文件函数式组件 -->
<template functional>
<div>
<h3>{{ props.title }}</h3>
<slot />
</div>
</template>Mixins — 混入
基础用法
js
// mixins/tableMixin.js
export const tableMixin = {
data() {
return {
loading: false,
list: [],
pagination: {
page: 1,
size: 10,
total: 0
}
}
},
methods: {
async fetchData() {
this.loading = true
try {
const res = await this.getList()
this.list = res.data.list
this.pagination.total = res.data.total
} finally {
this.loading = false
}
}
}
}// mixins/tableMixin.js
export const tableMixin = {
data() {
return {
loading: false,
list: [],
pagination: {
page: 1,
size: 10,
total: 0
}
}
},
methods: {
async fetchData() {
this.loading = true
try {
const res = await this.getList()
this.list = res.data.list
this.pagination.total = res.data.total
} finally {
this.loading = false
}
}
}
}js
// 组件中使用
import { tableMixin } from '@/mixins/tableMixin'
export default {
mixins: [tableMixin],
data() {
return { otherData: [] }
},
mounted() {
this.fetchData()
}
}// 组件中使用
import { tableMixin } from '@/mixins/tableMixin'
export default {
mixins: [tableMixin],
data() {
return { otherData: [] }
},
mounted() {
this.fetchData()
}
}选项合并规则
- data:递归合并,组件 data 优先级高于 mixin
- 生命周期钩子:合并为数组,mixin 钩子先执行,组件钩子后执行
- methods / computed:同名时组件选项覆盖 mixin
全局混入
js
// main.js — 谨慎使用
Vue.mixin({
created() {
console.log('全局 created')
}
})// main.js — 谨慎使用
Vue.mixin({
created() {
console.log('全局 created')
}
})全局 mixin 会影响所有组件,通常只用于插件开发。
Extends — 组件继承
js
// BaseComponent.js
export default {
data() {
return { baseData: 'base' }
},
methods: {
baseMethod() {
console.log('base method')
}
}
}// BaseComponent.js
export default {
data() {
return { baseData: 'base' }
},
methods: {
baseMethod() {
console.log('base method')
}
}
}js
// 组件继承
import BaseComponent from './BaseComponent'
export default {
extends: BaseComponent,
data() {
return { childData: 'child' }
},
mounted() {
console.log(this.baseData) // 'base'
this.baseMethod() // 'base method'
}
}// 组件继承
import BaseComponent from './BaseComponent'
export default {
extends: BaseComponent,
data() {
return { childData: 'child' }
},
mounted() {
console.log(this.baseData) // 'base'
this.baseMethod() // 'base method'
}
}extends 的合并策略与 mixins 相同。
边界情况
$root
js
// 访问根组件实例
this.$root.count
this.$root.$emit('global-event')// 访问根组件实例
this.$root.count
this.$root.$emit('global-event')不建议在子组件中大量依赖 $root,会导致组件难以测试和维护。
内联模板
vue
<my-component inline-template>
<div>
<p>内联模板内容,直接绑定了组件的数据和方法</p>
<button @click="sayHello">点击</button>
</div>
</my-component><my-component inline-template>
<div>
<p>内联模板内容,直接绑定了组件的数据和方法</p>
<button @click="sayHello">点击</button>
</div>
</my-component>X-Template
html
<script type="text/x-template" id="hello-template">
<p>Hello {{ name }}</p>
</script><script type="text/x-template" id="hello-template">
<p>Hello {{ name }}</p>
</script>js
Vue.component('hello', {
template: '#hello-template',
data() {
return { name: 'Vue' }
}
})Vue.component('hello', {
template: '#hello-template',
data() {
return { name: 'Vue' }
}
})
Cao Note