30 秒速记
- 父传属性,子发事件,单向数据来源清楚
- 插槽复用结构,暴露必要变化入口
- 深层共享可用注入,跨区域状态再评估存储
- 引用用于有限命令,不成为任意修改后门
- 订阅与事件总线都要管理生命周期
组件化实践的重点,是让组件之间怎样交换信息一眼能看明白。 父级提供输入,子级发事件表达操作,插槽把可变内容交给使用方;深层共享和全局状态再选择相应机制。引用适合聚焦等少量命令,不应让父级随意改子级内部数据。通信方式越隐含,排查越难,所以优先选择最局部、最明确的连接,并给外部订阅安排清理。
版本校准: 本文出现 Object.defineProperty、Dep、Watcher 和双端 Diff 时,主要描述 Vue 2 实现;Vue 3 使用 Proxy、effect 与新的渲染器路径。Vue 2 已于 2023-12-31 结束维护,新项目应以 Vue 3 为基线,旧项目参考 Vue 2 EOL 官方说明 制定迁移与安全策略。
# vue组件化实践
# 组件化
vue组件系统提供了一种抽象,让我们可以使用独立可复用的组件来构建大型应用,任意类型的应用界 面都可以抽象为一个组件树。组件化能提高开发效率,方便重复使用,简化调试步骤,提升项目可维护 性,便于多人协同开发。

# 组件通信常用方式
# 1. props
父给子传值
// child
props: { msg: String }
// parent
<HelloWorld msg="Welcome to Your Vue.js App"/>
# 2. 自定义事件
子给父传值
// child this.$emit('add', good)
// parent
<Cart @add="cartAdd($event)"></Cart>
# 3. 事件总线
任意两个组件之间传值常用事件总线 或 vuex的方式。
// Bus:事件派发、监听和回调管理 class Bus {
constructor(){ this.callbacks = {}
}
$on(name, fn){
this.callbacks[name] = this.callbacks[name] || []
this.callbacks[name].push(fn) }
$emit(name, args){ if(this.callbacks[name]){
this.callbacks[name].forEach(cb => cb(args)) }
} }
// main.js
Vue.prototype.$bus = new Bus()
// child1
this.$bus.$on('foo', handle) // child2 this.$bus.$emit('foo')
实践中通常用Vue代替Bus,因为Vue已经实现了相应接口
# 4. vuex
创建唯一的全局数据管理者store,通过它管理数据并通知组件状态变更。
# 5. $parent/$root
兄弟组件之间通信可通过共同祖辈搭桥,
$parent或$root。
// brother1
this.$parent.$on('foo', handle)
// brother2
this.$parent.$emit('foo')
# 6. $children
父组件可以通过
$children访问子组件实现父子通信。
// parent
this.$children[0].xx = 'xxx'
# 7. $attrs/$listeners
包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 ( class 和 style 除外)。当一个组件没有 声明任何 prop 时,这里会包含所有父作用域的绑定 ( class 和 style 除外),并且可以通过
v- bind="$attrs"传入内部组件——在创建高级别的组件时非常有用。
// child:并未在props中声明foo
<p>{{$attrs.foo}}</p>
// parent
<HelloWorld foo="foo"/>
# 8. refs
获取子节点引用
// parent
<HelloWorld ref="hw"/>
mounted() {
this.$refs.hw.xx = 'xxx'
}
# 9. provide/inject
能够实现祖先和后代之间传值
// ancestor
provide() {
return {foo: 'foo'}
}
// descendant
inject: ['foo']
实例
// index.vue
<template>
<div>
<h2>组件通信</h2>
<!-- props, 自定义事件 -->
<Child1 msg="some msg from parent" @some-event="onSomeEvent"></Child1>
<!-- 事件总线 -->
<Child2 msg="other msg"></Child2>
</div>
</template>
<script>
import Child1 from '@/components/communication/Child1.vue'
import Child2 from '@/components/communication/Child2.vue'
export default {
components: {
Child1, Child2,
// Child3: () => import('./Child3.vue')
},
methods: {
onSomeEvent(msg) {
console.log('Communition:', msg);
}
},
mounted () {
// $children持有所有自定义组件
// 它不保证顺序
console.log(this.$children);
},
}
</script>
<style scoped>
</style>
// child1.vue
<template>
<div @click="$emit('some-event', 'msg from child1')">
<h3>child1</h3>
<p>{{msg}}</p>
</div>
</template>
<script>
export default {
props: {
msg: {
type: String,
default: ''
},
},
mounted () {
// this.$bus.$on('event-from-child2', msg => {
// console.log('Child1:', msg);
// });
this.$parent.$on('event-from-child2', msg => {
console.log('Child1:', msg);
});
},
}
</script>
<style scoped>
</style>
// child2.vue
<template>
<div>
<!-- 展开$attrs对象 -->
<h3 v-bind="$attrs">child2</h3>
<button @click="sendToChild1">给child1发送消息</button>
<!-- $attrs -->
<p>{{$attrs.msg}}</p>
<!-- inject -->
<p>{{foo}}</p>
</div>
</template>
<script>
export default {
inheritAttrs: false,
inject: ['foo'],
methods: {
sendToChild1() {
// 利用事件总线发送事件
// this.$bus.$emit('event-from-child2', 'some msg from child2')
this.$parent.$emit('event-from-child2', 'some msg from child2')
}
},
}
</script>
<style scoped>
</style>
# 插槽
插槽语法是Vue 实现的内容分发 API,用于复合组件开发。该技术在通用组件库开发中有大量应用
# 1. 匿名插槽
// comp1
<div>
<slot></slot>
</div>
// parent
<comp>hello</comp>
# 2. 具名插槽
将内容分发到子组件指定位置
// comp2
<div>
<slot></slot>
<slot name="content"></slot>
</div>
// parent
<Comp2>
<!-- 默认插槽用default做参数 -->
<template v-slot:default>具名插槽</template> <!-- 具名插槽用插槽名做参数 -->
<template v-slot:content>内容...</