理解什么是生命周期

Q: Vue 组件的「生命周期」指的是什么?钩子函数有什么作用?

A: 生命周期是组件从创建到最终销毁所经历的一系列过程。在这个过程中,Vue 设置了一些特殊的时间点,开发者可以在这些时间点注册自己的函数——这类函数被称为钩子函数(Hook)。必须等开发者注册的钩子函数执行完毕后,Vue 才会继续走后面的流程。这就给了开发者在特定阶段执行自己业务逻辑的机会。

Vue3 完整的生命周期图如下:

Vue3 生命周期图示

图片展示了 Vue3 组件从创建、挂载、更新到卸载的完整生命周期流程,以及各阶段对应的钩子函数。

快速入门:onMounted

虽然生命周期钩子函数有很多,但在实际开发中,有一个是最先需要掌握的——onMounted

onMounted 会在组件完成了初始化渲染并且 DOM 节点已创建并插入之后被触发。这意味着在 onMounted 中,你可以安全地访问真实的 DOM 元素,也是请求后端数据的最佳时机

下面是一个典型的使用场景——在组件挂载后请求用户列表:

<template>
  <div>
    <h1>用户列表</h1>
    <ul v-if="!loading">
      <li v-for="(user, index) in users" :key="index">
        {{ user.name }} - {{ user.email }}
      </li>
    </ul>
    <div v-if="loading">加载中...</div>
    <div v-if="error">出错啦!</div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue';

const users = ref([]);
const loading = ref(false);
const error = ref(null);

onMounted(async () => {
  // 一般来讲,我们会在此生命周期钩子方法中请求数据
  loading.value = true;
  try {
    const res = await fetch('https://jsonplaceholder.typicode.com/users');
    if (res.ok) {
      users.value = await res.json();
    } else {
      throw new Error('请求失败');
    }
  } catch (err) {
    error.value = err.message;
  } finally {
    // 无论成功还是失败,都需要关闭 loading 状态
    loading.value = false;
  }
});
</script>

这个例子中包含了三个常见状态的管理——loading(加载中)、数据展示(成功)、error(失败),是真实项目中请求数据的标准范式。

Vue2 与 Vue3 的生命周期钩子差异

Vue3 是支持 Vue2 的选项式 API 写法的,所以两套生命周期方法在 Vue3 中可以共存,但命名有所不同:

生命周期阶段 Vue3(组合式 API) Vue2(选项式 API)
挂载后 onMounted mounted
更新后 onUpdated updated
卸载前 onBeforeUnmount beforeDestroy
卸载后 onUnmounted destroyed

如果同一个生命周期阶段同时设置了两种形式的钩子方法,Vue3 的组合式钩子会先于 Vue2 的选项式钩子执行

<template>
  <div>Vue2 和 Vue3 钩子函数执行时机对比</div>
</template>

<script>
import { onMounted } from 'vue';
export default {
  mounted() {
    console.log('执行Vue2的钩子函数');
  },
  setup() {
    onMounted(() => {
      console.log('执行Vue3的onMounted函数');
    });
  },
};
</script>

运行结果:Vue3 的 onMounted 会比 Vue2 的 mounted 先输出。不过这一点知道即可——真实开发中绝对不会两个版本混着用


侦听器(Watch)快速入门

Q: 侦听器和计算属性有什么区别?什么时候用侦听器?

A: 侦听器和计算属性类似,都是依赖响应式数据。区别在于:

  • 计算属性:依赖数据变化时,重新做二次计算,不涉及副作用(如修改 DOM、发请求)。
  • 侦听器:依赖数据变化时,允许执行副作用操作——更改 DOM、发送异步请求、操作本地存储等。

下面是一个经典的侦听器示例——智能机器人问答:

<template>
  <div>
    <h1>智能机器人</h1>
    <div>
      <input v-model="question" placeholder="请输入问题" />
    </div>
    <div v-if="loading">正在加载中...</div>
    <div v-else>{{ answer }}</div>
  </div>
