Skip to content

生命周期钩子

每个 Vue 组件实例在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。

生命周期图示

创建阶段:
  beforeCreate  → 实例初始化之后,数据观测和事件配置之前
  created       → 实例创建完成,数据观测、属性和方法的运算已完成

挂载阶段:
  beforeMount   → 挂载开始之前,render 函数首次被调用
  mounted       → 实例被挂载后调用,DOM 已经创建完成

更新阶段:
  beforeUpdate  → 数据更新时调用,发生在虚拟 DOM 重新渲染之前
  updated       → 数据更新导致的虚拟 DOM 重新渲染后调用

卸载阶段:
  beforeUnmount → 卸载组件实例之前调用
  unmounted     → 卸载组件实例后调用

组合式 API 中的生命周期钩子

在组合式 API 中,生命周期钩子需要在 setup() 函数中调用:

vue
<template>
  <div class="lifecycle-demo">
    <h2>生命周期演示</h2>
    
    <div class="demo-section">
      <h3>组件状态</h3>
      <p>计数器: {{ count }}</p>
      <p>消息: {{ message }}</p>
      <button @click="increment">增加计数</button>
      <button @click="updateMessage">更新消息</button>
    </div>
    
    <div class="demo-section">
      <h3>生命周期日志</h3>
      <div class="log-container">
        <div 
          v-for="(log, index) in lifecycleLogs" 
          :key="index"
          class="log-entry"
        >
          <span class="log-time">{{ log.time }}</span>
          <span class="log-hook">{{ log.hook }}</span>
          <span class="log-message">{{ log.message }}</span>
        </div>
      </div>
      <button @click="clearLogs" class="clear-btn">清空日志</button>
    </div>
    
    <div class="demo-section">
      <h3>子组件控制</h3>
      <button @click="toggleChild" class="toggle-btn">
        {{ showChild ? '隐藏' : '显示' }}子组件
      </button>
      
      <ChildComponent v-if="showChild" :count="count" @log="addLog" />
    </div>
  </div>
</template>

<script setup>
import { 
  ref, 
  onBeforeMount, 
  onMounted, 
  onBeforeUpdate, 
  onUpdated, 
  onBeforeUnmount, 
  onUnmounted 
} from 'vue'
import ChildComponent from './ChildComponent.vue'

const count = ref(0)
const message = ref('Hello Vue!')
const showChild = ref(true)
const lifecycleLogs = ref([])

function addLog(hook, message) {
  lifecycleLogs.value.push({
    time: new Date().toLocaleTimeString(),
    hook,
    message
  })
}

function increment() {
  count.value++
}

function updateMessage() {
  message.value = `更新时间: ${new Date().toLocaleTimeString()}`
}

function toggleChild() {
  showChild.value = !showChild.value
}

function clearLogs() {
  lifecycleLogs.value = []
}

// 生命周期钩子
onBeforeMount(() => {
  addLog('onBeforeMount', '组件挂载前 - DOM 还未创建')
  console.log('onBeforeMount: 组件即将挂载')
})

onMounted(() => {
  addLog('onMounted', '组件已挂载 - 可以访问 DOM')
  console.log('onMounted: 组件已挂载,DOM 可用')
  
  // 在这里可以进行 DOM 操作、发起网络请求等
  // 例如:获取元素引用、初始化第三方库等
})

onBeforeUpdate(() => {
  addLog('onBeforeUpdate', '组件更新前 - 数据已变化,DOM 未更新')
  console.log('onBeforeUpdate: 组件即将更新')
})

onUpdated(() => {
  addLog('onUpdated', '组件已更新 - DOM 已重新渲染')
  console.log('onUpdated: 组件已更新,DOM 已同步')
  
  // 注意:避免在此钩子中修改组件状态,可能导致无限循环
})

onBeforeUnmount(() => {
  addLog('onBeforeUnmount', '组件卸载前 - 清理工作的最佳时机')
  console.log('onBeforeUnmount: 组件即将卸载')
  
  // 清理工作:移除事件监听器、取消定时器、清理订阅等
})

onUnmounted(() => {
  addLog('onUnmounted', '组件已卸载 - 所有清理工作完成')
  console.log('onUnmounted: 组件已卸载')
})
</script>

