Code前端首页关于Code前端联系我们

Vue3 beforeMount生命周期钩子到底能帮你解决哪些开发痛点?用法和注意事项全解析

terry 39分钟前 阅读数 10 #Vue

很多刚接触Vue3的开发者,可能会觉得beforeMount这个钩子有点“鸡肋”——它在created之后、DOM挂载之前触发,既没有created那样能早拿到数据和实例方法的优势,又不像mounted那样能直接操作真实DOM,好像夹在中间没什么用,但其实,只要用对了场景,beforeMount是个能帮你避开不少坑、提升开发效率的小帮手。

先理清楚:beforeMount在Vue3的生命周期里处于什么位置?

要想用好任何钩子,得先明确它的“出场时间线”,咱们先回忆下Vue3组件从创建到销毁的完整核心流程,重点看beforeMount前后的节点:

  1. setup阶段(Vue3新增的Composition API入口,替代了之前大部分的created/created之前的逻辑):组合式函数在这里执行,响应式数据、计算属性、侦听器、实例方法可以在这里定义,注意,setup里是拿不到this的。
  2. created钩子:组件实例已经创建完成,响应式数据、计算属性、侦听器、方法都已经初始化,this也可以正常访问了,但此时虚拟DOM还没有生成,更别说真实DOM渲染到页面上了。
  3. beforeMount钩子:虚拟DOM树已经编译完成、挂载前的准备工作(比如数据预渲染、静态内容优化?不过优化一般在构建阶段)也做好了,但虚拟DOM还没有“贴”到真实DOM上,页面还是空的(或者只有组件挂载前的初始HTML)。
  4. mounted钩子:真实DOM已经完全挂载到页面上,此时可以直接获取DOM元素、操作DOM、调用第三方库(比如ECharts、Layui表格)的初始化方法了。

从这个时间线能看出来,beforeMount是唯一“虚拟DOM已生成但还没渲染”的钩子——这就是它的核心价值所在。

beforeMount能解决哪些具体的开发痛点?

很多开发者之所以觉得beforeMount没用,是因为没找到它和setup、created、mounted的区别场景,下面咱们结合3个真实项目中常见的例子,看看beforeMount到底能派上什么用场。

不修改原有响应式逻辑,但需要临时调整虚拟DOM的渲染内容

假设你现在要做一个电商商品详情页,组件里的商品信息是通过setup里的API请求获取的,拿到的数据里有一个“商品展示模式”字段——0代表普通图文模式,1代表AR预览模式,但产品经理临时加了个需求:如果用户是从分享链接(URL里有share=1参数)进来的,不管商品原来的展示模式是什么,第一次渲染都强制用普通图文模式,用户点击切换按钮之后才能恢复正常模式。

这时候你会怎么做?

  • 直接修改setup里响应式的“商品展示模式”?不行,因为如果用户刷新页面或者重新进来,原来的展示模式就丢了(除非你存到本地存储,但这样多了一步存储/读取的逻辑,还要处理边界情况,比如用户手动删除本地存储)。
  • 在created里临时改?不行,因为created之后虚拟DOM还没生成,你改响应式数据的话,之后虚拟DOM编译的时候就会直接用修改后的值,但这样第一次渲染之后,数据确实改了——万一后续逻辑依赖原来的展示模式呢?比如AR模式的入口文案,应该是“切换到AR模式”还是“切换回AR模式”?如果临时改了响应式数据,还要额外加一个变量记录原来的值,逻辑会变复杂。
  • 在mounted里改?更不行,mounted的时候真实DOM已经渲染了,你再改的话,会触发一次额外的DOM更新,用户可能会看到页面“闪一下”——先显示AR预览的占位符,再跳回图文模式,体验非常差。

这时候beforeMount就派上用场了!因为beforeMount里虚拟DOM已经生成,但还没渲染到页面上,你可以直接读取setup/created里的响应式数据作为临时参考,然后通过其他方式调整虚拟DOM——或者更简单一点,加一个非响应式的临时变量控制第一次渲染的内容,beforeMount里处理这个临时变量的逻辑,完全不影响原有的响应式逻辑。

具体的代码逻辑大概是这样的(用Composition API写的,因为现在Vue3主要用这个):

