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..edc6e21c78 --- /dev/null +++ b/packages/taro-vchart/src/utils/spec-equal.ts @@ -0,0 +1,72 @@ +function isPlainObject(value: object): boolean { + const prototype = Object.getPrototypeOf(value); + return prototype === Object.prototype || prototype === null; +} + +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; + } + if (typeof left !== typeof right || left === null || right === null) return false; + if (typeof left !== 'object' || typeof right !== 'object') return false; + + if (left instanceof Date || right instanceof Date) { + if (!(left instanceof Date) || !(right instanceof Date)) return false; + const leftTime = left.getTime(); + const rightTime = right.getTime(); + 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 activeRight = active.get(leftObject); + if (activeRight) return activeRight === rightObject; + active.set(leftObject, rightObject); + + if (leftIsArray) { + const leftArray = left as unknown[]; + const rightArray = right as unknown[]; + if (leftArray.length !== rightArray.length) { + active.delete(leftObject); + return false; + } + for (let index = leftArray.length - 1; index >= 0; index--) { + if (!equalSpecValue(leftArray[index], rightArray[index], active)) { + active.delete(leftObject); + return false; + } + } + active.delete(leftObject); + 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) { + active.delete(leftObject); + return false; + } + for (const key of leftKeys) { + if ( + !Object.prototype.hasOwnProperty.call(rightRecord, key) || + !equalSpecValue(leftRecord[key], rightRecord[key], active) + ) { + active.delete(leftObject); + return false; + } + } + active.delete(leftObject); + 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()); +}