22 * @Author : lym
33 * @Date : 2025-02-24 09:32:53
44 * @LastEditors : lym
5- * @LastEditTime : 2025-03-21 19:38:26
5+ * @LastEditTime : 2025-04-01 17:33:53
66 * @Description :
77 */
88import type {
@@ -23,8 +23,7 @@ import {
2323 isNil ,
2424 isObject ,
2525 isString ,
26- styleStringToObject ,
27- uniqArray
26+ styleStringToObject
2827} from '@visactor/vutils' ;
2928import type { VNode } from 'vue' ;
3029import { render } from 'vue' ;
@@ -53,12 +52,28 @@ export class VTableVueAttributePlugin extends HtmlAttributePlugin implements IPl
5352 lastPosition ?: { x : number ; y : number } | null ;
5453 /** 上次样式 */
5554 lastStyle ?: Record < string , any > ;
55+ // 最后访问的时间戳
56+ lastAccessed ?: number ;
5657 }
5758 > ;
5859 /** 渲染队列 */
5960 private renderQueue = new Set < IGraphic > ( ) ;
6061 /** 是否正在渲染 */
6162 private isRendering = false ;
63+ /** 最大缓存节点数(兜底值) */
64+ private MAX_CACHE_COUNT = 100 ;
65+ /** 记录节点访问顺序(LRU用) */
66+ private accessQueue : string [ ] = [ ] ;
67+ /** 目标可视区区(可视区外一定范围内的节点,保留) */
68+ private VIEWPORT_BUFFER = 100 ;
69+ /** 缓冲区(非可视区且非缓冲区的节点需清理) */
70+ private BUFFER_ZONE = 500 ;
71+ // 新增批量更新队列
72+ private styleUpdateQueue = new Map < string , Partial < CSSStyleDeclaration > > ( ) ;
73+ /** 样式更新中 */
74+ private styleUpdateRequested = false ;
75+ /** 事件集 */
76+ private eventHandlers = new WeakMap < HTMLElement , ( e : WheelEvent ) => void > ( ) ;
6277 /** 当前上下文 */
6378 currentContext ?: any ;
6479
@@ -129,12 +144,19 @@ export class VTableVueAttributePlugin extends HtmlAttributePlugin implements IPl
129144 this . checkToPassAppContext ( element , graphic ) ;
130145 // 渲染或更新 Vue 组件
131146 if ( ! targetMap || ! this . checkDom ( targetMap . wrapContainer ) ) {
132- const { wrapContainer, nativeContainer } = this . getWrapContainer ( stage , actualContainer , { id, options } ) ;
147+ // 缓存节点检查
148+ this . checkAndClearCache ( graphic ) ;
149+ const { wrapContainer, nativeContainer, reuse } = this . getWrapContainer ( stage , actualContainer , { id, options } ) ;
133150 if ( wrapContainer ) {
134151 const dataRenderId = `${ this . renderId } ` ;
135152 wrapContainer . id = id ;
136153 wrapContainer . setAttribute ( 'data-vue-renderId' , dataRenderId ) ;
137- render ( element , wrapContainer ) ;
154+ // 先隐藏
155+ wrapContainer . style . display = 'none' ;
156+ if ( ! reuse ) {
157+ // 仅在非复用时需要重新渲染
158+ render ( element , wrapContainer ) ;
159+ }
138160 targetMap = {
139161 wrapContainer,
140162 nativeContainer,
@@ -147,14 +169,14 @@ export class VTableVueAttributePlugin extends HtmlAttributePlugin implements IPl
147169 } ;
148170 this . htmlMap [ id ] = targetMap ;
149171 }
150- } else {
151- render ( element , targetMap . wrapContainer ) ;
152172 }
153173
154174 // 更新样式并记录渲染 ID
155175 if ( targetMap ) {
156- this . updateStyleOfWrapContainer ( graphic , stage , targetMap . wrapContainer , targetMap . nativeContainer ) ;
157176 targetMap . renderId = this . renderId ;
177+ targetMap . lastAccessed = Date . now ( ) ;
178+ this . updateAccessQueue ( id ) ;
179+ this . updateStyleOfWrapContainer ( graphic , stage , targetMap . wrapContainer , targetMap . nativeContainer ) ;
158180 }
159181 }
160182
@@ -179,23 +201,31 @@ export class VTableVueAttributePlugin extends HtmlAttributePlugin implements IPl
179201 * @param {IGraphic } graphic
180202 * @return {* }
181203 */
182- checkToPassAppContext ( vnode : VNode , graphic : IGraphic ) {
204+ private checkToPassAppContext ( vnode : VNode , graphic : IGraphic ) {
183205 try {
184- const { stage } = getTargetGroup ( graphic ) ;
185- const { table } = stage || { } ;
186- const userAppContext = table ?. options ?. customConfig ?. getVueUserAppContext ?.( ) ?? this . currentContext ;
206+ const customConfig = this . getCustomConfig ( graphic ) ;
207+ const userAppContext = customConfig ?. getVueUserAppContext ?.( ) ?? this . currentContext ;
187208 // 简单校验合法性
188209 if ( ! ! userAppContext ?. components && ! ! userAppContext ?. directives ) {
189210 vnode . appContext = userAppContext ;
190211 }
191212 } catch ( error ) { }
192213 }
214+ /**
215+ * @description : 获取自定义配置
216+ * @param {IGraphic } graphic
217+ * @return {* }
218+ */
219+ private getCustomConfig ( graphic : IGraphic ) {
220+ const target = getTargetGroup ( graphic ) ;
221+ return target ?. stage ?. table ?. options ?. customConfig ;
222+ }
193223 /**
194224 * @description : 检查是否需要渲染
195225 * @param {IGraphic } graphic
196226 * @return {* }
197227 */
198- checkNeedRender ( graphic : IGraphic ) {
228+ private checkNeedRender ( graphic : IGraphic ) {
199229 const { id, options } = this . getGraphicOptions ( graphic ) || { } ;
200230 if ( ! id ) {
201231 return false ;
@@ -215,40 +245,133 @@ export class VTableVueAttributePlugin extends HtmlAttributePlugin implements IPl
215245 // 不在可视区内暂时不需要移除,因为在 clearCacheContainer 方法中提前被移除了
216246 return isInViewport ;
217247 }
248+
249+ /**
250+ * @description : 判断是否在可视范围内
251+ * @param {IGraphic } graphic
252+ * @return {* }
253+ */
254+ private checkInViewport ( graphic : IGraphic ) {
255+ return this . checkInViewportByZone ( graphic , this . VIEWPORT_BUFFER ) ;
256+ }
257+ /**
258+ * @description : 判断是否在缓冲区内
259+ * @param {IGraphic } graphic
260+ * @return {* }
261+ */
262+ private checkInBuffer ( graphic : IGraphic ) {
263+ return this . checkInViewportByZone ( graphic , this . BUFFER_ZONE ) ;
264+ }
218265 /**
219- * @description : 判断当前是否在可视区内
266+ * @description : 判断当前是否在指定视口范围内
220267 * @param {IGraphic } graphic
268+ * @param {number } buffer
221269 * @return {* }
222270 */
223- checkInViewport ( graphic : IGraphic ) {
271+ private checkInViewportByZone ( graphic : IGraphic , buffer : number = 0 ) {
224272 const { stage, globalAABBBounds : cBounds } = graphic ;
225273 if ( ! stage ) {
226274 return false ;
227275 }
228- // 设立缓冲区 100px,提前加载
229- const BUFFER = 100 ;
230276 // 获取视口的AABB边界
231277 //@ts -ignore
232278 const { AABBBounds : vBounds } = stage ;
233279 // 扩展视口判断范围
234280 const eBounds = {
235- x1 : vBounds . x1 - BUFFER ,
236- x2 : vBounds . x2 + BUFFER ,
237- y1 : vBounds . y1 - BUFFER ,
238- y2 : vBounds . y2 + BUFFER
281+ x1 : vBounds . x1 - buffer ,
282+ x2 : vBounds . x2 + buffer ,
283+ y1 : vBounds . y1 - buffer ,
284+ y2 : vBounds . y2 + buffer
239285 } ;
240286 // 判断两个区域是否相交
241287 const isIntersecting =
242288 cBounds . x1 < eBounds . x2 && cBounds . x2 > eBounds . x1 && cBounds . y1 < eBounds . y2 && cBounds . y2 > eBounds . y1 ;
243289
244290 return isIntersecting ;
245291 }
292+
293+ /**
294+ * @description : 节点访问顺序队列
295+ * @param {string } id
296+ * @return {* }
297+ */
298+ private updateAccessQueue ( id : string ) {
299+ // 移除旧记录
300+ const index = this . accessQueue . indexOf ( id ) ;
301+ if ( index > - 1 ) {
302+ this . accessQueue . splice ( index , 1 ) ;
303+ }
304+ // 添加到队列头部
305+ this . accessQueue . unshift ( id ) ;
306+ }
307+
308+ /**
309+ * @description : 在添加新节点前检查缓存大小
310+ * @param {IGraphic } graphic
311+ * @return {* }
312+ */
313+ private checkAndClearCache ( graphic : IGraphic ) {
314+ const { viewportNodes, bufferNodes, cacheNodes } = this . classifyNodes ( ) ;
315+ const total = viewportNodes . length + bufferNodes . length + cacheNodes . length ;
316+ const customConfig = this . getCustomConfig ( graphic ) ;
317+ const maxTotal = customConfig ?. maxDomCacheCount ?? this . MAX_CACHE_COUNT ;
318+
319+ // 仅当总数超过阈值时清理
320+ if ( total <= maxTotal ) {
321+ return ;
322+ }
323+ const exceedingCount = total - maxTotal ;
324+
325+ // 优先清理缓存区节点: 移除缓存区的前 exceedingCount 个节点
326+ let toRemove = cacheNodes . slice ( 0 , exceedingCount ) ;
327+
328+ // 若缓存区节点不满足阈值,为了控制内存占用率,按最后访问时间清除最早访问的缓冲区节点
329+ if ( toRemove . length < exceedingCount ) {
330+ const bufferCandidates = bufferNodes
331+ . sort ( ( a , b ) => this . htmlMap [ a ] . lastAccessed - this . htmlMap [ b ] . lastAccessed )
332+ . slice ( 0 , exceedingCount - toRemove . length ) ;
333+ toRemove = toRemove . concat ( bufferCandidates ) ;
334+ }
335+
336+ // 执行清理
337+ toRemove . forEach ( id => this . removeElement ( id , true ) ) ;
338+ }
339+
340+ /**
341+ * @description : 节点按可视区/缓存区/缓冲区分类
342+ * @return {* }
343+ */
344+ private classifyNodes ( ) {
345+ /** 可视区节点 */
346+ const viewportNodes : string [ ] = [ ] ;
347+ /** 缓冲区节点 */
348+ const bufferNodes : string [ ] = [ ] ;
349+ /** 既不在可视区也不在缓冲区的节点 */
350+ const cacheNodes : string [ ] = [ ] ;
351+
352+ Object . keys ( this . htmlMap ) . forEach ( id => {
353+ const node = this . htmlMap [ id ] ;
354+ if ( node . isInViewport ) {
355+ viewportNodes . push ( id ) ;
356+ } else if ( this . checkInBuffer ( node . graphic ) ) {
357+ bufferNodes . push ( id ) ;
358+ } else {
359+ cacheNodes . push ( id ) ;
360+ }
361+ } ) ;
362+
363+ return {
364+ viewportNodes,
365+ bufferNodes,
366+ cacheNodes
367+ } ;
368+ }
246369 /**
247370 * @description : 检查 dom 是否存在
248371 * @param {HTMLElement } dom
249372 * @return {* }
250373 */
251- checkDom ( dom : HTMLElement ) {
374+ private checkDom ( dom : HTMLElement ) {
252375 if ( ! dom ) {
253376 return false ;
254377 }
@@ -285,14 +408,19 @@ export class VTableVueAttributePlugin extends HtmlAttributePlugin implements IPl
285408 if ( ! wrapContainer ) {
286409 return ;
287410 }
288- // 卸载子组件
289- render ( null , wrapContainer ) ;
290411 if ( ! clear ) {
291412 // 移除 dom 但保留在 htmlMap 中,供下次进入可视区时快速复用
292413 wrapContainer . remove ( ) ;
293414 // 标记不在视口
294415 record . isInViewport = false ;
416+ // 清理访问队列
417+ const index = this . accessQueue . indexOf ( id ) ;
418+ if ( index > - 1 ) {
419+ this . accessQueue . splice ( index , 1 ) ;
420+ }
295421 } else {
422+ // 卸载子组件
423+ render ( null , wrapContainer ) ;
296424 this . checkDom ( wrapContainer ) && super . removeElement ( id ) ;
297425 // 清理引用
298426 delete this . htmlMap [ id ] ;
@@ -327,6 +455,7 @@ export class VTableVueAttributePlugin extends HtmlAttributePlugin implements IPl
327455 nativeContainer . appendChild ( wrapContainer ) ;
328456 }
329457 return {
458+ reuse : true ,
330459 wrapContainer,
331460 nativeContainer
332461 } ;
@@ -373,11 +502,13 @@ export class VTableVueAttributePlugin extends HtmlAttributePlugin implements IPl
373502 }
374503
375504 // 默认自定义区域内也可带动表格画布滚动
376- const { pointerEvents, penetrateEventList = [ 'wheel' ] } = options ;
505+ const { pointerEvents } = options ;
377506 const calculateStyle = this . parseDefaultStyleFromGraphic ( graphic ) ;
378507 // 单元格样式
379508 const style = this . convertCellStyle ( graphic ) ;
380509 Object . assign ( calculateStyle , {
510+ width : `${ width } px` ,
511+ height : `${ height } px` ,
381512 overflow : 'hidden' ,
382513 ...( style || { } ) ,
383514 ...( rest || { } ) ,
@@ -389,13 +520,7 @@ export class VTableVueAttributePlugin extends HtmlAttributePlugin implements IPl
389520 } ) ;
390521
391522 if ( calculateStyle . pointerEvents !== 'none' ) {
392- this . removeWrapContainerEventListener ( wrapContainer ) ;
393- uniqArray ( penetrateEventList ) . forEach ( ( event : any ) => {
394- if ( event === 'wheel' ) {
395- wrapContainer . addEventListener ( 'wheel' , this . onWheel ) ;
396- wrapContainer . addEventListener ( 'wheel' , e => e . preventDefault ( ) , true ) ;
397- }
398- } ) ;
523+ this . checkToAddEventListener ( wrapContainer ) ;
399524 }
400525
401526 if ( type === 'text' && options . anchorType === 'position' ) {
@@ -406,17 +531,56 @@ export class VTableVueAttributePlugin extends HtmlAttributePlugin implements IPl
406531 // 样式变化检查
407532 const styleChanged = ! isEqual ( record . lastStyle , calculateStyle ) ;
408533 if ( styleChanged ) {
534+ this . styleUpdateQueue . set ( wrapContainer . id , calculateStyle ) ;
535+ // 请求批量更新
536+ this . requestStyleUpdate ( ) ;
409537 // TODO 确认是否需要对接 VTableBrowserEnvContribution
410- application . global . updateDom ( wrapContainer , {
411- width,
412- height,
413- style : calculateStyle
414- } ) ;
538+ // application.global.updateDom(wrapContainer, {
539+ // width,
540+ // height,
541+ // style: calculateStyle
542+ // });
415543
416544 record . lastStyle = calculateStyle ;
417545 }
418546 }
419547
548+ /**
549+ * @description : 事件监听器管理
550+ * @param {HTMLElement } wrapContainer
551+ * @return {* }
552+ */
553+ private checkToAddEventListener ( wrapContainer : HTMLElement ) {
554+ if ( ! this . eventHandlers . has ( wrapContainer ) ) {
555+ const handler = ( e : WheelEvent ) => {
556+ e . preventDefault ( ) ;
557+ this . onWheel ( e ) ;
558+ } ;
559+ wrapContainer . addEventListener ( 'wheel' , handler , { passive : false } ) ;
560+ this . eventHandlers . set ( wrapContainer , handler ) ;
561+ }
562+ }
563+
564+ /**
565+ * @description : 样式更新
566+ * @return {* }
567+ */
568+ private requestStyleUpdate ( ) {
569+ if ( ! this . styleUpdateRequested ) {
570+ this . styleUpdateRequested = true ;
571+ requestAnimationFrame ( ( ) => {
572+ this . styleUpdateQueue . forEach ( ( changes , id ) => {
573+ const container = this . htmlMap ?. [ id ] ?. wrapContainer ;
574+ if ( container ) {
575+ Object . assign ( container . style , changes ) ;
576+ }
577+ } ) ;
578+ this . styleUpdateQueue . clear ( ) ;
579+ this . styleUpdateRequested = false ;
580+ } ) ;
581+ }
582+ }
583+
420584 /**
421585 * @description : 转换单元格样式
422586 * @param {IGraphic } graphic
0 commit comments