阅读视图

发现新文章,点击刷新页面。

Vue生命周期与keep-alive实战理解

Vue 生命周期与 keep-alive:我用真实项目终于搞清楚了

"生命周期"这个词在 Vue 教程里会很早出现,但很多人学了之后还是一知半解。 因为光看文档,没有感觉。这篇文章想用真实项目代码,帮你把它们"落地"。


先说一个语言上的误导

"生命周期" 这个词,听起来像是整个项目从打开到关闭的一段大流程。

但实际上,它属于每一个组件。

正确的理解是:

每个 Vue 组件实例,都有自己的出生、挂载、更新、离开、缓存激活的过程。 这些阶段,Vue 会自动调用对应的钩子函数,给你一个"插队"的机会。


🗓️ 最常用的几个生命周期钩子

created —— 数据准备好了,但页面还没画出来

✅ 适合做什么:
- 发起不依赖 DOM 的数据请求
- 初始化变量
- 读取 Vuex / props

mounted —— 页面已经渲染出来了,DOM 摸得到了

✅ 适合做什么:
- 操作 $refs(真实 DOM)
- 绑定滚动、键盘、resize 事件
- 初始化第三方 JS 库(图表、播放器、WebSocket 等)

beforeDestroy / destroyed —— 组件要销毁了

✅ 适合做什么:
- 清除定时器
- 解绑事件监听
- 关闭 WebSocket
- 释放资源

activated —— keep-alive 缓存的页面"重新回来了"

✅ 适合做什么:
- 每次回到这个页面时,重新拉数据
- 恢复某些需要刷新的状态

deactivated —— keep-alive 缓存的页面"离开了,但没销毁"

✅ 适合做什么:
- 暂停正在播放的音频
- 停止轮询
- 暂停实时监听

🔁 keep-alive 会改变什么

keep-alive 是 Vue 的内置组件,作用是缓存被包住的组件实例

没有 keep-alive 的情况下,离开一个页面 = 销毁这个组件。

有了 keep-alive,离开页面之后组件不一定被销毁,而是被"冷冻"起来:

首次进入缓存页:
  created → mounted → activated

离开缓存页(不销毁):
  deactivated

再次进入缓存页:
  activated  ← 直接从这里开始,跳过了 created 和 mounted!

这就是为什么你会在缓存相关的组件里,经常看到 activated 而不是 created


🔧 用真实项目代码来拆解

根实例的 created:应用一启动就执行一次

// src/main.js
new Vue({
  router,
  store,
  render: h => h(App),
  created() {
    // 这里的 created 只在"整个应用开机"时执行一次
    store.commit('businessCoulmn/setWorkDetail', '');        // 重置业务状态
    store.commit('instantMessaging/setScreenfullBut', false); // 重置 IM 全屏状态
    store.commit('siteBar/addMenu', router);                  // 注入动态菜单
  },
}).$mount('#app');

这里的 created 是根实例的生命周期,属于"整个应用开机初始化",不属于任何页面。


登录页的 created + mounted:先清状态,再做页面效果

created:
  - 清空本地存储
  - 重置菜单和 tab
  - 清空 token

mounted:
  - 启动背景动画效果

典型用法:

  • created 适合"先把旧状态清干净"
  • mounted 适合"页面展示出来之后做的事情"

IM 页面入口:created 做初始化

created:
  1. 拿老师的聊天账号
  2. 拿 IM 配置
  3. 初始化 TIM SDK
  4. 绑定 TIM 事件
  5. 登录 IM

为什么用 created 而不是 mounted

因为这些步骤不需要操作 DOM,提前在 created 里做,能更快完成初始化。


IM 布局组件:mounted 初始化 WebSocket

mounted:
  - 初始化 WebSocket 连接

beforeDestroy:
  - 关闭 WebSocket
  - 停掉轮询

mounted 之所以适合初始化 WebSocket,是因为:

  • WebSocket 有时候需要操作 DOM
  • mounted 保证页面已经渲染,更安全

beforeDestroy 负责配套的清理工作,避免资源泄漏。


ConversationList 用 activated 而不是 created,是因为……

// src/components/.../ConversationList/index.vue
activated() {
  this.handleGetWechatStudentList(); // 每次"回到页面"时刷新数据
}

为什么不用 created

因为这个组件被 keep-alive 包着。

  • 第一次进来会走 created
  • 切走再切回来,created 不会重新触发
  • activated 每次回来都会触发

