Vue 3 响应性系统与 Web API ResizeObserver 的集成:观测结果纳入依赖追踪
大家好,今天我们来深入探讨 Vue 3 的响应性系统与 Web API ResizeObserver 的集成。ResizeObserver 是一个强大的 Web API,允许我们监听 HTML 元素的尺寸变化。将其观测结果纳入 Vue 3 的依赖追踪,可以实现组件对元素尺寸变化的响应式更新,从而构建更加灵活和动态的用户界面。
1. 响应性系统概述:reactive、ref、computed
Vue 3 的响应性系统是其核心特性之一。它允许我们创建响应式的数据,当这些数据发生变化时,依赖于这些数据的组件会自动更新。主要有以下三个核心 API:
reactive: 用于创建对象的响应式代理。当对象上的属性发生变化时,会触发依赖该属性的副作用。ref: 用于创建基本类型(如数字、字符串、布尔值)的响应式引用。ref对象包含一个.value属性,用于访问和修改其内部的值。computed: 用于创建基于其他响应式数据的计算属性。计算属性的值会被缓存,只有当依赖的响应式数据发生变化时,才会重新计算。
例如:
import { reactive, ref, computed } from 'vue';
// 使用 reactive 创建响应式对象
const state = reactive({
width: 100,
height: 50
});
// 使用 ref 创建响应式数字
const count = ref(0);
// 使用 computed 创建计算属性
const area = computed(() => state.width * state.height);
// 修改响应式数据
state.width = 200; // 触发依赖 state.width 的副作用,area 会重新计算
count.value++; // 触发依赖 count 的副作用
2. ResizeObserver API 简介
ResizeObserver API 允许我们异步地监听 HTML 元素的尺寸变化。与传统的 window.onresize 事件相比,ResizeObserver 具有以下优点:
- 元素级别监听: 可以监听单个元素的尺寸变化,而不是整个窗口。
- 异步回调: 回调函数在浏览器空闲时执行,避免阻塞主线程。
- 提供尺寸信息: 回调函数接收一个
ResizeObserverEntry数组,每个ResizeObserverEntry包含目标元素的新尺寸信息。
基本用法如下:
const element = document.getElementById('my-element');
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
const width = entry.contentRect.width;
const height = entry.contentRect.height;
console.log(`Element size changed: width=${width}, height=${height}`);
}
});
observer.observe(element);
// 停止监听
// observer.disconnect();
3. 集成方案:ref + onMounted + onBeforeUnmount
要将 ResizeObserver 的观测结果纳入 Vue 3 的响应性系统,我们可以使用 ref 来存储元素的尺寸信息,并在组件的 onMounted 钩子中启动 ResizeObserver,在 onBeforeUnmount 钩子中停止 ResizeObserver。
具体步骤如下:
-
创建
ref对象存储尺寸信息: 使用ref创建两个响应式引用,分别用于存储元素的宽度和高度。 -
在
onMounted中启动ResizeObserver: 在组件的onMounted钩子中,获取目标元素,创建ResizeObserver实例,并启动监听。在回调函数中,更新ref对象的值。 -
在
onBeforeUnmount中停止ResizeObserver: 在组件的onBeforeUnmount钩子中,停止ResizeObserver,避免内存泄漏。
下面是一个示例组件:
<template>
<div ref="elementRef" :style="{ width: '100%', height: '200px', border: '1px solid black' }">
Width: {{ width }}, Height: {{ height }}
</div>
</template>
<script>
import { ref, onMounted, onBeforeUnmount } from 'vue';
export default {
setup() {
const elementRef = ref(null);
const width = ref(0);
const height = ref(0);
let observer = null;
onMounted(() => {
observer = new ResizeObserver(entries => {
for (const entry of entries) {
width.value = entry.contentRect.width;
height.value = entry.contentRect.height;
}
});
observer.observe(elementRef.value);
});
onBeforeUnmount(() => {
observer.disconnect();
});
return {
elementRef,
width,
height
};
}
};
</script>
在这个例子中,我们使用 elementRef 来引用 DOM 元素。width 和 height 两个 ref 对象存储元素的尺寸信息。ResizeObserver 在 onMounted 钩子中启动,并在回调函数中更新 width 和 height 的值。 onBeforeUnmount 钩子中停止监听,避免内存泄漏。
4. 封装成可复用的 Composition Function
为了提高代码的可复用性,我们可以将上述逻辑封装成一个 Composition Function。
import { ref, onMounted, onBeforeUnmount } from 'vue';
export function useResizeObserver(elementRef) {
const width = ref(0);
const height = ref(0);
let observer = null;
onMounted(() => {
observer = new ResizeObserver(entries => {
for (const entry of entries) {
width.value = entry.contentRect.width;
height.value = entry.contentRect.height;
}
});
observer.observe(elementRef.value);
});
onBeforeUnmount(() => {
observer.disconnect();
});
return {
width,
height
};
}
使用这个 Composition Function 的组件如下:
<template>
<div ref="elementRef" :style="{ width: '100%', height: '200px', border: '1px solid black' }">
Width: {{ width }}, Height: {{ height }}
</div>
</template>
<script>
import { ref } from 'vue';
import { useResizeObserver } from './useResizeObserver';
export default {
setup() {
const elementRef = ref(null);
const { width, height } = useResizeObserver(elementRef);
return {
elementRef,
width,
height
};
}
};
</script>
这种方式更加简洁和易于维护。
5. 考虑边界情况:服务端渲染 (SSR) 和 elementRef 的初始值
在服务端渲染 (SSR) 环境下,document 对象和 ResizeObserver API 可能不可用。我们需要进行条件判断,避免在服务器端执行相关代码。
此外,elementRef 在组件挂载之前可能为 null。我们需要确保 elementRef.value 在 ResizeObserver 启动时存在。
修改后的 Composition Function 如下:
import { ref, onMounted, onBeforeUnmount, onUpdated } from 'vue';
export function useResizeObserver(elementRef) {
const width = ref(0);
const height = ref(0);
let observer = null;
onMounted(() => {
if (typeof ResizeObserver === 'undefined') {
console.warn('ResizeObserver is not supported in this environment.');
return;
}
// 使用 onUpdated 确保 elementRef.value 已经存在
onUpdated(() => {
if (elementRef.value && !observer) { // 确保observer只初始化一次
observer = new ResizeObserver(entries => {
for (const entry of entries) {
width.value = entry.contentRect.width;
height.value = entry.contentRect.height;
}
});
observer.observe(elementRef.value);
}
});
});
onBeforeUnmount(() => {
if (observer) {
observer.disconnect();
}
});
return {
width,
height
};
}
我们添加了 typeof ResizeObserver === 'undefined' 的判断,避免在不支持 ResizeObserver 的环境中报错。使用 onUpdated 钩子确保了 elementRef.value 在 ResizeObserver 启动时已经存在。同时,保证了observer只初始化一次
6. 进阶应用:响应式布局和动态调整组件尺寸
将 ResizeObserver 的观测结果纳入 Vue 3 的响应性系统,可以实现各种高级功能,例如:
-
响应式布局: 根据元素的尺寸变化,动态调整布局。例如,当容器宽度小于某个阈值时,切换到移动端布局。
-
动态调整组件尺寸: 根据元素的尺寸变化,动态调整组件的尺寸。例如,根据图片的高度,动态调整容器的高度。
-
瀑布流布局: 监听每个瀑布流元素的尺寸变化,动态调整元素的位置。
示例:响应式布局
假设我们想要在容器宽度小于 768px 时,切换到移动端布局。
<template>
<div ref="containerRef">
<div v-if="isMobile">
Mobile Layout
</div>
<div v-else>
Desktop Layout
</div>
</div>
</template>
<script>
import { ref, computed } from 'vue';
import { useResizeObserver } from './useResizeObserver';
export default {
setup() {
const containerRef = ref(null);
const { width } = useResizeObserver(containerRef);
const isMobile = computed(() => width.value < 768);
return {
containerRef,
isMobile
};
}
};
</script>
在这个例子中,我们使用 useResizeObserver 监听容器的宽度变化,并使用 computed 创建一个计算属性 isMobile,用于判断是否为移动端布局。
7. 性能优化:节流 (Throttling) 和防抖 (Debouncing)
ResizeObserver 的回调函数可能会频繁触发,特别是在元素尺寸变化剧烈的情况下。为了避免性能问题,我们可以使用节流 (Throttling) 或防抖 (Debouncing) 来限制回调函数的执行频率。
-
节流 (Throttling): 在指定的时间间隔内,只执行一次回调函数。
-
防抖 (Debouncing): 在指定的时间间隔内,如果没有再次触发事件,则执行回调函数。
以下是使用 lodash 库实现节流的示例:
import { ref, onMounted, onBeforeUnmount, onUpdated } from 'vue';
import { throttle } from 'lodash-es';
export function useResizeObserver(elementRef, throttleWait = 100) {
const width = ref(0);
const height = ref(0);
let observer = null;
onMounted(() => {
if (typeof ResizeObserver === 'undefined') {
console.warn('ResizeObserver is not supported in this environment.');
return;
}
onUpdated(() => {
if (elementRef.value && !observer) {
const throttledCallback = throttle(entries => {
for (const entry of entries) {
width.value = entry.contentRect.width;
height.value = entry.contentRect.height;
}
}, throttleWait);
observer = new ResizeObserver(throttledCallback);
observer.observe(elementRef.value);
}
});
});
onBeforeUnmount(() => {
if (observer) {
observer.disconnect();
}
});
return {
width,
height
};
}
我们使用 lodash-es 库的 throttle 函数来限制回调函数的执行频率。throttleWait 参数指定了节流的时间间隔。
8. 兼容性考虑
虽然 ResizeObserver 已经得到了广泛的支持,但仍然有一些旧版本的浏览器不支持它。为了确保兼容性,我们可以使用 polyfill。一个常用的 polyfill 是 resize-observer-polyfill。
安装:
npm install resize-observer-polyfill
使用:
import ResizeObserver from 'resize-observer-polyfill';
if (typeof window.ResizeObserver === 'undefined') {
window.ResizeObserver = ResizeObserver;
}
在你的应用程序的入口文件中,添加上述代码,即可为不支持 ResizeObserver 的浏览器提供 polyfill。
9. 表格总结:API 与 钩子函数的应用
| API / 钩子函数 | 作用 |
|---|---|
reactive |
创建响应式对象(虽然这里没有直接用到 reactive,但 ref 是其底层实现) |
ref |
创建基本类型的响应式引用,用于存储元素的尺寸信息 |
computed |
创建基于响应式数据的计算属性,例如 isMobile |
onMounted |
组件挂载后执行,用于启动 ResizeObserver |
onBeforeUnmount |
组件卸载前执行,用于停止 ResizeObserver,防止内存泄漏 |
onUpdated |
组件更新后执行,确保在 ResizeObserver 初始化时 elementRef.value 已经存在 |
ResizeObserver |
Web API,用于监听 HTML 元素的尺寸变化 |
throttle (lodash) |
节流函数,用于限制回调函数的执行频率,提高性能 |
代码示例:完整的响应式布局组件 (包含节流和兼容性处理)
<template>
<div ref="containerRef">
<div v-if="isMobile">
Mobile Layout
</div>
<div v-else>
Desktop Layout
</div>
</div>
</template>
<script>
import { ref, computed, onMounted, onBeforeUnmount, onUpdated } from 'vue';
import { throttle } from 'lodash-es';
import ResizeObserver from 'resize-observer-polyfill';
// Polyfill for older browsers
if (typeof window.ResizeObserver === 'undefined') {
window.ResizeObserver = ResizeObserver;
}
export default {
setup() {
const containerRef = ref(null);
const width = ref(0);
let observer = null;
const throttleWait = 100; // 节流时间间隔
onMounted(() => {
if (typeof ResizeObserver === 'undefined') {
console.warn('ResizeObserver is not supported in this environment.');
return;
}
onUpdated(() => {
if (containerRef.value && !observer) {
const throttledCallback = throttle(entries => {
for (const entry of entries) {
width.value = entry.contentRect.width;
}
}, throttleWait);
observer = new ResizeObserver(throttledCallback);
observer.observe(containerRef.value);
}
});
});
onBeforeUnmount(() => {
if (observer) {
observer.disconnect();
}
});
const isMobile = computed(() => width.value < 768);
return {
containerRef,
isMobile
};
}
};
</script>
通过以上代码,我们成功地将 ResizeObserver 的观测结果纳入了 Vue 3 的响应性系统,实现了响应式布局的功能。
尺寸变化驱动的 UI 更新
通过将 ResizeObserver 的观测结果纳入 Vue 3 的响应性系统,我们可以轻松地构建响应式和动态的用户界面,从而提升用户体验。
性能优化与兼容性处理
使用节流函数和 polyfill 可以进一步优化性能和提高兼容性,确保应用在各种环境下都能正常运行。
总结:ref、ResizeObserver与钩子函数的结合
使用 ref 存储尺寸信息,在onMounted中启动ResizeObserver,在onBeforeUnmount中停止监听,结合onUpdated保证初始化时dom已挂载,是集成ResizeObserver与Vue3响应式系统的关键步骤。
更多IT精英技术系列讲座,到智猿学院