引言 ant.design/docs/blog/c… 因为看了这篇文章,组件级别cssInjs,性能和各方面都是领先,因此出于好奇深入了解了antd的主题色实现原理。
1. 主题色功能基本介绍
初步功能介绍:用过antd的朋友都知道antd 5.x主题色是基于cssInjs实现的。使用的方式如下:
(1. 设置token变量全局修改主题色
import { Button, ConfigProvider, Space, theme } from 'antd';
import React from 'react';
const App: React.FC = () => {
const { token } = theme.useToken();
return (
<ConfigProvider
theme={{
token: {
// Seed Token,影响范围大
colorPrimary: '#00b96b',
borderRadius: 2,
// 派生变量,影响范围小
colorBgContainer: '#f6ffed',
components: {
// 将 Popover 的文本颜色设为白色
Popover: { colorText: token.colorTextLightSolid },
// 将 Checkbox 的文本颜色设为白色,主色设为更强一级的颜色
Checkbox: {
colorPrimary: token['blue-7'],
colorText: token.colorTextLightSolid
},
// 将 Button 的颜色设为更强一级的颜色
Button: { colorPrimary: token['blue-7'], algorithm: true, *// 启用算法(继承全局的主题色) 或algorithm: ()=>{}(自定义主题色函数) *}
}
},
}}
>
<Space>
<Button type="primary">Primary</Button>
<Button>Default</Button>
</Space>
</ConfigProvider>
)}
(2. 设置官方主题或自定义主题 通过修改算法可以快速生成风格迥异的主题,我们默认提供三套预设算法,分别是:
- 默认算法
theme.defaultAlgorithm - 暗色算法
theme.darkAlgorithm - 紧凑算法
theme.compactAlgorithm
你可以通过 theme 中的 algorithm 属性来切换算法,并且支持配置多种算法,将会依次生效。
官方给的主题
import React from 'react';
import { Button, ConfigProvider, Input, Space, theme } from 'antd';
const App: React.FC = () => (
<ConfigProvider
theme={{
// 1. 单独使用暗色算法
algorithm: theme.darkAlgorithm,
// 2. 组合使用暗色算法与紧凑算法
// algorithm: [theme.darkAlgorithm, theme.compactAlgorithm],
}}
>
<Space>
<Input placeholder="Please Input" />
<Button type="primary">Submit</Button>
</Space>
</ConfigProvider>
);
export default App;
自定义主题
import { theme } from 'antd';
import type { MappingAlgorithm } from 'antd/es/config-provider/context';
// 定义 studio 暗色模式算法
export const studioDarkAlgorithm: MappingAlgorithm = (seedToken, mapToken) => {
// 使用 antd 默认的暗色算法生成基础token,这样其他不需要定制的部分则保持原样
const baseToken = theme.darkAlgorithm(seedToken, mapToken);
return {
...baseToken,
colorBgLayout: '#20252b', // Layout 背景色
colorBgContainer: '#282c34', // 组件容器背景色
colorBgElevated: '#32363e', // 悬浮容器背景色
};
};
// 在应用中集成
const Container =()=>{
return (
<ConfigProvider theme={{ algorithm: studioDarkAlgorithm }}>
...
</ConfigProvider>
)
}
2. 实现流程概览
数据注入到样式生成流程:
2.1 provider数据注入
2.1.1 provider嵌套示意图
2.1.2 provider核心代码
// ================================ Dynamic theme ================================
const memoTheme = React.useMemo(() => {
const { algorithm, token, components, cssVar, ...rest } = mergedTheme || {};
const themeObj =
algorithm && (!Array.isArray(algorithm) || algorithm.length > 0)
? createTheme(algorithm)
: defaultTheme;
const parsedComponents: any = {};
Object.entries(components || {}).forEach(([componentName, componentToken]) => {
const parsedToken: typeof componentToken & { theme?: typeof defaultTheme } = {
...componentToken,
};
if ('algorithm' in parsedToken) {
if (parsedToken.algorithm === true) {
parsedToken.theme = themeObj;
} else if (
Array.isArray(parsedToken.algorithm) ||
typeof parsedToken.algorithm === 'function'
) {
parsedToken.theme = createTheme(parsedToken.algorithm);
}
delete parsedToken.algorithm;
}
parsedComponents[componentName] = parsedToken;
});
const mergedToken = {
...defaultSeedToken,
...token,
};
return {
...rest,
theme: themeObj,
token: mergedToken,
components: parsedComponents,
override: {
override: mergedToken,
...parsedComponents,
},
cssVar: cssVar as Exclude<ThemeConfig['cssVar'], boolean>,
};
}, [mergedTheme]);
if (theme) {
childNode = (
<DesignTokenContext.Provider value={memoTheme}>{childNode}</DesignTokenContext.Provider>
);
}
provider注入总结
antd在provider注入阶段, 用户在给configProvider传入主题相关配置后,内部的处理逻辑:
-
注入token(这里的token为seedToken,说明见附录1)
注意源码中token为mergedTheme,是因为在context层,antd支持嵌套Provider,这里mergedTheme为聚合后的token,这里将用户传入的token和默认的seedToken做了聚合。 -
生成主题对象themeObj
algorith用户传入是一个函数, 通过createTheme转换函数为themeObj themeObj结构如下:{id: 1, derivative: [theme.darkAlgorithm], getDerivativeToken:()=> {}} -
components: 目的是将用户如传入的
Input: { algorithm: true, colorBgContainer: '#f5f5f5', } 转换为 Input: { colorBgContainer: '#f5f5f5', // 作用:确保该组件严格使用全局配置的算法。(组件如果自身传入了算法也支持组件级算法函数) theme: themeObj },
2.2 context消费主题数据
数据消费到实际看到页面生效主题色:
消费数据:useToken.tsx
const {
token: rootDesignToken,
hashed,
theme,
override,
cssVar,
} = React.useContext(DesignTokenContext);
const salt = `${version}-${hashed || ''}`;
const mergedTheme = theme || defaultTheme;
const [token, hashId, realToken] = useCacheToken<GlobalToken, SeedToken>(
mergedTheme,
[defaultSeedToken, rootDesignToken],
{
salt,
override,
getComputedToken,
// formatToken will not be consumed after 1.15.0 with getComputedToken.
// But token will break if @ant-design/cssinjs is under 1.15.0 without it
formatToken,
cssVar: cssVar && {
prefix: cssVar.prefix,
key: cssVar.key,
unitless,
ignore,
preserve,
},
},
);
return [mergedTheme, realToken, hashed ? hashId : '', token, cssVar];
useToken内部实现步骤
插入样式到style标签节点:useStyleRegister
useStyleRegister插入样式逻辑实现步骤
数据消费context生成样式总结
- 第一步 通过useToken生成全局主题token变量(全局缓存,只要配置token和theme的算法没有改变,每次render都是缓存token对象)
- 第二步 每个组件比如Button、Card都从全局token变量中获取做为自身的样式变量,生成带token变量的css对象。
- 第三步 将带token变量的css对象注册到style标签中,这里有一个点注意如果开启了cssVar,那么组件也有组件级的cssVar,例如 fontSize: token.contrliOutlineWidth / 2 那么组件算出来就是一个动态的值,用户开启了cssVar,要将组件自身的css属性转换为cssVar,同时插入style组件级的css变量,比如--ant-btn-color前缀 全局变量--ant前缀
3. antd源码之旅
上面内容为阅读源码必看的概括,下面会详细的以antd源码组件的Button为案例,详细讲解整个组件的主题色是如何实现和应用的。
3.1 启动项目
拉取antd项目+antd/cssInjs项目, 本地实现调试运行,将antd的npm包依赖本地的antd/cssInjs,项目本地跑起来。
编写测试demo 手动控制整个主题色渲染和卸载,本次主题的此时以Button按钮为案例
/* eslint-disable */
import React, { useState } from 'react';
import { Button, ConfigProvider, theme } from 'antd';
const PureDemo = () => {
const [show, setShow] = useState(false);
console.log('PureDemo Rendered, show:', show);
return (
<div style={{ padding: 50 }}>
{/* 这是一个原生按钮,用来控制渲染时机 */}
<button
onClick={() => setShow(prev => !prev)}
style={{ marginBottom: 20, padding: '8px 16px', fontSize: 16 }}
>
{show ? '销毁 Antd 组件' : '点击渲染 Antd 组件 (触发 useStyle)'}
</button>
{show && (
<div style={{ border: '1px dashed #ccc', padding: 20 }}>
{/* 这里才是 Antd 组件的第一次挂载 */}
<ConfigProvider theme={{ algorithm: theme.defaultAlgorithm }}>
<div style={{ display: 'flex', gap: 16 }}>
<Button type="primary">Primary Button</Button>
<Button>Default Button</Button>
</div>
</ConfigProvider>
</div>
)}
</div>
);
};
export default PureDemo;
效果如下:
3.1 Button组件主题样式渲染
以下的代码片段均来自antd5.x版本源码,提取的都是关键代码。
// 第一步 渲染Button组件可以看到Button是被wrapCSSVar包裹
import useStyle from './style';
const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls);
const classes = classNames(
prefixCls,
hashId, // useStyle获取 全局主题色生成后,产生的唯一id, 同一主题下所有组件的hashId一样
cssVarCls, //useStyle上获取 开启css变量模式才有
{
[`${prefixCls}-${shape}`]: shape !== 'default' && shape,
[`${prefixCls}-${type}`]: type,
[`${prefixCls}-${sizeCls}`]: sizeCls,
[`${prefixCls}-icon-only`]: !children && children !== 0 && !!iconType,
[`${prefixCls}-background-ghost`]: ghost && !isUnBorderedButtonType(type),
[`${prefixCls}-loading`]: innerLoading,
[`${prefixCls}-two-chinese-chars`]: hasTwoCNChar && autoInsertSpace && !innerLoading,
[`${prefixCls}-block`]: block,
[`${prefixCls}-dangerous`]: !!danger,
[`${prefixCls}-rtl`]: direction === 'rtl',
},
compactItemClassnames,
className,
rootClassName,
button?.className,
);
const buttonNode = (
<button
{...rest}
type={htmlType}
className={classes}
style={fullStyle}
onClick={handleClick}
disabled={mergedDisabled}
ref={buttonRef as React.Ref<HTMLButtonElement>}
>
{iconNode}
{kids}
{/* Styles: compact */}
{compactItemClassnames && <CompactCmp key="compact" prefixCls={prefixCls} />}
</button>
);
return wrapCSSVar(buttonNode);
// 第二步 style.tsx
// (1) useStyle是通过genStyleHooks 工厂hooks函数生成的
export default genStyleHooks(
'Button', // 组件的唯一标识
(token) => {
const buttonToken = prepareToken(token);
return [
// Shared
genSharedButtonStyle(buttonToken),
// Size
genSizeSmallButtonStyle(buttonToken),
genSizeBaseButtonStyle(buttonToken),
genSizeLargeButtonStyle(buttonToken),
// Block
genBlockButtonStyle(buttonToken),
// Group (type, ghost, danger, loading)
genTypeButtonStyle(buttonToken),
// Button Group
genGroupStyle(buttonToken),
];
}, // 生成button相关的css对象最终会转换为style
prepareComponentToken, //默认Button的token变量
{
unitless: {
fontWeight: true, // 那些属性禁止加变量,因为数字一般会自动加px
},
},
);
//(2) genStyleHooks
export const genStyleHooks = <C extends OverrideComponent>(
component: C | [C, string],
styleFn: GenStyleFn<C>,
getDefaultToken?: GetDefaultToken<C>,
options?: {
resetStyle?: boolean;
deprecatedTokens?: [ComponentTokenKey<C>, ComponentTokenKey<C>][];
/**
* Chance to format component token with user input.
* Useful when need calculated token as css variables.
*/
format?: FormatComponentToken<C>;
/**
* Component tokens that do not need unit.
*/
unitless?: {
[key in ComponentTokenKey<C>]: boolean;
};
/**
* Only use component style in client side. Ignore in SSR.
*/
clientOnly?: boolean;
/**
* Set order of component style.
* @default -999
*/
order?: number;
/**
* Whether generate styles
* @default true
*/
injectStyle?: boolean;
},
) => {
const useStyle = genComponentStyleHook(component, styleFn, getDefaultToken, options);
const useCSSVar = genCSSVarRegister(
Array.isArray(component) ? component[0] : component,
getDefaultToken,
options,
);
return (prefixCls: string, rootCls: string = prefixCls) => {
const [, hashId] = useStyle(prefixCls); // 作用是生成token + 将token转换为button的样式字符串, 插入到<Style>{ .ant-btn{xxx} }</Style>
const [wrapCSSVar, cssVarCls] = useCSSVar(rootCls); //组件级的css变量注册 类似-antd-button-font-weight: 400
return [wrapCSSVar, hashId, cssVarCls] as const;
}; //这里是用户实际使用hooks
};
genComponentStyleHook的分析 这个hook作用如下
- Button组件token的生成
- Button的样式插入到标签
export default function genComponentStyleHook<C extends OverrideComponent>(
componentName: C | [C, string],
styleFn: GenStyleFn<C>,
getDefaultToken?:
| null
| OverrideTokenWithoutDerivative[C]
| ((token: GlobalToken) => OverrideTokenWithoutDerivative[C]),
options: {
resetStyle?: boolean;
// Deprecated token key map [["oldTokenKey", "newTokenKey"], ["oldTokenKey", "newTokenKey"]]
deprecatedTokens?: [ComponentTokenKey<C>, ComponentTokenKey<C>][];
/**
* Only use component style in client side. Ignore in SSR.
*/
clientOnly?: boolean;
/**
* Set order of component style. Default is -999.
*/
order?: number;
format?: FormatComponentToken<C>;
injectStyle?: boolean;
} = {},
) {
const cells = (Array.isArray(componentName) ? componentName : [componentName, componentName]) as [
C,
string,
];
const [component] = cells;
const concatComponent = cells.join('-');
return (prefixCls: string): UseComponentStyleResult => {
const [theme, realToken, hashId, token, cssVar] = useToken(); //获取全局主题变量
const { getPrefixCls, iconPrefixCls, csp } = useContext(ConfigContext);
const rootPrefixCls = getPrefixCls();
const type = cssVar ? 'css' : 'js';
const calc = genCalc(type);
const { max, min } = genMaxMin(type);
// Shared config
const sharedConfig: Omit<Parameters<typeof useStyleRegister>[0], 'path'> = {
theme,
token,
hashId,
nonce: () => csp?.nonce!,
clientOnly: options.clientOnly,
// antd is always at top of styles
order: options.order || -999,
};
// Generate style for all a tags in antd component.
// 根据token生成全局统一Style标签(只会执行一次)
useStyleRegister(
{ ...sharedConfig, clientOnly: false, path: ['Shared', rootPrefixCls] },
() => [
{
// Link
'&': genLinkStyle(token),
},
],
);
// 组件级token和Style注册
const wrapSSR = useStyleRegister(
{ ...sharedConfig, path: [concatComponent, prefixCls, iconPrefixCls] },
() => {
if (options.injectStyle === false) {
return [];
}
const { token: proxyToken, flush } = statisticToken(token);
const defaultComponentToken = getDefaultComponentToken(
component,
realToken,
getDefaultToken,
);
const componentCls = `.${prefixCls}`;
const componentToken = getComponentToken(component, realToken, defaultComponentToken, {
deprecatedTokens: options.deprecatedTokens,
format: options.format,
});
if (cssVar) {
Object.keys(defaultComponentToken).forEach((key) => {
defaultComponentToken[key] = `var(${token2CSSVar(
key,
getCompVarPrefix(component, cssVar.prefix),
)})`;
});
}
const mergedToken = mergeToken<
TokenWithCommonCls<GlobalTokenWithComponent<OverrideComponent>>
>(
proxyToken,
{
componentCls,
prefixCls,
iconCls: `.${iconPrefixCls}`,
antCls: `.${rootPrefixCls}`,
calc,
max,
min,
},
cssVar ? defaultComponentToken : componentToken,
);
const styleInterpolation = styleFn(mergedToken as unknown as FullToken<C>, {
hashId,
prefixCls,
rootPrefixCls,
iconPrefixCls,
});
flush(component, componentToken);
return [
options.resetStyle === false ? null : genCommonStyle(mergedToken, prefixCls),
styleInterpolation,
];
},
);
}
}
genCSSVarRegister主要为了生成css变量插入到Style标签。(组件级的)
const genCSSVarRegister = <C extends OverrideComponent>(
component: C,
getDefaultToken?: GetDefaultToken<C>,
options?: {
unitless?: {
[key in ComponentTokenKey<C>]: boolean;
};
deprecatedTokens?: [ComponentTokenKey<C>, ComponentTokenKey<C>][];
format?: FormatComponentToken<C>;
injectStyle?: boolean;
},
) => {
function prefixToken(key: string) {
return `${component}${key.slice(0, 1).toUpperCase()}${key.slice(1)}`;
}
const { unitless: originUnitless = {}, injectStyle = true } = options ?? {};
const compUnitless: any = {
[prefixToken('zIndexPopup')]: true,
};
Object.keys(originUnitless).forEach((key: keyof ComponentTokenKey<C>) => {
compUnitless[prefixToken(key)] = originUnitless[key];
});
const CSSVarRegister: FC<CSSVarRegisterProps> = ({ rootCls, cssVar }) => {
const [, realToken] = useToken();
useCSSVarRegister(
{
path: [component],
prefix: cssVar.prefix,
key: cssVar?.key!,
unitless: {
...unitless,
...compUnitless,
},
ignore,
token: realToken,
scope: rootCls,
},
() => {
const defaultToken = getDefaultComponentToken(component, realToken, getDefaultToken);
const componentToken = getComponentToken(component, realToken, defaultToken, {
format: options?.format,
deprecatedTokens: options?.deprecatedTokens,
});
Object.keys(defaultToken).forEach((key) => {
componentToken[prefixToken(key)] = componentToken[key];
delete componentToken[key];
});
return componentToken;
},
);
return null;
};
const useCSSVar = (rootCls: string) => {
const [, , , , cssVar] = useToken();
return [
(node: ReactElement): ReactElement =>
injectStyle && cssVar ? (
<>
<CSSVarRegister rootCls={rootCls} cssVar={cssVar} component={component} />
{node}
</>
) : (
node
),
cssVar?.key,
] as const;
};
return useCSSVar;
};
以上就是antd主题色生成和实现的主流程和实现原理
拓展篇
antd主题色的核心hook useToken和useStyleRegister
useToken详解
export function useToken(): [
Theme<any, any>,
DerivativeToken,
string,
string | undefined,
] {
const {
token: rootDesignToken = {},
hashed,
cssVar,
} = React.useContext(DesignTokenContext); // 获取全局antd configProvider中配置的token、hashed、cssVar
const theme = React.useContext(ThemeContext); //主题算法 algorithm: [theme.darkAlgorithm, theme.compactAlgorithm],
const [token, hashId] = useCacheToken<DerivativeToken, DesignToken>(
theme,
[defaultDesignToken, rootDesignToken],
{
salt: typeof hashed === 'string' ? hashed : '',
cssVar: cssVar && {
prefix: 'rc',
key: cssVar.key,
unitless: {
lineHeight: true,
},
},
},
);
return [theme, token, hashed ? hashId : '', cssVar?.key];
} // 该 Hook 用于基于 Theme 和基础 DesignToken 计算并缓存最终的 Token 对象与唯一 Hash ID,同时支持 CSS 变量的生成配置。
useToken中ThemeContext详解
- 创建主题对象
// 创建主题对象
createTheme(derivative) // 返回值 {themeId,
// 模拟推导过程 类似[theme.defaultAlgorithm theme.darkAlgorithm theme.compactAlgorithm]
function derivative(designToken: DesignToken): DerivativeToken {
return {
...designToken,
primaryColorDisabled: new TinyColor(designToken.primaryColor)
.setAlpha(0.5)
.toString(),
};
}
- createTheme的实现
const cacheThemes = new ThemeCache(); // 创建全局主题缓存
/**
* Same as new Theme, but will always return same one if `derivative` not changed.
*/
export default function createTheme<
DesignToken extends TokenType,
DerivativeToken extends TokenType,
>(
derivatives:
| DerivativeFunc<DesignToken, DerivativeToken>[]
| DerivativeFunc<DesignToken, DerivativeToken>,
) {
const derivativeArr = Array.isArray(derivatives)
? derivatives
: [derivatives];
// Create new theme if not exist
if (!cacheThemes.has(derivativeArr)) {
cacheThemes.set(derivativeArr, new Theme(derivativeArr));
}
// Get theme from cache and return
return cacheThemes.get(derivativeArr)!; // 返回new Theme(derivativeArr)的缓存
}
- Theme类实现
import { warning } from 'rc-util/lib/warning';
import type { DerivativeFunc, TokenType } from './interface';
let uuid = 0;
/**
* Theme with algorithms to derive tokens from design tokens.
* Use `createTheme` first which will help to manage the theme instance cache.
*/
export default class Theme<
DesignToken extends TokenType,
DerivativeToken extends TokenType,
> {
private derivatives: DerivativeFunc<DesignToken, DerivativeToken>[];
public readonly id: number;
constructor(
derivatives:
| DerivativeFunc<DesignToken, DerivativeToken>
| DerivativeFunc<DesignToken, DerivativeToken>[],
) {
this.derivatives = Array.isArray(derivatives) ? derivatives : [derivatives];
this.id = uuid;
if (derivatives.length === 0) {
warning(
derivatives.length > 0,
'[Ant Design CSS-in-JS] Theme should have at least one derivative function.',
);
}
uuid += 1;
}
getDerivativeToken(token: DesignToken): DerivativeToken {
return this.derivatives.reduce<DerivativeToken>(
(result, derivative) => derivative(token, result),
undefined as any,
);
}
}
createTheme 缓存策略详解
const cacheThemes = new ThemeCache(); // 创建全局主题缓存
内部维护了一个全局缓存池,通过以下流程确保单例复用:
2. 缓存结构设计
ThemeCache 是一个专门为了解决 “多层级缓存键(Key)” 和 “LRU(最近最少使用)淘汰策略” 而设计的缓存类。
用户最开始 传入假设我们存 [FnA, FnB] -> Theme1 和 [FnA, FnC] -> Theme2,结构如下:
Map (Root)
└── FnA
└── { map: Map }
├── FnB
│ └── { value: [Theme1, timestamp] }
└── FnC
└── { value: [Theme2, timestamp] }
这是典型的 Trie 树(字典树)
为什么这么做?因为 JS 的 Map 只能用单一对象做 Key。如果直接用数组 [FnA, FnB] 做 Key,除非引用完全一致,否则取不到值。用 Trie 树结构,我们可以逐层匹配数组中的每个函数,从而精确找到对应的缓存。 比如:
用 Trie 树结构,我们可以逐层匹配数组中的每个函数,从而精确找到对应的缓存。
const App: React.FC = () => ( <ConfigProvider theme={{ algorithm: [theme.darkAlgorithm, theme.compactAlgorithm], }} >
<Space>
‘<Input placeholder="Please Input" />
<Button type="primary">Submit</Button>
</Space>
</ConfigProvider> );
那么每次数组对象都会是新的引用,所以不能用数组直接做为map的key,同时antd建议主题色的算法写到固定文件中, 缓存返回的结果是什么 缓存了一个主题对象带id 同时带处理多主题的合并方法。
<ConfigProvider
theme={{ // 1. 单独使用暗色算法 algorithm: theme.darkAlgorithm, // 2. 组合使用暗色算法与紧凑算法 //
algorithm: [theme.darkAlgorithm, theme.compactAlgorithm], }} >
<Space>
<Input placeholder="Please Input" />
<Button type="primary">Submit</Button>
</Space>
</ConfigProvider>
这里有个问题 theme.darkAlgorithm 一定是相同的引用,不然整个缓存theme.id会更新 整个缓存都会被击穿。
2. 主要功能点
A. 存值 (set) 路径构建:遍历传入的函数数组 derivativeOption,在 Trie 树中逐层向下找。如果没有节点,就 new Map() 创建新节点。
存值:走到数组最后一个函数时,把 value(Theme 实例)存进去。 淘汰机制:在存之前,检查缓存大小是否超标(MAX_CACHE_SIZE + OFFSET)。
如果超了,触发 LRU 淘汰:遍历所有 Key,找到 callTimes(访问时间戳)最小的那个 Key,把它删掉。
B. 取值 (get) 也是逐层遍历 Trie 树。
更新时间戳:每次成功 get,都会更新该节点的 callTimes(把它设为当前最新的自增 ID)。
这保证了热点数据不会被淘汰。
C. 删除 (delete) 这也是个递归过程 (deleteByPath)。
清理空枝:如果删掉一个叶子节点后,父节点变空了(既没有 value 也没有子 map),那么父节点也应该顺手删掉,防止内存泄漏。
3. LRU 策略细节 cacheCallTimes: 全局自增计数器,模拟“逻辑时间”。
每次 get 或 set,对应数据的 callTimes 就变成当前最大值。
淘汰时,遍历 this.keys,比较谁的 callTimes 最小,谁就是最久没被用过的,踢掉它。
我们用一个生动的例子来精准拆解这个缓存思路。
### 核心痛点
我们要缓存 Theme 对象。但是,决定一个 Theme 是否相同的,不是一个简单的字符串 ID,而是一组“处理函数”的组合。
比如:
- 组合 A:[基础算法, 暗黑模式算法]
<!---->
- 组合 B:[基础算法, 紧凑模式算法]
<!---->
- 组合 C:[基础算法]
难点:JS 的 Map 只能用单一引用做 Key。你不能直接 map.set([funcA, funcB], value),因为每次传进来的 [funcA, funcB] 都是个新数组,内存地址不同,Map 认为是不同的 Key。
### 解决方案:双管齐下
为了解决这个问题,代码用了两个数据结构配合工作:
#### 1. 存数据的 cache (多层级 Map) —— 解决“怎么找”
为了匹配数组 [funcA, funcB],我们把它变成了一棵树(类似文件目录路径)。
- 存:像建文件夹一样。
<!---->
- 来一个 [A, B]:先找文件夹 A,没有就建;进入 A,再找文件夹 B,没有就建;在 B 里放文件 Theme1。
<!---->
- 取:像找文件一样。
<!---->
- 来一个 [A, B]:先进目录 A,再进目录 B,拿到 Theme1。
这样就绕开了“数组引用不同”的问题,因为我们比对的是数组里的每一个函数引用,函数引用是稳定的。
#### 2. 记目录的 keys (数组) —— 解决“怎么删”
既然是缓存,就不能只进不出。如果存了几千个主题,内存就爆了。我们需要一个LRU(最近最少使用)策略:删掉最久没用过的那个。但是,那棵“多层级 Map 树”太深了,想知道里面到底存了哪几个 Theme,得递归遍历整棵树,效率极低。keys 就是那个“记账本”。每当你成功存入一个新 Theme(比如 [A, B]),我就在 keys 这个小本本上记一笔:> "我有存过 [A, B] 哦。"当缓存满了(超过 20 个)要删人时:
1. 我不去爬那棵复杂的树。
<!---->
1. 我直接翻 keys 这个小本本,上面列出了所有存过的组合。
<!---->
1. 我拿着这些组合去树里查一下它们各自的“最后访问时间”。
<!---->
1. 发现 [A, C] 是上个世纪访问的,最老。
<!---->
1. 从树里删掉 [A, C] 对应的节点。
<!---->
1. 从小本本 keys 里划掉 [A, C]。
* * *
### 总结一下它的精确思路
1. 分层存储 (Trie):把“函数数组”拆解,用嵌套 Map 逐层匹配,解决了“数组无法做 Key”的问题。
<!---->
1. 扁平索引 (Keys):为了避免遍历复杂的树结构,额外维护一个数组 keys 记录所有存在的 Key,专门用于快速计算淘汰目标。
<!---->
1. 引用计数 (CallTime):每个节点存一个自增数字,每次访问就更新成最大值。谁的数字最小,谁就是最久没用的(LRU 目标)。
这是一个典型的空间换时间 + 索引优化的缓存设计。
useToken中useCacheToken的详解
const [token, hashId] = useCacheToken<DerivativeToken, DesignToken>(
theme,
[defaultDesignToken, rootDesignToken],
{
salt: typeof hashed === 'string' ? hashed : '',
cssVar: cssVar && {
prefix: 'rc',
key: cssVar.key,
unitless: {
lineHeight: true,
},
},
},
);
// ===》useCacheToken 《=====
export default function useCacheToken<
DerivativeToken = object,
DesignToken = DerivativeToken,
>(
theme: Theme<any, any>,
tokens: Partial<DesignToken>[],
option: Option<DerivativeToken, DesignToken> = {},
): TokenCacheValue<DerivativeToken> {
const {
cache: { instanceId },
container,
} = useContext(StyleContext);
const {
salt = '',
override = EMPTY_OVERRIDE,
formatToken,
getComputedToken: compute,
cssVar,
} = option;
// Basic - We do basic cache here
//
const mergedToken = memoResult(() => Object.assign({}, ...tokens), tokens);
const tokenStr = flattenToken(mergedToken); // token对象转换为字符串
const overrideTokenStr = flattenToken(override); // 强制覆盖token对象转为字符串(override作用在于多层前套主题的provider)
const cssVarStr = cssVar ? flattenToken(cssVar) : ''; //css变量配置对象转换为字符串
const cachedToken = useGlobalCache<TokenCacheValue<DerivativeToken>>(
TOKEN_PREFIX,
[salt, theme.id, tokenStr, overrideTokenStr, cssVarStr], // 作为useEffect的依赖项,有变动执行缓存
() => {
// 将token和主题色算法以及覆盖主题色变量生成主题对象
let mergedDerivativeToken = compute
? compute(mergedToken, override, theme)
: getComputedToken(mergedToken, override, theme, formatToken);
// 如果开启了css变量 替换token中的值为css变量
const actualToken = { ...mergedDerivativeToken };
let cssVarsStr = '';
if (!!cssVar) {
[mergedDerivativeToken, cssVarsStr] = transformToken(
mergedDerivativeToken,
cssVar.key!,
{
prefix: cssVar.prefix,
ignore: cssVar.ignore,
unitless: cssVar.unitless,
preserve: cssVar.preserve,
},
);
}
// Optimize for `useStyleRegister` performance
// 优化useStyleRegister性能 useStyleRegister会根据 mergedDerivativeToken._token2key 的变化决定是否注册Style的属性。
const tokenKey = token2key(mergedDerivativeToken, salt);
mergedDerivativeToken._tokenKey = tokenKey;
actualToken._tokenKey = token2key(actualToken, salt);
const themeKey = cssVar?.key ?? tokenKey;
mergedDerivativeToken._themeKey = themeKey;
recordCleanToken(themeKey); // 这里记录插入css变量的主题key 在切换主题后
const hashId = `${hashPrefix}-${hash(tokenKey)}`;
mergedDerivativeToken._hashId = hashId; // Not used
return [
mergedDerivativeToken,
hashId,
actualToken,
cssVarsStr,
cssVar?.key || '',
];
},
(cache) => {
// Remove token will remove all related style
cleanTokenStyle(cache[0]._themeKey, instanceId); // 组件卸载的时候, 如果使用该主题变量的组件的数量为0, 移除style标签中的css变量。
},
([token, , , cssVarsStr]) => {
if (cssVar && cssVarsStr) {
const style = updateCSS(
cssVarsStr,
hash(`css-variables-${token._themeKey}`),
{
mark: ATTR_MARK,
prepend: 'queue',
attachTo: container,
priority: -999,
},
); // 将css变量插入到style标签节点。
(style as any)[CSS_IN_JS_INSTANCE] = instanceId;
// Used for `useCacheToken` to remove on batch when token removed
style.setAttribute(ATTR_TOKEN, token._themeKey);
}
},
);
return cachedToken;
}
核心useGlobalCache讲解
- 图解
import * as React from 'react';
import { pathKey, type KeyType } from '../Cache';
import StyleContext from '../StyleContext';
import useCompatibleInsertionEffect from './useCompatibleInsertionEffect';
import useEffectCleanupRegister from './useEffectCleanupRegister';
import useHMR from './useHMR';
export type ExtractStyle<CacheValue> = (
cache: CacheValue,
effectStyles: Record<string, boolean>,
options?: {
plain?: boolean;
},
) => [order: number, styleId: string, style: string] | null;
export default function useGlobalCache<CacheType>(
prefix: string,
keyPath: KeyType[],
cacheFn: () => CacheType,
onCacheRemove?: (cache: CacheType, fromHMR: boolean) => void,
// Add additional effect trigger by `useInsertionEffect`
onCacheEffect?: (cachedValue: CacheType) => void,
): CacheType {
const { cache: globalCache } = React.useContext(StyleContext);
const fullPath = [prefix, ...keyPath];
const fullPathStr = pathKey(fullPath);
const register = useEffectCleanupRegister([fullPathStr]);
const HMRUpdate = useHMR();
type UpdaterArgs = [times: number, cache: CacheType];
const buildCache = (updater?: (data: UpdaterArgs) => UpdaterArgs) => {
globalCache.opUpdate(fullPathStr, (prevCache) => {
const [times = 0, cache] = prevCache || [undefined, undefined];
// HMR should always ignore cache since developer may change it
let tmpCache = cache;
if (process.env.NODE_ENV !== 'production' && cache && HMRUpdate) {
onCacheRemove?.(tmpCache, HMRUpdate);
tmpCache = null;
}
const mergedCache = tmpCache || cacheFn();
const data: UpdaterArgs = [times, mergedCache];
// Call updater if need additional logic
return updater ? updater(data) : data;
});
};
// Create cache
React.useMemo(
() => {
buildCache();
},
/* eslint-disable react-hooks/exhaustive-deps */
[fullPathStr],
/* eslint-enable */
);
let cacheEntity = globalCache.opGet(fullPathStr);
// HMR clean the cache but not trigger `useMemo` again
// Let's fallback of this
// ref https://github.com/ant-design/cssinjs/issues/127
if (process.env.NODE_ENV !== 'production' && !cacheEntity) {
buildCache();
cacheEntity = globalCache.opGet(fullPathStr);
}
const cacheContent = cacheEntity![1];
// Remove if no need anymore
useCompatibleInsertionEffect(
() => {
onCacheEffect?.(cacheContent);
},
(polyfill) => {
// It's bad to call build again in effect.
// But we have to do this since StrictMode will call effect twice
// which will clear cache on the first time.
buildCache(([times, cache]) => {
if (polyfill && times === 0) {
onCacheEffect?.(cacheContent);
}
return [times + 1, cache];
});
return () => {
globalCache.opUpdate(fullPathStr, (prevCache) => {
const [times = 0, cache] = prevCache || [];
const nextCount = times - 1;
if (nextCount === 0) {
// Always remove styles in useEffect callback
register(() => {
// With polyfill, registered callback will always be called synchronously
// But without polyfill, it will be called in effect clean up,
// And by that time this cache is cleaned up.
if (polyfill || !globalCache.opGet(fullPathStr)) {
onCacheRemove?.(cache, false);
}
});
return null;
}
return [times - 1, cache];
});
};
},
[fullPathStr],
);
return cacheContent;
}
// Create cache 这里之所以用useMemo是为了优先构建缓存和计算缓存tokenkeys数量 最后在卸载时候才能精准计算是否取消挂载
React.useMemo(
() => {
buildCache();
},
/* eslint-disable react-hooks/exhaustive-deps */
[fullPathStr],
/* eslint-enable */
);
useMemo的触发时机
useStyleRegister详解
export default function useStyleRegister(
info: {
theme: Theme<any, any>;
token: any;
path: string[];
hashId?: string;
layer?: LayerConfig;
nonce?: string | (() => string);
clientOnly?: boolean;
/**
* Tell cssinjs the insert order of style.
* It's useful when you need to insert style
* before other style to overwrite for the same selector priority.
*/
order?: number;
},
styleFn: () => CSSInterpolation,
) {
console.log('kls')
const { token, path, hashId, layer, nonce, clientOnly, order = 0 } = info;
const {
autoClear,
mock,
defaultCache,
hashPriority,
container,
ssrInline,
transformers,
linters,
cache,
layer: enableLayer,
} = React.useContext(StyleContext);
const tokenKey = token._tokenKey as string;
const fullPath = [tokenKey];
if (enableLayer) {
fullPath.push('layer');
}
fullPath.push(...path);
//[assas%token, 'ant-btn']
// Check if need insert style
let isMergedClientSide = isClientSide;
if (process.env.NODE_ENV !== 'production' && mock !== undefined) {
isMergedClientSide = mock === 'client';
}
const [cachedStyleStr, cachedTokenKey, cachedStyleId] =
useGlobalCache<StyleCacheValue>(
STYLE_PREFIX,
fullPath,
// Create cache if needed
() => {
const cachePath = fullPath.join('|');
// Get style from SSR inline style directly
if (existPath(cachePath)) {
console.log(cachePath,'缓存路径观察呢')
const [inlineCacheStyleStr, styleHash] = getStyleAndHash(cachePath);
if (inlineCacheStyleStr) {
return [
inlineCacheStyleStr,
tokenKey,
styleHash,
{},
clientOnly,
order,
];
}
}
// Generate style
const styleObj = styleFn();
const [parsedStyle, effectStyle] = parseStyle(styleObj, {
hashId,
hashPriority,
layer: enableLayer ? layer : undefined,
path: path.join('-'),
transformers,
linters,
});
const styleStr = normalizeStyle(parsedStyle);
const styleId = uniqueHash(fullPath, styleStr);
return [styleStr, tokenKey, styleId, effectStyle, clientOnly, order];
},
// Remove cache if no need
([, , styleId], fromHMR) => {
if ((fromHMR || autoClear) && isClientSide) {
removeCSS(styleId, { mark: ATTR_MARK, attachTo: container });
}
},
// Effect: Inject style here
([styleStr, _, styleId, effectStyle]) => {
if (isMergedClientSide && styleStr !== CSS_FILE_STYLE) {
const mergedCSSConfig: Parameters<typeof updateCSS>[2] = {
mark: ATTR_MARK,
prepend: enableLayer ? false : 'queue',
attachTo: container,
priority: order,
};
const nonceStr = typeof nonce === 'function' ? nonce() : nonce;
if (nonceStr) {
mergedCSSConfig.csp = { nonce: nonceStr };
}
// ================= Split Effect Style =================
// We will split effectStyle here since @layer should be at the top level
const effectLayerKeys: string[] = [];
const effectRestKeys: string[] = [];
Object.keys(effectStyle).forEach((key) => {
if (key.startsWith('@layer')) {
effectLayerKeys.push(key);
} else {
effectRestKeys.push(key);
}
});
// ================= Inject Layer Style =================
// Inject layer style
effectLayerKeys.forEach((effectKey) => {
updateCSS(
normalizeStyle(effectStyle[effectKey]),
`_layer-${effectKey}`,
{ ...mergedCSSConfig, prepend: true },
);
});
// ==================== Inject Style ====================
// Inject style
const style = updateCSS(styleStr, styleId, mergedCSSConfig);
(style as any)[CSS_IN_JS_INSTANCE] = cache.instanceId;
// Used for `useCacheToken` to remove on batch when token removed
style.setAttribute(ATTR_TOKEN, tokenKey);
// Debug usage. Dev only
if (process.env.NODE_ENV !== 'production') {
style.setAttribute(ATTR_CACHE_PATH, fullPath.join('|'));
}
// ================ Inject Effect Style =================
// Inject client side effect style
effectRestKeys.forEach((effectKey) => {
updateCSS(
normalizeStyle(effectStyle[effectKey]),
`_effect-${effectKey}`,
mergedCSSConfig,
);
});
}
},
);
return (node: React.ReactElement) => {
let styleNode: React.ReactElement;
if (!ssrInline || isMergedClientSide || !defaultCache) {
styleNode = <Empty />;
} else {
styleNode = (
<style
{...{
[ATTR_TOKEN]: cachedTokenKey,
[ATTR_MARK]: cachedStyleId,
}}
dangerouslySetInnerHTML={{ __html: cachedStyleStr }}
/>
);
}
return (
<>
{styleNode}
{node}
</>
);
};
}
4. 总结
以上就是antd5主题色实现方式的核心流程,根据图解和代码片段更好的解读实现过程的原理,同时antd6出现后,默认移除了cssInjs的功能,也是能看到虽然antd的主题有缓存策略,但是在运行时操作dom还是比较消耗性能,开启了cssVar可以大幅的下降性能的损耗。
antd采设计的组件级cssInjs性能测评
附录1
Token的定义
组成antd主题色的变量对象 一共分为seedToken mapToken AliasToken, 它们之间的关系如下: Ant Design v5 的 Design Token 系统采用了三层分级架构。这种分层设计是为了解耦“设计意图”与“具体样式”,让定制主题更灵活、逻辑更清晰。
以下是 SeedToken、MapToken 和 AliasToken 的关系与职责详解:
1. 三层关系金字塔
它们的关系是层层派生(Derive)的:
- 输入:最基础的变量(Seed)。
- 中间处理:通过算法扩展成详细的梯度变量(Map)。
- 输出:给开发者使用的、语义化的最终变量(Alias)。
🌱 第一层:Seed Token (种子变量)
- 职责:定义品牌基调。这是整个设计系统的“DNA”。
- 特点:
- 数量最少,最抽象。
- 不包含具体的 UI 细节(如“输入框背景色”),只包含基础属性。
- 通常只需要修改这一层,就能改变整个应用的色调。
- 典型示例:
- colorPrimary: #1677ff (品牌主色)
- colorBgBase: #ffffff (基础背景色)
- borderRadius: 6 (基础圆角)
第二层:Map Token (映射变量)
- 职责:基于算法生成的梯度变量。它是 Seed Token 经过算法(Algorithm)运算后的产物。
- 特点:
- 算法介入:例如,算法会根据 colorPrimary (#1677ff) 自动计算出悬浮态颜色、点击态颜色、浅色背景色等一系列梯度颜色。
- 更加具体:包含了一组完整的色板、不同层级的圆角大小、不同层级的间距等。
- 这层通常不需要用户手动修改,而是通过切换算法(如 Dark Mode 算法)来改变。
- 典型示例:
- colorPrimaryBg: #e6f7ff (主色对应的浅色背景,算法算出来的)
- colorPrimaryHover: #4096ff (主色对应的悬浮色,算法算出来的)
- borderRadiusSM: 4 (小号圆角)
- borderRadiusLG: 8 (大号圆角)
第三层:Alias Token (别名变量) - 开发者主要用这个
- 职责:语义化映射。它将抽象的 Map Token 映射到具体的 UI 用途 上。
- 特点:
- 面向组件:直接告诉组件“你的背景该用什么颜色”。
- 复用性:多个组件可能共用同一个 Map Token,但通过 Alias Token 可以赋予不同的语义。
- 在组件开发中,绝大多数情况下只应该使用 Alias Token。