fix: 修复主站自动刷新内存泄漏,并完善 chan_tv 图表体验
主站重建前完整 dispose、去掉重复 sync 监听,自动刷新默认增量更新;顺带消除首屏重复 analyze、复用 ChanMACD,以及全版 TV 指标/未完成中枢/布局本地缓存。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+54
-271
@@ -1,33 +1,56 @@
|
||||
/* chart_tv.js — split from chart.js */
|
||||
|
||||
/** 释放 Lightweight Charts 实例、DOM 与全局事件,避免自动刷新内存泄漏 */
|
||||
function disposeTradingViewCharts() {
|
||||
try {
|
||||
if (window._tvInitCleanups && Array.isArray(window._tvInitCleanups)) {
|
||||
window._tvInitCleanups.forEach(function (fn) { try { fn(); } catch (e) {} });
|
||||
}
|
||||
window._tvInitCleanups = [];
|
||||
if (window._bindSyncCleanups && Array.isArray(window._bindSyncCleanups)) {
|
||||
window._bindSyncCleanups.forEach(function (fn) { try { fn(); } catch (e) {} });
|
||||
}
|
||||
window._bindSyncCleanups = [];
|
||||
if (window._tooltipCleanups && Array.isArray(window._tooltipCleanups)) {
|
||||
window._tooltipCleanups.forEach(function (fn) { try { fn(); } catch (e) {} });
|
||||
}
|
||||
window._tooltipCleanups = [];
|
||||
|
||||
document.querySelectorAll(
|
||||
'.volume-crosshair-line, .atr-crosshair-line, .macd-crosshair-line, .chanmacd-crosshair-line'
|
||||
).forEach(function (el) { try { el.remove(); } catch (e) {} });
|
||||
|
||||
if (typeof clearEMA52Series === 'function') {
|
||||
try { clearEMA52Series(); } catch (e) {}
|
||||
}
|
||||
|
||||
if (tvWidget) {
|
||||
['mainChart', 'volumeChart', 'macdChart', 'chanMacdChart', 'atrChart'].forEach(function (key) {
|
||||
try {
|
||||
if (tvWidget[key] && typeof tvWidget[key].remove === 'function') {
|
||||
tvWidget[key].remove();
|
||||
}
|
||||
} catch (e) {}
|
||||
tvWidget[key] = null;
|
||||
});
|
||||
if (tvWidget.state) {
|
||||
tvWidget.state.isInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
var chartRoot = document.getElementById('tradingview_chart');
|
||||
if (chartRoot) {
|
||||
chartRoot.innerHTML = '';
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('disposeTradingViewCharts 失败(可忽略):', e);
|
||||
}
|
||||
}
|
||||
|
||||
function initTradingView(symbol, timeframe) {
|
||||
try {
|
||||
// 在重新初始化前,尝试释放旧图表与系列资源,避免 GPU 内存累积
|
||||
try {
|
||||
if (tvWidget && tvWidget.state && tvWidget.state.isInitialized) {
|
||||
// 主图
|
||||
if (tvWidget.mainChart && typeof tvWidget.mainChart.remove === 'function') {
|
||||
tvWidget.mainChart.remove();
|
||||
}
|
||||
// 成交量
|
||||
if (tvWidget.volumeChart && typeof tvWidget.volumeChart.remove === 'function') {
|
||||
tvWidget.volumeChart.remove();
|
||||
}
|
||||
// 旧 MACD(若存在)
|
||||
if (tvWidget.macdChart && typeof tvWidget.macdChart.remove === 'function') {
|
||||
tvWidget.macdChart.remove();
|
||||
}
|
||||
// 新 ChanMACD(若存在)
|
||||
if (tvWidget.chanMacdChart && typeof tvWidget.chanMacdChart.remove === 'function') {
|
||||
tvWidget.chanMacdChart.remove();
|
||||
}
|
||||
// ATR
|
||||
if (tvWidget.atrChart && typeof tvWidget.atrChart.remove === 'function') {
|
||||
tvWidget.atrChart.remove();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('释放旧图表资源失败(可忽略):', e);
|
||||
}
|
||||
// 每次重建前完整释放,防止自动刷新导致 GPU/监听器泄漏
|
||||
disposeTradingViewCharts();
|
||||
console.log('初始化TradingView图表:', symbol, timeframe);
|
||||
|
||||
// 获取当前交易对的配置
|
||||
@@ -98,11 +121,7 @@ try {
|
||||
candles = filterTradingHours(candles, symbolConfig);
|
||||
console.log(`A股数据过滤: ${originalLength} -> ${candles.length} 条记录`);
|
||||
}
|
||||
// 清除图表容器(释放旧 DOM 与 Canvas)
|
||||
const chartRoot = document.getElementById('tradingview_chart');
|
||||
if (chartRoot) chartRoot.innerHTML = '';
|
||||
|
||||
// 重置图表对象
|
||||
// 重置图表对象(容器已在 disposeTradingViewCharts 清空)
|
||||
tvWidget = {
|
||||
mainChart: null,
|
||||
volumeChart: null,
|
||||
@@ -235,8 +254,7 @@ try {
|
||||
container.appendChild(chanMacdChartContainer);
|
||||
}
|
||||
|
||||
// 防止同步过程中的无限循环
|
||||
let syncInProgress = false;
|
||||
// 防止同步过程中的无限循环(实际同步由 bindSyncEvents 负责)
|
||||
|
||||
// 创建统一的图表选项
|
||||
const createChartOptions = (showTimeScale = true, chartType = 'main') => {
|
||||
@@ -1052,243 +1070,8 @@ try {
|
||||
window.kluDivMarkersSubSub = [];
|
||||
}
|
||||
|
||||
// 实现三图联动滚动
|
||||
|
||||
// 同步图表的时间范围
|
||||
function syncCharts(sourceChart, sourceContainer) {
|
||||
// 防止无限循环 - 使用更精确的检查
|
||||
if (syncInProgress) {
|
||||
console.log('🔄 同步正在进行中,跳过此次同步');
|
||||
return;
|
||||
}
|
||||
|
||||
syncInProgress = true;
|
||||
console.log('🚀 开始同步图表,来源:',
|
||||
sourceChart === mainChart ? '主图' :
|
||||
sourceChart === volumeChart ? '成交量图' :
|
||||
sourceChart === atrChart ? 'ATR图' :
|
||||
sourceChart === macdChart ? 'MACD图' :
|
||||
sourceChart === chanMacdChart ? 'ChanMACD图' : '未知图表');
|
||||
|
||||
try {
|
||||
if (sourceChart && sourceChart.timeScale) {
|
||||
const logicalRange = sourceChart.timeScale().getVisibleLogicalRange();
|
||||
|
||||
if (logicalRange && logicalRange.from !== undefined && logicalRange.to !== undefined) {
|
||||
console.log('📊 同步时间范围:', logicalRange);
|
||||
|
||||
// 同步主图
|
||||
if (sourceChart !== mainChart && mainChart && mainChart.timeScale) {
|
||||
try {
|
||||
mainChart.timeScale().setVisibleLogicalRange(logicalRange);
|
||||
console.log('✅ 主图同步完成');
|
||||
} catch (e) {
|
||||
console.error('❌ 主图同步失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 同步成交量图
|
||||
if (sourceChart !== volumeChart && volumeChart && volumeChart.timeScale) {
|
||||
try {
|
||||
volumeChart.timeScale().setVisibleLogicalRange(logicalRange);
|
||||
console.log('✅ 成交量图同步完成');
|
||||
} catch (e) {
|
||||
console.error('❌ 成交量图同步失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 同步ATR图
|
||||
if (sourceChart !== atrChart && atrChart && atrChart.timeScale) {
|
||||
try {
|
||||
atrChart.timeScale().setVisibleLogicalRange(logicalRange);
|
||||
console.log('✅ ATR图同步完成');
|
||||
} catch (e) {
|
||||
console.error('❌ ATR图同步失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 同步MACD图
|
||||
if (showMacd && macdChart && sourceChart !== macdChart && macdChart.timeScale) {
|
||||
try {
|
||||
macdChart.timeScale().setVisibleLogicalRange(logicalRange);
|
||||
console.log('✅ MACD图同步完成');
|
||||
} catch (e) {
|
||||
console.error('❌ MACD图同步失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 同步ChanMACD图
|
||||
if (showMacd && chanMacdChart && sourceChart !== chanMacdChart && chanMacdChart.timeScale) {
|
||||
try {
|
||||
chanMacdChart.timeScale().setVisibleLogicalRange(logicalRange);
|
||||
console.log('✅ ChanMACD图同步完成');
|
||||
} catch (e) {
|
||||
console.error('❌ ChanMACD图同步失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存当前的可见范围到全局状态
|
||||
if (tvWidget && tvWidget.state) {
|
||||
tvWidget.state.logicalRange = logicalRange;
|
||||
}
|
||||
} else {
|
||||
console.warn('⚠️ 无效的逻辑范围:', logicalRange);
|
||||
}
|
||||
} else {
|
||||
console.warn('⚠️ 无效的源图表或时间刻度');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('💥 同步图表出错:', e);
|
||||
}
|
||||
|
||||
// 立即重置同步标志,提高响应速度
|
||||
setTimeout(() => {
|
||||
syncInProgress = false;
|
||||
console.log('🔓 同步标志已重置');
|
||||
}, 1);
|
||||
}
|
||||
|
||||
// 用于跟踪所有图表的拖动状态
|
||||
let localDragStates = {
|
||||
main: false,
|
||||
volume: false,
|
||||
atr: false,
|
||||
macd: false,
|
||||
chanmacd: false
|
||||
};
|
||||
|
||||
// 全局鼠标抬起事件(只添加一次)
|
||||
document.addEventListener('mouseup', () => {
|
||||
// 重置所有拖动状态
|
||||
Object.keys(localDragStates).forEach(key => {
|
||||
if (localDragStates[key]) {
|
||||
console.log(`全局鼠标抬起,重置${key}图表拖动状态`);
|
||||
localDragStates[key] = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 为每个图表添加事件监听
|
||||
const addChartSyncEvents = (chartContainer, chart) => {
|
||||
console.log('为图表添加同步事件监听:',
|
||||
chart === mainChart ? '主图' :
|
||||
chart === volumeChart ? '成交量图' :
|
||||
chart === atrChart ? 'ATR图' :
|
||||
chart === macdChart ? 'MACD图' :
|
||||
chart === chanMacdChart ? 'ChanMACD图' : '未知图表');
|
||||
|
||||
// 确定当前图表类型
|
||||
const chartType = chart === mainChart ? 'main' :
|
||||
chart === volumeChart ? 'volume' :
|
||||
chart === atrChart ? 'atr' :
|
||||
chart === macdChart ? 'macd' :
|
||||
chart === chanMacdChart ? 'chanmacd' : 'unknown';
|
||||
|
||||
// 使用LightweightCharts内置的时间范围变化事件(这是最可靠的方法)
|
||||
chart.timeScale().subscribeVisibleTimeRangeChange(() => {
|
||||
// 使用图表特定的同步标志防止递归
|
||||
if (!syncInProgress) {
|
||||
console.log('✅ 检测到时间范围变化,触发同步:', chartType, '当前范围:', chart.timeScale().getVisibleLogicalRange());
|
||||
syncCharts(chart, chartContainer);
|
||||
} else {
|
||||
console.log('⏸️ 同步进行中,跳过时间范围变化事件:', chartType);
|
||||
}
|
||||
});
|
||||
|
||||
// 备用的DOM事件监听(用于调试和额外保障)
|
||||
let isScrolling = false;
|
||||
|
||||
// 鼠标按下事件
|
||||
chartContainer.addEventListener('mousedown', (e) => {
|
||||
localDragStates[chartType] = true;
|
||||
console.log('鼠标按下开始拖动:', chartType);
|
||||
});
|
||||
|
||||
// 鼠标抬起事件
|
||||
chartContainer.addEventListener('mouseup', (e) => {
|
||||
if (localDragStates[chartType]) {
|
||||
localDragStates[chartType] = false;
|
||||
console.log('鼠标抬起,结束拖动:', chartType);
|
||||
}
|
||||
});
|
||||
|
||||
// 鼠标离开事件
|
||||
chartContainer.addEventListener('mouseleave', (e) => {
|
||||
if (localDragStates[chartType]) {
|
||||
localDragStates[chartType] = false;
|
||||
console.log('鼠标离开容器,结束拖动:', chartType);
|
||||
}
|
||||
});
|
||||
|
||||
// 滚轮缩放事件(保持原有逻辑)
|
||||
chartContainer.addEventListener('wheel', (e) => {
|
||||
if (!isScrolling) {
|
||||
isScrolling = true;
|
||||
console.log('滚轮缩放:', chartType);
|
||||
setTimeout(() => {
|
||||
if (!syncInProgress) {
|
||||
syncCharts(chart, chartContainer);
|
||||
}
|
||||
isScrolling = false;
|
||||
}, 50);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 添加事件监听
|
||||
addChartSyncEvents(mainChartContainer, mainChart);
|
||||
addChartSyncEvents(volumeChartContainer, volumeChart);
|
||||
addChartSyncEvents(atrChartContainer, atrChart);
|
||||
if (showMacd && macdChart) {
|
||||
addChartSyncEvents(macdChartContainer, macdChart);
|
||||
}
|
||||
if (showMacd && chanMacdChart) {
|
||||
addChartSyncEvents(chanMacdChartContainer, chanMacdChart);
|
||||
}
|
||||
|
||||
// 窗口大小变化时重绘图表
|
||||
window.addEventListener('resize', () => {
|
||||
// 调整主图大小
|
||||
mainChart.applyOptions({
|
||||
width: mainChartContainer.clientWidth,
|
||||
height: mainChartContainer.clientHeight
|
||||
});
|
||||
|
||||
// 调整成交量图大小
|
||||
volumeChart.applyOptions({
|
||||
width: volumeChartContainer.clientWidth,
|
||||
height: volumeChartContainer.clientHeight
|
||||
});
|
||||
|
||||
// 调整ATR图大小
|
||||
atrChart.applyOptions({
|
||||
width: atrChartContainer.clientWidth,
|
||||
height: atrChartContainer.clientHeight
|
||||
});
|
||||
|
||||
// 调整MACD图大小
|
||||
if (showMacd && macdChart && macdChartContainer) {
|
||||
macdChart.applyOptions({
|
||||
width: macdChartContainer.clientWidth,
|
||||
height: macdChartContainer.clientHeight
|
||||
});
|
||||
}
|
||||
|
||||
// 调整ChanMACD图大小
|
||||
if (showMacd && chanMacdChart && chanMacdChartContainer) {
|
||||
chanMacdChart.applyOptions({
|
||||
width: chanMacdChartContainer.clientWidth,
|
||||
height: chanMacdChartContainer.clientHeight
|
||||
});
|
||||
}
|
||||
|
||||
// 重新同步 - 使用主图作为同步源
|
||||
setTimeout(() => {
|
||||
if (mainChart) {
|
||||
syncCharts(mainChart, mainChartContainer);
|
||||
}
|
||||
}, 200);
|
||||
});
|
||||
// 图表同步事件统一由文末 bindSyncEvents 注册(带 cleanup),此处不再重复 addEventListener,
|
||||
// 否则每次自动刷新/重建都会在 document/window 上堆积监听导致内存泄漏。
|
||||
// 显示笔的绘制 - 分别处理主周期、次周期和次次周期
|
||||
if ($('#showMainBi').is(':checked') || $('#showElementBi').is(':checked') || $('#showSubSubBi').is(':checked')) {
|
||||
console.log('绘制笔 - 已启用');
|
||||
|
||||
Reference in New Issue
Block a user