孟凡的博客blog.mengfanyaoapp.com
课程资料库›Vue 3 + Element Plus 前端开发›第9次 · 生命周期钩子与模板引用
模块总览

第9次 · 生命周期钩子与模板引用

2026-09-15 · Vue 3 + Element Plus 前端开发 · 课程笔记
课堂练习资料 09-生命周期钩子与模板引用.zip
下载 ZIP 压缩包

第9次 · 生命周期钩子与模板引用

第1/7页 · 先知道今天要做什么

前八课都在处理“数据怎么变成界面”“组件之间怎么说话”。但有一类事情和数据没关系,它们发生在特定时刻:页面第一次出现时要去后端拉数据、组件消失时要把定时器关掉、想直接操作某个 DOM 元素(让它获得焦点、滚动到某处)。

这些“特定时刻”就是生命周期。 组件从创建到销毁,会经过几个固定的时间点,Vue 允许你在这些时间点上挂一段自己的代码。今天要掌握的就是:在什么时刻该做什么事。

同时学一个配套技能:模板引用(template ref)——怎么在代码里拿到某个元素的真实 DOM。

今天的场景

一个“服务器监控小面板”。它要演示三件事:

  1. 组件第一次挂载时启动一个定时器,每秒刷新数据;
  2. 组件被销毁时把定时器清掉(不清掉就是内存泄漏,这是新手最容易犯的错);
  3. 用模板引用拿到输入框,页面一打开就自动聚焦,点按钮也能重新聚焦。

下课时拿出这三样结果

做到什么 在哪里看 完成的证据
会在 onMounted 里做初始化 面板数据 页面一打开,时间与 CPU 数值就自动出现并每秒刷新。
会在 onUnmounted 里清理副作用 Console 日志 卸载组件后,Console 不再打印任何东西(证明定时器被清掉了)。
会用模板引用操作 DOM 输入框 页面加载后光标自动在输入框里;点按钮能再次聚焦。

你只需要带着这些基础来

会 ref、computed、watch、v-if、组件拆分;第 8 课的工程还能跑。

本课时 45 分钟怎么上

时间 老师带着做 学生留下什么
0—6 分钟 现场演示“不清理定时器”的后果:卸载组件后 Console 还在刷。 明白副作用必须成对出现。
6—20 分钟 讲六个钩子的执行顺序,重点讲 onMounted 与 onUnmounted。 笔记里写下六钩子的顺序。
20—30 分钟 讲模板引用:怎么声明、什么时候能拿到值、nextTick 解决什么。 能说出“为什么不能在 setup 顶层直接读 DOM”。
30—42 分钟 粘贴本课 App.vue,做挂载/卸载与聚焦实验。 三个实验全部跑通。
42—45 分钟 抽查:问“定时器为什么必须清”。 全员提交。

从哪里开始

复制第 8 课的工程,改名 lesson-vue-09。不需要新增依赖。

今天先掌握到这里

第 10 课是阶段一综合实战,会把第 1—9 课的内容全部串成一个学员管理系统。

来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档(生命周期钩子 / 模板引用章节)· 组件依据 Element Plus 2.8 官方文档


第2/7页 · 把新词说清楚

新词与本课含义

名字 用熟悉的话理解 本课里长什么样
生命周期钩子 组件生命中的固定时间点,你可以挂一段代码上去。 onMounted(() => ...)
挂载 mounted 组件的界面第一次真正出现在页面上。 拉数据、启动定时器、聚焦输入框
卸载 unmounted 组件的界面被移除了,它的一切都要收尾。 清定时器、取消订阅、断开连接
副作用的成对原则 凡是“开启”了什么,就要在卸载时“关闭”什么。 定时器 / 事件监听 / WebSocket
模板引用 在代码里拿到某个 DOM 元素或子组件的引用。 const inputRef = ref(null)
nextTick 等这一轮界面更新完成之后,再执行下一句。 改完 v-if 后立刻读 DOM

六个钩子与执行顺序

组件创建
  ↓
setup()                       ← 你写代码的地方(相当于早期)
  ↓ onBeforeMount             ← 界面还没生成
  ↓ onMounted                 ← 界面已生成,能读 DOM 了 ★最常用
  ↓ 【数据变化】onBeforeUpdate  ← 数据变了,界面还没重画
  ↓ 【数据变化】onUpdated       ← 界面重画完
  ↓
