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

Vue3开发别只会用window.onresize!watch监听DOM元素和窗口尺寸的实用方法有哪些坑要避?

terry 2小时前 阅读数 34 #Vue

日常做PC端、移动端适配或者响应式组件的时候,尺寸变化是绕不开的需求,很多刚接触Vue3的同学可能下意识还是沿用Vue2或者原生JS的老思路,直接写window.onresize,殊不知这个方法在Vue3里有性能、响应式绑定、作用域这一堆麻烦事;也有人会直接用watch去监听data里存的innerWidth/innerHeight,但要么没处理防抖节流卡成狗,要么组件销毁没清理内存出bug,要么想监听特定DOM容器却找不到正确的监听源。

今天咱们就把Vue3监听尺寸的两种核心场景——全局窗口尺寸监听局部DOM元素尺寸监听——拆开来聊,每种场景都讲实用、可直接复制的方案,重点踩踩新手容易掉进去的那些坑,最后再总结一个不同需求下的方案选择清单,保证看完就能上手。

全局窗口尺寸监听:别再写window.onresize了!

先说说最常见的全局窗口场景,比如适配不同屏幕宽度切换布局断点,或者顶部导航栏、侧边栏的高度调整,为什么不能直接写window.onresize?咱们举个例子你就懂了:

// ❌ Vue3里的错误写法(虽然能跑,但毛病一堆)
<script setup>
import { ref } from 'vue'
const screenWidth = ref(window.innerWidth)
window.onresize = () => {
  screenWidth.value = window.innerWidth
}
</script>

这代码的问题能列一堆:

  1. 作用域冲突:如果同一个页面有好几个组件都写了window.onresize,后面的会把前面的直接覆盖掉,比如侧边栏的布局没生效,只有最后一个加载的底部组件触发了逻辑。
  2. 没有防抖/节流:用户拖动窗口边缘的时候,resize事件会每秒触发几十次甚至上百次,直接修改ref或者执行DOM操作的话,浏览器会疯狂重绘重排,页面卡得像PPT。
  3. 组件销毁没清理:页面跳转到没有这个组件的路由时,resize的回调函数还在内存里运行,时间长了会内存泄漏,手机端或者低配电脑上尤其明显,甚至会直接闪退。

那正确的打开方式是什么?用Vue3提供的addEventListener结合ref+onMounted+onUnmounted,再加个防抖就行,这里的防抖可以自己写个简单的,也可以用lodash-es的debounce,不过很多项目不想引入额外的小工具包,那咱们就自己写一个通用的防抖函数:

自己写通用防抖+原生API组合的方案

// ✅ 正确写法:自己写防抖,手动绑定+清理事件
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
// 通用防抖函数,delay默认设200ms,适配大多数场景
const debounce = (fn, delay = 200) => {
  let timer = null
  return (...args) => {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      fn.apply(this, args)
    }, delay)
  }
}
const screenWidth = ref(window.innerWidth)
const screenHeight = ref(window.innerHeight)
// 定义更新尺寸的函数
const updateScreenSize = () => {
  screenWidth.value = window.innerWidth
  screenHeight.value = window.innerHeight
}
// 用防抖包装一下
const debouncedUpdate = debounce(updateScreenSize)
onMounted(() => {
  // 组件挂载后绑定事件
  window.addEventListener('resize', debouncedUpdate)
  // 可以额外绑定orientationchange事件,适配手机横竖屏切换
  window.addEventListener('orientationchange', debouncedUpdate)
})
onUnmounted(() => {
  // 组件卸载前必须清理事件!!这个是新手最容易忘的坑
  window.removeEventListener('resize', debouncedUpdate)
  window.removeEventListener('orientationchange', debouncedUpdate)
})
</script>

这个方案基本上解决了前面提到的所有问题:用addEventListener避免覆盖,自己写的防抖控制触发频率,onMounted/onUnmounted绑定和清理事件防内存泄漏,额外加的orientationchange适配手机端。

不过如果你项目里已经用了lodash或者lodash-es,那直接用debounce或者throttle就行,不用自己写,但要注意:lodash是CommonJS规范,Vue3的script setup是ES Module,所以要引入lodash-es的debounce,不然打包的时候会有问题,还得额外配置babel,另外要搞清楚防抖和节流的区别:防抖是“等用户停下来不操作了再执行”,比如搜索框输入、窗口调整;节流是“每隔固定时间执行一次”,比如滚动条滚动、鼠标移动,所以监听resize一般用防抖就行,但如果是那种需要实时看到变化的场景(比如做一个拖拽调整窗口大小的预览工具),可以把delay设得短一点,比如50ms,或者换成throttle。

用VueUse的useWindowSize:懒人福音!

如果你的项目允许引入第三方库,那VueUse绝对是Vue3开发的神器,里面封装了超多实用的Composition API,其中就有专门监听全局窗口尺寸的useWindowSize,人家已经帮你把防抖、事件绑定清理、手机横竖屏这些事情都做好了,直接用就行:

