| | |
| | | import { Map } from '@tarojs/components'; |
| | | import { memo, useCallback, useRef } from 'react'; |
| | | import { memo } from 'react'; |
| | | import type { ReactNode } from 'react'; |
| | | |
| | | type BeginRouteMapCanvasProps = { |
| | |
| | | /** |
| | | * 地图画布包装组件。 |
| | | * |
| | | * 双层优化,分别解决两类重渲染: |
| | | * 受控 <Map longitude/latitude/scale> 在每次元素更新时都会把相机重新写回原生地图;行程中 |
| | | * 大量无关状态(计时器、弹窗等)会触发页面重渲染,若 <Map> 跟着重渲染,就会把受控相机重新下发, |
| | | * 与用户手势/原生视口产生争抢,表现为拖动被弹回、视口抖动。 |
| | | * |
| | | * 1) React.memo(拦「无关」重渲染):行程中大量无关状态(计时器、弹窗等)会触发页面重渲染, |
| | | * 只要传入属性(相机 / markers / polyline / showLocation / 事件回调 / children)未变, |
| | | * memo 就跳过,<Map> 不被触碰。要求上层保证函数属性引用稳定、children 仅在覆盖物真正变化时换新。 |
| | | * 这里用 React.memo 包裹:仅当地图相关属性(相机 / markers / polyline / showLocation / |
| | | * 事件回调 / children)发生变化时才重渲染,无关重渲染一律跳过,<Map> 不再被反复触碰。 |
| | | * |
| | | * 2) 手势期间冻结(拦「有关」重渲染):行程中车辆在动,markers / 覆盖物 / 路线距离持续变化, |
| | | * 这些属性变化会打穿 memo 使 <Map> 重渲染;而受控 <Map longitude/latitude/scale> 每次更新都会 |
| | | * 把相机重新写回原生地图,在用户拖拽/缩放手势进行中就会和原生视口争抢,表现为「拖动被弹回」。 |
| | | * 因此在 regionchange 'begin'(用户手势)→'end' 之间复用同一个 <Map> 元素引用,命中 React 的 |
| | | * same-element bailout,跳过该子树 reconcile,原生地图停留在用户操作后的位置。 |
| | | * 手势期间车标仍由命令式 translateMarker 原生动画推进(不依赖 markers 属性回灌), |
| | | * Android overlay 在 begin 时本就隐藏、end 再重算,故冻结不影响跟车与覆盖物表现。 |
| | | * 注意:使用方需保证传入的函数属性引用稳定、children 仅在覆盖物真正变化时换新, |
| | | * 否则浅比较会被打穿、memo 失效。 |
| | | */ |
| | | function BeginRouteMapCanvas(props: BeginRouteMapCanvasProps) { |
| | | const { |
| | |
| | | children, |
| | | } = props; |
| | | |
| | | // 用户手势进行中标记:仅用户拖拽/缩放手势期间为 true,程序化视口调整(update)不置位。 |
| | | const interactingRef = useRef(false); |
| | | // 缓存的 <Map> 元素引用:手势期间复用,命中 same-element bailout 避免相机被重新写回。 |
| | | const cachedMapRef = useRef<JSX.Element | null>(null); |
| | | |
| | | // 包裹上层 onRegionChange,在转发前维护手势进行中标记。 |
| | | // useCallback 依赖 onRegionChange(上层已稳定),保持引用稳定。 |
| | | const handleRegionChange = useCallback((event: any) => { |
| | | const causedBy = String(event?.causedBy || event?.detail?.causedBy || '').toLowerCase(); |
| | | const eventType = String(event?.type || event?.detail?.type || '').toLowerCase(); |
| | | const isUserGesture = causedBy === 'gesture' || causedBy === 'drag' || causedBy === 'scale'; |
| | | if (eventType === 'begin') { |
| | | if (isUserGesture) { |
| | | interactingRef.current = true; |
| | | } |
| | | } else if (eventType === 'end') { |
| | | // 上层 onRegionChange 会在 'end' 把原生实际 scale/center 回灌;先解锁, |
| | | // 使 end 之后第一次重渲染按最新属性重建 <Map> 完成同步。 |
| | | interactingRef.current = false; |
| | | } |
| | | onRegionChange?.(event); |
| | | }, [onRegionChange]); |
| | | |
| | | // 手势进行中且已有缓存:复用旧引用冻结相机;其余情况按最新属性重建元素。 |
| | | if (!interactingRef.current || !cachedMapRef.current) { |
| | | cachedMapRef.current = ( |
| | | <Map |
| | | id='map' |
| | | longitude={longitude} |
| | | latitude={latitude} |
| | | scale={scale} |
| | | markers={markers} |
| | | polyline={polyline} |
| | | showLocation={showLocation} |
| | | onCalloutTap={onCalloutTap} |
| | | onRegionChange={handleRegionChange} |
| | | onPolylineTap={onPolylineTap} |
| | | style={{ width: '100%', height: '87vh' }} |
| | | > |
| | | {children} |
| | | </Map> |
| | | ); |
| | | } |
| | | |
| | | return cachedMapRef.current; |
| | | return ( |
| | | <Map |
| | | id='map' |
| | | longitude={longitude} |
| | | latitude={latitude} |
| | | scale={scale} |
| | | markers={markers} |
| | | polyline={polyline} |
| | | showLocation={showLocation} |
| | | onCalloutTap={onCalloutTap} |
| | | onRegionChange={onRegionChange} |
| | | onPolylineTap={onPolylineTap} |
| | | style={{ width: '100%', height: '87vh' }} |
| | | > |
| | | {children} |
| | | </Map> |
| | | ); |
| | | } |
| | | |
| | | export default memo(BeginRouteMapCanvas); |