</template>

<script setup>
import { ref, watch } from 'vue';
const question = ref(''); // 存储用户输入的问题,以 ? 结束
const answer = ref('');   // 存储机器人的回答
const loading = ref(false);

// 侦听 question 的变化
watch(question, async (newValue) => {
  if (newValue.includes('?')) {
    loading.value = true;
    answer.value = '思考中....';
    try {
      const res = await fetch('https://yesno.wtf/api');
      const result = await res.json();
      answer.value = result.answer;
    } catch (err) {
      answer.value = '抱歉,我无法回答您的问题';
    } finally {
      loading.value = false;
    }
  }
});
</script>

在这个例子中,watch 侦听 question 这个 ref 的变化,回调函数接收两个参数:新的状态值旧的状态值。当用户输入包含 ? 时,触发异步请求获取答案——这就是典型的副作用操作。

侦听的数据源类型

watch 的第一个参数(数据源)支持多种类型:

1. 侦听 ref: 上面已经演示。

2. 侦听计算属性:

<template>
  <div>
    <input type="text" v-model="firstName" placeholder="first name" />
    <input type="text" v-model="lastName" placeholder="last name" />
    <p>全名:{{ fullName }}</p>
  </div>
</template>

<script setup>
import { ref, computed, watch } from 'vue';
const firstName = ref('John');
const lastName = ref('Doe');
const fullName = computed(() => `${firstName.value} ${lastName.value}`);

watch(fullName, (newVal, oldVal) => {
  console.log(`new: ${newVal}, old: ${oldVal}`);
});
</script>

3. 侦听 reactive 响应式对象:

<template>
  <div>
    <input type="text" v-model="user.name" placeholder="name" />
    <input type="text" v-model="user.age" placeholder="age" />
    <p>用户信息:{{ user.name }} - {{ user.age }}</p>
  </div>
</template>

<script setup>
import { reactive, watch } from 'vue';
const user = reactive({
  name: 'John',
  age: 18,
});

watch(user, () => {
  console.log('触发了侦听器回调函数');
});
</script>

4. 侦听 Getter 函数(函数的返回值):

<template>
  <div>
    <input type="number" v-model="count" />
    <p>是否为偶数?{{ isEven() }}</p>
    <div>count2: {{ count2 }}</div>
    <button @click="count2++">+1</button>
  </div>
</template>

<script setup>
import { ref, watch } from 'vue';
const count = ref(0);
const count2 = ref(0);

function isEven() {
  console.log('isEvent 函数被重新执行了');
  if (count2.value === 5) return 'this is a test';
  return count.value % 2 === 0;
}

// 侦听 isEven 函数的返回值
watch(isEven, () => {
  console.log('触发了侦听器回调函数');
});
</script>

这里侦听的是 isEven() 这个函数的返回值结果——只有当返回值发生变化时,才会触发回调。

5. 侦听多个数据源组成的数组:

<template>
  <div>
    <div><input type="text" v-model="title" /></div>
    <div><textarea v-model="description" cols="30" rows="10"></textarea></div>
  </div>
</template>

<script setup>
import { ref, watch } from 'vue';
const title = ref('');
const description = ref('');

// 数组中任何一个数据发生变化,都会触发回调函数
watch([title, description], () => {
  console.log('侦听器的回调函数执行了');
});
</script>

这种方式非常适合需要同时监控多个输入的场景,比如表单的实时校验、自动保存等。

侦听层次与 deep 选项

Q: 侦听 reactive 对象时,嵌套属性变化会触发吗?

A: 当侦听的数据源是 reactive 类型时,默认就是深层次侦听——哪怕嵌套很深的对象属性发生变化,侦听器的回调也会执行。

<template>
  <div>
    <h1>任务列表</h1>
    <ul>
      <li v-for="task in tasks.list" :key="task.id">
        {{ task.title }} - {{ task.completed ? '已完成' : '未完成' }}
        <button @click="task.completed = !task.completed">切换状态</button>
      </li>
    </ul>
  </div>
