From db3d83f4e2dfe5db5b976dd2e2228fa77aa9b7e2 Mon Sep 17 00:00:00 2001 From: dajiaohuang Date: Thu, 22 Oct 2026 10:33:37 +0800 Subject: [PATCH 1/4] fix: compare Taro specs without JSON serialization --- packages/taro-vchart/package.json | 1 + .../taro-vchart/scripts/test-spec-equal.cjs | 36 +++++++++++ .../src/components/general-chart/index.tsx | 5 +- .../src/components/web-chart/index.tsx | 3 +- packages/taro-vchart/src/utils/index.ts | 1 + packages/taro-vchart/src/utils/spec-equal.ts | 61 +++++++++++++++++++ 6 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 packages/taro-vchart/scripts/test-spec-equal.cjs create mode 100644 packages/taro-vchart/src/utils/spec-equal.ts diff --git a/packages/taro-vchart/package.json b/packages/taro-vchart/package.json index 33e3706c3c..0624c33687 100644 --- a/packages/taro-vchart/package.json +++ b/packages/taro-vchart/package.json @@ -9,6 +9,7 @@ "cjs" ], "scripts": { + "test": "node scripts/test-spec-equal.cjs", "build": "bundle --clean", "build:tt": "taro build --type tt", "build:lark": "taro build --type lark", diff --git a/packages/taro-vchart/scripts/test-spec-equal.cjs b/packages/taro-vchart/scripts/test-spec-equal.cjs new file mode 100644 index 0000000000..31c6b65221 --- /dev/null +++ b/packages/taro-vchart/scripts/test-spec-equal.cjs @@ -0,0 +1,36 @@ +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const typescript = require('typescript'); + +const source = fs.readFileSync(path.join(__dirname, '../src/utils/spec-equal.ts'), 'utf8'); +const { outputText, diagnostics = [] } = typescript.transpileModule(source, { + compilerOptions: { + module: typescript.ModuleKind.CommonJS, + target: typescript.ScriptTarget.ES2015 + }, + reportDiagnostics: true +}); +assert.equal(diagnostics.length, 0, 'spec comparator should transpile without diagnostics'); + +const compiledModule = { exports: {} }; +new Function('exports', 'require', 'module', outputText)(compiledModule.exports, require, compiledModule); +const { isSpecEqual } = compiledModule.exports; + +const firstFormatter = () => 'first'; +const secondFormatter = () => 'second'; +assert.equal(isSpecEqual({ formatter: firstFormatter }, { formatter: secondFormatter }), false); +assert.equal(isSpecEqual({ formatter: firstFormatter }, { formatter: firstFormatter }), true); +assert.equal(isSpecEqual({ axis: { visible: true } }, { axis: { visible: true } }), true); +assert.equal(isSpecEqual({ date: new Date(0) }, { date: new Date(0) }), true); +assert.equal(isSpecEqual({ date: new Date(0) }, { date: new Date(1) }), false); + +function cyclicSpec(value) { + const spec = { value }; + spec.self = spec; + return spec; +} +assert.equal(isSpecEqual(cyclicSpec(1), cyclicSpec(1)), true); +assert.equal(isSpecEqual(cyclicSpec(1), cyclicSpec(2)), false); + +console.log('Taro VChart spec equality checks passed'); diff --git a/packages/taro-vchart/src/components/general-chart/index.tsx b/packages/taro-vchart/src/components/general-chart/index.tsx index d86e969bd9..f3ea2b6636 100644 --- a/packages/taro-vchart/src/components/general-chart/index.tsx +++ b/packages/taro-vchart/src/components/general-chart/index.tsx @@ -6,7 +6,8 @@ import { style_cs_canvas, style_cs_canvas_hidden, style_cs_tooltip_canvas, - style_container + style_container, + isSpecEqual } from '../../utils'; import { IChartProps, IVChart, IDomRef, RenderMode } from '../../typings'; @@ -83,7 +84,7 @@ export class GeneralChart extends React.Component { if ( this.ttCanvas && this.ttCanvas.chartInstance && - JSON.stringify(prevProps.spec) !== JSON.stringify(this.props.spec) + !isSpecEqual(prevProps.spec, this.props.spec) ) { this.ttCanvas.updateSpec(this.props); } diff --git a/packages/taro-vchart/src/components/web-chart/index.tsx b/packages/taro-vchart/src/components/web-chart/index.tsx index e72cc0b4dd..0d71ab0b48 100644 --- a/packages/taro-vchart/src/components/web-chart/index.tsx +++ b/packages/taro-vchart/src/components/web-chart/index.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { isSpecEqual } from '../../utils'; import { IChartProps, IVChart, RenderMode } from '../../typings'; export interface WebChartProps extends IChartProps { @@ -48,7 +49,7 @@ export class WebChart extends React.Component { const { spec, onChartReady } = this.props; - if (JSON.stringify(prevProps.spec) !== JSON.stringify(spec)) { + if (!isSpecEqual(prevProps.spec, spec)) { this.vchart.updateSpec(spec, true); onChartReady && onChartReady(this.vchart); } diff --git a/packages/taro-vchart/src/utils/index.ts b/packages/taro-vchart/src/utils/index.ts index 940d974749..e53815a324 100644 --- a/packages/taro-vchart/src/utils/index.ts +++ b/packages/taro-vchart/src/utils/index.ts @@ -1,3 +1,4 @@ export { TTCanvas } from './tt-canvas'; +export { isSpecEqual } from './spec-equal'; export { style_cs_canvas, style_cs_canvas_hidden, style_cs_tooltip_canvas, style_container } from './container-styles'; diff --git a/packages/taro-vchart/src/utils/spec-equal.ts b/packages/taro-vchart/src/utils/spec-equal.ts new file mode 100644 index 0000000000..923b67446d --- /dev/null +++ b/packages/taro-vchart/src/utils/spec-equal.ts @@ -0,0 +1,61 @@ +function isPlainObject(value: object): boolean { + const prototype = Object.getPrototypeOf(value); + return prototype === Object.prototype || prototype === null; +} + +function equalSpecValue(left: unknown, right: unknown, visited: WeakMap>): boolean { + if (left === right) return true; + if (typeof left === 'number' && typeof right === 'number' && Number.isNaN(left) && Number.isNaN(right)) { + return true; + } + if (typeof left !== typeof right || left === null || right === null) return false; + if (typeof left !== 'object' || typeof right !== 'object') return false; + + const leftTag = Object.prototype.toString.call(left); + if (leftTag !== Object.prototype.toString.call(right)) return false; + if (leftTag === '[object Date]') { + const leftTime = Date.prototype.getTime.call(left as Date); + const rightTime = Date.prototype.getTime.call(right as Date); + return leftTime === rightTime || (Number.isNaN(leftTime) && Number.isNaN(rightTime)); + } + + const leftIsArray = Array.isArray(left); + if (leftIsArray !== Array.isArray(right)) return false; + if (!leftIsArray && (!isPlainObject(left) || !isPlainObject(right))) return false; + + const leftObject = left as object; + const rightObject = right as object; + const seenMatches = visited.get(leftObject); + if (seenMatches?.has(rightObject)) return true; + if (seenMatches) { + seenMatches.add(rightObject); + } else { + visited.set(leftObject, new WeakSet([rightObject])); + } + + if (leftIsArray) { + const leftArray = left as unknown[]; + const rightArray = right as unknown[]; + if (leftArray.length !== rightArray.length) return false; + for (let index = leftArray.length - 1; index >= 0; index--) { + if (!equalSpecValue(leftArray[index], rightArray[index], visited)) return false; + } + return true; + } + + const leftRecord = left as Record; + const rightRecord = right as Record; + const leftKeys = Object.keys(leftRecord); + const rightKeys = Object.keys(rightRecord); + if (leftKeys.length !== rightKeys.length) return false; + for (const key of leftKeys) { + if (!Object.prototype.hasOwnProperty.call(rightRecord, key)) return false; + if (!equalSpecValue(leftRecord[key], rightRecord[key], visited)) return false; + } + return true; +} + +/** Compare chart specs without serializing callback values or cyclic data. */ +export function isSpecEqual(left: unknown, right: unknown): boolean { + return equalSpecValue(left, right, new WeakMap()); +} From 22658e41138a058cfc2b98da5dbeca56cf4ae474 Mon Sep 17 00:00:00 2001 From: dajiaohuang Date: Thu, 22 Oct 2026 10:36:52 +0800 Subject: [PATCH 2/4] perf: reduce Taro spec comparison overhead --- packages/taro-vchart/src/utils/spec-equal.ts | 47 ++++++++++++-------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/packages/taro-vchart/src/utils/spec-equal.ts b/packages/taro-vchart/src/utils/spec-equal.ts index 923b67446d..edc6e21c78 100644 --- a/packages/taro-vchart/src/utils/spec-equal.ts +++ b/packages/taro-vchart/src/utils/spec-equal.ts @@ -3,7 +3,7 @@ function isPlainObject(value: object): boolean { return prototype === Object.prototype || prototype === null; } -function equalSpecValue(left: unknown, right: unknown, visited: WeakMap>): boolean { +function equalSpecValue(left: unknown, right: unknown, active: WeakMap): boolean { if (left === right) return true; if (typeof left === 'number' && typeof right === 'number' && Number.isNaN(left) && Number.isNaN(right)) { return true; @@ -11,11 +11,10 @@ function equalSpecValue(left: unknown, right: unknown, visited: WeakMap= 0; index--) { - if (!equalSpecValue(leftArray[index], rightArray[index], visited)) return false; + if (!equalSpecValue(leftArray[index], rightArray[index], active)) { + active.delete(leftObject); + return false; + } } + active.delete(leftObject); return true; } @@ -47,11 +49,20 @@ function equalSpecValue(left: unknown, right: unknown, visited: WeakMap; const leftKeys = Object.keys(leftRecord); const rightKeys = Object.keys(rightRecord); - if (leftKeys.length !== rightKeys.length) return false; + if (leftKeys.length !== rightKeys.length) { + active.delete(leftObject); + return false; + } for (const key of leftKeys) { - if (!Object.prototype.hasOwnProperty.call(rightRecord, key)) return false; - if (!equalSpecValue(leftRecord[key], rightRecord[key], visited)) return false; + if ( + !Object.prototype.hasOwnProperty.call(rightRecord, key) || + !equalSpecValue(leftRecord[key], rightRecord[key], active) + ) { + active.delete(leftObject); + return false; + } } + active.delete(leftObject); return true; } From 9a4fd7f39d5bca14665566fa51143ce599332c1f Mon Sep 17 00:00:00 2001 From: dajiaohuang Date: Wed, 28 Oct 2026 01:16:34 +0800 Subject: [PATCH 3/4] fix: compare Taro specs with class data and cycle topology --- .../taro-vchart/scripts/test-spec-equal.cjs | 23 ++++++++++ packages/taro-vchart/src/utils/spec-equal.ts | 42 +++++++++++++------ 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/packages/taro-vchart/scripts/test-spec-equal.cjs b/packages/taro-vchart/scripts/test-spec-equal.cjs index 31c6b65221..92dcf67546 100644 --- a/packages/taro-vchart/scripts/test-spec-equal.cjs +++ b/packages/taro-vchart/scripts/test-spec-equal.cjs @@ -25,6 +25,15 @@ assert.equal(isSpecEqual({ axis: { visible: true } }, { axis: { visible: true } assert.equal(isSpecEqual({ date: new Date(0) }, { date: new Date(0) }), true); assert.equal(isSpecEqual({ date: new Date(0) }, { date: new Date(1) }), false); +class Row { + constructor(name, value) { + this.name = name; + this.value = value; + } +} +assert.equal(isSpecEqual({ data: [new Row('A', 3)] }, { data: [new Row('A', 3)] }), true); +assert.equal(isSpecEqual({ data: [new Row('A', 3)] }, { data: [new Row('A', 4)] }), false); + function cyclicSpec(value) { const spec = { value }; spec.self = spec; @@ -33,4 +42,18 @@ function cyclicSpec(value) { assert.equal(isSpecEqual(cyclicSpec(1), cyclicSpec(1)), true); assert.equal(isSpecEqual(cyclicSpec(1), cyclicSpec(2)), false); +function cyclicData(twoNodes) { + const first = { value: 1 }; + if (twoNodes) { + const second = { value: 1 }; + first.next = second; + second.next = first; + } else { + first.next = first; + } + return { data: first }; +} +assert.equal(isSpecEqual(cyclicData(true), cyclicData(false)), false); +assert.equal(isSpecEqual(cyclicData(false), cyclicData(true)), false); + console.log('Taro VChart spec equality checks passed'); diff --git a/packages/taro-vchart/src/utils/spec-equal.ts b/packages/taro-vchart/src/utils/spec-equal.ts index edc6e21c78..dfee321f25 100644 --- a/packages/taro-vchart/src/utils/spec-equal.ts +++ b/packages/taro-vchart/src/utils/spec-equal.ts @@ -1,9 +1,18 @@ -function isPlainObject(value: object): boolean { +function isComparableObject(value: object): boolean { const prototype = Object.getPrototypeOf(value); - return prototype === Object.prototype || prototype === null; + return ( + prototype === Object.prototype || + prototype === null || + Object.keys(value).length > 0 + ); } -function equalSpecValue(left: unknown, right: unknown, active: WeakMap): boolean { +function equalSpecValue( + left: unknown, + right: unknown, + active: WeakMap, + activeReverse: WeakMap +): boolean { if (left === right) return true; if (typeof left === 'number' && typeof right === 'number' && Number.isNaN(left) && Number.isNaN(right)) { return true; @@ -20,28 +29,35 @@ function equalSpecValue(left: unknown, right: unknown, active: WeakMap { + active.delete(leftObject); + activeReverse.delete(rightObject); + }; if (leftIsArray) { const leftArray = left as unknown[]; const rightArray = right as unknown[]; if (leftArray.length !== rightArray.length) { - active.delete(leftObject); + untrack(); return false; } for (let index = leftArray.length - 1; index >= 0; index--) { - if (!equalSpecValue(leftArray[index], rightArray[index], active)) { - active.delete(leftObject); + if (!equalSpecValue(leftArray[index], rightArray[index], active, activeReverse)) { + untrack(); return false; } } - active.delete(leftObject); + untrack(); return true; } @@ -50,23 +66,23 @@ function equalSpecValue(left: unknown, right: unknown, active: WeakMap Date: Wed, 28 Oct 2026 01:41:43 +0800 Subject: [PATCH 4/4] fix: ignore undefined optional spec fields --- packages/taro-vchart/scripts/test-spec-equal.cjs | 6 ++++++ packages/taro-vchart/src/utils/spec-equal.ts | 14 ++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/taro-vchart/scripts/test-spec-equal.cjs b/packages/taro-vchart/scripts/test-spec-equal.cjs index 92dcf67546..740b8ed9e4 100644 --- a/packages/taro-vchart/scripts/test-spec-equal.cjs +++ b/packages/taro-vchart/scripts/test-spec-equal.cjs @@ -22,6 +22,12 @@ const secondFormatter = () => 'second'; assert.equal(isSpecEqual({ formatter: firstFormatter }, { formatter: secondFormatter }), false); assert.equal(isSpecEqual({ formatter: firstFormatter }, { formatter: firstFormatter }), true); assert.equal(isSpecEqual({ axis: { visible: true } }, { axis: { visible: true } }), true); +assert.equal(isSpecEqual({ seriesField: undefined }, {}), true); +assert.equal(isSpecEqual({}, { seriesField: undefined }), true); +assert.equal( + isSpecEqual({ seriesField: undefined }, { seriesField: 'value' }), + false +); assert.equal(isSpecEqual({ date: new Date(0) }, { date: new Date(0) }), true); assert.equal(isSpecEqual({ date: new Date(0) }, { date: new Date(1) }), false); diff --git a/packages/taro-vchart/src/utils/spec-equal.ts b/packages/taro-vchart/src/utils/spec-equal.ts index dfee321f25..d6879865cc 100644 --- a/packages/taro-vchart/src/utils/spec-equal.ts +++ b/packages/taro-vchart/src/utils/spec-equal.ts @@ -65,14 +65,24 @@ function equalSpecValue( const rightRecord = right as Record; const leftKeys = Object.keys(leftRecord); const rightKeys = Object.keys(rightRecord); - if (leftKeys.length !== rightKeys.length) { + let leftDefinedKeyCount = 0; + let rightDefinedKeyCount = 0; + for (const key of leftKeys) { + if (leftRecord[key] !== undefined) leftDefinedKeyCount++; + } + for (const key of rightKeys) { + if (rightRecord[key] !== undefined) rightDefinedKeyCount++; + } + if (leftDefinedKeyCount !== rightDefinedKeyCount) { untrack(); return false; } for (const key of leftKeys) { + const leftValue = leftRecord[key]; + if (leftValue === undefined) continue; if ( !Object.prototype.hasOwnProperty.call(rightRecord, key) || - !equalSpecValue(leftRecord[key], rightRecord[key], active, activeReverse) + !equalSpecValue(leftValue, rightRecord[key], active, activeReverse) ) { untrack(); return false;