onBeforeUnmount               ← 组件即将销毁
  ↓ onUnmounted               ← 已销毁,做清理 ★必须记住

把资源花在这两个上就够了:

钩子 什么时候用 典型代码
onMounted 需要“界面存在之后”才能做的事 拉接口、启动定时器、初始化图表、聚焦输入框
onUnmounted 需要“组件消失之后”必须收尾的事 clearInterval、removeEventListener、关闭 WebSocket

onUpdated 要格外小心:在它里面改数据,很容易造成“改了又更新、更新了又改”的死循环。多数情况下你想做的事应该用 watch 或 computed 完成。

为什么副作用必须成对

// ❌ 危险的写法:只开不关
onMounted(() => {
  setInterval(() => { tick() }, 1000)
})

// ✅ 正确写法:开了就要关
let timer = null
onMounted(() => {
  timer = setInterval(() => { tick() }, 1000)
})
onUnmounted(() => {
  clearInterval(timer)     // 不清就是内存泄漏 + 后台空转
})

为什么必须清? 定时器不属于组件,它是浏览器的。组件被卸载了,定时器还在跑,回调里还在改一个“已经不存在”的组件的数据,既浪费性能又容易报错。同理,window.addEventListener 不加 removeEventListener,每次挂载都多一个监听器,挂载十次就有十个。

模板引用:三步

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

const inputRef = ref(null)          // ① 起一个同名的 ref 变量,初始值 null

onMounted(() => {
  inputRef.value.focus()            // ③ 挂载后才能用(此时才真正是 DOM)
})
</script>

<template>
  <input ref="inputRef" />          <!-- ② 在标签上写 ref="变量名" -->
</template>

三个必须知道的细节:

  1. 只能在 onMounted 之后读。在 setup 顶层打印 inputRef.value,得到的是 null——因为那时界面还没生成。
  2. ref="inputRef" 里写的是字符串名字,必须和变量名一致。
  3. 元素是 v-if 控制的时候,被隐藏期间 inputRef.value 会变成 null,用之前要判断。

nextTick 解决什么问题

数据一改,Vue 不会立刻重画界面(它要攒一批一起改,这样更快)。所以下面这段是错的:

show.value = true
inputRef.value.focus()      // ❌ 此时输入框还没被创建,inputRef.value 是 null

正确写法:

import { nextTick } from 'vue'

show.value = true
await nextTick()            // 等界面更新完成
inputRef.value.focus()      // ✅ 现在能拿到了

一句话记法:改完数据想立刻碰 DOM,就 await nextTick()。

只看这几行:一份完整的“开与关”

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

const now = ref('')
let timer = null

onMounted(() => {
  console.log('组件挂载了')
  timer = setInterval(() => { now.value = new Date().toLocaleTimeString() }, 1000)
})

onUnmounted(() => {
  console.log('组件卸载了,清掉定时器')
  clearInterval(timer)
})
</script>

先口头预测:如果只写 onMounted 不写 onUnmounted,把这个组件用 v-if 隐藏掉,Console 里的时间还会继续走吗?

会继续走。now 每秒钟还在更新,只是页面看不见它了。这就是“没清理”的代价——白耗性能。而且在真实项目里,这类残留还会导致内存占用越来越高。

来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档(生命周期钩子 / 模板引用章节)· 组件依据 Element Plus 2.8 官方文档


第3/7页 · 先看一个小例子

本课使用的 App.vue(单文件,含一个带生命周期的内联组件)

<script setup>
import { ref, computed, onMounted, onUnmounted, nextTick, defineComponent, h } from 'vue'
import {
  ElCard, ElButton, ElInput, ElTag, ElProgress, ElAlert, ElSwitch,
  ElDescriptions, ElDescriptionsItem, ElMessage
} from 'element-plus'
import 'element-plus/dist/index.css'