所以这里的逻辑是:

created  → 第一次初始化
activated → 每次"回来"时刷新数据

这是 keep-alive 场景下非常经典的写法。


MessageWindow 用 deactivated,是因为……

// src/components/.../MessageWindow/index.vue
deactivated() {
  this.handlePauseAudio(); // 离开页面时暂停音频
}

为什么不用 beforeDestroy

因为这个组件被缓存了,切走页面时不是真正销毁,不会触发 beforeDestroy

会触发的是 deactivated,专门处理"缓存页离开时的收尾动作"。


🔄 用一次完整流程来理解

1. 用户打开项目
   └─ main.js 创建根实例
   └─ 根实例 created 执行(初始化菜单、重置 IM 状态)

2. 用户登录
   └─ login.vue created 清空旧数据
   └─ login.vue mounted 显示背景动画

3. 进入 IM 聊天页
   └─ instant-messaging/index.vue created 做 IM 初始化
   └─ Layout/index.vue mounted 初始化 WebSocket

4. 用户切走页面
   └─ 缓存页面 → 触发 deactivated(不销毁)
   └─ 非缓存页面 → 触发 beforeDestroy(销毁)

5. 用户切回 IM 页面
   └─ 触发 activated
   └─ ConversationList 重新拉数据

⚠️ 不要混淆的三个东西

概念 是什么
生命周期 组件的出生、挂载、更新、离开、重现的各个阶段
watch 监听某个数据变化,不是生命周期
keep-alive 缓存机制,它会改变组件的生命周期表现

keep-alive 的存在,让 deactivated / activated 有意义。

没有 keep-alive 包着的组件,这两个钩子永远不会触发。


🏁 一句话总结

Vue 生命周期不是"整个项目统一跑一遍的流程"
而是"每个组件实例自己的阶段变化"

keep-alive 改变的是"离开""回来"的行为:
  - 离开时不销毁 → deactivated
  - 回来时不重建 → activated

所以:
  created / mounted    → 用于首次初始化
  activated            → 用于每次回来时刷新
  deactivated          → 用于离开时收尾
  beforeDestroy        → 用于真正销毁时清理

这是 Vue2 学习系列第四篇。

下一篇:Vue、SPA、MPA 傻傻分不清?一篇弄清楚三者的关系。

Home双router-view与布局切换逻辑

Home.vue 里为什么有两个 router-view?我看了三遍才读懂

如果你第一次看到 Home.vue 里写了两个 <router-view>,可能会困惑:这不是重复了吗? 其实不是。这两个出口,各守一个"平行世界"。


先看那段让人迷惑的代码

<template>
  <fragment>
    <el-container class="cus-root-container">

      <!-- 第一个 router-view:只在即时聊天全屏时出现 -->
      <router-view
        :key="$route.path"
        v-if="$route.path == '/review-service/instant-messaging'
              && $store.state.instantMessaging.bScreenfullBut == true"
      />

      <!-- 后台壳区域:只在非全屏时显示 -->
      <site-bar v-if="!$store.state.instantMessaging.bScreenfullBut" />

      <el-container class="cus-right-wrap"
        v-if="!$store.state.instantMessaging.bScreenfullBut">
        <el-header class="cus-header">
          <personal-bar />
          <tabs-bar />
        </el-header>
        <el-main class="cus-main">
          <keep-alive :exclude="excludelist">
            <!-- 第二个 router-view:后台正常模式的内容出口 -->
            <router-view :key="$route.path" />
          </keep-alive>
        </el-main>
      </el-container>

    </el-container>
  </fragment>
</template>

初看很乱,但把它分成两块就很清楚了。


🗺️ 一张图搞懂两个出口

Home.vue
├─ 🖥️ 全屏分支(上面那个 router-view)
│   └─ 条件:路由 = IM 页 && 全屏状态 = true
│   └─ 效果:聊天页面完全接管屏幕,没有侧边栏和顶部栏
│
└─ 🖥️ 普通后台分支(下面那个 router-view)
    └─ 条件:全屏状态 = false
    └─ 效果:正常后台布局,有侧边栏、顶部栏、标签页
    └─ 外面套了 keep-alive 做页面缓存

它们永远不会同时出现。 因为 v-if 条件互斥:

  • 全屏状态为 true → 上面显示,下面隐藏
  • 全屏状态为 false → 下面显示,上面隐藏