</template>

<script setup>
import { reactive, watch } from 'vue';
const tasks = reactive({
  list: [
    { id: 1, title: 'Task 1', completed: false },
    { id: 2, title: 'Task 2', completed: true },
  ],
});

watch(tasks, () => {
  console.log('侦听器触发了!');
});
</script>

点击「切换状态」按钮修改 tasks.list[0].completed,虽然这是嵌套属性,侦听器依然会被触发。

注意:深层侦听虽然方便,但遍历大型数据结构的开销很大,只在必要时使用

另外,侦听 reactive 对象时不能直接侦听其属性值

const obj = reactive({ count: 0 });

// ❌ 错误:watch() 得到的参数是一个 number
watch(obj.count, (count) => {
  console.log(`count is: ${count}`);
});

需要改为 Getter 函数的形式:

const obj = reactive({ count: 0 });

// ✅ 正确:使用 Getter 函数
watch(
  () => obj.count,
  (count) => {
    console.log(`count is: ${count}`);
  },
);

第三个参数:配置选项

watch 接收三个参数:

  • 第一个参数:侦听的数据源
  • 第二个参数:数据变化时的回调函数
  • 第三个参数:选项对象

关键配置项:

选项 类型 说明
immediate boolean 默认回调是懒执行的;设为 true 后立即执行一次(常用于请求初始数据)
once boolean 回调只执行一次
deep boolean 强制深层次侦听(主要针对计算属性/Getter 返回的对象,因为它们默认非深层)

deep 选项的一个典型场景:侦听由计算属性或 Getter 函数返回的对象时,默认不是深层侦听,需要通过 deep: true 强制开启:

<script setup>
import { reactive, computed, watch } from 'vue';

const tasks = reactive([
  { id: 1, title: 'Learn Vue', completed: false },
  { id: 2, title: 'Read Documentation', completed: false },
  { id: 3, title: 'Build Something Awesome', completed: false },
]);

const selectedId = reactive({ id: null });

const selectedTask = computed(() => {
  return tasks.find((task) => task.id === selectedId.id);
});

// 侦听 Getter 函数返回的计算属性值
// deep: true 确保嵌套属性变化也能触发
watch(
  () => selectedTask.value,
  () => {
    console.log('Task details changed');
  },
  { deep: true },
);
</script>

watchEffect:自动跟踪依赖

Q: watchEffect 和 watch 有什么区别?什么时候用哪个?

A: watchEffect 简化了 watch 的写法——不再需要显式指定响应式数据依赖。回调函数中用到了哪个响应式数据,哪个数据就会自动成为依赖。

对比写法:

watch:

const todoId = ref(1);
const data = ref(null);

watch(
  todoId, // 需要显式指定依赖
  async () => {
    const response = await fetch(
      `https://jsonplaceholder.typicode.com/todos/${todoId.value}`,
    );
    data.value = await response.json();
  },
  { immediate: true },
);

watchEffect:

// 不需要显式指定依赖,回调中用到 todoId 就自动追踪
watchEffect(async () => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/todos/${todoId.value}`,
  );
  data.value = await response.json();
});

对于单个依赖的场景,watchEffect 的收益不大。但当涉及多个依赖项时,watchEffect 的优势就体现出来了——你不需要手动罗列所有依赖。

另外,watchEffect 侦听嵌套数据结构时,只会侦听回调中实际用到的属性,而不是递归侦听所有属性:

<script setup>
import { reactive, watchEffect } from 'vue';
const team = reactive({
  members: [
    { id: 1, name: 'Alice', role: 'Leader', status: 'Active' },
    { id: 2, name: 'Bob', role: 'Member', status: 'Inactive' },
  ],
});

function updateLeaderStatus() {
  const leader = team.members.find((me) => me.role === 'Leader');
  leader.status = leader.status === 'Active' ? 'Inactive' : 'Active';
}

function updateMemberStatus() {
  const member = team.members.find((member) => member.role === 'Member');
  member.status = member.status === 'Active' ? 'Inactive' : 'Active';
}

watchEffect(() => {
  // 只关注 leader 的状态
  const leader = team.members.find((me) => me.role === 'Leader');
  console.log('Leader状态:', leader.status);
  // 修改 Member 的状态不会触发此侦听器
});
</script>

这种精确追踪的特性在大型数据结构中能显著提升性能。

回调触发时机:flush 选项

默认情况下,侦听器回调在父组件更新之后所属组件的 DOM 更新之前被调用。这意味着在回调中访问所属组件的 DOM 时,拿到的是 DOM 更新之前的状态。

<template>
  <div>
    <button @click="isShow = !isShow">显示/隐藏</button>
    <div v-if="isShow" ref="divRef">
      <p>this is a test</p>
    </div>
    <p>上面的高度为:{{ height }} pixels</p>
  </div>
</template>

<script setup>
import { ref, watch } from 'vue';
const isShow = ref(false);
const height = ref(0);
const divRef = ref(null);

watch(isShow, () => {
  // 此时拿到的是 DOM 更新前的高度(0)
  height.value = divRef.value ? divRef.value.offsetHeight : 0;
  console.log(`当前获取的高度为:${height.value}`);
});
</script>

如果需要在 DOM 更新之后再执行回调,设置 flush: 'post' 即可:

watch(
  isShow,
  () => {
    height.value = divRef.value ? divRef.value.offsetHeight : 0;
    console.log(`当前获取的高度为:${height.value}`);
  },
  { flush: 'post' },
);

停止侦听器

大多数情况下不需要关心停止侦听器——组件卸载时会自动停止组件上设置的侦听器。

但是这个自动停止仅限于同步设置的侦听器。如果是异步设置的侦听器,组件卸载也不会自动停止:

<script setup>
import { watchEffect } from 'vue';

// ✅ 自动停止
watchEffect(() => {});

// ❌ 不会自动停止!
setTimeout(() => {
  watchEffect(() => {});
}, 100);
</script>

这种情况下需要手动停止。watch/watchEffect 的返回值是一个停止函数,调用它即可:

const unwatch = watchEffect(() => {});
// 手动停止
unwatch();

一个实际场景——当值达到某个条件后停止侦听:

<template>
  <div>
    <button @click="a++">+1</button>
    <p>当前 a 的值为:{{ a }}</p>
    <p>{{ message }}</p>
  </div>
</template>

<script setup>
import { ref, watch } from 'vue';
const a = ref(1);
const message = ref('');

const unwatch = watch(
  a,
  (newVal) => {
    if (newVal > 5) {
      unwatch(); // 超过 5 后停止侦听
    }
    message.value = `当前 a 的值为:${a.value}`;
  },
  { immediate: true },
);
</script>

小结

本文系统梳理了 Vue3 的生命周期与侦听器两大核心机制:

  • 生命周期:组件从创建到销毁的过程,钩子函数让开发者在特定阶段插入逻辑。onMounted 是请求数据、操作 DOM 的最常用钩子。Vue3 组合式钩子优先于 Vue2 选项式钩子执行,但不要混用。
  • watch 侦听器:支持 ref、reactive、计算属性、Getter 函数、多源数组等多种数据源。reactive 默认深层侦听(性能开销大),侦听属性值需使用 Getter 函数。通过 immediateoncedeepflush 选项精细控制行为。
  • watchEffect:自动追踪回调中的响应式依赖,适合多依赖场景,且仅追踪实际用到的属性(相比 watch 的深层递归更高效)。
  • 停止侦听:同步侦听器自动停止;异步侦听器需手动调用返回的停止函数。

掌握生命周期和侦听器,意味着你能够精确控制在「什么时间点」执行「什么逻辑」——这是构建复杂 Vue 应用的基础能力。