<script setup>
import { ref, onBeforeMount } from 'vue'
// 原有逻辑:获取商品数据
const product = ref({ displayMode: 1, name: '最新款AR智能眼镜' })
// 新增逻辑:非响应式临时变量,控制第一次渲染
let firstRenderForceNormal = false
// 检查URL参数
onBeforeMount(() => {
  const urlParams = new URLSearchParams(window.location.search)
  if (urlParams.get('share') === '1') {
    firstRenderForceNormal = true
  }
})
</script>
<template>
  <!-- 用非响应式临时变量和原有响应式数据结合控制渲染 -->
  <div v-if="firstRenderForceNormal || product.displayMode === 0" class="normal-mode">
    <!-- 普通图文模式内容 -->
    <h2>{{ product.name }}</h2>
    <img src="product-normal.jpg" alt="商品图片" />
  </div>
  <div v-else class="ar-mode">
    <!-- AR预览模式占位符 -->
    <div class="ar-placeholder">正在加载AR预览...</div>
  </div>
</template>

哦对了,这里还要补充一句:非响应式变量在模板里直接用的话,第一次渲染会生效,但之后不会自动更新——刚好符合我们的需求:只控制第一次渲染,用户点击切换的时候,我们只需要修改product.displayMode,firstRenderForceNormal这个变量不用管,它只会在第一次beforeMount触发的时候赋值一次,之后就没用了,逻辑非常清晰。

需要给虚拟DOM的动态内容做“预计算优化”,避免mounted/DOM更新时重复计算

再举个例子:你要做一个大数据可视化的仪表盘组件,组件里有一个展示“近30天用户增长趋势”的折线图,图表的X轴标签是“日期”,需要从后端返回的“用户增长数据数组”里提取出来——后端返回的数组里,每个元素是{date: '2024-01-01', count: 123}这样的格式,X轴标签需要把日期转成“MM-DD”的格式(比如01-01)。

这个提取和转换的逻辑,你会放在哪里?

  • 放在setup里?当然可以,但如果后端返回的数组非常大(比如不是30天,而是365天),而且这个图表组件是按需加载的(用户点击“查看趋势”按钮才会显示),那放在setup里的话,组件刚加载的时候就会执行一次不必要的计算——浪费了一点性能,虽然可能不多,但积少成多嘛。
  • 放在created里?和放在setup里差不多,也是组件刚创建就执行,按需加载的场景下没用。
  • 放在模板的插值表达式或者计算属性里?插值表达式里写复杂逻辑肯定不行,每次DOM更新都会重新计算;计算属性是响应式缓存的,看起来不错——但如果后端返回的数据不是响应式的?或者说,这个图表组件只需要第一次渲染的时候用转换后的X轴标签,之后后端不会更新数据?那计算属性的缓存其实也用不上,而且计算属性还是要依赖响应式数据,多了一点响应式追踪的开销。
  • 放在mounted里?更麻烦,mounted的时候真实DOM已经渲染了,你要先拿到图表容器的DOM元素,然后再计算X轴标签,再初始化ECharts——如果计算逻辑复杂的话,可能会导致图表初始化稍微慢一点,用户看到的是空白的图表容器,体验不好。

这时候beforeMount又是一个好选择!尤其是结合v-if按需加载组件的场景——因为v-if为true的时候,组件才会开始走创建到挂载的生命周期,beforeMount只会在组件真正要渲染的时候触发,不会有不必要的预计算,而且beforeMount里可以直接读取props或者setup/created里的响应式数据(但不需要修改它们,除非有必要),计算出来的结果可以存到一个非响应式的普通变量里(因为之后不用更新),然后模板里直接用这个变量,或者mounted里初始化第三方库的时候直接用这个变量——完全没有响应式追踪的开销,也没有不必要的预计算,性能最优。

具体的代码逻辑大概是这样的:

<script setup>
import { ref, onBeforeMount, onMounted, watch } from 'vue'
import * as echarts from 'echarts'
// 父组件传过来的用户增长数据,按需加载的时候才会有值
const props = defineProps(['userGrowthData'])
// 非响应式变量,存转换后的X轴标签和Y轴数据
let xAxisLabels = []
let yAxisData = []
// 图表容器的DOM引用
const chartRef = ref(null)
// 按需加载时,beforeMount触发,才执行计算
onBeforeMount(() => {
  if (!props.userGrowthData || props.userGrowthData.length === 0) return
  // 只执行一次转换,没有缓存,没有响应式追踪
  xAxisLabels = props.userGrowthData.map(item => {
    const [year, month, day] = item.date.split('-')
    return `${month}-${day}`
  })
  yAxisData = props.userGrowthData.map(item => item.count)
})
// mounted里直接用计算好的变量初始化ECharts
onMounted(() => {
  if (!chartRef.value || xAxisLabels.length === 0) return
  const myChart = echarts.init(chartRef.value)
  const option = {
    xAxis: { type: 'category', data: xAxisLabels },
    yAxis: { type: 'value' },
    series: [{ type: 'line', data: yAxisData }]
  }
  myChart.setOption(option)
  // 窗口大小改变时重绘图表
  window.addEventListener('resize', () => myChart.resize())
})
// 如果父组件后续会更新userGrowthData,再加一个watch即可,watch里也可以直接计算(但如果数据更新频繁的话,还是建议用计算属性)
// watch(() => props.userGrowthData, (newVal) => {
//   if (!newVal || newVal.length === 0) return
//   xAxisLabels = newVal.map(...)
//   yAxisData = newVal.map(...)
//   // 这里可以获取myChart的实例(需要存到ref里),然后更新option
// }, { immediate: false, deep: true })
</script>
<template>
  <!-- 按需加载组件 -->
  <div v-if="userGrowthData && userGrowthData.length > 0" ref="chartRef" style="width: 100%; height: 400px;"></div>
</template>

需要拦截虚拟DOM的挂载过程,给所有动态生成的元素提前加一些“全局预处理”

这个场景可能稍微复杂一点,但也非常实用——比如你要做一个公司内部的管理后台,所有输入框(包括第三方UI库的输入框,比如Element Plus的el-input)在第一次渲染的时候,都需要自动聚焦?不对,自动聚焦mounted里也能做,但如果是所有输入框都需要自动加一个“防抖输入”的功能?或者所有图片在第一次渲染的时候,都需要自动替换成公司CDN的压缩地址?

哦对,图片替换这个场景,beforeMount非常合适——因为图片的src属性是在虚拟DOM里的,beforeMount里可以直接修改虚拟DOM的属性,修改之后虚拟DOM再挂载到真实DOM上,真实DOM的src属性就是替换后的CDN压缩地址了,不会先加载原图再跳转到压缩图,节省了带宽,也避免了用户看到原图加载失败或者加载慢的情况。

不过这里要注意:在Vue3的Composition API里,直接修改虚拟DOM的属性,不能像Options API那样在beforeMount里通过this.$options.render或者this._vnode来修改——因为Vue3的虚拟DOM结构和Options API不太一样,而且这些内部属性是不建议直接访问的,可能会在后续版本中改变,那怎么办呢?

可以用Vue3的渲染函数(h函数) 或者自定义指令?不对,自定义指令是在DOM挂载之后才会触发bind和inserted钩子的(inserted钩子对应mounted之后),那渲染函数呢?或者更简单一点,用Vue3的模板预处理器?比如用VueLoader的transformAssetUrls选项?哦对,transformAssetUrls是构建阶段的,只能处理静态资源的路径,不能处理动态绑定的src属性。

那还是回到beforeMount——虽然不建议直接访问内部的_vnode,但如果是小范围的、自己控制的组件,还是可以用的,或者我们可以换一种方式:用响应式数据的代理?不对,或者我们可以在beforeMount里,把动态绑定的图片src数组(或者对象)里的所有地址都替换成CDN压缩地址,然后存到一个非响应式的变量里,模板里用这个变量——哦对,这个和场景二的思路差不多,更安全,不依赖内部属性。

比如假设你有一个动态生成的商品列表,每个商品有一个主图和多个副图,主图需要用CDN的300x300压缩地址,副图需要用100x100压缩地址:

<script setup>
import { ref, onBeforeMount } from 'vue'
// 原有逻辑:获取商品列表,主图和副图都是原图地址
const productList = ref([
  {
    id: 1,
    name: '商品1',
    mainImg: 'https://example.com/original/product1.jpg',
    subImgs: ['https://example.com/original/product1-1.jpg', 'https://example.com/original/product1-2.jpg']
  },
  {
    id: 2,
    name: '商品2',
    mainImg: 'https://example.com/original/product2.jpg',
    subImgs: ['https://example.com/original/product2-1.jpg', 'https://example.com/original/product2-2.jpg']
  }
])
// 非响应式变量,存替换后的商品列表
let processedProductList = []
// CDN压缩函数
const compressImg = (url, size) => {
  return url.replace('https://example.com/original/', `https://cdn.example.com/compressed/${size}/`)
}
// beforeMount里处理所有图片地址
onBeforeMount(() => {
  if (!productList.value || productList.value.length === 0) return
  processedProductList = productList.value.map(product => ({
    ...product, // 保留其他属性
    mainImg: compressImg(product.mainImg, '300x300'),
    subImgs: product.subImgs.map(img => compressImg(img, '100x100'))
  }))
})
</script>
<template>
  <div class="product-list">
    <!-- 模板里直接用处理后的非响应式变量 -->
    <div v-for="product in processedProductList" :key="product.id" class="product-item">
      <h3>{{ product.name }}</h3>
      <img :src="product.mainImg" :alt="product.name" class="main-img" />
      <div class="sub-img-list">
        <img v-for="(img, index) in product.subImgs" :key="index" :src="img" :alt="`${product.name}-${index + 1}`" class="sub-img" />
      </div>
    </div>
  </div>
</template>

同样的,这里用非响应式变量的原因是:如果商品列表之后不会更新,那非响应式变量没有任何问题;如果商品列表之后会更新,我们可以加一个watch,在watch里重新处理,或者直接把processedProductList改成响应式的——但如果只是替换图片地址这种简单的、不需要频繁更新的逻辑,非响应式变量的性能更好。

使用beforeMount有哪些绝对不能碰的注意事项?

虽然beforeMount有它的价值,但如果用错了场景,会带来很多问题,下面咱们说几个绝对不能碰的注意事项:

注意事项一:不要在beforeMount里操作真实DOM

这个是最基础的,也是很多新手容易犯的错误——因为beforeMount里真实DOM还没有挂载到页面上,你用document.getElementById、document.querySelector或者ref.value去获取DOM元素,拿到的都是null或者undefined,操作了也没用,甚至会报错。

比如下面的代码就是错误的:

<script setup>
import { ref, onBeforeMount } from 'vue'
const titleRef = ref(null)
onBeforeMount(() => {
  // 错误!titleRef.value此时是nullRef.value.style.color = 'red'
})
</script>
<template>
  <h1 ref="titleRef">Hello Vue3</h1>
</template>

如果你想操作DOM,一定要等到mounted钩子触发之后再做。

注意事项二:不要在beforeMount里修改会影响虚拟DOM结构的响应式数据(除非你知道自己在做什么)

哦,等一下,场景一里我们不是修改了吗?不,场景一里我们没有修改响应式数据,而是加了一个非响应式的临时变量——如果你在beforeMount里修改了会影响虚拟DOM结构的响应式数据(比如v-if、v-for依赖的变量),会发生什么?

因为beforeMount里虚拟DOM已经生成了,你修改响应式数据的话,会触发Vue的响应式系统,重新生成虚拟DOM,然后再挂载——相当于多了一次虚拟DOM的编译和对比,浪费了性能,虽然不会报错,但这是一种不好的开发习惯,而且如果修改的逻辑有问题,可能会导致无限循环(比如你在beforeMount里修改了一个变量,这个变量的watch又触发了一次修改,等等)。

那什么时候可以在beforeMount里修改响应式数据呢?比如你修改的响应式数据不会影响当前组件的虚拟DOM结构,只会影响子组件的——或者你确实需要重新生成虚拟DOM,但又不想等到mounted之后触发DOM更新(因为那样会有闪烁)——这时候可以修改,但一定要非常小心,避免无限循环。

注意事项三:不要在beforeMount里依赖父组件传递过来的、通过异步操作获取的props数据(除非你加了v-if或者watch)