📺 第一个 router-view:即时聊天全屏模式

触发条件

当前路由是 /review-service/instant-messaging
+ Vuex 里 bScreenfullBut == true

两个条件同时满足,才会走这个出口。

它解决什么问题

即时聊天页面有一个**"全屏按钮"**。

用户点击全屏后,希望聊天窗口占满整个屏幕,不要有侧边菜单、顶部栏这些干扰。

但这个操作不是跳转到另一个页面,而是原地切换布局。

所以需要一个"纯净出口"——只渲染聊天页面本身,不带任何后台壳。

这就是第一个 router-view 存在的意义。


📋 第二个 router-view:日常后台模式

<keep-alive :exclude="excludelist">
  <router-view :key="$route.path" />
</keep-alive>

这是最常用的那个出口。

整个后台系统 90% 的页面都走这里渲染。

为什么外面套了 keep-alive?

后台系统的页面有一个特点:

用户在列表页设了很多筛选条件
→ 跳去详情页看了一会儿
→ 返回列表页
→ 希望筛选条件还在!

如果没有 keep-alive,每次返回都会重新创建组件,状态全丢。

加了 keep-alive 之后,组件实例被缓存起来,切回去时状态原封不动


为什么要有 excludelist?

keep-alive 虽然好用,但不是所有页面都适合缓存。

有些页面每次进入都应该是"全新"的,比如:

  • 编辑页:如果缓存了上次编辑的数据,会出现"脏数据"问题
  • 详情页:缓存后可能显示旧记录,而不是当前要看的那条
  • 表单创建页:上次填的内容不该保留

这些页面的 name 会被写进 excludelist,从而绕过缓存,每次都重建。


🔑 两个 router-view 都写了 :key="$route.path",为什么?

这是一个很容易忽略的细节。

如果不加 :key,Vue 在某些路由切换时会复用旧的组件实例,导致:

  • 生命周期钩子没有重新触发
  • 旧数据没清空
  • 表单状态异常

加了 :key="$route.path" 之后:

  • 每个路由路径对应一个唯一标识
  • 路由一变,key 就变,Vue 就知道需要重新创建这个组件
  • 页面切换更干净、更可控

🔄 路由和 Home.vue 的关系

你可能会看到很多路由这样配置:

{
  path: '/some-path',
  component: () => import('../views/Home.vue'),  // 注意这里是 Home.vue
  children: [
    {
      path: '',
      component: () => import('../views/some-page.vue'),
    }
  ]
}

这里的意思是:

  • 进入这个路由,先用 Home.vue 作为壳
  • Home.vue 再通过 router-viewsome-page.vue 渲染出来

所以:

router/index.js → 决定"哪个页面进 Home"
Home.vue       → 决定"这个页面以什么布局显示"

路由配置管"谁进来",Home.vue 管"怎么展示"。


✅ 怎么判断一个页面走哪个出口

看到一个页面时,可以按这个逻辑判断:

if (路由是 IM 页 && 全屏状态 == true) {
  走第一个 router-view → 全屏,没有后台壳
} else {
  走第二个 router-view → 正常后台模式,有侧边栏顶部栏
}

🏁 一句话总结

两个 router-view 不是重复,是两种布局的切换开关:
上面 → 即时聊天全屏专用出口
下面 → 普通后台模式 + keep-alive 缓存
条件互斥,永远不会同时工作

这是 Vue2 学习系列第三篇。

下一篇:Vue 生命周期 + keep-alive 实战,用真实项目代码来拆解。

src-components调用链与即时聊天组件树

src/components 里的文件,没人引用就是一堆废纸

很多 Vue 初学者以为,把文件放到 src/components 文件夹里,就会自动生效。 其实完全不是。这篇文章就来说清楚:组件怎么才算"真正被用到"。


🤔 先说一个常见误区

初学时,很多人会这样理解:

"我把组件放到 src/components,它就可以在模板里用了。"

错。

src/components 只是一个约定俗成的存放目录,不是自动生效的魔法文件夹。

放在里面的组件,如果没有人引用它,它就永远不会渲染,永远不会执行。


✅ 组件真正"生效",只有两种方式

方式一:全局注册(所有页面都能直接用)
方式二:局部引用(谁需要谁 import,谁 import 谁才能用)

🌐 全局注册是什么感觉