<style scoped>
.lifecycle-demo {
  max-width: 900px;
  margin: 0 auto;
  padding: 20px;
}

.demo-section {
  margin-bottom: 25px;
  padding: 20px;
  background-color: #f8f9fa;
  border-radius: 8px;
  border: 1px solid #dee2e6;
}

.demo-section h3 {
  margin: 0 0 15px 0;
  color: #333;
}

.demo-section button {
  padding: 10px 20px;
  margin-right: 10px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-size: 16px;
  transition: all 0.3s;
}

.demo-section button:hover {
  background-color: #0056b3;
  transform: translateY(-1px);
}

.toggle-btn {
  background-color: #28a745 !important;
}

.toggle-btn:hover {
  background-color: #1e7e34 !important;
}

.clear-btn {
  background-color: #dc3545 !important;
  margin-top: 15px;
}

.clear-btn:hover {
  background-color: #c82333 !important;
}

.log-container {
  max-height: 300px;
  overflow-y: auto;
  background-color: white;
  border-radius: 6px;
  padding: 15px;
  margin-bottom: 15px;
}

.log-entry {
  display: flex;
  padding: 8px;
  margin-bottom: 5px;
  border-bottom: 1px solid #f0f0f0;
  font-family: monospace;
  font-size: 14px;
}

.log-time {
  color: #6c757d;
  margin-right: 15px;
  min-width: 90px;
}

.log-hook {
  color: #007bff;
  font-weight: bold;
  margin-right: 15px;
  min-width: 120px;
}

.log-message {
  color: #333;
  flex: 1;
}

@media (max-width: 768px) {
  .demo-section button {
    display: block;
    width: 100%;
    margin: 5px 0;
  }
  
  .log-entry {
    flex-direction: column;
    gap: 5px;
  }
  
  .log-time,
  .log-hook {
    min-width: auto;
  }
}
</style>

实际应用示例

数据获取和清理

vue
<template>
  <div class="data-fetch-demo">
    <h2>数据获取示例</h2>
    
    <div class="demo-section">
      <h3>用户列表</h3>
      
      <div v-if="loading" class="loading">
        <div class="spinner"></div>
        <p>加载中...</p>
      </div>
      
      <div v-else-if="error" class="error">
        <p>❌ 加载失败: {{ error }}</p>
        <button @click="fetchUsers" class="retry-btn">重试</button>
      </div>
      
      <div v-else class="user-list">
        <div 
          v-for="user in users" 
          :key="user.id"
          class="user-card"
        >
          <div class="user-avatar">
            {{ user.name.charAt(0) }}
          </div>
          <div class="user-info">
            <h4>{{ user.name }}</h4>
            <p>{{ user.email }}</p>
            <p>{{ user.phone }}</p>
          </div>
        </div>
      </div>
      
      <button @click="refreshUsers" class="refresh-btn">
        🔄 刷新数据
      </button>
    </div>
    
    <div class="demo-section">
      <h3>实时时间</h3>
      <div class="time-display">
        <p class="current-time">{{ currentTime }}</p>
        <p class="uptime">运行时间: {{ uptime }}秒</p>
      </div>
    </div>
    
    <div class="demo-section">
      <h3>窗口大小监听</h3>
      <div class="window-info">
        <p>窗口宽度: {{ windowWidth }}px</p>
        <p>窗口高度: {{ windowHeight }}px</p>
        <p>设备类型: {{ deviceType }}</p>
      </div>
    </div>
  </div>
</template>

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

const users = ref([])
const loading = ref(false)
const error = ref('')
const currentTime = ref('')
const uptime = ref(0)
const windowWidth = ref(0)
const windowHeight = ref(0)

let timeInterval = null
let uptimeInterval = null

const deviceType = computed(() => {
  if (windowWidth.value < 768) return '移动设备'
  if (windowWidth.value < 1024) return '平板设备'
  return '桌面设备'
})