// ✅ 更简单的写法:用VueUse的useWindowSize
<script setup>
import { useWindowSize } from '@vueuse/core'
// 可以直接传options配置防抖/节流
const { width, height } = useWindowSize({
  initialWidth: 1920, // 初始宽度,SSR的时候有用
  initialHeight: 1080, // 初始高度
  listenOrientation: true, // 监听手机横竖屏,默认true
  debounce: 200, // 防抖时间,也可以传throttle
})
</script>

这个方案超级简单,代码量少,而且经过了VueUse团队的优化,性能和兼容性都没问题,适合大多数懒人开发者,不过如果你项目里不想引入太多第三方库,那还是用前面的原生API组合方案吧。


局部DOM元素尺寸监听:别再用offsetWidth/offsetHeight定时轮询了!

接下来是另一种常见的场景:监听特定DOM元素的尺寸变化,比如做一个可拖拽调整大小的容器、富文本编辑器的高度自适应、图片加载后父容器的高度变化等等,很多刚接触这个需求的同学可能会用setInterval或者setTimeout定时去获取元素的offsetWidth/offsetHeight,然后对比之前的值,如果不一样就执行逻辑:

// ❌ 局部DOM监听的错误写法:定时轮询,性能差,不够精准
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const containerRef = ref(null)
const containerWidth = ref(0)
const containerHeight = ref(0)
let timer = null
onMounted(() => {
  // 每500ms轮询一次
  timer = setInterval(() => {
    if (containerRef.value) {
      const newWidth = containerRef.value.offsetWidth
      const newHeight = containerRef.value.offsetHeight
      if (newWidth !== containerWidth.value || newHeight !== containerHeight.value) {
        containerWidth.value = newWidth
        containerHeight.value = newHeight
        // 执行你的逻辑
      }
    }
  }, 500)
})
onUnmounted(() => {
  // 记得清理定时器
  if (timer) clearInterval(timer)
})
</script>

这个写法的问题也很明显:

  1. 性能差:不管元素尺寸有没有变化,都会每500ms执行一次获取和对比操作,频繁调用offsetWidth/offsetHeight会触发浏览器的重排(Reflow),因为offsetWidth/offsetHeight是需要浏览器计算布局的属性,不像clientWidth/clientHeight那么“轻量”,但即使是clientWidth/clientHeight,频繁获取也不好。
  2. 不够精准:如果元素尺寸变化的间隔小于500ms,那就会丢失变化,比如图片加载后父容器高度瞬间从0变到500px,但定时器下一次轮询要等499ms,用户就会看到空白或者布局闪烁;如果把轮询间隔设得太短,比如50ms,那性能问题就更严重了。
  3. 不够优雅:定时轮询是一种“主动查询”的方式,而现代浏览器提供了一种“被动监听”的方式——ResizeObserver,性能更好,更精准,更优雅。

那正确的打开方式是什么?用ResizeObserver结合ref+onMounted+onUnmounted就行,不需要引入任何第三方库,ResizeObserver是现代浏览器(Chrome 64+、Firefox 69+、Safari 13.1+、Edge 79+)原生支持的API,专门用来监听DOM元素的尺寸变化,支持监听content-box、border-box、padding-box三种盒子模型,性能非常好,因为它只会在元素尺寸真正变化的时候触发回调,而且回调是在浏览器的下一次渲染之前执行的,不会造成布局闪烁。

原生ResizeObserver+Vue3组合的方案

// ✅ 正确写法:用ResizeObserver被动监听
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const containerRef = ref(null)
const containerWidth = ref(0)
const containerHeight = ref(0)
let resizeObserver = null
// 定义更新尺寸的函数
const updateContainerSize = (entries) => {
  // entries是一个数组,因为ResizeObserver可以同时监听多个元素
  const entry = entries[0]
  // 获取content-box的尺寸,默认就是content-box
  const { width, height } = entry.contentRect
  // 如果要获取border-box的尺寸,可以用entry.borderBoxSize
  // const borderBoxSize = entry.borderBoxSize[0] || entry.borderBoxSize
  // const width = borderBoxSize.inlineSize
  // const height = borderBoxSize.blockSize
  // 更新ref
  containerWidth.value = width
  containerHeight.value = height
  // 执行你的逻辑,比如调整子元素的布局
}
onMounted(() => {
  if (containerRef.value) {
    // 初始化ResizeObserver
    resizeObserver = new ResizeObserver(updateContainerSize)
    // 监听容器元素
    resizeObserver.observe(containerRef.value)
  }
})
onUnmounted(() => {
  // 组件卸载前必须清理ResizeObserver!!这个也是新手最容易忘的坑
  if (resizeObserver) {
    // 先取消监听当前元素
    resizeObserver.unobserve(containerRef.value)
    // 再断开ResizeObserver实例,释放内存
    resizeObserver.disconnect()
  }
})
</script>

这个方案完美解决了前面定时轮询的所有问题:被动监听,只有元素尺寸变化的时候才触发,性能好;触发及时,不会丢失变化,不会造成布局闪烁;支持多种盒子模型,非常灵活;代码量也不多,容易理解。