/* ============ 内联子组件:Monitor(演示完整的生命周期) ============ */
const Monitor = defineComponent({
  name: 'Monitor',
  emits: ['log'],
  setup(props, { emit }) {
    const now = ref('')
    const cpu = ref(20)
    const mem = ref(45)
    const ticks = ref(0)
    let timer = null

    function tick() {
      ticks.value++
      now.value = new Date().toLocaleTimeString()
      // 造一些看起来在波动的数据
      cpu.value = Math.min(98, Math.max(5, cpu.value + Math.round((Math.random() - 0.5) * 24)))
      mem.value = Math.min(95, Math.max(10, mem.value + Math.round((Math.random() - 0.5) * 10)))
    }

    onMounted(() => {
      emit('log', 'Monitor 挂载完成 → 启动定时器')
      tick()
      timer = setInterval(tick, 1000)
    })

    // ⚠️ 关键:组件被销毁时必须清掉定时器,否则它会在后台一直跑
    onUnmounted(() => {
      emit('log', 'Monitor 已卸载 → 清除定时器(Console 不再打印)')
      clearInterval(timer)
    })

    function statusType(v) {
      if (v >= 85) return 'danger'
      if (v >= 60) return 'warning'
      return 'success'
    }

    return () => h('div', null, [
      h(ElDescriptions, { column: 2, border: true }, {
        default: () => [
          h(ElDescriptionsItem, { label: '当前时间' }, () => now.value || '—'),
          h(ElDescriptionsItem, { label: '已刷新次数' }, () => String(ticks.value)),
          h(ElDescriptionsItem, { label: 'CPU 使用率' }, () => [
            h(ElTag, { type: statusType(cpu.value), size: 'small' }, () => cpu.value + '%')
          ]),
          h(ElDescriptionsItem, { label: '内存使用率' }, () => [
            h(ElTag, { type: statusType(mem.value), size: 'small' }, () => mem.value + '%')
          ])
        ]
      }),
      h('div', { class: 'bars' }, [
        h(ElProgress, { percentage: cpu.value, strokeWidth: 14,
          color: cpu.value >= 85 ? '#f56c6c' : cpu.value >= 60 ? '#e6a23c' : '#67c23a' }),
        h(ElProgress, { percentage: mem.value, strokeWidth: 14 })
      ])
    ])
  }
})

/* ============ 父组件 ============ */
const showMonitor = ref(true)
const logs = ref([])
const inputRef = ref(null)
const draft = ref('')
const showSearchBox = ref(false)
const searchRef = ref(null)

const lastLog = computed(() => logs.value[logs.value.length - 1] || '(还没有日志)')

function addLog(text) {
  const t = new Date().toLocaleTimeString()
  logs.value.push(`[${t}] ${text}`)
  console.log(text)
  if (logs.value.length > 12) logs.value.shift()
}

// ① 页面加载完成后自动聚焦输入框
onMounted(() => {
  addLog('父组件挂载完成 → 自动聚焦输入框')
  inputRef.value && inputRef.value.focus()
})

// ② 手动聚焦
function focusInput() {
  inputRef.value && inputRef.value.focus()
  ElMessage.success('已聚焦到输入框')
}

// ③ nextTick 演示:先显示搜索框,再聚焦(不 await 就会失败)
async function openSearch() {
  showSearchBox.value = true
  // 试一试把下面这行注释掉,会出现 Cannot read properties of null
  await nextTick()
  searchRef.value && searchRef.value.focus()
  addLog('搜索框已显示并聚焦(nextTick 之后才拿到 DOM)')
}

onUnmounted(() => {
  console.log('父组件卸载')
})
</script>

<template>
  <div class="page">
    <h1 class="title">生命周期钩子与模板引用</h1>

    <el-card class="block">
      <template #header>
        <div class="hd">
          <span>服务器监控(内联子组件 Monitor)</span>
          <el-switch v-model="showMonitor" active-text="挂载 / 卸载" />
        </div>
      </template>

      <!-- v-if 为 false 时组件被卸载,onUnmounted 被触发 -->
      <Monitor v-if="showMonitor" @log="addLog" />
      <p v-else class="gone">组件已卸载。观察 Console:不再有任何时间打印。</p>
    </el-card>

    <el-card class="block">
      <template #header>模板引用:自动聚焦</template>
      <el-input ref="inputRef" v-model="draft" placeholder="页面加载后光标应该已经在这里" clearable />
      <div class="ops">
        <el-button type="primary" @click="focusInput">重新聚焦</el-button>
        <el-button @click="openSearch">显示搜索框并聚焦(演示 nextTick)</el-button>
      </div>
      <el-input
        v-if="showSearchBox"
        ref="searchRef"
        placeholder="这个框是刚被 v-if 创建出来的"
        clearable
      />
    </el-card>

    <el-card class="block">
      <template #header>生命周期日志(最新一条:{{ lastLog }})</template>
      <pre class="log">{{ logs.join('\n') || '暂无' }}</pre>
      <el-alert
        title="做实验:把右上角开关关掉,Console 应该停止打印时间与 CPU 数据——这证明 onUnmounted 里的 clearInterval 生效了。"
        type="info" :closable="false" show-icon
      />
    </el-card>
  </div>