// 模拟 API 调用
function fetchUsers() {
  loading.value = true
  error.value = ''
  
  // 模拟网络延迟
  setTimeout(() => {
    try {
      // 模拟随机失败
      if (Math.random() < 0.2) {
        throw new Error('网络连接失败')
      }
      
      // 模拟用户数据
      users.value = [
        {
          id: 1,
          name: '张三',
          email: 'zhangsan@example.com',
          phone: '138-0000-0001'
        },
        {
          id: 2,
          name: '李四',
          email: 'lisi@example.com',
          phone: '138-0000-0002'
        },
        {
          id: 3,
          name: '王五',
          email: 'wangwu@example.com',
          phone: '138-0000-0003'
        },
        {
          id: 4,
          name: '赵六',
          email: 'zhaoliu@example.com',
          phone: '138-0000-0004'
        }
      ]
      
      loading.value = false
    } catch (err) {
      error.value = err.message
      loading.value = false
    }
  }, 1000 + Math.random() * 2000) // 1-3秒随机延迟
}

function refreshUsers() {
  fetchUsers()
}

function updateTime() {
  currentTime.value = new Date().toLocaleTimeString()
}

function updateWindowSize() {
  windowWidth.value = window.innerWidth
  windowHeight.value = window.innerHeight
}

// 组件挂载后的初始化
onMounted(() => {
  console.log('组件已挂载,开始初始化...')
  
  // 1. 获取初始数据
  fetchUsers()
  
  // 2. 设置定时器更新时间
  updateTime()
  timeInterval = setInterval(updateTime, 1000)
  
  // 3. 设置运行时间计数器
  uptimeInterval = setInterval(() => {
    uptime.value++
  }, 1000)
  
  // 4. 监听窗口大小变化
  updateWindowSize()
  window.addEventListener('resize', updateWindowSize)
  
  console.log('初始化完成')
})

// 组件卸载前的清理
onBeforeUnmount(() => {
  console.log('组件即将卸载,开始清理...')
  
  // 1. 清理定时器
  if (timeInterval) {
    clearInterval(timeInterval)
    timeInterval = null
  }
  
  if (uptimeInterval) {
    clearInterval(uptimeInterval)
    uptimeInterval = null
  }
  
  // 2. 移除事件监听器
  window.removeEventListener('resize', updateWindowSize)
  
  console.log('清理完成')
})
</script>

<style scoped>
.data-fetch-demo {
  max-width: 900px;
  margin: 0 auto;
  padding: 20px;
}

.demo-section {
  margin-bottom: 25px;
  padding: 20px;
  background-color: #f8f9fa;
  border-radius: 8px;
  border: 1px solid #dee2e6;
}

.demo-section h3 {
  margin: 0 0 15px 0;
  color: #333;
}

.loading {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 40px;
}

.spinner {
  width: 40px;
  height: 40px;
  border: 4px solid #f3f3f3;
  border-top: 4px solid #007bff;
  border-radius: 50%;
  animation: spin 1s linear infinite;
  margin-bottom: 15px;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

.error {
  text-align: center;
  padding: 20px;
  color: #dc3545;
}

.retry-btn {
  padding: 10px 20px;
  background-color: #dc3545;
  color: white;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  margin-top: 10px;
}

.user-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 15px;
  margin-bottom: 20px;
}

.user-card {
  display: flex;
  align-items: center;
  padding: 15px;
  background-color: white;
  border-radius: 8px;
  border: 1px solid #dee2e6;
  transition: all 0.3s;
}

.user-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}

.user-avatar {
  width: 50px;
  height: 50px;
  background-color: #007bff;
  color: white;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 20px;
  font-weight: bold;
  margin-right: 15px;
}

.user-info h4 {
  margin: 0 0 5px 0;
  color: #333;
}

.user-info p {
  margin: 3px 0;
  color: #666;
  font-size: 14px;
}

.refresh-btn {
  padding: 10px 20px;
  background-color: #28a745;
  color: white;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-size: 16px;
}

.refresh-btn:hover {
  background-color: #1e7e34;
}

.time-display {
  text-align: center;
  padding: 20px;
  background-color: white;
  border-radius: 8px;
  border: 1px solid #dee2e6;
}

.current-time {
  font-size: 32px;
  font-weight: bold;
  color: #007bff;
  margin: 0 0 10px 0;
  font-family: monospace;
}

