组件化开发

组件是 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 }
};

// 全局注册(main.js,全项目可用)
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
// utils/EventBus.js
import Vue from "vue";
export default new Vue();
1
2
3
4
5
6
7
8
9
10
11
12
// 组件 A 发事件
import EventBus from "./utils/EventBus.js";
EventBus.$emit("send", data);

// 组件 B 收事件(在 created 里注册)
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(); }
}
}
};

// 全局注册(main.js)
Vue.directive("focus", {
inserted(el) { el.focus(); }
});
1
<input v-focus>

2. 指令的值

1
2
3
4
5
Vue.directive("color", {
inserted(el, binding) {
el.style.color = binding.value; // 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){} })

八、复习要点

  1. 组件 = 独立可复用的”UI + 逻辑”块,.vue 是三段式(template/script/style)。
  2. 组件里 data 必须是函数(避免实例间共享数据);scoped 隔离样式。
  3. 组件通信四板斧:父传子 props、子传父 $emit、跨组件 EventBus、跨层级 provide/inject。
  4. props 只读,子组件要改就 $emit 让父组件改。
  5. v-model 本质是 :value + @input;组件上 v-model 用 value prop + input 事件。
  6. .sync = :prop + @update:prop,让子组件”直接改”父组件 prop。
  7. 插槽三种:默认插槽、具名插槽(#name)、作用域插槽(子传数据给父)。
  8. 自定义指令:全局 Vue.directive、局部 directives,常用钩子 inserted/update。
  9. 忘了某条,回本文对应小节看示例即可。

下一篇预告:vue-router 路由——路由基本使用、导航传参、编程式导航、路由模式等。