Skip to content
第 120 / 250 章前端⏱ 10 分钟阅读

第 120 章:生命周期

学习目标

  • 理解 Vue 3 组件的生命周期
  • 掌握各个生命周期钩子的使用场景
  • 学会在生命周期中处理副作用
  • 区分 Composition API 与 Options API 的生命周期

一、生命周期总览

二、生命周期钩子详解

2.1 创建阶段

setup()

vue
<script setup lang="ts">
import { ref } from 'vue';

// setup 在所有生命周期之前执行
// 是 Composition API 的入口
const count = ref(0);
console.log('setup');

// ❌ 不要在 setup 中用 onMounted 等异步副作用 API
// ✅ 同步逻辑可以直接写
</script>

onBeforeMount

vue
<script setup lang="ts">
import { onBeforeMount } from 'vue';

onBeforeMount(() => {
  // 组件挂载到 DOM 之前
  // 此时模板还没渲染,$el 不可用
  console.log('组件即将挂载');
});
</script>

2.2 挂载阶段

onMounted

vue
<script setup lang="ts">
import { onMounted, ref } from 'vue';

const el = ref<HTMLDivElement | null>(null);

onMounted(() => {
  // 组件已挂载到 DOM,$el 可用
  console.log('组件已挂载', el.value);
  // 适合做:DOM 操作、数据初始化、订阅事件
});
</script>

<template>
  <div ref="el">Hello</div>
</template>

典型用途:

  • 请求初始数据
  • DOM 操作(focus、scroll 等)
  • 启动定时器
  • 订阅事件(window.resize 等)

2.3 更新阶段

onBeforeUpdate

vue
<script setup lang="ts">
import { onBeforeUpdate, ref } from 'vue';

const count = ref(0);

onBeforeUpdate(() => {
  // 数据变了,但 DOM 还没更新
  console.log('组件即将更新', count.value);
  // 适合做:在更新前读取旧 DOM 状态
});
</script>

onUpdated

vue
<script setup lang="ts">
import { onUpdated, ref } from 'vue';

const count = ref(0);

onUpdated(() => {
  // 数据变了,DOM 也更新了
  console.log('组件已更新', count.value);
  // 适合做:基于新 DOM 的操作
});
</script>

注意

不要在 onUpdated 中修改数据,会再次触发更新,可能导致死循环。

2.4 卸载阶段

onBeforeUnmount

vue
<script setup lang="ts">
import { onBeforeUnmount } from 'vue';

onBeforeUnmount(() => {
  // 组件即将卸载
  // 组件还可用,可以做最后的清理
  console.log('组件即将卸载');
});
</script>

onUnmounted

vue
<script setup lang="ts">
import { onUnmounted } from 'vue';

let timer: number;

onUnmounted(() => {
  // 组件已卸载
  // 清理:定时器、事件监听、订阅
  clearInterval(timer);
});
</script>

典型用途:

  • 清理定时器
  • 取消事件监听
  • 取消网络请求
  • 清理订阅

2.5 其他钩子

onActivated / onDeactivated(KeepAlive)

vue
<script setup lang="ts">
import { onActivated, onDeactivated } from 'vue';

// KeepAlive 包裹的组件激活时
onActivated(() => {
  console.log('组件被激活');
});

// 失活时(被缓存)
onDeactivated(() => {
  console.log('组件被缓存');
});
</script>

onErrorCaptured(错误捕获)

vue
<script setup lang="ts">
import { onErrorCaptured } from 'vue';

onErrorCaptured((err, instance, info) => {
  console.error('捕获到子组件错误', err);
  // 返回 false 阻止向上传播
  return false;
});
</script>

onRenderTracked / onRenderTriggered(调试)

vue
<script setup lang="ts">
import { onRenderTracked, onRenderTriggered } from 'vue';

// 依赖被追踪时
onRenderTracked((event) => {
  console.log('追踪', event);
});

// 重新渲染被触发时
onRenderTriggered((event) => {
  console.log('触发渲染', event);
});
</script>

三、Composition API vs Options API 对照

Options APIComposition API
beforeCreatesetup()
createdsetup()
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
errorCapturedonErrorCaptured
activatedonActivated
deactivatedonDeactivated

3.1 对照示例

typescript
// ❌ Options API
export default {
  data() {
    return { count: 0 };
  },
  mounted() {
    console.log('mounted');
  },
  beforeUnmount() {
    console.log('beforeUnmount');
  }
};

// ✅ Composition API
import { ref, onMounted, onBeforeUnmount } from 'vue';

const count = ref(0);
onMounted(() => console.log('mounted'));
onBeforeUnmount(() => console.log('beforeUnmount'));

四、生命周期执行顺序

4.1 单组件

4.2 嵌套组件

vue
<!-- Parent.vue -->
<script setup>
import { onMounted, onBeforeMount, onUnmounted } from 'vue';
import Child from './Child.vue';

onBeforeMount(() => console.log('Parent: beforeMount'));
onMounted(() => console.log('Parent: mounted'));
onUnmounted(() => console.log('Parent: unmounted'));
</script>

<template>
  <Child />
</template>
vue
<!-- Child.vue -->
<script setup>
import { onMounted, onBeforeMount, onUnmounted } from 'vue';