不过这里有几个新手容易踩的小坑,咱们单独说一下:

  1. entries是数组:因为ResizeObserver的构造函数可以同时观察多个DOM元素,所以回调函数的第一个参数entries是一个数组,每个元素对应一个被观察的元素的信息,如果你只观察一个元素,直接取entries[0]就行。
  2. contentRect和borderBoxSize/paddingBoxSize的区别:contentRect是只读的,返回的是content-box的尺寸,单位是px,而且会四舍五入到整数;borderBoxSize和paddingBoxSize是ResizeObserver Level 2新增的属性,返回的是数组(为了兼容多列布局),每个元素有inlineSize和blockSize两个属性,单位也是px,但不会四舍五入,是浮点数,更精准,如果你需要支持老版本的浏览器(比如Chrome 64-87),那还是用contentRect吧,因为这些浏览器不支持borderBoxSize和paddingBoxSize;如果只需要支持现代浏览器,那推荐用borderBoxSize/paddingBoxSize,更精准。
  3. SSR的时候要注意:ResizeObserver是浏览器端的API,在Node.js环境下(比如SSR渲染的时候)是不存在的,所以如果你的项目是SSR的,那要加个判断,只有在浏览器端的时候才初始化ResizeObserver:
    onMounted(() => {
    if (typeof window !== 'undefined' && containerRef.value) {
     // 初始化ResizeObserver的代码
    }
    })

用VueUse的useResizeObserver:再次安利VueUse!

和useWindowSize一样,VueUse也封装了专门监听局部DOM元素尺寸的useResizeObserver,人家已经帮你把盒子模型选择、浏览器兼容性处理、SSR处理、实例清理这些事情都做好了,直接用就行:

// ✅ 更简单的写法:用VueUse的useResizeObserver
<script setup>
import { ref } from 'vue'
import { useResizeObserver } from '@vueuse/core'
const containerRef = ref(null)
const containerWidth = ref(0)
const containerHeight = ref(0)
// 直接传ref和回调函数
useResizeObserver(containerRef, (entries) => {
  const entry = entries[0]
  const { width, height } = entry.contentRect
  containerWidth.value = width
  containerHeight.value = height
  // 执行你的逻辑
})
</script>

你看,这个代码量是不是更少了?而且VueUse的useResizeObserver还支持同时监听多个元素,只需要传一个数组就行:

const container1Ref = ref(null)
const container2Ref = ref(null)
useResizeObserver([container1Ref, container2Ref], (entries) => {
  entries.forEach((entry) => {
    console.log(entry.target, entry.contentRect)
  })
})

这个方案超级适合懒人开发者,再次强烈安利VueUse!


不同需求下的方案选择清单

最后咱们总结一个不同需求下的方案选择清单,方便大家快速找到适合自己的方案: | 需求场景 | 是否允许引入第三方库 | 推荐方案 | |------------------------------|----------------------|-------------------------------------------| | 全局窗口尺寸监听(PC/移动端) | 否 | 原生addEventListener+ref+onMounted+onUnmounted+自己写的防抖 | | 全局窗口尺寸监听(PC/移动端) | 是 | VueUse的useWindowSize | | 局部DOM元素尺寸监听 | 否 | 原生ResizeObserver+ref+onMounted+onUnmounted | | 局部DOM元素尺寸监听 | 是 | VueUse的useResizeObserver | | 需要支持老版本浏览器(Chrome<64) | 是/否 | 定时轮询(但要注意性能和精准度)+ 可以尝试引入resize-observer-polyfill |

这里再补充一下,如果你的项目需要支持老版本的浏览器(比如Chrome<64、Firefox<69、Safari<13.1),那原生的ResizeObserver是用不了的,这时候你可以选择两种方案:

  1. 定时轮询:这个方案不需要引入任何第三方库,但要注意性能和精准度,建议把轮询间隔设为100-200ms,而且只在元素可能变化的时候开启轮询,比如图片加载的时候、富文本编辑器输入的时候,其他时候可以关闭轮询。
  2. 引入resize-observer-polyfill:这个polyfill是由Google的一位工程师写的,兼容性非常好,支持IE11+,而且性能也不错,和原生的ResizeObserver用法几乎一样,只需要先引入polyfill就行:
    // 先引入polyfill
    import ResizeObserver from 'resize-observer-polyfill'
    // 然后就可以像原生的一样用了

最后的最后:新手避坑总结

  1. 全局窗口监听别用window.onresize,要用addEventListener:避免作用域冲突。
  2. 不管是全局还是局部监听,都要加防抖/节流:全局监听用防抖,滚动条、鼠标移动用节流,控制触发频率,避免性能问题。
  3. 不管是全局还是局部监听,组件卸载前都要清理:全局监听清理addEventListener,局部监听清理ResizeObserver的unobserve和disconnect,避免内存泄漏。
  4. 局部DOM监听别用定时轮询,要用ResizeObserver:被动监听,性能好,更精准,更优雅。
  5. 如果允许引入第三方库,强烈安利VueUse:里面封装了超多实用的Composition API,代码量少,性能好,兼容性好,能大大提高开发效率。

好啦,今天关于Vue3 watch resize的实用方法和避坑指南就聊到这里啦,希望能对大家有所帮助!如果大家还有什么问题,欢迎在评论区留言哦!

版权声明

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

热门