有些组件在整个项目里到处都要用,比如:

  • 顶部栏
  • 左侧菜单
  • 标签页
  • 全局提示

这类组件写进全局注册之后,任何页面不用单独 import,直接在模板里写标签就能用:

<site-bar />
<personal-bar />
<tabs-bar />
<base-tip />

全局注册的核心逻辑(以常见写法为例):

// src/components/globalComponents.js
import Vue from 'vue'
import SiteBar from './SiteBar/index.vue'

const globalComponents = {
  install(Vue) {
    Vue.component('SiteBar', SiteBar)
    // ... 其他全局组件
  }
}

export default globalComponents
// src/main.js
import globalComponents from './components/globalComponents'
Vue.use(globalComponents) // 在这里注册进去

这样,SiteBar 就变成了整个项目都能用的"公共组件"。


🔗 局部引用:大多数组件的使用方式

大部分业务组件不会全局注册。

它们是:谁需要,谁去 import,谁 import 才能用。

以即时聊天模块为例,它的组件树是一层一层嵌套引用的:

src/views/review-service/instant-messaging/index.vue
  └─ <im-layout />            ← 这个页面 import 了 Layout

src/components/.../Layout/index.vue
  ├─ <menu-bar />             ← Layout 里 import 了 MenuBar
  ├─ <conversation-list />    ← 会话列表(menuBar 切换到 Tab 1 时)
  ├─ <contacts-list />        ← 通讯录(menuBar 切换到 Tab 2 时)
  ├─ <message-window />       ← 有当前会话时显示
  ├─ <welcome-page />         ← 没有会话时显示
  ├─ <business-column />      ← 右侧业务栏
  └─ <image-previewer />      ← 图片预览

src/components/.../BusinessColumn/index.vue
  ├─ <BaseInfo />             ← 学员基本信息
  ├─ <SpeechList />           ← 个人话术
  └─ <WokNotice />            ← 工作通知

这就是典型的局部引用调用链:一层引用一层,没被引用到的,永远不会渲染。


🗂️ 即时聊天模块,每一层在做什么

页面入口层

instant-messaging/index.vue 是聊天功能的入口页面。

它主要负责"初始化",不负责画界面:

  • 获取老师的聊天账号
  • 拉取 IM 配置
  • 初始化 TIM SDK
  • 绑定 TIM 事件
  • 登录 IM

初始化完毕之后,它把 <im-layout /> 渲染出来,让布局层接管页面。


布局层

Layout/index.vue 是聊天页面的"骨架"。

它负责把聊天界面的各个区域分好:

  • 左侧菜单栏(MenuBar)
  • 根据 Tab 切换的会话列表 / 通讯录
  • 中间的消息窗口(有会话时显示)/ 欢迎页(无会话时显示)
  • 右侧业务栏
  • 图片预览浮层
  • 音频通知元素

这层是"结构性组件",它不负责具体业务,只负责把各个子组件摆放到正确的位置。


业务栏层

BusinessColumn/index.vue 是聊天右侧的业务信息区域。

它会根据当前会话和业务类型,切换显示:

  • 学员基本信息(BaseInfo)
  • 个人话术列表(SpeechList)
  • 工作通知(WokNotice)

🔎 怎么快速找到一个组件在哪里被调用

以后看代码时,不要在 src/components 文件夹里盲搜。

最快的方式是直接搜组件名

# 找 BaseInfo 被哪里引用了
rg -n "BaseInfo|<BaseInfo|import BaseInfo" src

# 找 ImLayout 被哪里引用了
rg -n "ImLayout|<im-layout|import ImLayout" src

搜到之后,从父组件往子组件方向追,一层一层拆开,很快就清楚了。


🧠 整个应用的调用链长这样

main.js
└─ 注册全局组件、插件、store、router

App.vue
└─ <router-view />(总出口)

Home.vue(后台壳)
└─ <site-bar />(全局)
└─ <tabs-bar />(全局)
└─ <router-view />(后台页面出口)

即时聊天入口页
└─ <im-layout />(局部引入)

Layout
└─ <ConversationList /><MessageWindow /><BusinessColumn />...

BusinessColumn
└─ <BaseInfo /><SpeechList /><WokNotice />

从最上面的 main.js 到最深的 BaseInfo,是完整的一条引用链。

断掉任何一层的引用,下面的组件就不会渲染。


🏁 一句话总结