这个和created、setup里的注意事项是一样的——如果父组件传递过来的props数据是通过异步操作获取的,那么在子组件的beforeMount、created、setup阶段,这个props数据可能还是null或者undefined。

比如父组件的代码是这样的:

<script setup>
import { ref, onMounted } from 'vue'
import ChildComponent from './ChildComponent.vue'
// 异步获取商品数据
const product = ref(null)
onMounted(() => {
  // 模拟异步请求
  setTimeout(() => {
    product.value = { name: '商品1', price: 123 }
  }, 1000)
})
</script>
<template>
  <!-- 没有加v-if -->
  <ChildComponent :product="product" />
</template>

子组件的beforeMount里直接读取product.value的话,拿到的就是null——这时候你如果在beforeMount里做一些依赖product.value的计算或者操作,就会报错或者得不到预期的结果。

解决方法有两个:

  1. 给子组件加v-if:只有当product.value有值的时候,才渲染子组件——这样beforeMount只会在product.value有值的时候触发,就没问题了。
  2. 在子组件里加watch:监听product.value的变化,有值的时候再做计算或者操作——这时候计算或者操作的逻辑就不能放在beforeMount里了,要放在watch里。

注意事项四:Options API里的beforeMount和Composition API里的onBeforeMount可以同时存在,但执行顺序要注意

这个是给还在使用Options API或者混合使用Options API和Composition API的开发者提的——Vue3里,Options API的生命周期钩子和Composition API的onXxx钩子是可以同时存在的,但执行顺序是:Composition API的onXxx钩子先执行,然后是Options API的Xxx钩子。

比如下面的代码:

<script>
export default {
  beforeMount() {
    console.log('Options API beforeMount')
  }
}
</script>
<script setup>
import { onBeforeMount } from 'vue'
onBeforeMount(() => {
  console.log('Composition API onBeforeMount')
})
</script>

控制台的输出顺序是:

Composition API onBeforeMount
Options API beforeMount

虽然这个执行顺序一般不会影响开发,但如果你同时使用了两种API,并且在beforeMount里有依赖关系的逻辑,一定要注意这个顺序。

beforeMount和created、mounted的区别到底在哪里?一张表格帮你总结清楚

为了方便大家记忆,咱们把beforeMount和created、mounted的核心区别整理成一张表格:

对比维度 created beforeMount mounted
触发时间 组件实例创建完成,响应式数据初始化完成 虚拟DOM生成完成,挂载前准备完成 真实DOM完全挂载到页面上
是否能访问this 是(Options API) 是(Options API) 是(Options API)
是否能访问ref.value 否(ref.value此时是null/undefined) 否(ref.value此时是null/undefined) 是(ref.value指向真实DOM)
是否能操作真实DOM 否 否 是
是否能修改虚拟DOM 否(虚拟DOM还没生成) 可以(间接修改,比如替换非响应式变量),不建议直接修改内部属性 否(虚拟DOM已经挂载,再修改会触发DOM更新)
适合的场景 早期的数据请求、初始化非DOM相关的第三方库 临时调整第一次渲染的内容、预计算不频繁更新的内容、预处理动态资源 操作真实DOM、初始化DOM相关的第三方库、获取元素尺寸位置

beforeMount真的是“必备钩子”吗?

答案是:不是必备的,但在某些特定场景下,它是最优解。

现在Vue3主要用Composition API,setup阶段已经能处理大部分的逻辑——数据请求可以用setup里的async/await(或者配合Suspense组件),非响应式的临时变量可以直接在setup里定义但延迟赋值(不过延迟赋值的话,第一次渲染可能会有问题),预计算可以用computed(如果需要响应式缓存)或者直接在setup里写一个函数(但按需加载的场景下不如beforeMount)。

但如果你遇到了咱们上面说的那三个场景——临时调整第一次渲染的内容、不修改原有响应式逻辑、按需加载时的预计算优化——beforeMount绝对是你的好帮手,能帮你避开不少坑,提升开发效率和用户体验。

不要觉得beforeMount是“鸡肋”,多了解它的“出场时间线”和“核心价值”,遇到合适的场景就大胆用吧!

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门