@@ -16,6 +16,13 @@ class OutlineFeature {
1616 this . lastSize = null ; // 记录最后的大小
1717 this . COLLAPSE_THRESHOLD = 750 ; // 思维导图尺寸小于这个值时自动折叠
1818 this . titleBarHeight = 45 ; // 标题栏高度
19+
20+ // 添加防抖和监听器管理
21+ this . updateTimer = null ;
22+ this . mutationObserver = null ;
23+ this . isUpdating = false ;
24+ this . lastUpdateTime = 0 ;
25+ this . UPDATE_DEBOUNCE_DELAY = 300 ; // 防抖延迟300ms
1926 }
2027
2128 /**
@@ -34,6 +41,9 @@ class OutlineFeature {
3441 */
3542 init ( ) {
3643 console . log ( 'OutlineFeature: init called' ) ;
44+ // 先清理之前的实例(如果有的话)
45+ this . cleanupObservers ( ) ;
46+
3747 // 先检查并删除已存在的大纲窗口
3848 const existingWindow = document . getElementById ( 'vx-outline-window' ) ;
3949 if ( existingWindow ) {
@@ -581,13 +591,44 @@ class OutlineFeature {
581591 }
582592
583593 /**
584- * 更新大纲窗口内容
594+ * 更新大纲窗口内容(带防抖)
585595 * 步骤:
586596 * 1. 清空现有内容
587597 * 2. 获取根节点数据
588598 * 3. 递归渲染节点结构
589599 */
590600 updateOutlineWindow ( ) {
601+ // 防抖处理 - 清除之前的定时器
602+ if ( this . updateTimer ) {
603+ clearTimeout ( this . updateTimer ) ;
604+ }
605+
606+ // 如果正在更新,直接返回
607+ if ( this . isUpdating ) {
608+ return ;
609+ }
610+
611+ // 检查更新频率限制
612+ const now = Date . now ( ) ;
613+ const timeSinceLastUpdate = now - this . lastUpdateTime ;
614+
615+ if ( timeSinceLastUpdate < 500 ) { // 500ms内不重复更新
616+ this . updateTimer = setTimeout ( ( ) => {
617+ this . doUpdateOutlineWindow ( ) ;
618+ } , this . UPDATE_DEBOUNCE_DELAY ) ;
619+ return ;
620+ }
621+
622+ // 设置延迟更新
623+ this . updateTimer = setTimeout ( ( ) => {
624+ this . doUpdateOutlineWindow ( ) ;
625+ } , 50 ) ; // 短延迟确保DOM更新完成
626+ }
627+
628+ /**
629+ * 实际执行大纲窗口更新
630+ */
631+ doUpdateOutlineWindow ( ) {
591632 if ( ! this . outlineWindow ) {
592633 console . warn ( 'OutlineFeature: outlineWindow not found' ) ;
593634 return ;
@@ -600,6 +641,9 @@ class OutlineFeature {
600641 }
601642
602643 try {
644+ this . isUpdating = true ;
645+ this . lastUpdateTime = Date . now ( ) ;
646+
603647 // 获取MindElixir数据
604648 const allData = this . core . mindElixir && this . core . mindElixir . getAllData ( ) ;
605649
@@ -614,6 +658,13 @@ class OutlineFeature {
614658 } catch ( error ) {
615659 console . error ( 'OutlineFeature: Error updating outline window:' , error ) ;
616660 content . innerHTML = '<div style="color: #e74c3c; text-align: center; padding: 20px;">数据加载失败</div>' ;
661+ } finally {
662+ this . isUpdating = false ;
663+ // 清除定时器
664+ if ( this . updateTimer ) {
665+ clearTimeout ( this . updateTimer ) ;
666+ this . updateTimer = null ;
667+ }
617668 }
618669 }
619670
@@ -948,15 +999,95 @@ class OutlineFeature {
948999 * 监听思维导图变化并更新大纲
9491000 */
9501001 setupDOMObserver ( ) {
951- const observer = new MutationObserver ( ( ) => {
952- this . updateOutlineWindow ( ) ;
953- } ) ;
1002+ // 清理之前的监听器
1003+ this . cleanupObservers ( ) ;
1004+
1005+ // 监听 MindElixir 的 operation 事件
1006+ this . core . mindElixir . bus . addListener ( 'operation' , ( operation ) => {
1007+ console . log ( 'OutlineFeature: Operation detected:' , operation . name ) ;
1008+
1009+ // 根据操作类型决定是否需要更新大纲
1010+ const outlineUpdateOperations = [
1011+ 'addChild' , // 添加子节点
1012+ 'removeNode' , // 删除节点
1013+ 'moveNode' , // 移动节点
1014+ 'finishEdit' // 完成编辑(文本内容变化)
1015+ ] ;
1016+
1017+ const immediateUpdateOperations = [
1018+ 'addChild' ,
1019+ 'removeNode' ,
1020+ 'moveNode'
1021+ ] ;
1022+
1023+ const delayedUpdateOperations = [
1024+ 'finishEdit' // 编辑完成时再更新,避免输入过程中频繁更新
1025+ ] ;
1026+
1027+ if ( immediateUpdateOperations . includes ( operation . name ) ) {
1028+ // 立即更新(有50ms防抖)
1029+ this . updateOutlineWindow ( ) ;
1030+ } else if ( delayedUpdateOperations . includes ( operation . name ) ) {
1031+ // 延迟更新,给更多时间让用户完成编辑
1032+ if ( this . updateTimer ) {
1033+ clearTimeout ( this . updateTimer ) ;
1034+ }
1035+ this . updateTimer = setTimeout ( ( ) => {
1036+ this . doUpdateOutlineWindow ( ) ;
1037+ } , this . UPDATE_DEBOUNCE_DELAY ) ;
1038+ }
9541039
955- observer . observe ( document . getElementById ( 'vx-mindmap' ) , {
956- childList : true ,
957- subtree : true ,
958- characterData : true
1040+ // 不再监听 editStyle, editTags, editIcons 等,这些不影响大纲结构
9591041 } ) ;
1042+
1043+ // 创建单一的 MutationObserver 作为备用监听器
1044+ // 只监听结构性变化,不监听文本内容变化
1045+ const mindmapElement = document . getElementById ( 'vx-mindmap' ) ;
1046+ if ( mindmapElement ) {
1047+ this . mutationObserver = new MutationObserver ( ( mutations ) => {
1048+ let needsUpdate = false ;
1049+
1050+ mutations . forEach ( ( mutation ) => {
1051+ // 只关注子节点的添加/删除,忽略文本内容变化
1052+ if ( mutation . type === 'childList' &&
1053+ ( mutation . addedNodes . length > 0 || mutation . removedNodes . length > 0 ) ) {
1054+ needsUpdate = true ;
1055+ }
1056+ } ) ;
1057+
1058+ if ( needsUpdate ) {
1059+ console . log ( 'OutlineFeature: DOM structure change detected via MutationObserver' ) ;
1060+ this . updateOutlineWindow ( ) ;
1061+ }
1062+ } ) ;
1063+
1064+ // 只监听子节点变化,不监听characterData
1065+ this . mutationObserver . observe ( mindmapElement , {
1066+ childList : true ,
1067+ subtree : true
1068+ // 不包含 characterData: true,避免文本编辑时的频繁触发
1069+ } ) ;
1070+ }
1071+ }
1072+
1073+ /**
1074+ * 清理所有观察器和监听器
1075+ */
1076+ cleanupObservers ( ) {
1077+ // 清理定时器
1078+ if ( this . updateTimer ) {
1079+ clearTimeout ( this . updateTimer ) ;
1080+ this . updateTimer = null ;
1081+ }
1082+
1083+ // 清理 MutationObserver
1084+ if ( this . mutationObserver ) {
1085+ this . mutationObserver . disconnect ( ) ;
1086+ this . mutationObserver = null ;
1087+ }
1088+
1089+ // 重置状态
1090+ this . isUpdating = false ;
9601091 }
9611092
9621093 /**
@@ -999,4 +1130,29 @@ class OutlineFeature {
9991130 } ) ;
10001131 }
10011132 }
1133+
1134+ /**
1135+ * 销毁大纲功能,清理所有资源
1136+ */
1137+ destroy ( ) {
1138+ console . log ( 'OutlineFeature: destroy called' ) ;
1139+
1140+ // 清理所有观察器和监听器
1141+ this . cleanupObservers ( ) ;
1142+
1143+ // 移除大纲窗口
1144+ if ( this . outlineWindow && this . outlineWindow . parentNode ) {
1145+ this . outlineWindow . parentNode . removeChild ( this . outlineWindow ) ;
1146+ }
1147+
1148+ // 清理所有引用
1149+ this . outlineWindow = null ;
1150+ this . core = null ;
1151+ this . nodeDataMap . clear ( ) ;
1152+ this . collapsedNodes = null ;
1153+ this . lastPosition = null ;
1154+ this . lastSize = null ;
1155+
1156+ console . log ( 'OutlineFeature: destroyed successfully' ) ;
1157+ }
10021158}
0 commit comments