React中OpenLayers窗口resize时moveend事件触发过多的问题咨询
解决OpenLayers moveend事件过度触发导致重复请求的问题
问题背景
在React 18.3.1环境中使用OpenLayers 10.1,通过Context控制地图显示(如选中组件时缩放地图)。当前遇到的问题:仅调整窗口大小(1-2秒内移动100像素),moveend事件就会触发20多次,导致地图加载数据时向后端发起过多请求。尝试过dragend等类似事件,触发次数同样过多,同时怀疑React中的实现方式是否合理(地图resize触发数据重载,数据加载又引发重渲染)。
当前代码实现
'use client'; import { useRef, useState, useCallback, useContext } from 'react'; import { useQuery } from '@tanstack/react-query'; import { Feature } from 'ol'; import { fromLonLat, toLonLat } from 'ol/proj'; import { getBottomLeft, getTopRight } from 'ol/extent'; import { Point } from 'ol/geom'; import { Icon, Style } from 'ol/style'; import { fetcher } from 'utils/axios'; import { MapContext } from 'context/ResizeableMapContext'; import OSMTileLayer from 'components/atoms/OSMTileLayer'; import MapComponent from 'components/atoms/Map'; import VectorLayer from 'components/atoms/VectorLayer'; const AuthenticatedMap = function () { const { mapArea, mapObject, setMapObject } = useContext(MapContext); const [mapBox, setMapBox] = useState({}); const onMoveEnd = useCallback((e) => { console.log('ON MOVE END TRIGGERS'); const { map } = e; const extent = map.getView().calculateExtent(map.getSize()); const bottomLeft = toLonLat(getBottomLeft(extent)); const topRight = toLonLat(getTopRight(extent)); setMapBox({ tr: { lon: topRight[0], lat: topRight[1], }, bl: { lon: bottomLeft[0], lat: bottomLeft[1], }, }); }, []); const getUrlAndKey = () => { const baseUrl = `/api/ships/visible-ships/?`; const baseKey = 'k:'; if ('tr' in mapBox && 'bl' in mapBox) { const { tr: { lon: trLon, lat: trLat }, bl: { lon: blLon, lat: blLat }, } = mapBox; return [ `${baseUrl}min_lon=${blLon}&min_lat=${blLat}&max_lon=${trLon}&max_lat=${trLat}`, `${blLon}&${blLat}&${trLon}&${trLat}`, ]; } return [baseUrl, baseKey]; }; const [url, key] = getUrlAndKey(); const { isPending, isError, data } = useQuery({ queryKey: ['publiclyAvailableShips', key], refetchInterval: 1000 * 60, queryFn: () => fetcher(url), enabled: 'tr' in mapBox && 'bl' in mapBox, }); const features = []; if (!isPending && !isError && data.length) { data.forEach((ship) => { const feature = new Feature({ geometry: new Point(fromLonLat([ship.latest_position.lon, ship.latest_position.lat])), }); feature.setStyle( new Style({ image: new Icon({ src: "data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' " + "width='32' height='32' viewBox='0 0 24 24' fill='%23e5f2fe' stroke='%23000' " + "stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath " + "d='M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7Z'/%3e%3ccircle " + "cx='12' cy='9' r='3'/%3e%3c/svg%3e", className: 'shadow-xl', scale: 1, }), }), ); feature.set('name', ship.name); features.push(feature); }); } return ( <MapComponent zoom={10} center={fromLonLat([24.637865, 59.511668])} width={mapArea} map={mapObject} setMap={setMapObject} moveEndCallback={onMoveEnd} > <OSMTileLayer /> <VectorLayer>{features}</VectorLayer> </MapComponent> ); }; export default AuthenticatedMap;
当前尝试的防抖处理
目前在MapComponent中做了基础防抖:
mapObj.on('moveend', (event) => { // Clear previous timeout to reset the delay if (moveEndTimeoutRef.current) { clearTimeout(moveEndTimeoutRef.current); } // Set a new timeout to call the moveEndCallback after 100ms moveEndTimeoutRef.current = setTimeout(() => { moveEndCallback(event); }, 150); // 150ms delay });
优化方案
你的防抖思路是正确的,但可以结合以下优化进一步减少不必要的触发和请求:
1. 区分事件源,针对性防抖
moveend会在拖拽结束、缩放结束、窗口resize时触发,不同场景的用户交互节奏不同,可以分开处理:
// 处理resize事件(用户调整窗口是连续动作,用更长防抖时长) mapObj.on('change:size', (event) => { if (resizeTimeoutRef.current) clearTimeout(resizeTimeoutRef.current); resizeTimeoutRef.current = setTimeout(() => { moveEndCallback(event); }, 300); }); // 处理拖拽/缩放触发的moveend(动作结束后快速响应) mapObj.on('moveend', (event) => { if (moveEndTimeoutRef.current) clearTimeout(moveEndTimeoutRef.current); moveEndTimeoutRef.current = setTimeout(() => { moveEndCallback(event); }, 150); });
2. 截断坐标精度,避免微小变化触发请求
地图视图的像素级偏移会导致坐标微小变化,进而触发react-query请求。可以在onMoveEnd中对坐标做精度截断:
const onMoveEnd = useCallback((e) => { const { map } = e; const extent = map.getView().calculateExtent(map.getSize()); const bottomLeft = toLonLat(getBottomLeft(extent)); const topRight = toLonLat(getTopRight(extent)); setMapBox({ tr: { lon: parseFloat(topRight[0].toFixed(4)), lat: parseFloat(topRight[1].toFixed(4)), }, bl: { lon: parseFloat(bottomLeft[0].toFixed(4)), lat: parseFloat(bottomLeft[1].toFixed(4)), }, }); }, []);
仅当地图范围变化超过小数点后4位时才更新mapBox,减少不必要的请求触发。
3. 缓存features,避免不必要重渲染
每次组件渲染都会重新创建features数组,导致VectorLayer频繁重渲染。用useMemo缓存:
const features = useMemo(() => { if (isPending || isError || !data?.length) return []; return data.map((ship) => { const feature = new Feature({ geometry: new Point(fromLonLat([ship.latest_position.lon, ship.latest_position.lat])), }); feature.setStyle( new Style({ image: new Icon({ src: "data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' " + "width='32' height='32' viewBox='0 0 24 24' fill='%23e5f2fe' stroke='%23000' " + "stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath " + "d='M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7Z'/%3e%3ccircle " + "cx='12' cy='9' r='3'/%3e%3c/svg%3e", className: 'shadow-xl', scale: 1, }), }), ); feature.set('name', ship.name); return feature; }); }, [data, isPending, isError]);
4. 优化react-query请求策略
通过staleTime和refetchOnWindowFocus减少不必要的重复请求:
const { isPending, isError, data } = useQuery({ queryKey: ['publiclyAvailableShips', key], refetchInterval: 1000 * 60, queryFn: () => fetcher(url), enabled: 'tr' in mapBox && 'bl' in mapBox, refetchOnWindowFocus: false, // 窗口聚焦时不自动重请求 staleTime: 1000 * 30, // 30秒内数据视为新鲜,避免频繁请求 });
5. 替换moveend为更精准的视图事件
OpenLayers视图的change:center和change:resolution事件分别对应地图中心变化和缩放级别变化,触发逻辑更精准,结合防抖效果更好:
const view = mapObj.getView(); view.on(['change:center', 'change:resolution'], (event) => { if (viewTimeoutRef.current) clearTimeout(viewTimeoutRef.current); viewTimeoutRef.current = setTimeout(() => { moveEndCallback({ map: mapObj }); }, 200); });
内容的提问来源于stack exchange,提问作者Odif Yltsaeb
相关产品推荐
相关产品推荐