.uptime {
  font-size: 18px;
  color: #666;
  margin: 0;
}

.window-info {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 15px;
  text-align: center;
}

.window-info p {
  padding: 15px;
  background-color: white;
  border-radius: 6px;
  border: 1px solid #dee2e6;
  margin: 0;
  font-size: 16px;
  font-weight: 500;
}

@media (max-width: 768px) {
  .user-list {
    grid-template-columns: 1fr;
  }
  
  .current-time {
    font-size: 24px;
  }
  
  .window-info {
    grid-template-columns: 1fr;
  }
}
</style>

第三方库集成

vue
<template>
  <div class="chart-demo">
    <h2>图表集成示例</h2>
    
    <div class="demo-section">
      <h3>销售数据图表</h3>
      <div class="chart-controls">
        <button @click="updateData" class="update-btn">
          🔄 更新数据
        </button>
        <button @click="changeChartType" class="type-btn">
          📊 切换图表类型
        </button>
      </div>
      
      <!-- 图表容器 -->
      <div ref="chartContainer" class="chart-container">
        <div class="chart-placeholder">
          📈 图表将在这里显示
          <p>(这是一个模拟的图表容器)</p>
        </div>
      </div>
      
      <div class="chart-info">
        <p>图表类型: {{ chartType }}</p>
        <p>数据点数量: {{ chartData.length }}</p>
        <p>最后更新: {{ lastUpdate }}</p>
      </div>
    </div>
    
    <div class="demo-section">
      <h3>地图组件</h3>
      <div ref="mapContainer" class="map-container">
        <div class="map-placeholder">
          🗺️ 地图将在这里显示
          <p>(这是一个模拟的地图容器)</p>
        </div>
      </div>
    </div>
  </div>
</template>

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

const chartContainer = ref(null)
const mapContainer = ref(null)
const chartType = ref('line')
const chartData = ref([])
const lastUpdate = ref('')

// 模拟图表实例
let chartInstance = null
let mapInstance = null

// 模拟第三方图表库的初始化
function initChart() {
  if (!chartContainer.value) return
  
  console.log('初始化图表...')
  
  // 模拟图表库初始化
  chartInstance = {
    container: chartContainer.value,
    type: chartType.value,
    data: chartData.value,
    
    // 模拟图表方法
    updateData(newData) {
      this.data = newData
      console.log('图表数据已更新:', newData)
    },
    
    changeType(newType) {
      this.type = newType
      console.log('图表类型已切换:', newType)
    },
    
    destroy() {
      console.log('图表已销毁')
    }
  }
  
  // 生成初始数据
  generateChartData()
  
  console.log('图表初始化完成')
}

// 模拟地图库的初始化
function initMap() {
  if (!mapContainer.value) return
  
  console.log('初始化地图...')
  
  // 模拟地图库初始化
  mapInstance = {
    container: mapContainer.value,
    center: [116.404, 39.915], // 北京坐标
    zoom: 10,
    
    // 模拟地图方法
    setCenter(coords) {
      this.center = coords
      console.log('地图中心已设置:', coords)
    },
    
    setZoom(level) {
      this.zoom = level
      console.log('地图缩放级别已设置:', level)
    },
    
    destroy() {
      console.log('地图已销毁')
    }
  }
  
  console.log('地图初始化完成')
}

// 生成模拟数据
function generateChartData() {
  const data = []
  for (let i = 0; i < 12; i++) {
    data.push({
      month: `${i + 1}月`,
      value: Math.floor(Math.random() * 1000) + 100
    })
  }
  chartData.value = data
  lastUpdate.value = new Date().toLocaleTimeString()
}

// 更新图表数据
function updateData() {
  generateChartData()
  
  if (chartInstance) {
    chartInstance.updateData(chartData.value)
  }
}

// 切换图表类型
function changeChartType() {
  const types = ['line', 'bar', 'pie', 'area']
  const currentIndex = types.indexOf(chartType.value)
  const nextIndex = (currentIndex + 1) % types.length
  chartType.value = types[nextIndex]
  
  if (chartInstance) {
    chartInstance.changeType(chartType.value)
  }
}

