组件化开发
组件是 Vue 最重要的概念——把页面拆成一块块**可复用的”积木”**,每个组件有自己的模板、数据、样式和逻辑。
这一篇讲组件的基础(注册、scoped、data 函数)和进阶(组件通信、v-model 原理、$refs、插槽、自定义指令)。其中组件通信是 Vue 的绝对重点。
Java 对照:组件 ≈ 一个”封装了 UI + 逻辑的类”,props 类似构造参数(只读传入),$emit 类似回调通知父组件。
一、组件基础
1. 单文件组件 .vue 三段式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <template> <div class="box">{{ msg }}</div> </template>
<script> export default { data() { return { msg: "hello" }; } }; </script>
<style scoped> .box { color: red; } </style>
|
2. 组件注册
1 2 3 4 5 6 7 8 9
| import MyComponent from "./MyComponent.vue"; export default { components: { MyComponent } };
import MyComponent from "./MyComponent.vue"; Vue.component("MyComponent", MyComponent);
|
3. scoped 样式隔离
1 2 3
| <style scoped> /* 加了 scoped,样式只作用于当前组件,不会污染其他组件 */ </style>
|
4. data 为什么是函数(重点)
1 2 3 4 5 6 7
| data: { count: 0 }
data() { return { count: 0 }; }
|
⚠️ 组件复用时如果 data 是对象,多个实例会共用一份数据、相互干扰。所以组件里 data 必须是函数,返回新对象。
二、组件通信(重点)
1. 父传子:props
1 2
| <!-- 父组件 --> <Son :msg="message" :count="10" />
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| <!-- 子组件 --> <script> export default { props: { msg: String, // 类型校验 count: { type: Number, required: true, // 必填 default: 0 // 默认值 } } }; </script>
|
⚠️ props 是只读的,子组件不能直接改 props。要改,就通过 $emit 让父组件改。
2. 子传父:自定义事件 $emit
1 2 3 4 5 6 7 8 9 10 11 12 13
| <!-- 子组件 --> <button @click="$emit('add', 1)">+1</button> <!-- 或 --> <button @click="doAdd">+1</button> <script> export default { methods: { doAdd() { this.$emit("add", 1); // 触发自定义事件,携带参数 } } }; </script>
|
1 2 3 4 5 6 7 8 9
| <!-- 父组件 --> <Son @add="handleAdd" /> <script> export default { methods: { handleAdd(n) { this.count += n; } } }; </script>
|
3. 事件总线 EventBus(任意组件通信)
兄弟组件或任意组件间通信,用一个空的 Vue 实例当”中转站”:
1 2 3
| import Vue from "vue"; export default new Vue();
|
1 2 3 4 5 6 7 8 9 10 11 12
| import EventBus from "./utils/EventBus.js"; EventBus.$emit("send", data);
import EventBus from "./utils/EventBus.js"; EventBus.$on("send", data => { });
beforeDestroy() { EventBus.$off("send"); }
|
4. provide / inject(跨层级传递)
祖先后代跨多层传值,不用一层层传 props:
1 2 3 4 5 6 7
| provide() { return { msg: "hello" }; }
inject: ["msg"]
|
三、v-model 原理与组件封装
1. v-model 本质
1 2 3
| <input v-model="msg">
<input :value="msg" @input="msg = $event.target.value">
|
2. 组件上使用 v-model
1 2 3
| <!-- 父组件 --> <MyInput v-model="msg" /> <!-- 等价于 :value + @input -->
|
1 2 3 4 5 6 7 8 9
| <!-- 子组件 MyInput.vue --> <template> <input :value="value" @input="$emit('input', $event.target.value)" /> </template> <script> export default { props: ["value"] // 接收 value,触发 input }; </script>
|
组件上 v-model 默认用 value prop + input 事件,让父组件能直接 v-model 绑定子组件。
3. .sync 修饰符
让子组件”直接改”父组件的 prop:
1 2 3
| <!-- 父组件 --> <MyDialog :visible.sync="isShow" /> <!-- 等价于 :visible="isShow" @update:visible="isShow = $event" -->
|
1 2
| this.$emit("update:visible", false);
|
记忆:.sync = :prop + @update:prop 的语法糖,常用于弹窗显示/隐藏、开关状态。
四、$refs 获取 DOM / 组件
1 2 3 4 5 6 7 8 9 10 11 12 13
| <template> <div ref="box">内容</div> <MyChart ref="chart" /> </template> <script> export default { mounted() { this.$refs.box; // 获取 DOM 元素 this.$refs.chart; // 获取组件实例(可调用它的方法) this.$refs.chart.init(); } }; </script>
|
五、插槽 slot
插槽让父组件往子组件里塞自定义内容,提高组件复用性。
1. 默认插槽
1 2 3 4 5 6 7 8 9 10 11
| <!-- 子组件 MyDialog --> <template> <div class="dialog"> <slot></slot> <!-- 父组件内容插到这里 --> </div> </template>
<!-- 父组件 --> <MyDialog> <p>自定义内容</p> </MyDialog>
|
2. 后备内容(默认内容)
1
| <slot>这是默认内容,父组件不传时显示</slot>
|
3. 具名插槽
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <!-- 子组件 --> <template> <div> <header><slot name="header"></slot></header> <main><slot></slot></main> <!-- 默认插槽 --> <footer><slot name="footer"></slot></footer> </div> </template>
<!-- 父组件(v-slot 简写 #) --> <MyDialog> <template #header>标题</template> <p>正文</p> <template #footer>底部</template> </MyDialog>
|
4. 作用域插槽
子组件把数据传给父组件的插槽内容:
1 2 3 4 5 6 7
| <!-- 子组件:把 item 传给插槽 --> <slot :row="item"></slot>
<!-- 父组件:接收数据 --> <template #default="slotProps"> {{ slotProps.row }} </template>
|
作用域插槽常用于”列表渲染交给父组件自定义”(如封装的表格组件)。
六、自定义指令
1. 注册
1 2 3 4 5 6 7 8 9 10 11 12 13
| export default { directives: { focus: { inserted(el) { el.focus(); } } } };
Vue.directive("focus", { inserted(el) { el.focus(); } });
|
2. 指令的值
1 2 3 4 5
| Vue.directive("color", { inserted(el, binding) { el.style.color = binding.value; } });
|
1
| <p v-color="'red'">文字</p>
|
3. v-loading 封装(示例)
1 2 3 4 5 6 7 8 9 10
| Vue.directive("loading", { inserted(el, binding) { if (binding.value) el.classList.add("loading"); }, update(el, binding) { binding.value ? el.classList.add("loading") : el.classList.remove("loading"); } });
|
1
| <div v-loading="isLoading">内容</div>
|
常用指令钩子:bind(绑定)、inserted(插入 DOM)、update(更新)、unbind(解绑)。
七、速查表
| 需求 |
写法 |
| 局部注册组件 |
components: { MyComponent } |
| 全局注册组件 |
Vue.component("MyComponent", MyComponent) |
| 样式隔离 |
<style scoped> |
| 组件 data 用函数 |
data() { return {} } |
| 父传子 |
props: { msg: String } + :msg="x" |
| 子传父 |
this.$emit("add", 1) + @add="fn" |
| 任意组件通信 |
EventBus:$emit / $on / $off |
| 跨层级传值 |
provide() / inject: [] |
| 组件 v-model |
:value prop + $emit("input", val) |
| .sync |
:prop.sync + $emit("update:prop", val) |
| 获取 DOM/组件 |
this.$refs.xxx |
| 默认插槽 |
<slot></slot> |
| 具名插槽 |
<slot name="header"> + <template #header> |
| 作用域插槽 |
<slot :row="item"> + #default="props" |
| 自定义指令 |
Vue.directive("name", { inserted(el, binding){} }) |
八、复习要点
- 组件 = 独立可复用的”UI + 逻辑”块,
.vue 是三段式(template/script/style)。
- 组件里 data 必须是函数(避免实例间共享数据);
scoped 隔离样式。
- 组件通信四板斧:父传子
props、子传父 $emit、跨组件 EventBus、跨层级 provide/inject。
- props 只读,子组件要改就
$emit 让父组件改。
v-model 本质是 :value + @input;组件上 v-model 用 value prop + input 事件。
.sync = :prop + @update:prop,让子组件”直接改”父组件 prop。
- 插槽三种:默认插槽、具名插槽(
#name)、作用域插槽(子传数据给父)。
- 自定义指令:全局
Vue.directive、局部 directives,常用钩子 inserted/update。
- 忘了某条,回本文对应小节看示例即可。
下一篇预告:vue-router 路由——路由基本使用、导航传参、编程式导航、路由模式等。