onBeforeMount(() => console.log('Child: beforeMount'));
onMounted(() => console.log('Child: mounted'));
onUnmounted(() => console.log('Child: unmounted'));
</script>

挂载顺序:Parent beforeMount → Child beforeMount → Child mounted → Parent mounted

卸载顺序:Parent beforeUnmount → Child beforeUnmount → Child unmounted → Parent unmounted

五、实战案例

5.1 数据请求

vue
<script setup lang="ts">
import { ref, onMounted } from 'vue';

const users = ref<User[]>([]);
const loading = ref(false);
const error = ref<Error | null>(null);

async function fetchUsers() {
  loading.value = true;
  try {
    const res = await fetch('/api/users');
    users.value = await res.json();
  } catch (e) {
    error.value = e as Error;
  } finally {
    loading.value = false;
  }
}

onMounted(fetchUsers);
</script>

<template>
  <div v-if="loading">加载中...</div>
  <div v-else-if="error">错误: {{ error.message }}</div>
  <ul v-else>
    <li v-for="user in users" :key="user.id">{{ user.name }}</li>
  </ul>
</template>

5.2 定时器

vue
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';

const now = ref(new Date());
let timer: number;

function update() {
  now.value = new Date();
}

onMounted(() => {
  update();
  timer = window.setInterval(update, 1000);
});

onUnmounted(() => {
  clearInterval(timer);
});
</script>

<template>
  <div>{{ now.toLocaleTimeString() }}</div>
</template>

5.3 监听事件

vue
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';

const width = ref(window.innerWidth);

function handleResize() {
  width.value = window.innerWidth;
}

onMounted(() => {
  window.addEventListener('resize', handleResize);
});

onUnmounted(() => {
  window.removeEventListener('resize', handleResize);
});
</script>

<template>
  <div>窗口宽度: {{ width }}px</div>
</template>

5.4 自动滚动到底部

vue
<script setup lang="ts">
import { ref, watch, nextTick } from 'vue';

const messages = ref<Message[]>([]);
const containerRef = ref<HTMLDivElement | null>(null);

watch(
  () => messages.value.length,
  async () => {
    await nextTick();
    if (containerRef.value) {
      containerRef.value.scrollTop = containerRef.value.scrollHeight;
    }
  }
);
</script>

<template>
  <div ref="containerRef" class="message-list">
    <div v-for="msg in messages" :key="msg.id">{{ msg.text }}</div>
  </div>
</template>

5.5 自动保存草稿

vue
<script setup lang="ts">
import { ref, watch, onMounted, onUnmounted } from 'vue';

const draft = ref(localStorage.getItem('draft') || '');
let timer: number;

watch(draft, (val) => {
  // debounce:延迟 500ms 保存
  clearTimeout(timer);
  timer = window.setTimeout(() => {
    localStorage.setItem('draft', val);
  }, 500);
});

onUnmounted(() => {
  clearTimeout(timer);
  // 最终保存
  localStorage.setItem('draft', draft.value);
});
</script>

<template>
  <textarea v-model="draft" placeholder="开始输入..." />
</template>

六、VueUse 简化副作用

推荐用 VueUse 处理常见的副作用逻辑。

6.1 useEventListener

typescript
import { useEventListener } from '@vueuse/core';

// 自动清理
useEventListener(window, 'resize', () => {
  console.log(window.innerWidth);
});

6.2 useIntervalFn

typescript
import { useIntervalFn } from '@vueuse/core';

const { pause, resume } = useIntervalFn(() => {
  console.log('tick');
}, 1000);

// pause() / resume()

6.3 useElementVisibility

typescript
import { useElementVisibility } from '@vueuse/core';

const target = ref<HTMLDivElement | null>(null);
const visible = useElementVisibility(target);

watch(visible, (v) => {
  if (v) console.log('元素可见');
});

七、常见错误

7.1 在 setup 中用 DOM API

vue
<script setup>
// ❌ 此时 DOM 还没渲染
onMounted(() => {
  document.querySelector('.my-el');
});

// ❌ 同步执行,DOM 不存在
const width = window.innerWidth;
</script>

7.2 在 onUpdated 修改数据

vue
<script setup>
const count = ref(0);

onUpdated(() => {
  // ❌ 会触发死循环
  count.value++;
});
</script>

7.3 忘记清理副作用

vue
<script setup>
// ❌ 内存泄漏
let timer = setInterval(() => {
  console.log('tick');
}, 1000);

// ✅ 必须清理
onUnmounted(() => clearInterval(timer));
</script>

八、本章小结

生命周期触发时机用途
setup创建时初始化数据
onBeforeMount挂载前极少用
onMounted挂载后数据请求、DOM 操作
onBeforeUpdate更新前极少用
onUpdated更新后基于新 DOM 操作
onBeforeUnmount卸载前最后的清理
onUnmounted卸载后清理副作用
onActivatedKeepAlive 激活恢复状态
onDeactivatedKeepAlive 失活保存状态

动手练习

  1. 数据请求:实现一个用户列表组件,挂载时请求数据
  2. 定时器:实现一个秒表组件,挂载启动、卸载停止
  3. 事件监听:监听 window.resize,实现响应式布局
  4. KeepAlive:实现一个标签页切换,缓存已访问页面状态

推荐阅读


下一章第 121 章:setup 与组合式 API

本站基于 VitePress 构建 · 由 Codebook 团队维护