Compare commits
7 Commits
39b773f409
...
b73a52c022
Author | SHA1 | Date |
---|---|---|
|
b73a52c022 | |
|
90e512b977 | |
|
3872c8f29e | |
|
76ef5f7e5b | |
|
f2b0e74cd2 | |
|
e6e280d8f1 | |
|
9ecf17621e |
|
@ -4,13 +4,16 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"serve": "vue-cli-service serve",
|
||||||
|
"dev": "pnpm serve",
|
||||||
"build": "vue-cli-service build",
|
"build": "vue-cli-service build",
|
||||||
"lint": "vue-cli-service lint"
|
"lint": "vue-cli-service lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"animate.css": "^4.1.1",
|
||||||
"core-js": "^3.8.3",
|
"core-js": "^3.8.3",
|
||||||
|
"nanoid": "^5.1.5",
|
||||||
"vue": "^2.6.14",
|
"vue": "^2.6.14",
|
||||||
"vue-router": "3",
|
"vue-router": "^3.6.5",
|
||||||
"vuex": "3"
|
"vuex": "3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -8,14 +8,20 @@ importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
animate.css:
|
||||||
|
specifier: ^4.1.1
|
||||||
|
version: 4.1.1
|
||||||
core-js:
|
core-js:
|
||||||
specifier: ^3.8.3
|
specifier: ^3.8.3
|
||||||
version: 3.43.0
|
version: 3.43.0
|
||||||
|
nanoid:
|
||||||
|
specifier: ^5.1.5
|
||||||
|
version: 5.1.5
|
||||||
vue:
|
vue:
|
||||||
specifier: ^2.6.14
|
specifier: ^2.6.14
|
||||||
version: 2.7.16
|
version: 2.7.16
|
||||||
vue-router:
|
vue-router:
|
||||||
specifier: '3'
|
specifier: ^3.6.5
|
||||||
version: 3.6.5(vue@2.7.16)
|
version: 3.6.5(vue@2.7.16)
|
||||||
vuex:
|
vuex:
|
||||||
specifier: '3'
|
specifier: '3'
|
||||||
|
@ -1031,6 +1037,9 @@ packages:
|
||||||
ajv@8.17.1:
|
ajv@8.17.1:
|
||||||
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
|
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
|
||||||
|
|
||||||
|
animate.css@4.1.1:
|
||||||
|
resolution: {integrity: sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ==}
|
||||||
|
|
||||||
ansi-colors@4.1.3:
|
ansi-colors@4.1.3:
|
||||||
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
|
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
@ -2577,6 +2586,11 @@ packages:
|
||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
nanoid@5.1.5:
|
||||||
|
resolution: {integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==}
|
||||||
|
engines: {node: ^18 || >=20}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
natural-compare@1.4.0:
|
natural-compare@1.4.0:
|
||||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||||
|
|
||||||
|
@ -5237,6 +5251,8 @@ snapshots:
|
||||||
json-schema-traverse: 1.0.0
|
json-schema-traverse: 1.0.0
|
||||||
require-from-string: 2.0.2
|
require-from-string: 2.0.2
|
||||||
|
|
||||||
|
animate.css@4.1.1: {}
|
||||||
|
|
||||||
ansi-colors@4.1.3: {}
|
ansi-colors@4.1.3: {}
|
||||||
|
|
||||||
ansi-escapes@3.2.0: {}
|
ansi-escapes@3.2.0: {}
|
||||||
|
@ -6662,6 +6678,8 @@ snapshots:
|
||||||
|
|
||||||
nanoid@3.3.11: {}
|
nanoid@3.3.11: {}
|
||||||
|
|
||||||
|
nanoid@5.1.5: {}
|
||||||
|
|
||||||
natural-compare@1.4.0: {}
|
natural-compare@1.4.0: {}
|
||||||
|
|
||||||
negotiator@0.6.3: {}
|
negotiator@0.6.3: {}
|
||||||
|
|
|
@ -6,7 +6,12 @@
|
||||||
<hr />
|
<hr />
|
||||||
<Demo3 /> -->
|
<Demo3 /> -->
|
||||||
<!-- <Demo4 /> -->
|
<!-- <Demo4 /> -->
|
||||||
<Demo6 />
|
<!-- <Demo6 /> -->
|
||||||
|
<!-- <Todo /> -->
|
||||||
|
<!-- <Demo8 /> -->
|
||||||
|
<!-- <Demo9 /> -->
|
||||||
|
<!-- <Demo10 /> -->
|
||||||
|
<Demo11 />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -16,11 +21,15 @@
|
||||||
// import Demo3 from "@/views/demo3/index.vue";
|
// import Demo3 from "@/views/demo3/index.vue";
|
||||||
// import Demo4 from "@/views/demo4/index.vue";
|
// import Demo4 from "@/views/demo4/index.vue";
|
||||||
// import Demo5 from "@/views/demo5/index.vue";
|
// import Demo5 from "@/views/demo5/index.vue";
|
||||||
import Demo6 from "@/views/demo6/index.vue";
|
// import Todo from "@/views/todo/index.vue";
|
||||||
|
// import Demo8 from "@/views/demo8/index.vue";
|
||||||
|
// import Demo9 from "@/views/demo9/index.vue";
|
||||||
|
// import Demo10 from "@/views/demo10/index.vue";
|
||||||
|
import Demo11 from "@/views/demo11/index.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "App",
|
name: "App",
|
||||||
// components: { Demo1, Demo2, Demo3 },
|
// components: { Demo1, Demo2, Demo3 },
|
||||||
components: { Demo6 },
|
components: { Demo11 },
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import "animate.css";
|
||||||
import Vue from "vue";
|
import Vue from "vue";
|
||||||
import VueRouter from "vue-router";
|
import VueRouter from "vue-router";
|
||||||
import App from "./App.vue";
|
import App from "./App.vue";
|
||||||
|
|
|
@ -3,10 +3,12 @@ import VueRouter from "vue-router";
|
||||||
const router = new VueRouter({
|
const router = new VueRouter({
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
|
name: "About",
|
||||||
path: "/about",
|
path: "/about",
|
||||||
component: () => import("@/views/demo6/components/About.vue"),
|
component: () => import("@/views/demo6/components/About.vue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
name: "Home",
|
||||||
path: "/home",
|
path: "/home",
|
||||||
component: () => import("@/views/demo6/components/Home.vue"),
|
component: () => import("@/views/demo6/components/Home.vue"),
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,175 @@
|
||||||
|
# Vue2 Transition 动画系统详解
|
||||||
|
|
||||||
|
## 一、基本动画实现
|
||||||
|
|
||||||
|
### 1. Transition 组件核心用法
|
||||||
|
```html
|
||||||
|
<Transition name="bunny" :appear="true">
|
||||||
|
<div v-show="visibility">内容</div>
|
||||||
|
</Transition>
|
||||||
|
```
|
||||||
|
|
||||||
|
| 属性 | 说明 | 示例值 |
|
||||||
|
| -------- | -------------------- | -------- |
|
||||||
|
| `name` | 动画类名前缀 | "bunny" |
|
||||||
|
| `appear` | 初始渲染是否应用动画 | true |
|
||||||
|
| `mode` | 过渡模式 | "out-in" |
|
||||||
|
|
||||||
|
### 2. 动画类名系统
|
||||||
|
```css
|
||||||
|
/* 进入动画 */
|
||||||
|
.bunny-enter-active { /* 激活阶段样式 */ }
|
||||||
|
.bunny-enter { /* 开始状态 */ }
|
||||||
|
.bunny-enter-to { /* 结束状态 */ }
|
||||||
|
|
||||||
|
/* 离开动画 */
|
||||||
|
.bunny-leave-active { /* 激活阶段样式 */ }
|
||||||
|
.bunny-leave { /* 开始状态 */ }
|
||||||
|
.bunny-leave-to { /* 结束状态 */ }
|
||||||
|
```
|
||||||
|
|
||||||
|
## 二、动画效果进阶
|
||||||
|
|
||||||
|
### 1. CSS 动画实现
|
||||||
|
```css
|
||||||
|
/* 关键帧动画 */
|
||||||
|
@keyframes slip {
|
||||||
|
from { transform: translateX(-100%); }
|
||||||
|
to { transform: translateX(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.bunny-enter-active {
|
||||||
|
animation: slip 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bunny-leave-active {
|
||||||
|
animation: slip 0.5s ease-in reverse;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 过渡效果实现
|
||||||
|
```css
|
||||||
|
/* 过渡效果 */
|
||||||
|
.bunny-enter-active, .bunny-leave-active {
|
||||||
|
transition: all 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bunny-enter, .bunny-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.8);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 三、最佳实践指南
|
||||||
|
|
||||||
|
### 1. 性能优化建议
|
||||||
|
- 优先使用 `transform` 和 `opacity` 属性
|
||||||
|
- 避免动画期间触发布局重排
|
||||||
|
- 对移动设备减少动画持续时间
|
||||||
|
|
||||||
|
### 2. 动画调试技巧
|
||||||
|
```css
|
||||||
|
/* 调试用边框 */
|
||||||
|
.bunny-enter-active {
|
||||||
|
outline: 2px solid red;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 响应式动画方案
|
||||||
|
```javascript
|
||||||
|
computed: {
|
||||||
|
animationDuration() {
|
||||||
|
return this.isMobile ? 300 : 500
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 四、高级应用场景
|
||||||
|
|
||||||
|
### 1. 列表过渡
|
||||||
|
```html
|
||||||
|
<TransitionGroup name="list" tag="ul">
|
||||||
|
<li v-for="item in items" :key="item.id">
|
||||||
|
{{ item.text }}
|
||||||
|
</li>
|
||||||
|
</TransitionGroup>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.list-move {
|
||||||
|
transition: transform 0.5s ease;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 动态过渡
|
||||||
|
```html
|
||||||
|
<Transition :name="transitionName">
|
||||||
|
<!-- ... -->
|
||||||
|
</Transition>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
transitionName: isMobile ? 'slide' : 'fade'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. JavaScript 钩子
|
||||||
|
```html
|
||||||
|
<Transition
|
||||||
|
@before-enter="beforeEnter"
|
||||||
|
@enter="enter"
|
||||||
|
@after-enter="afterEnter"
|
||||||
|
>
|
||||||
|
<!-- ... -->
|
||||||
|
</Transition>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
methods: {
|
||||||
|
beforeEnter(el) {
|
||||||
|
el.style.opacity = 0
|
||||||
|
},
|
||||||
|
enter(el, done) {
|
||||||
|
anime({
|
||||||
|
targets: el,
|
||||||
|
opacity: 1,
|
||||||
|
duration: 500,
|
||||||
|
complete: done
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 五、常见问题解决方案
|
||||||
|
|
||||||
|
1. **动画不生效排查**:
|
||||||
|
- 检查类名前缀是否匹配
|
||||||
|
- 确认元素初始状态是否正确
|
||||||
|
- 验证CSS属性是否可动画
|
||||||
|
|
||||||
|
2. **闪烁问题处理**:
|
||||||
|
```css
|
||||||
|
.v-enter {
|
||||||
|
opacity: 0.01; /* 避免初始状态检测问题 */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **多元素过渡**:
|
||||||
|
```html
|
||||||
|
<Transition mode="out-in">
|
||||||
|
<div :key="currentView">{{ currentView }}</div>
|
||||||
|
</Transition>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 六、与Vue3的对比
|
||||||
|
|
||||||
|
| 特性 | Vue2 | Vue3 |
|
||||||
|
| -------- | ---------------------- | ---------------- |
|
||||||
|
| 类名前缀 | `v-` 或自定义 | 相同 |
|
||||||
|
| 过渡模式 | 支持 `in-out`/`out-in` | 新增 `in-out-in` |
|
||||||
|
| 性能 | 基于CSS | 基于FLIP优化 |
|
||||||
|
| 新特性 | - | 过渡持久化 |
|
||||||
|
|
|
@ -0,0 +1,166 @@
|
||||||
|
# Vue2 过渡动画与 Animate.css 整合指南
|
||||||
|
|
||||||
|
## 一、Transition 与 TransitionGroup 对比
|
||||||
|
|
||||||
|
| 特性 | Transition | TransitionGroup |
|
||||||
|
| -------- | ----------------- | --------------- |
|
||||||
|
| 用途 | 单个元素/组件过渡 | 列表项过渡 |
|
||||||
|
| 要求 | 需要条件渲染 | 需要 `key` 属性 |
|
||||||
|
| 动画类 | 自动生成 | 可自定义类名 |
|
||||||
|
| 额外效果 | - | 自动处理定位 |
|
||||||
|
|
||||||
|
## 二、Animate.css 集成方案
|
||||||
|
|
||||||
|
### 1. 基本集成方法
|
||||||
|
```html
|
||||||
|
<TransitionGroup
|
||||||
|
name="animate__animated animate__bounce"
|
||||||
|
appear
|
||||||
|
enter-active-class="animate__flipInX"
|
||||||
|
leave-active-class="animate__flipOutX"
|
||||||
|
>
|
||||||
|
<!-- 列表项 -->
|
||||||
|
</TransitionGroup>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 常用动画效果
|
||||||
|
| 进入效果 | 离开效果 | 说明 |
|
||||||
|
| ---------------------- | ------------------------ | -------- |
|
||||||
|
| `animate__fadeIn` | `animate__fadeOut` | 淡入淡出 |
|
||||||
|
| `animate__zoomIn` | `animate__zoomOut` | 缩放效果 |
|
||||||
|
| `animate__slideInLeft` | `animate__slideOutRight` | 滑动效果 |
|
||||||
|
| `animate__flipInX` | `animate__flipOutX` | 3D翻转 |
|
||||||
|
|
||||||
|
## 三、自定义动画与库动画结合
|
||||||
|
|
||||||
|
### 1. 混合使用示例
|
||||||
|
```html
|
||||||
|
<!-- 自定义单个元素动画 -->
|
||||||
|
<Transition name="bunny">
|
||||||
|
<h3 v-show="visibility">自定义动画</h3>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
|
<!-- Animate.css 列表动画 -->
|
||||||
|
<TransitionGroup
|
||||||
|
enter-active-class="animate__animated animate__fadeInUp"
|
||||||
|
leave-active-class="animate__animated animate__fadeOutDown"
|
||||||
|
>
|
||||||
|
<li v-for="item in items" :key="item.id">{{ item.text }}</li>
|
||||||
|
</TransitionGroup>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 动画性能优化
|
||||||
|
```css
|
||||||
|
/* 启用GPU加速 */
|
||||||
|
.animate__animated {
|
||||||
|
animation-duration: 0.5s;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 限制动画影响范围 */
|
||||||
|
.bunny-enter-active, .bunny-leave-active {
|
||||||
|
will-change: transform;
|
||||||
|
isolation: isolate;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 四、最佳实践建议
|
||||||
|
|
||||||
|
### 1. 动画选择原则
|
||||||
|
- **移动端**:使用较轻量的动画(如fade)
|
||||||
|
- **重要交互**:使用明显的反馈动画(如bounce)
|
||||||
|
- **连续操作**:保持动画风格一致
|
||||||
|
|
||||||
|
### 2. 时间控制技巧
|
||||||
|
```javascript
|
||||||
|
// 动态控制动画时长
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
duration: window.innerWidth < 768 ? 300 : 500
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 无障碍访问
|
||||||
|
```css
|
||||||
|
/* 减少运动偏好设置 */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.animate__animated,
|
||||||
|
.bunny-enter-active,
|
||||||
|
.bunny-leave-active {
|
||||||
|
animation: none !important;
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 五、进阶应用场景
|
||||||
|
|
||||||
|
### 1. 路由过渡动画
|
||||||
|
```html
|
||||||
|
<Transition :name="transitionName" mode="out-in">
|
||||||
|
<router-view></router-view>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
transitionName: 'slide'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
'$route'(to, from) {
|
||||||
|
this.transitionName = to.meta.transition || 'fade'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 状态驱动的动画
|
||||||
|
```javascript
|
||||||
|
computed: {
|
||||||
|
animationClass() {
|
||||||
|
return this.isError
|
||||||
|
? 'animate__shakeX'
|
||||||
|
: 'animate__tada'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 动画事件钩子
|
||||||
|
```html
|
||||||
|
<Transition
|
||||||
|
@before-enter="beforeEnter"
|
||||||
|
@after-enter="afterEnter"
|
||||||
|
>
|
||||||
|
<div v-if="show">内容</div>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
methods: {
|
||||||
|
beforeEnter(el) {
|
||||||
|
el.style.opacity = 0
|
||||||
|
},
|
||||||
|
afterEnter(el) {
|
||||||
|
// 动画结束后的处理
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 六、常见问题解决
|
||||||
|
|
||||||
|
1. **动画不连贯**:
|
||||||
|
- 使用 `mode="out-in"` 确保顺序执行
|
||||||
|
- 检查 `key` 属性是否唯一
|
||||||
|
|
||||||
|
2. **初始渲染无动画**:
|
||||||
|
- 添加 `appear` 属性
|
||||||
|
- 检查初始状态是否正确
|
||||||
|
|
||||||
|
3. **移动端性能差**:
|
||||||
|
- 减少动画复杂度
|
||||||
|
- 使用 `will-change` 提示浏览器优化
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
<template>
|
||||||
|
<div class="container card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h1>动画效果:{{ visibility }}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<Transition name="bunny" :appear="true">
|
||||||
|
<h3 v-show="visibility">显示动画</h3>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<TransitionGroup
|
||||||
|
name="animate__animated animate__bounce"
|
||||||
|
appear
|
||||||
|
enter-active-class="animate__flipInX"
|
||||||
|
leave-active-class="animate__flipOutX"
|
||||||
|
>
|
||||||
|
<li key="1" v-show="visibility">嗬嗬嗬嗬嗬嗬嗬嗬嗬嗬嗬</li>
|
||||||
|
<li key="2" v-show="!visibility">哈哈哈哈哈哈哈哈哈哈哈</li>
|
||||||
|
</TransitionGroup>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-footer">
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-primary" @click="visibility = true">显示</button>
|
||||||
|
<button class="btn btn-success" @click="visibility = false">
|
||||||
|
隐藏
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "Demo-10",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
visibility: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.bunny-enter-active {
|
||||||
|
animation: slip 0.5s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bunny-leave-active {
|
||||||
|
animation: slip 0.5s linear reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slip {
|
||||||
|
from {
|
||||||
|
transform: translateX(-300px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,175 @@
|
||||||
|
# Vue2 路由缓存与导航控制最佳实践
|
||||||
|
|
||||||
|
## 一、Keep-Alive 路由缓存机制
|
||||||
|
|
||||||
|
### 1. 基本使用方式
|
||||||
|
```html
|
||||||
|
<!-- 缓存单个组件 -->
|
||||||
|
<keep-alive include="Home">
|
||||||
|
<router-view/>
|
||||||
|
</keep-alive>
|
||||||
|
|
||||||
|
<!-- 缓存多个组件 -->
|
||||||
|
<keep-alive :include="['Home', 'About']">
|
||||||
|
<router-view/>
|
||||||
|
</keep-alive>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 核心属性说明
|
||||||
|
| 属性 | 类型 | 说明 |
|
||||||
|
| --------- | ------------------- | ---------------------- |
|
||||||
|
| `include` | String/Regexp/Array | 匹配的组件名将被缓存 |
|
||||||
|
| `exclude` | String/Regexp/Array | 匹配的组件名不会被缓存 |
|
||||||
|
| `max` | Number | 最大缓存组件数 |
|
||||||
|
|
||||||
|
## 二、组件生命周期变化
|
||||||
|
|
||||||
|
### 1. 缓存状态下的生命周期
|
||||||
|
```
|
||||||
|
首次加载:created → mounted → activated
|
||||||
|
离开组件:deactivated
|
||||||
|
再次进入:activated
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 新增生命周期钩子
|
||||||
|
```javascript
|
||||||
|
export default {
|
||||||
|
activated() {
|
||||||
|
// 组件被激活时调用
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
deactivated() {
|
||||||
|
// 组件被停用时调用
|
||||||
|
this.clearTimer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 三、最佳实践建议
|
||||||
|
|
||||||
|
### 1. 缓存策略优化
|
||||||
|
```html
|
||||||
|
<!-- 动态决定是否缓存 -->
|
||||||
|
<keep-alive :include="cachedViews">
|
||||||
|
<router-view :key="$route.fullPath"/>
|
||||||
|
</keep-alive>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
computed: {
|
||||||
|
cachedViews() {
|
||||||
|
return this.$store.state.tagsView.cachedViews
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 数据刷新方案
|
||||||
|
```javascript
|
||||||
|
// 方案1:监听路由变化
|
||||||
|
watch: {
|
||||||
|
'$route'(to, from) {
|
||||||
|
if (to.name === 'Home') {
|
||||||
|
this.refreshData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方案2:使用activated钩子
|
||||||
|
activated() {
|
||||||
|
this.loadData()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 内存管理技巧
|
||||||
|
```javascript
|
||||||
|
// 清除缓存数据
|
||||||
|
deactivated() {
|
||||||
|
this.list = []
|
||||||
|
this.pagination = { page: 1, size: 10 }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 四、高级应用场景
|
||||||
|
|
||||||
|
### 1. 多级路由缓存
|
||||||
|
```javascript
|
||||||
|
// 父路由配置
|
||||||
|
{
|
||||||
|
path: '/nested',
|
||||||
|
component: Layout,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'child1',
|
||||||
|
component: Child1,
|
||||||
|
meta: { keepAlive: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'child2',
|
||||||
|
component: Child2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 动态缓存
|
||||||
|
<keep-alive>
|
||||||
|
<router-view v-if="$route.meta.keepAlive"/>
|
||||||
|
</keep-alive>
|
||||||
|
<router-view v-if="!$route.meta.keepAlive"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 滚动位置保持
|
||||||
|
```javascript
|
||||||
|
// router配置
|
||||||
|
scrollBehavior(to, from, savedPosition) {
|
||||||
|
if (savedPosition) {
|
||||||
|
return savedPosition
|
||||||
|
} else if (to.meta.keepAlive) {
|
||||||
|
return { x: 0, y: to.meta.scrollTop || 0 }
|
||||||
|
}
|
||||||
|
return { x: 0, y: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件内记录位置
|
||||||
|
deactivated() {
|
||||||
|
this.$route.meta.scrollTop = document.documentElement.scrollTop
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 缓存状态管理
|
||||||
|
```javascript
|
||||||
|
// 动态移除缓存
|
||||||
|
this.$vnode.parent.componentInstance.cache = {}
|
||||||
|
this.$vnode.parent.componentInstance.keys = []
|
||||||
|
```
|
||||||
|
|
||||||
|
## 五、常见问题解决方案
|
||||||
|
|
||||||
|
1. **缓存不生效排查**:
|
||||||
|
- 检查组件`name`是否与`include`匹配
|
||||||
|
- 验证路由配置是否正确
|
||||||
|
- 确保没有使用`v-if`干扰
|
||||||
|
|
||||||
|
2. **数据不更新问题**:
|
||||||
|
```javascript
|
||||||
|
// 使用路由参数作为key强制更新
|
||||||
|
<router-view :key="$route.fullPath"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **内存泄漏预防**:
|
||||||
|
```javascript
|
||||||
|
deactivated() {
|
||||||
|
// 清除定时器/事件监听
|
||||||
|
clearInterval(this.timer)
|
||||||
|
window.removeEventListener('resize', this.handleResize)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 六、与Vue3的区别
|
||||||
|
|
||||||
|
| 特性 | Vue2 | Vue3 |
|
||||||
|
| --------- | ----------------------- | ------------------------- |
|
||||||
|
| 缓存控制 | `include/exclude` | 新增`max`属性 |
|
||||||
|
| 生命周期 | `activated/deactivated` | 需要导入`onActivated`钩子 |
|
||||||
|
| 路由缓存 | 需要`keep-alive` | 内置路由组件缓存 |
|
||||||
|
| 组合式API | 不支持 | 提供`onActivated`等钩子 |
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
<template>
|
||||||
|
<div class="container card">
|
||||||
|
<div class="card-header btn-group">
|
||||||
|
<!-- 路由导航 -->
|
||||||
|
<button class="btn btn-success" @click="$router.push({ name: 'About' })">
|
||||||
|
显示About
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-success" @click="$router.push({ name: 'Home' })">
|
||||||
|
显示Home
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 前进和后退 -->
|
||||||
|
<button class="btn btn-primary" @click="back">后退</button>
|
||||||
|
<button class="btn btn-dark" @click="forward">前进</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- include 包含的是组件名:让不展示的路由保持挂载,不被销毁 -->
|
||||||
|
<!-- <keep-alive include="Home"> -->
|
||||||
|
<keep-alive :include="['Home', 'About']">
|
||||||
|
<router-view />
|
||||||
|
</keep-alive>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "Demo-11",
|
||||||
|
data() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/* 后退 */
|
||||||
|
back() {
|
||||||
|
this.$router.back();
|
||||||
|
},
|
||||||
|
|
||||||
|
/* 前进 */
|
||||||
|
forward() {
|
||||||
|
this.$router.forward();
|
||||||
|
},
|
||||||
|
|
||||||
|
/* 跳转指定步数 */
|
||||||
|
go() {
|
||||||
|
this.$router.go(-2);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -7,5 +7,11 @@
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: "AboutPage",
|
name: "AboutPage",
|
||||||
|
beforeDestroy() {
|
||||||
|
console.log("AboutPage销毁");
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
console.log("AboutPage挂载");
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -7,5 +7,11 @@
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: "HomePage",
|
name: "HomePage",
|
||||||
|
beforeDestroy() {
|
||||||
|
console.log("HomePage销毁");
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
console.log("HomePage挂载");
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="card-footer d-flex justify-content-between align-items-center"
|
||||||
|
v-if="count"
|
||||||
|
>
|
||||||
|
<div @click="onSelectAll(selectAll)">
|
||||||
|
<input type="checkbox" class="me-2" :checked="selectAll" />
|
||||||
|
<span>已完成 {{ accomplish }} / 全部 {{ count }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-danger" @click="onDeletedCommpleted">
|
||||||
|
清除已完成
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "TodoFooter",
|
||||||
|
props: ["list", "onSelectAll", "deletedTodo"],
|
||||||
|
computed: {
|
||||||
|
/* 完成的数量 */
|
||||||
|
accomplish() {
|
||||||
|
return this.list.filter((todo) => todo.completed).length;
|
||||||
|
},
|
||||||
|
|
||||||
|
/* 总数量 */
|
||||||
|
count() {
|
||||||
|
return this.list.length;
|
||||||
|
},
|
||||||
|
|
||||||
|
/* 是否已经全选 */
|
||||||
|
selectAll() {
|
||||||
|
const flag = this.list.filter((todo) => todo.completed === true);
|
||||||
|
return flag.length === this.list.length;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onDeletedCommpleted() {
|
||||||
|
this.$emit("deleted-commpleted");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -0,0 +1,43 @@
|
||||||
|
<template>
|
||||||
|
<div class="card-header my-3">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
v-model.trim="content"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="用户的输入..."
|
||||||
|
@keyup.enter="onAdd"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "TodoHeader",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
content: "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onAdd(ev) {
|
||||||
|
if (!this.content) {
|
||||||
|
alert("没有内容!!!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const todoObj = {
|
||||||
|
id: nanoid(),
|
||||||
|
content: ev.target.value.trim(),
|
||||||
|
completed: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.$emit("onAdd", todoObj);
|
||||||
|
|
||||||
|
// 清空输入框
|
||||||
|
this.content = "";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -0,0 +1,47 @@
|
||||||
|
<template>
|
||||||
|
<li
|
||||||
|
class="list-group-item d-flex justify-content-between align-items-center list-group-item-action"
|
||||||
|
>
|
||||||
|
<div class="py-2" @click="onChange">
|
||||||
|
<input type="checkbox" class="me-2" :checked="todo.completed" />
|
||||||
|
<span> {{ todo.content }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-danger deleted" @click="onDeleted">删除</button>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "TodoItem",
|
||||||
|
props: ["todo"],
|
||||||
|
methods: {
|
||||||
|
/* 修改todo的状态 */
|
||||||
|
onChange() {
|
||||||
|
const id = this.todo.id;
|
||||||
|
|
||||||
|
this.$root.$emit("change-todo-status", id);
|
||||||
|
},
|
||||||
|
|
||||||
|
/* 删除todo */
|
||||||
|
onDeleted() {
|
||||||
|
const id = this.todo.id;
|
||||||
|
|
||||||
|
const result = confirm(`确认删除:${id}`);
|
||||||
|
if (result) {
|
||||||
|
this.$root.$emit("delete-todo", id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.deleted {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
li:hover .deleted {
|
||||||
|
visibility: visible !important;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,15 @@
|
||||||
|
<template>
|
||||||
|
<ul class="list-group card-body">
|
||||||
|
<todo-item v-for="todo in list" :key="todo.id" :todo="todo" />
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import TodoItem from "./todo-item";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "TodoList",
|
||||||
|
components: { TodoItem },
|
||||||
|
props: ["list"],
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -0,0 +1,89 @@
|
||||||
|
<template>
|
||||||
|
<div class="container card">
|
||||||
|
<todo-header @onAdd="onAdd" />
|
||||||
|
<todo-list :list="todolist" />
|
||||||
|
<todo-footer
|
||||||
|
:list="todolist"
|
||||||
|
:on-select-all="onSelectAll"
|
||||||
|
@deleted-commpleted="onDeletedCommpleted"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import TodoFooter from "./components/todo-footer";
|
||||||
|
import TodoHeader from "./components/todo-header";
|
||||||
|
import TodoList from "./components/todo-list";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "TODOList",
|
||||||
|
components: { TodoHeader, TodoList, TodoFooter },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
todolist: [
|
||||||
|
{ id: 1, content: "吃饭", completed: true },
|
||||||
|
{ id: 2, content: "吃饭", completed: false },
|
||||||
|
{ id: 3, content: "吃饭", completed: true },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/* 添加Todo内容 */
|
||||||
|
onAdd(todo) {
|
||||||
|
this.todolist.unshift(todo);
|
||||||
|
},
|
||||||
|
|
||||||
|
/* 修改todo状态是否完成 */
|
||||||
|
changeTodoStatus() {
|
||||||
|
// 先移除旧的监听器避免重复
|
||||||
|
this.$root.$off("change-todo-status");
|
||||||
|
this.$root.$on("change-todo-status", (id) => {
|
||||||
|
this.todolist.forEach((todo) => {
|
||||||
|
if (todo.id === id) {
|
||||||
|
todo.completed = !todo.completed;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/* 删除todo */
|
||||||
|
deletedTodo() {
|
||||||
|
this.$root.$off("delete-todo");
|
||||||
|
this.$root.$on("delete-todo", (id) => {
|
||||||
|
const list = this.todolist.filter((todo) => todo.id !== id);
|
||||||
|
this.todolist = list;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/* 删除已完成的todo */
|
||||||
|
onDeletedCommpleted() {
|
||||||
|
const ids = this.todolist
|
||||||
|
.filter((todo) => todo.completed === true)
|
||||||
|
.map((todo) => todo.id);
|
||||||
|
|
||||||
|
const completedList = this.todolist.filter(
|
||||||
|
(todo) => todo.completed === false
|
||||||
|
);
|
||||||
|
|
||||||
|
// 删除时的提示
|
||||||
|
const result = confirm(`是否确认删除:${ids}`);
|
||||||
|
if (result) {
|
||||||
|
this.todolist = completedList;
|
||||||
|
alert(`清除已完成id:${ids}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/* 选择全部 */
|
||||||
|
onSelectAll(selectAll) {
|
||||||
|
this.todolist = this.todolist.map((todo) => ({
|
||||||
|
...todo,
|
||||||
|
completed: !selectAll,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.changeTodoStatus();
|
||||||
|
this.deletedTodo();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -0,0 +1,174 @@
|
||||||
|
# Vue2 组件通信与事件处理最佳实践
|
||||||
|
|
||||||
|
## 一、事件系统核心机制
|
||||||
|
|
||||||
|
### 1. 事件修饰符对比
|
||||||
|
| 修饰符 | 说明 | 示例 |
|
||||||
|
| ---------- | ------------ | --------------------------- |
|
||||||
|
| `.once` | 只触发一次 | `@click.once="handler"` |
|
||||||
|
| `.native` | 监听原生事件 | `@click.native="handler"` |
|
||||||
|
| `.stop` | 阻止事件冒泡 | `@click.stop="handler"` |
|
||||||
|
| `.prevent` | 阻止默认行为 | `@submit.prevent="handler"` |
|
||||||
|
|
||||||
|
### 2. 事件绑定与解绑
|
||||||
|
```javascript
|
||||||
|
// 子组件 StudentInfo.vue
|
||||||
|
methods: {
|
||||||
|
emitEvent() {
|
||||||
|
this.$emit('custom-event', payload)
|
||||||
|
},
|
||||||
|
unbind() {
|
||||||
|
this.$off('custom-event') // 解绑特定事件
|
||||||
|
this.$off() // 解绑所有事件
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 二、组件通信模式详解
|
||||||
|
|
||||||
|
### 1. Props 向下传递
|
||||||
|
```javascript
|
||||||
|
// 父组件
|
||||||
|
<Child :title="pageTitle" />
|
||||||
|
|
||||||
|
// 子组件
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '默认标题'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Events 向上通信
|
||||||
|
```javascript
|
||||||
|
// 子组件
|
||||||
|
this.$emit('update', newValue)
|
||||||
|
|
||||||
|
// 父组件
|
||||||
|
<Child @update="handleUpdate" />
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 高级通信方式
|
||||||
|
| 方式 | 适用场景 | 示例 |
|
||||||
|
| -------------- | ---------- | ------------------------------------- |
|
||||||
|
| ref | 父调子方法 | `this.$refs.child.method()` |
|
||||||
|
| provide/inject | 跨级组件 | `provide() { return { key: value } }` |
|
||||||
|
| Event Bus | 任意组件 | `Vue.prototype.$bus = new Vue()` |
|
||||||
|
| Vuex | 全局状态 | `this.$store.commit('mutation')` |
|
||||||
|
|
||||||
|
## 三、最佳实践指南
|
||||||
|
|
||||||
|
### 1. 事件命名规范
|
||||||
|
- 使用 kebab-case 命名(如 `user-updated`)
|
||||||
|
- 避免与原生事件重名(如 `click`)
|
||||||
|
- 语义化命名(如 `form-submitted`)
|
||||||
|
|
||||||
|
### 2. 性能优化建议
|
||||||
|
```javascript
|
||||||
|
// 1. 适时解绑事件
|
||||||
|
beforeDestroy() {
|
||||||
|
this.$off('custom-event')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 防抖处理高频事件
|
||||||
|
methods: {
|
||||||
|
handleInput: _.debounce(function() {
|
||||||
|
// 处理逻辑
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 安全通信模式
|
||||||
|
```javascript
|
||||||
|
// 添加事件存在性检查
|
||||||
|
if (this._events['custom-event']) {
|
||||||
|
this.$emit('custom-event', data)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 四、高级应用场景
|
||||||
|
|
||||||
|
### 1. 动态事件处理
|
||||||
|
```javascript
|
||||||
|
// 根据条件绑定不同处理函数
|
||||||
|
<template>
|
||||||
|
<Child @event="handlerMap[handlerType]" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
handlerType: 'typeA',
|
||||||
|
handlerMap: {
|
||||||
|
typeA: this.handleTypeA,
|
||||||
|
typeB: this.handleTypeB
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 自定义事件总线
|
||||||
|
```javascript
|
||||||
|
// event-bus.js
|
||||||
|
import Vue from 'vue'
|
||||||
|
export const EventBus = new Vue()
|
||||||
|
|
||||||
|
// 组件A
|
||||||
|
EventBus.$emit('message', 'Hello')
|
||||||
|
|
||||||
|
// 组件B
|
||||||
|
EventBus.$on('message', (msg) => {
|
||||||
|
console.log(msg)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 异步事件处理
|
||||||
|
```javascript
|
||||||
|
// 返回Promise的事件处理
|
||||||
|
methods: {
|
||||||
|
async submitForm() {
|
||||||
|
try {
|
||||||
|
await this.$emitAsync('form-submit')
|
||||||
|
this.showSuccess()
|
||||||
|
} catch (error) {
|
||||||
|
this.showError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 五、常见问题解决方案
|
||||||
|
|
||||||
|
1. **事件未触发排查**:
|
||||||
|
- 检查事件名称大小写是否一致
|
||||||
|
- 确认子组件是否正确调用 `$emit`
|
||||||
|
- 验证父组件监听的事件名是否匹配
|
||||||
|
|
||||||
|
2. **内存泄漏预防**:
|
||||||
|
```javascript
|
||||||
|
// 清除所有监听器
|
||||||
|
beforeDestroy() {
|
||||||
|
this.$off()
|
||||||
|
EventBus.$off('event', this.handler)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **跨组件通信优化**:
|
||||||
|
```javascript
|
||||||
|
// 使用Vue.observable实现轻量状态管理
|
||||||
|
const state = Vue.observable({ count: 0 })
|
||||||
|
```
|
||||||
|
|
||||||
|
## 六、与Vue3的对比
|
||||||
|
|
||||||
|
| 特性 | Vue2 | Vue3 |
|
||||||
|
| -------- | ----------- | ---------------- |
|
||||||
|
| 事件系统 | `$emit/$on` | `emits` 选项声明 |
|
||||||
|
| 移除API | - | `$off`, `$once` |
|
||||||
|
| 新特性 | - | `v-model` 参数 |
|
||||||
|
| 性能 | 基于观察者 | 基于Proxy |
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
<template>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-header-tabs">
|
||||||
|
<h3>{{ title }}</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-header-pills">
|
||||||
|
<h3>名称:{{ userinfo.sutdentName }}</h3>
|
||||||
|
<h3>地址:{{ userinfo.address }}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body btn-group">
|
||||||
|
<button class="btn btn-primary" @click="getUserinfo">获取用户信息</button>
|
||||||
|
<button class="btn btn-danger" @click="unbind">解除绑定</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "StudentInfo",
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: "标题显示",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
userinfo: {
|
||||||
|
sutdentName: "Bunny",
|
||||||
|
address: "昆山印象花园",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/* 触发用户事件 */
|
||||||
|
getUserinfo() {
|
||||||
|
this.$emit("getUserinfo", this.userinfo);
|
||||||
|
},
|
||||||
|
|
||||||
|
/* 接触绑定 */
|
||||||
|
unbind() {
|
||||||
|
console.info("解除绑定");
|
||||||
|
|
||||||
|
// 解绑单个
|
||||||
|
this.$off("getUserinfo");
|
||||||
|
|
||||||
|
// 解绑多个
|
||||||
|
// this.$off(["getUserinfo"]);
|
||||||
|
|
||||||
|
// 解绑所有
|
||||||
|
// this.$off();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -0,0 +1,25 @@
|
||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<!-- 可以只触发一次 -->
|
||||||
|
<Student @getUserinfo.once="getUserinfo" title="可以只触发一次" />
|
||||||
|
<Student @getUserinfo="getUserinfo" title="普通触发" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Student from "./components/Sutdent";
|
||||||
|
export default {
|
||||||
|
name: "Demo-8",
|
||||||
|
components: { Student },
|
||||||
|
data() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/* 获取用户信息 */
|
||||||
|
getUserinfo(userinfo) {
|
||||||
|
const data = { ...userinfo };
|
||||||
|
console.log(data);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -0,0 +1,186 @@
|
||||||
|
# Vue2 组件通信与事件处理深度指南
|
||||||
|
|
||||||
|
## 一、组件通信核心方式
|
||||||
|
|
||||||
|
### 1. 父子组件通信模式
|
||||||
|
|
||||||
|
#### Props 向下传递
|
||||||
|
```javascript
|
||||||
|
// 父组件
|
||||||
|
<Child :title="pageTitle" />
|
||||||
|
|
||||||
|
// 子组件
|
||||||
|
props: {
|
||||||
|
title: String
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Events 向上传递
|
||||||
|
```javascript
|
||||||
|
// 子组件
|
||||||
|
this.$emit('update', newValue)
|
||||||
|
|
||||||
|
// 父组件
|
||||||
|
<Child @update="handleUpdate" />
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 引用直接访问
|
||||||
|
```javascript
|
||||||
|
// 父组件
|
||||||
|
<Child ref="childRef" />
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
callChildMethod() {
|
||||||
|
this.$refs.childRef.childMethod()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 二、事件系统高级用法
|
||||||
|
|
||||||
|
### 1. 原生事件绑定
|
||||||
|
```html
|
||||||
|
<!-- 必须添加.native修饰符 -->
|
||||||
|
<StudentInfo @click.native="handleClick" />
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 手动事件管理
|
||||||
|
```javascript
|
||||||
|
// 绑定事件(推荐在mounted钩子中)
|
||||||
|
mounted() {
|
||||||
|
this.$refs.studenInfo.$on("getStudentName", this.getStudentName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解绑事件(必须在beforeDestroy中清理)
|
||||||
|
beforeDestroy() {
|
||||||
|
this.$refs.studenInfo.$off("getStudentName")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 事件参数传递
|
||||||
|
```javascript
|
||||||
|
// 子组件触发时可带多个参数
|
||||||
|
this.$emit('event-name', arg1, arg2, ...)
|
||||||
|
|
||||||
|
// 父组件接收所有参数
|
||||||
|
handler(arg1, arg2, ...rest) {
|
||||||
|
// 处理参数
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 三、最佳实践建议
|
||||||
|
|
||||||
|
### 1. 事件命名规范
|
||||||
|
- 使用 kebab-case 命名(如 `student-updated`)
|
||||||
|
- 避免与原生事件重名(添加业务前缀)
|
||||||
|
- 语义化命名(如 `form-submitted`)
|
||||||
|
|
||||||
|
### 2. 安全通信模式
|
||||||
|
```javascript
|
||||||
|
// 添加事件存在性检查
|
||||||
|
if (this._events['custom-event']) {
|
||||||
|
this.$emit('custom-event', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用try-catch包裹可能出错的事件处理
|
||||||
|
try {
|
||||||
|
this.$emit('critical-action', payload)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('事件处理失败:', error)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 性能优化方案
|
||||||
|
```javascript
|
||||||
|
// 防抖处理高频事件
|
||||||
|
methods: {
|
||||||
|
handleInput: _.debounce(function() {
|
||||||
|
this.$emit('input-changed', this.value)
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 四、高级通信场景
|
||||||
|
|
||||||
|
### 1. 跨级组件通信
|
||||||
|
```javascript
|
||||||
|
// 祖先组件
|
||||||
|
provide() {
|
||||||
|
return {
|
||||||
|
appData: this.sharedData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 后代组件
|
||||||
|
inject: ['appData']
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 动态事件处理器
|
||||||
|
```javascript
|
||||||
|
// 根据状态切换处理函数
|
||||||
|
<Child @custom-event="handlerMap[currentHandler]" />
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
currentHandler: 'default',
|
||||||
|
handlerMap: {
|
||||||
|
default: this.handleDefault,
|
||||||
|
special: this.handleSpecial
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 事件总线模式
|
||||||
|
```javascript
|
||||||
|
// event-bus.js
|
||||||
|
import Vue from 'vue'
|
||||||
|
export default new Vue()
|
||||||
|
|
||||||
|
// 组件A
|
||||||
|
EventBus.$emit('data-updated', payload)
|
||||||
|
|
||||||
|
// 组件B
|
||||||
|
EventBus.$on('data-updated', handler)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 五、常见问题解决方案
|
||||||
|
|
||||||
|
1. **事件未触发排查**:
|
||||||
|
- 检查组件引用是否正确(`ref` 命名)
|
||||||
|
- 确认事件名称完全匹配(大小写敏感)
|
||||||
|
- 验证事件绑定时机(确保在 mounted 之后)
|
||||||
|
|
||||||
|
2. **内存泄漏预防**:
|
||||||
|
```javascript
|
||||||
|
beforeDestroy() {
|
||||||
|
// 清除所有自定义事件监听
|
||||||
|
this.$off()
|
||||||
|
// 清除事件总线监听
|
||||||
|
EventBus.$off('data-updated', this.handler)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **异步事件处理**:
|
||||||
|
```javascript
|
||||||
|
// 返回Promise的事件处理
|
||||||
|
async handleEvent() {
|
||||||
|
try {
|
||||||
|
await this.$nextTick()
|
||||||
|
const result = await this.$emitAsync('async-event')
|
||||||
|
// 处理结果
|
||||||
|
} catch (error) {
|
||||||
|
// 错误处理
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 六、与Vue3的对比
|
||||||
|
|
||||||
|
| 特性 | Vue2 | Vue3 |
|
||||||
|
| -------- | -------------- | -------------------- |
|
||||||
|
| 事件定义 | 隐式声明 | `emits` 选项显式声明 |
|
||||||
|
| 原生事件 | 需要 `.native` | 自动识别 |
|
||||||
|
| 移除API | - | 移除 `$on`, `$off` |
|
||||||
|
| 性能 | 基于观察者 | 基于Proxy的响应式 |
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
<template>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>学生姓名:{{ studenName }}</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-primary" @click="onEmitStudentName">
|
||||||
|
向父组件传递学生姓名
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "StudentInfo",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
studenName: "Bunny",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onEmitStudentName() {
|
||||||
|
this.$emit("getStudentName", this.studenName);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -0,0 +1,47 @@
|
||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<h4>
|
||||||
|
学生姓名:
|
||||||
|
<span class="badge text-bg-secondary"> {{ studenName }}</span>
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<!-- 需要在前面加上native,否则会当成自定义事件 -->
|
||||||
|
<StudentInfo ref="studenInfo" @click.native="showMessage" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import StudentInfo from "./components/SutdentInfo.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Demo-9",
|
||||||
|
components: { StudentInfo },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
studenName: "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/* 获取学生名 */
|
||||||
|
getStudentName(name, ...params) {
|
||||||
|
console.log(name, params);
|
||||||
|
|
||||||
|
this.studenName = name;
|
||||||
|
},
|
||||||
|
|
||||||
|
/* 点击子组件显示消息 */
|
||||||
|
showMessage() {
|
||||||
|
alert("点击子组件触发点击事件。。。");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$refs.studenInfo.$on("getStudentName", this.getStudentName);
|
||||||
|
|
||||||
|
// 绑定事件,只能写箭头函数,否则当前this是Vue被绑定的组件
|
||||||
|
// this.$refs.studenInfo.$on("getStudentName", (name, ...params) => {
|
||||||
|
// console.log(name, params);
|
||||||
|
// this.studenName = name;
|
||||||
|
// });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
Loading…
Reference in New Issue