Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/taro-vchart/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
36 changes: 36 additions & 0 deletions packages/taro-vchart/scripts/test-spec-equal.cjs
Original file line number Diff line number Diff line change
@@ -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');
5 changes: 3 additions & 2 deletions packages/taro-vchart/src/components/general-chart/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -83,7 +84,7 @@ export class GeneralChart extends React.Component<GeneralChartProps> {
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);
}
Expand Down
3 changes: 2 additions & 1 deletion packages/taro-vchart/src/components/web-chart/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import { isSpecEqual } from '../../utils';
import { IChartProps, IVChart, RenderMode } from '../../typings';

export interface WebChartProps extends IChartProps {
Expand Down Expand Up @@ -48,7 +49,7 @@ export class WebChart extends React.Component<WebChartProps> {

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);
}
Expand Down
1 change: 1 addition & 0 deletions packages/taro-vchart/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -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';
72 changes: 72 additions & 0 deletions packages/taro-vchart/src/utils/spec-equal.ts
Original file line number Diff line number Diff line change
@@ -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<object, object>): 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Avoid treating recreated class-valued data rows as changed specs

With freshly constructed data rows having the same enumerable fields (e.g. new Row('A', 3) on each render), this line returns false at the first row. VChart's datum shape accepts arbitrary fields, and both wrappers interpret false as a reason to call updateSpec(..., true). A Node 22 reproduction using this comparator returned unequal on 100/100 equivalent rerenders, whereas the former JSON guard returned equal. Large datasets or dashboards consequently merge/render unchanged charts repeatedly. Compare enumerable fields of supported row objects, or handle this case at the wrapper boundary. This reproduces the comparator/guard, not a full Taro integration test.


const leftObject = left as object;
const rightObject = right as object;
const activeRight = active.get(leftObject);
if (activeRight) return activeRight === rightObject;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Distinguish cyclic data graphs that share a right-hand node

active only tracks left-to-right pairs, so two different active left nodes may map to the same right node. For data values containing a two-node cycle versus a one-node self-cycle with identical scalar fields, isSpecEqual(oldSpec, newSpec) returns true while the reverse comparison returns false (reproduced using the actual comparator under Node 22). Since the wrappers call updateSpec only when this returns false, replacing such cyclic data is silently ignored. Track the reverse active mapping too, so changed cycle topology cannot compare equal. Full chart integration was not runnable without workspace dependencies.

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<string, unknown>;
const rightRecord = right as Record<string, unknown>;
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());
}
Loading