src/components 不是入口目录,组件不会自动生效。
组件必须被 import 或全局注册后才会激活。
搞清楚"谁引用了谁",比知道"目录里有什么"更重要。

这是 Vue2 学习系列第二篇。

下一篇:Home.vue 里为什么有两个 <router-view>?它们各管什么?

VUE-组件命名与注册机制

Vue2 组件四个"名字",我曾经傻傻分不清楚

这是我在学习 Vue2 真实项目时踩过的一个坑,花了不少时间才搞清楚。记录下来,希望对你也有用。


先说我当时有多懵

我第一次看到一个 Vue 组件的使用方式时,脑袋是懵的:

import ImLayout from '@/components/.../Layout/index.vue';
components: { ImLayout }
<im-layout />
name: 'im-layout'

四个地方,都像是在说同一件事,又感觉哪里不对。

它们到底是什么关系?


🎯 直接给结论

在 Vue 2 里,组件能不能在模板里使用,关键看三件事:

1. 组件有没有被 import 进来
✅ 2. 有没有在 components 里注册
✅ 3. 模板里有没有用对标签名

name,只是组件自己的**"身份名片"**,不负责让你用它。


📦 四个"名字",分开说

第一个:import ImLayout from ...

这是 JavaScript 层面的变量名,你把一个文件"抱进来",给它取个名字方便后续引用。

// 你也可以叫它 ChatLayout,只要后面跟着改
import ChatLayout from '@/components/.../Layout/index.vue';

名字本身不固定,你起的什么,后面就叫什么。


第二个:components: { ImLayout }

这是在当前页面注册组件,相当于告诉 Vue:

"我这个页面可以使用 ImLayout,它对应的实现就是刚才 import 的那个文件。"

完整写法等价于:

components: {
  ImLayout: ImLayout  // 键名就是你之后在模板里写的标签名
}

第三个:<im-layout />

这是在模板里真正调用组件的地方。

Vue 会自动把注册名 ImLayout 对应到 im-layout(驼峰 → 短横线),所以这两种写法都可以:

<ImLayout />
<im-layout />

项目里更常见的是带短横线的写法(kebab-case)。


第四个:name: 'im-layout'

这是组件给自己贴的名字,主要用于:

用途 说明
keep-alive 缓存控制 exclude/include 通过 name 识别组件
Vue Devtools 显示 调试时能看到有意义的组件名,而不是一堆 <Anonymous>
递归组件 组件在模板里引用自己时需要用 name

name 删掉,页面照样能正常渲染。 因为真正决定能不能用的是 components 注册。


🔍 用真实项目做个例子

假设你有一个即时聊天模块,在页面入口里这样写:

// 第一步:把文件引进来,起名叫 ImLayout
import ImLayout from '@/components/review-service/instant-messaging/Layout/index.vue';

export default {
  // 第二步:在当前页面注册这个组件
  components: { ImLayout },
};
<!-- 第三步:在模板里使用它 -->
<im-layout />

这能正常工作,是因为:

  1. import 把文件拿进来了
  2. components 把这个组件注册进了当前页面
  3. Vue 自动把 ImLayoutim-layout 对应起来

跟组件内部写没写 name: 'im-layout' 无关。


🚫 三个常见误区

❌ 误区一:文件名决定标签名

不对。index.vue 只是文件名,不能自动决定你在模板里写什么。

真正决定的是 components 注册时用的键名。


❌ 误区二:name 就是注册名

不对。name 是组件自我描述,注册是靠 components


❌ 误区三:import 名字必须和标签一致

不对。import 只是变量名,标签名取决于 components 如何注册。

只要注册正确,模板里 <ImLayout /><im-layout /> 都可以。


🗺️ 遇到组件时,用这个顺序判断

📌 1. 它有没有被 import?
📌 2. 它有没有写进 components?
📌 3. 模板里有没有用对标签名?
📌 4. name 只在缓存、调试、递归这几个场景才有意义

把这四步想明白,就不会再混淆了。


🏁 一句话总结

import   → 把文件拿进来(JS 变量名)
components → 注册进当前页面(决定能用什么标签)
<im-layout> → 在模板里调用组件
name     → 组件的自我描述(和能不能用无关)

这是我学习 Vue2 真实项目时的第一篇总结。学得慢没关系,搞清楚一个是一个。

下一篇:src/components 里的文件,到底从哪里被调用?

❌