</template>

<style>
html, body, #app {
  margin: 0; padding: 0; max-width: none; width: auto; height: auto;
  display: block; place-items: initial; text-align: left; background: #f5f7fa;
}
</style>

<style scoped>
.page { max-width: 900px; margin: 28px auto; padding: 0 16px; }
.title { font-size: 22px; color: #303133; margin: 0 0 18px; }
.block { margin-bottom: 16px; }
.hd { display: flex; justify-content: space-between; align-items: center; }
.bars { margin-top: 12px; }
.gone { color: #909399; margin: 4px 0; }
.ops { margin: 12px 0; display: flex; gap: 10px; flex-wrap: wrap; }
.log {
  background: #1f2430; color: #a9dc76; padding: 12px; border-radius: 6px;
  font-size: 12px; line-height: 1.7; margin: 0 0 12px; max-height: 220px; overflow: auto;
}
</style>

预期看见什么

  1. 第一张卡片:右上角一个“挂载/卸载”开关,下面显示当前时间、已刷新次数、CPU 与内存使用率,还有两条彩色进度条。这些东西每秒自己动一次。
  2. 第二张卡片:一个输入框,光标已经在里面(页面一打开就聚焦了)。下面两个按钮。
  3. 点“重新聚焦”,光标回到输入框。
  4. 点“显示搜索框并聚焦”,下面出现一个新输入框且光标已经在里面。
  5. 第三张卡片:一串带时间的日志。把第一张卡片的开关关掉,日志里出现“Monitor 已卸载 → 清除定时器”,Console 里的时间打印停止。

先猜一猜,读完代码再回答

为什么 openSearch 里必须写 await nextTick()?

async function openSearch() {
  showSearchBox.value = true
  await nextTick()
  searchRef.value && searchRef.value.focus()
}

因为 showSearchBox 改成 true 只是“改了数据”,Vue 不会立刻把输入框画到页面上。 如果不等,searchRef.value 还是 null,focus() 就会报错。nextTick 的意义就是“等这一次界面更新做完,再往下走”。可以自己把这个 await 注释掉试试,就能看到报错。

来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档(生命周期钩子 / 模板引用章节)· 组件依据 Element Plus 2.8 官方文档


第4/7页 · 一步一步做出来

第1步:亲眼看到六个钩子的顺序

<script setup>
import { onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted, ref } from 'vue'
const n = ref(0)

onBeforeMount(() => console.log('1 onBeforeMount'))
onMounted(() => console.log('2 onMounted'))
onBeforeUpdate(() => console.log('3 onBeforeUpdate'))
onUpdated(() => console.log('4 onUpdated'))
onBeforeUnmount(() => console.log('5 onBeforeUnmount'))
onUnmounted(() => console.log('6 onUnmounted'))
</script>
<template>
  <p>{{ n }}</p>
  <button @click="n++">改数据</button>
</template>

刷新页面,预期:Console 依次打印 1、2。点“改数据”,预期:打印 3、4。把整个组件用 v-if 隐藏,预期:打印 5、6。

第2步:制造一次内存泄漏

把第 3 页 Monitor 组件里的 onUnmounted 整段删掉。刷新页面后点开关卸载组件。预期:Console 里的时间与 CPU 打印继续出现,而且每次重新挂载都会多一个定时器,刷新速度越来越快(1 秒、0.5 秒、0.33 秒……)。看完把 onUnmounted 加回来。

第3步:体验 setup 阶段读不到 DOM

在 <script setup> 顶层加:

const inputRef = ref(null)
console.log('setup 阶段的 inputRef:', inputRef.value)   // 预期:null

再把同样的打印放到 onMounted 里,预期:这次拿到真实元素。

第4步:模板引用的三种情况

<!-- ① 普通元素 -->
<input ref="a" />

<!-- ② v-for 里:拿到的是数组 -->
<div v-for="i in 3" :key="i" ref="listRefs">{{ i }}</div>

<!-- ③ v-if 为 false 时:ref 的值是 null -->
<div v-if="show" ref="b">内容</div>

在 onMounted 里分别打印 a.value、listRefs.value.length、b.value,看清楚三种结果。

第5步:nextTick 取反实验

把 openSearch 里的 await nextTick() 注释掉,点按钮,预期:Console 报 Cannot read properties of null (reading 'focus')。加回来,预期:正常聚焦。

第6步:整段替换为本课完整版

把第 3 页的完整 App.vue 覆盖 src/App.vue,把三张卡片全部跑通。

本课文件速查

位置 本次作用
Monitor 的 onMounted 启动定时器、立刻刷新一次数据。
Monitor 的 onUnmounted 清掉定时器,避免后台空转。
父组件 onMounted 页面加载后自动聚焦输入框。
inputRef / searchRef 两个模板引用,分别指向两个输入框。
await nextTick() 等界面更新完成后才去操作新创建的 DOM。

完成修改后检查

操作或输入 预期观察
刷新页面 时间、CPU、内存立刻有值,并每秒变化。
关掉开关 日志出现“已卸载”,Console 停止打印时间。
再打开开关 定时器重新启动(只启动一个,刷新速度没有变快)。
点“重新聚焦” 光标回到输入框。
点“显示搜索框并聚焦” 新输入框出现且获得焦点。

来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档(生命周期钩子 / 模板引用章节)· 组件依据 Element Plus 2.8 官方文档


第5/7页 · 换成自己的代码

你的任务

  1. 换成你自己的定时任务:把 Monitor 改成你关心的一组数据(例如“今日待办剩余数”“课程完成百分比”),仍然每秒或每三秒刷新一次。
  2. 加一条事件监听:在 onMounted 里加 window.addEventListener('resize', onResize),记录窗口宽度变化次数并在页面上显示;在 onUnmounted 里 removeEventListener 把它去掉。
  3. 加一个“手动销毁”按钮:不用 v-if,而是通过一个 showMonitor 标志切换(这就是当前的做法),但要求在日志里能清楚看到“挂载 → 卸载 → 再挂载”的完整过程。
  4. 给第二个输入框加一个自动清空:点击“显示搜索框并聚焦”时,如果搜索框已经存在,则把它清空再聚焦(提示:可以直接用 searchRef.value.clear(),因为 Element Plus 的 el-input 暴露了 clear 方法)。
  5. 说清一件小事:把 onUpdated 加进 Monitor,在里面 console.log('更新了'),观察一秒打印多少次,并写下为什么不要在这个钩子里改数据。

按这张操作单完成

步骤 改哪里 运行后应该看到什么
1 换数据 Monitor 的 ref 与 render 页面显示你自己的指标。
2 加监听 onMounted / onUnmounted 改变窗口大小时计数增加。
3 完整日志 addLog 调用点 日志能读出“挂了 → 卸了 → 又挂了”。
4 搜索框清空 openSearch 已有内容时先清空再聚焦。
5 观察 onUpdated Monitor 加钩子 Console 每秒打印一次,明白它的触发频率。

最低完成标准:先把这三项做实

检查项 通过条件
成对原则 代码里凡是 setInterval / addEventListener,都对应一处 clearInterval / removeEventListener。
钩子选择正确 初始化逻辑在 onMounted,清理逻辑在 onUnmounted,没有把请求写在 setup 顶层。
模板引用可用 至少两处 ref="xxx" 能在代码里拿到并操作(focus / clear)。

在本地写练习记录

  1. 抄下第 1 步六个钩子的实际打印顺序。
  2. 记录“制造内存泄漏”实验的现象:删掉 onUnmounted 后 Console 的打印频率怎么变的。
  3. 写下 nextTick 取反实验的报错原文与结论。

最后交什么

提交 src/App.vue 与 练习记录.md。

来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档(生命周期钩子 / 模板引用章节)· 组件依据 Element Plus 2.8 官方文档


第6/7页 · 出错时从哪里查

把现象和检查动作对上

看到什么现象 / 报错 先检查哪里 修好后怎样确认
Cannot read properties of null (reading 'focus') 在 DOM 还没生成时就操作了它(setup 顶层,或 v-if 刚改完没等 nextTick)。 移到 onMounted 里,或在改完数据后 await nextTick()。
组件隐藏后 Console 还在打印 定时器没清,或 onUnmounted 里清的不是同一个变量。 在 onUnmounted 里确认清的就是 onMounted 里赋值的那个变量。
每次挂载后刷新速度变快 定时器被重复创建且旧的没清掉。 清理后速度正常。
onUpdated 里改了数据导致页面卡死/刷屏 在 onUpdated 里改数据。 换成 watch 或 computed。
ref="xxx" 拿到的永远是 null 变量名与 ref 属性值不一致;或元素在 v-if 里且条件是 false。 名字一致且元素可见后能拿到。
在 onMounted 里发请求后马上 console.log 为空 请求是异步的,此时还没回来。 把打印放进 .then() 或 async/await 之后。
卸载后仍报“组件不存在”相关错误 异步回调在组件销毁后还在执行。 清理时取消请求(AbortController)或加“是否已卸载”标志。

排错口诀

要碰 DOM 等挂载,挂载之前拿不到; 改了数据想碰 DOM,先 await nextTick; 凡有 interval / addEventListener,必有对应的清理; onUpdated 里别改数据,那是死循环的入口。

不要用这些办法“修复”

  1. 不要为了“让定时器不报错”而把 setInterval 写到 setup 顶层——那样每次组件重新创建都会再开一个,越跑越多。
  2. 不要在 onUnmounted 里把数据重置一遍当作“清理”。真正要做的是解除对外部资源(定时器、事件、连接)的占用。
  3. 不要用 setTimeout 猜时间(例如 setTimeout(fn, 300))来等 DOM 出现,nextTick 才是正确做法。

需要老师帮忙时交这些

出问题那段代码(钩子名 + 里面写了什么)、Console 报错原文、你做了什么操作、期望发生什么、实际发生什么。

老师巡视时只问三句话

  1. 你的初始化代码放在哪个钩子里?为什么不能放在 setup 顶层?(能答出此时还没有 DOM / 组件未挂载)
  2. 你开了哪些需要清理的东西?清理代码在哪?(能一一对应)
  3. 为什么这里必须 nextTick?(能说出“数据改了但界面还没更新”)

来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档(生命周期钩子 / 模板引用章节)· 组件依据 Element Plus 2.8 官方文档


第7/7页 · 用自己的话讲一遍

回到第 3 页:你刚才猜对了吗?

为什么 openSearch 必须等 nextTick?

参考解释:Vue 更新界面是“攒一批一起改”的。改 showSearchBox 只是改了数据,DOM 还没创建。nextTick 返回一个 Promise,等当前这一轮界面更新完成后才继续执行,此时 searchRef.value 才指向真实的输入框实例。

回答本课的两个问题

问题 参考解释
为什么副作用必须成对出现? 因为定时器、事件监听、连接都属于浏览器/操作系统的资源,不属于组件。组件销毁时它们不会自动消失,会继续执行回调、占用内存。必须在 onUnmounted 里手动解除。
为什么不能在 onUpdated 里改数据? onUpdated 在每次界面更新后触发,在里面改数据会再触发一次更新,于是无限循环。

五个必须能答上来的问题

考考自己 可以这样通俗回答
拉接口数据应该放在哪个钩子? onMounted(越早拿到越好,但要在界面存在之后)。
模板引用声明后,什么时候才有值? onMounted 之后。v-if 为 false 时又会变成 null。
nextTick 什么时候用? 改完数据马上要操作 DOM 的时候。
组件销毁时一定要清什么? 定时器、事件监听、WebSocket、正在进行的请求。
onBeforeUnmount 和 onUnmounted 的区别? 前者在销毁之前(DOM 还在),后者在销毁之后。清理通常写在 onUnmounted。

一分钟现场演示指南

  1. 展示自动刷新:指着时间和进度条说“这些都是 onMounted 里启动的定时器在干活”。
  2. 展示清理:关掉开关,Console 停止打印,说“这一行 clearInterval 就是合格与不合格的分界线”。
  3. 展示模板引用:刷新页面,光标已在输入框里;点按钮再次聚焦。
  4. 收尾一句:“该在什么时候做事,就写在对应的钩子里;开了什么,就要记得关掉什么。”

交作业前最后检查

  • src/App.vue 与 练习记录.md 已保存。
  • 每个 setInterval / addEventListener 都有对应清理。
  • 模板引用至少两处可用。
  • nextTick 取反实验的现象已记录。
  • Console 无红色报错。

课后再做一个小对照

把 Monitor 里“每秒自动刷新”改成“点按钮手动刷新一次”(去掉定时器)。写下你更倾向哪种方式、在真实项目里哪种更常见,以及理由(提示:轮询会带来额外请求量,实时性要求高时才值得每秒拉一次)。

来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档(生命周期钩子 / 模板引用章节)· 组件依据 Element Plus 2.8 官方文档