From bb335cfad608bef3fa18432c9913246c2f15d2d0 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Mon, 27 Jul 2026 15:59:34 +0800 Subject: [PATCH] fix: recalculate formula chains --- ...e-5234-formula-chain_2026-07-27-15-55.json | 11 ++ .../recalculation-issue-test.test.ts | 18 ++ packages/vtable-sheet/examples/menu.ts | 4 + .../sheet/issue-5234-formula-chain.ts | 186 ++++++++++++++++++ .../src/formula/formula-engine.ts | 8 +- 5 files changed, 222 insertions(+), 5 deletions(-) create mode 100644 common/changes/@visactor/vtable-sheet/fix-issue-5234-formula-chain_2026-07-27-15-55.json create mode 100644 packages/vtable-sheet/examples/sheet/issue-5234-formula-chain.ts diff --git a/common/changes/@visactor/vtable-sheet/fix-issue-5234-formula-chain_2026-07-27-15-55.json b/common/changes/@visactor/vtable-sheet/fix-issue-5234-formula-chain_2026-07-27-15-55.json new file mode 100644 index 0000000000..ab121f9786 --- /dev/null +++ b/common/changes/@visactor/vtable-sheet/fix-issue-5234-formula-chain_2026-07-27-15-55.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@visactor/vtable-sheet", + "comment": "fix: recalculate multi-level formula dependents", + "type": "patch" + } + ], + "packageName": "@visactor/vtable-sheet", + "email": "892739385@qq.com" +} diff --git a/packages/vtable-sheet/__tests__/recalculation-issue-test.test.ts b/packages/vtable-sheet/__tests__/recalculation-issue-test.test.ts index 74519b9fbd..2e6f13c6d0 100644 --- a/packages/vtable-sheet/__tests__/recalculation-issue-test.test.ts +++ b/packages/vtable-sheet/__tests__/recalculation-issue-test.test.ts @@ -71,6 +71,24 @@ describe('Recalculation Chain Issue Test', () => { } }); + test('should recalculate downstream formulas after an intermediate formula has a cached value', () => { + engine.setCellContent({ sheet: 'Sheet1', row: 1, col: 1 }, 1); // B2 + engine.setCellContent({ sheet: 'Sheet1', row: 2, col: 1 }, 10); // B3 + engine.setCellContent({ sheet: 'Sheet1', row: 1, col: 2 }, '=B2'); // C2 + engine.setCellContent({ sheet: 'Sheet1', row: 1, col: 3 }, '=C2+B3'); // D2 + + expect(engine.getCellValue({ sheet: 'Sheet1', row: 1, col: 2 })).toEqual({ value: 1, error: undefined }); + expect(engine.getCellValue({ sheet: 'Sheet1', row: 1, col: 3 })).toEqual({ value: 11, error: undefined }); + + engine.setCellContent({ sheet: 'Sheet1', row: 2, col: 1 }, 20); + expect(engine.getCellValue({ sheet: 'Sheet1', row: 1, col: 3 })).toEqual({ value: 21, error: undefined }); + + engine.setCellContent({ sheet: 'Sheet1', row: 1, col: 1 }, 5); + + expect(engine.getCellValue({ sheet: 'Sheet1', row: 1, col: 2 })).toEqual({ value: 5, error: undefined }); + expect(engine.getCellValue({ sheet: 'Sheet1', row: 1, col: 3 })).toEqual({ value: 25, error: undefined }); + }); + test('测试重新计算过程中的中间状态', () => { console.log('\n=== 测试重新计算过程中的中间状态 ==='); diff --git a/packages/vtable-sheet/examples/menu.ts b/packages/vtable-sheet/examples/menu.ts index 2265304622..2ed8ae521c 100644 --- a/packages/vtable-sheet/examples/menu.ts +++ b/packages/vtable-sheet/examples/menu.ts @@ -30,5 +30,9 @@ export const menus = [ { path: 'sheet', name: 'issue-5204-formula-manager' + }, + { + path: 'sheet', + name: 'issue-5234-formula-chain' } ]; diff --git a/packages/vtable-sheet/examples/sheet/issue-5234-formula-chain.ts b/packages/vtable-sheet/examples/sheet/issue-5234-formula-chain.ts new file mode 100644 index 0000000000..6c65222ec3 --- /dev/null +++ b/packages/vtable-sheet/examples/sheet/issue-5234-formula-chain.ts @@ -0,0 +1,186 @@ +import { VTableSheet } from '../../src/index'; + +const CONTAINER_ID = 'vTable'; +const SHEET_KEY = 'issue5234'; + +const removeDemoToolbar = () => { + document.getElementById('issue5234Toolbar')?.remove(); +}; + +const setStatus = (message: string, pass: boolean) => { + const statusNode = document.getElementById('issue5234Status'); + if (!statusNode) { + return; + } + statusNode.textContent = message; + statusNode.style.color = pass ? '#237804' : '#cf1322'; +}; + +const getActiveWorkSheet = (sheetInstance: VTableSheet) => sheetInstance.getActiveSheet(); + +const getCellValue = (sheetInstance: VTableSheet, col: number, row: number) => { + const worksheet = getActiveWorkSheet(sheetInstance); + return worksheet.tableInstance?.getCellValue(col, row); +}; + +const getFormulaValue = (sheetInstance: VTableSheet, col: number, row: number) => + sheetInstance.formulaManager.getCellValue({ + sheet: SHEET_KEY, + row, + col + }).value; + +const collectDependents = ( + sheetInstance: VTableSheet, + cell: { sheet: string; row: number; col: number }, + visited = new Set() +): Array<{ sheet: string; row: number; col: number }> => { + const cellKey = `${cell.sheet}!${cell.row},${cell.col}`; + if (visited.has(cellKey)) { + return []; + } + visited.add(cellKey); + + const directDependents = sheetInstance.formulaManager.getCellDependents(cell); + return directDependents.reduce>( + (dependents, dependent) => dependents.concat(dependent, collectDependents(sheetInstance, dependent, visited)), + [] + ); +}; + +const syncDependentsToTable = (sheetInstance: VTableSheet, col: number, row: number) => { + const worksheet = getActiveWorkSheet(sheetInstance); + const dependents = collectDependents(sheetInstance, { + sheet: SHEET_KEY, + row, + col + }); + + dependents.forEach(dependent => { + const result = sheetInstance.formulaManager.getCellValue(dependent); + worksheet.tableInstance?.changeCellValue( + dependent.col, + dependent.row, + result.error ? '#ERROR!' : result.value, + false, + false + ); + }); +}; + +const setCell = (sheetInstance: VTableSheet, col: number, row: number, value: unknown) => { + const worksheet = getActiveWorkSheet(sheetInstance); + sheetInstance.formulaManager.setCellContent( + { + sheet: SHEET_KEY, + row, + col + }, + value + ); + + if (typeof value === 'string' && value.startsWith('=')) { + const result = sheetInstance.formulaManager.getCellValue({ + sheet: SHEET_KEY, + row, + col + }); + worksheet.tableInstance?.changeCellValue(col, row, result.error ? '#ERROR!' : result.value, false, false); + } else { + worksheet.tableInstance?.changeCellValue(col, row, value, false, false); + } + + syncDependentsToTable(sheetInstance, col, row); +}; + +export function createTable() { + removeDemoToolbar(); + + const container = document.getElementById(CONTAINER_ID)!; + container.style.width = '760px'; + container.style.height = '420px'; + + const toolbar = document.createElement('div'); + toolbar.id = 'issue5234Toolbar'; + toolbar.style.cssText = ['display: flex', 'gap: 8px', 'align-items: center', 'height: 48px', 'font-size: 12px'].join( + ';' + ); + toolbar.innerHTML = ` + + + + 公式:C2=B2,D2=C2+B3;预期最终 C2=5、D2=25。 + + `; + container.before(toolbar); + + const sheetInstance = new VTableSheet(container, { + showFormulaBar: true, + showSheetTab: true, + defaultRowHeight: 36, + defaultColWidth: 120, + sheets: [ + { + sheetKey: SHEET_KEY, + sheetTitle: 'Issue 5234', + active: true, + rowCount: 12, + columnCount: 8, + showHeader: false, + data: [ + ['A', 'B', 'C', 'D'], + ['', 1, '', ''], + ['', 10, '', ''] + ] + } + ] + }); + + const resetFormulaChain = () => { + setCell(sheetInstance, 1, 1, 1); // B2 + setCell(sheetInstance, 1, 2, 10); // B3 + setCell(sheetInstance, 2, 1, '=B2'); // C2 + setCell(sheetInstance, 3, 1, '=C2+B3'); // D2 + + const c2 = getCellValue(sheetInstance, 2, 1); + const d2 = getCellValue(sheetInstance, 3, 1); + setStatus(`READY | C2=${String(c2)}, D2=${String(d2)}`, true); + }; + + const changeB3 = () => { + setCell(sheetInstance, 1, 2, 20); + const d2 = getCellValue(sheetInstance, 3, 1); + setStatus(`STEP1 | B3=20, D2=${String(d2)},预期 21`, d2 === 21); + }; + + const changeB2AndCheck = () => { + setCell(sheetInstance, 1, 1, 5); + + const c2 = getCellValue(sheetInstance, 2, 1); + const d2 = getCellValue(sheetInstance, 3, 1); + const engineC2 = getFormulaValue(sheetInstance, 2, 1); + const engineD2 = getFormulaValue(sheetInstance, 3, 1); + const pass = c2 === 5 && d2 === 25 && engineC2 === 5 && engineD2 === 25; + + setStatus( + `${pass ? 'PASS' : 'FAIL'} | table C2=${String(c2)}, D2=${String(d2)}; engine C2=${String(engineC2)}, D2=${String( + engineD2 + )}; 预期 C2=5, D2=25`, + pass + ); + }; + + document.getElementById('issue5234Reset')?.addEventListener('click', resetFormulaChain); + document.getElementById('issue5234ChangeB3')?.addEventListener('click', changeB3); + document.getElementById('issue5234ChangeB2')?.addEventListener('click', changeB2AndCheck); + + resetFormulaChain(); + + const release = sheetInstance.release.bind(sheetInstance); + sheetInstance.release = () => { + removeDemoToolbar(); + release(); + }; + + (window as any).sheetInstance = sheetInstance; +} diff --git a/packages/vtable-sheet/src/formula/formula-engine.ts b/packages/vtable-sheet/src/formula/formula-engine.ts index 52e17f9b97..b0a470651a 100644 --- a/packages/vtable-sheet/src/formula/formula-engine.ts +++ b/packages/vtable-sheet/src/formula/formula-engine.ts @@ -2487,11 +2487,6 @@ export class FormulaEngine { private recalculateDependentsWithTracking(changedCell: FormulaCell, visited: Set): void { const cellKey = this.getCellKey(changedCell); - // 防止循环依赖导致的无限递归 - if (visited.has(cellKey)) { - return; - } - const dependents = this.dependents.get(cellKey); if (!dependents || dependents.size === 0) { @@ -2502,6 +2497,9 @@ export class FormulaEngine { const sortedDependents = this.sortCellsByDependency([...dependents]); for (const dependentKey of sortedDependents) { + if (visited.has(dependentKey)) { + continue; + } this.recalculateSingleCellWithTracking(dependentKey, visited); } }