// 组件挂载后初始化第三方库
onMounted(async () => {
  console.log('组件已挂载,开始初始化第三方库...')
  
  // 等待 DOM 更新完成
  await nextTick()
  
  // 初始化图表
  initChart()
  
  // 初始化地图
  initMap()
  
  console.log('所有第三方库初始化完成')
})

// 组件卸载前清理第三方库
onBeforeUnmount(() => {
  console.log('组件即将卸载,开始清理第三方库...')
  
  // 销毁图表实例
  if (chartInstance) {
    chartInstance.destroy()
    chartInstance = null
  }
  
  // 销毁地图实例
  if (mapInstance) {
    mapInstance.destroy()
    mapInstance = null
  }
  
  console.log('第三方库清理完成')
})
</script>

<style scoped>
.chart-demo {
  max-width: 900px;
  margin: 0 auto;
  padding: 20px;
}

.demo-section {
  margin-bottom: 25px;
  padding: 20px;
  background-color: #f8f9fa;
  border-radius: 8px;
  border: 1px solid #dee2e6;
}

.demo-section h3 {
  margin: 0 0 15px 0;
  color: #333;
}

.chart-controls {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
}

.update-btn,
.type-btn {
  padding: 10px 20px;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-size: 16px;
  transition: all 0.3s;
}

.update-btn {
  background-color: #28a745;
  color: white;
}

.update-btn:hover {
  background-color: #1e7e34;
}

.type-btn {
  background-color: #007bff;
  color: white;
}

.type-btn:hover {
  background-color: #0056b3;
}

.chart-container,
.map-container {
  width: 100%;
  height: 300px;
  background-color: white;
  border: 2px dashed #dee2e6;
  border-radius: 8px;
  margin-bottom: 15px;
}

.chart-placeholder,
.map-placeholder {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100%;
  color: #6c757d;
  font-size: 24px;
}

.chart-placeholder p,
.map-placeholder p {
  margin: 10px 0 0 0;
  font-size: 16px;
}

.chart-info {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 15px;
}

.chart-info p {
  padding: 10px 15px;
  background-color: white;
  border-radius: 6px;
  border: 1px solid #dee2e6;
  margin: 0;
  text-align: center;
  font-weight: 500;
}

@media (max-width: 768px) {
  .chart-controls {
    flex-direction: column;
  }
  
  .chart-info {
    grid-template-columns: 1fr;
  }
}
</style>

生命周期钩子的最佳实践

1. 数据获取时机

javascript
// ✅ 推荐:在 onMounted 中获取数据
onMounted(async () => {
  try {
    const data = await fetchUserData()
    users.value = data
  } catch (error) {
    console.error('获取用户数据失败:', error)
  }
})

// ❌ 不推荐:在 setup 中直接获取数据
// 这会在服务端渲染时执行,可能导致问题

2. 清理资源

javascript
// ✅ 推荐:在 onBeforeUnmount 中清理资源
let timer = null

onMounted(() => {
  timer = setInterval(() => {
    // 定时任务
  }, 1000)
})

onBeforeUnmount(() => {
  if (timer) {
    clearInterval(timer)
    timer = null
  }
})

3. DOM 操作

javascript
// ✅ 推荐:在 onMounted 中进行 DOM 操作
const elementRef = ref(null)

onMounted(() => {
  // 此时 DOM 已经渲染完成
  if (elementRef.value) {
    elementRef.value.focus()
  }
})

4. 避免在 onUpdated 中修改状态

javascript
// ❌ 危险:可能导致无限循环
onUpdated(() => {
  count.value++ // 这会触发新的更新
})

// ✅ 推荐:只进行读取操作或 DOM 操作
onUpdated(() => {
  console.log('组件已更新')
  // 或者进行 DOM 测量等操作
})

选项式 API 对比

选项式 API组合式 API说明
beforeCreatesetup()组件实例创建前
createdsetup()组件实例创建后
beforeMountonBeforeMount挂载前
mountedonMounted挂载后
beforeUpdateonBeforeUpdate更新前
updatedonUpdated更新后
beforeUnmountonBeforeUnmount卸载前
unmountedonUnmounted卸载后

下一步

基于 Vue.js 官方文档构建的学习宝典