一、核心机制
前文提到,openinula-vchart提供两种组件声明方式。
-
**统一入口组件,**如:
<VChart ``/>和<VChartSimple /> -
语义化图表组件,包括:
-
图表,如:
<LineChart ``/``><BarChart ``/``>等 -
系列,如
<Line /><Bar />等 -
控件,如
<legend /><Axes />等
下图展示了openinula-vchart的实现机制:

接下来让我们分别介绍不同模块的具体实现:
二、Chart(图表)

组件入口
packages/openinula-vchart/src/VChart.tsx
packages/openinula-vchart/src/VChartSimple.tsx
packages/openinula-vchart/src/charts
无论是统一入口组件,还是语义化组件,都将走入createChart逻辑,createChart根据不同的参数创建不同的图表。
以<VChart />为例,该组件只做一件事,就是createChart:
import { BaseChartProps, createChart } from './charts/BaseChart';
import VChartCore from '@visactor/vchart';
export { VChartCore };
// 定义 VChart 组件属性,排除基础图表中不需要的 props
export type VChartProps = Omit<BaseChartProps, 'container' | 'data' | 'width' | 'height' | 'type'>;
// 创建 VChart 组件实例
export const VChart = createChart<VChartProps>('VChart', {
vchartConstrouctor: VChartCore // 构造器: VChart 核心库
});
创建图表容器
packages/openinula-vchart/src/charts/BaseChart.tsx
export const createChart = <T extends Props>( componentName: string, // 组件名称, 用于配置class等 defaultProps?: Partial<T>, // 组件属性,用于创建vchart实例、解析spec、挂载event等 callback?: (props: T, defaultProps?: Partial<T>) => T // 回调,用于处理props ) => { // 基于BaseChart封装容器,并设置css属性、挂在ref等 const Com = withContainer<ContainerProps, T>(BaseChart as any, componentName, (props: T) => { // 自定义属性处理 if (callback) { return callback(props, defaultProps); } // 如果有默认属性,则将组件属性与默认属性合并 if (defaultProps) { return Object.assign(props, defaultProps); } // 直接返回属性 return props; }); // 设置组件识别标志 Com.displayName = componentName; return Com; };
这一步主要根据传入的组件名称、组件属性和回调,进行:
-
container封装: 基于
**BaseChart**封装,封装时会设置css属性、挂载ref等操作 -
props处理: 如果有自定义属性处理 或 默认属性,进行自定义处理 或 合并默认属性
-
displayName:设置组件识别标志,用于react调试
BaseChart 图表基类
packages/openinula-vchart/src/charts/BaseChart.tsx
状态管理
// 状态管理
const [updateId, setUpdateId] = useState<number>(0); // 图表更新计数器
const chartContext = useRef<ChartContextType>({}); // 图表上下文引用
useImperativeHandle(ref, () => chartContext.current?.chart); // 对外暴露图表实例
const hasSpec = !!props.spec; // 是否存在全量 spec 配置
// 视图与生命周期
const [view, setView] = useState<IView>(null); // 底层 VGrammar 视图实例
const isUnmount = useRef<boolean>(false); // 组件卸载标记
// 配置缓存
const prevSpec = useRef(pickWithout(props, notSpecKeys)); // 过滤非 spec 属性后的配置
const specFromChildren = useRef<Omit<ISpec, 'type' | 'data' | 'width' | 'height'>>(null); // 子组件生成的 spec
// 事件系统
const eventsBinded = React.useRef<BaseChartProps>(null); // 已绑定的事件属性缓存
// 性能优化
const skipFunctionDiff = !!props.skipFunctionDiff; // 是否跳过函数对比
// tooltip节点
const [tooltipNode, setTooltipNode] = useState<ReactNode>(null); // 自定义 tooltip 节点
其中两个核心设计:
-
差异对比优化 :通过 prevSpec 和 pickWithout 实现精准的配置变更检测
-
双更新模式 :根据 hasSpec 变量 辨别全量 spec 更新和声明式组件更新 两种模式
子组件spec解析
const parseSpecFromChildren = (props: Props) => {
// 初始化空 spec 对象(排除 type/data/width/height 字段)
const specFromChildren: Omit<ISpec, 'type' | 'data' | 'width' | 'height'> = {};
// 将子组件转换为数组并遍历
toArray(props.children).map((child, index) => {
// 获取子组件的 parseSpec 方法(需组件实现)
const parseSpec = child && (child as any).type && (child as any).type.parseSpec;
if (parseSpec && (child as any).props) {
// 生成子组件 props:自动添加 componentId
const childProps = isNil((child as any).props.componentId)
? {
...(child as any).props,
componentId: getComponentId(child, index) // 生成唯一组件ID
}
: (child as any).props;
// 调用子组件的规范解析方法
const specResult = parseSpec(childProps);
// 合并解析结果到总 spec
if (specResult.isSingle) {
// 单例模式(如标题组件)
specFromChildren[specResult.specName] = specResult.spec;
} else {
// 多例模式(如多个数据标记)
if (!specFromChildren[specResult.specName]) {
specFromChildren[specResult.specName] = [];
}
specFromChildren[specResult.specName].push(specResult.spec);
}
}
});
return specFromChildren;
};
本模块主要做的是将子组件中的spec解析出来并挂载到specFromChildren上。由于不同的组件配置模式不同,有的是单例,有的是多例,所以解析的逻辑也略有不同。
本模块的重点内容:
- 声明式组件转换 :
将类似这样的 JSX 声明:
<LineChart> <Mark type="point" /> <Axis orient="bottom" /> </LineChart>
转换为 VChart 标准的 JSON spec:
{ "mark": [{ "type": "point" }], "axes": [{ "orient": "bottom" }] }
- 组件唯一标识 :
通过 getComponentId 生成的 ID 结构为 组件类型-索引 (如 'Mark-0'),用于:
-
精准的组件更新跟踪
-
避免重复组件冲突
-
调试时组件识别
- 双模式spec合并策略 :

典型子组件实现
以 Marker 组件为例:
// 实现 parseSpec 方法
class MarkPoint extends BaseComponent {
static parseSpec(props: MarkProps) {
return {
specName: 'markPoint', // 对应 spec 中的字段名
isSingle: false, // 允许多个 MarkPoint 组件
spec: {
type: props.type,
style: props.style
}
};
}
}
创建图表
const createChart = (props: Props) => {
// 1. 实例化图表(利用传入的图表构造器)
const cs = new props.vchartConstrouctor(
parseSpec(props), // 合并后的图表spec
{
...props.options, // 透传图表配置
onError: props.onError, // 异常处理回调
autoFit: true, // 开启自动尺寸适配
dom: props.container // 绑定 DOM 容器
}
);
// 2. 更新上下文引用
chartContext.current = { ...chartContext.current, chart: cs };
// 3. 重置卸载标记
isUnmount.current = false;
};
spec解析
const parseSpec = (props: Props) => {
// 决策逻辑:优先使用全量 spec 配置
let spec: ISpec = undefined;
// 全量 spec 模式(直接使用传入的 spec)
if (hasSpec && props.spec) {
spec = props.spec;
}
// 声明式组件模式(合并 props 和子组件生成的 spec)
else {
spec = {
...prevSpec.current, // 来自组件 props 的配置
...specFromChildren.current // 来自子组件解析的配置
} as ISpec;
}
// 自定义 tooltip 处理(React 组件与 VChart 的桥接)
const tooltipSpec = initCustomTooltip(setTooltipNode, props, spec.tooltip);
if (tooltipSpec) {
spec.tooltip = tooltipSpec; // 覆盖默认 tooltip 配置
}
return spec;
};
渲染图表
const renderChart = () => {
if (chartContext.current.chart) {
chartContext.current.chart.renderSync({
reuse: false
});
handleChartRender();
}
};
通过chartContext图表上下文拿到刚才挂载好的实例,并调用实例的renderSync方法渲染图表。