antd主题色实现5.x版本 最详细的原理解析

73 阅读15分钟

引言 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. 实现流程概览

数据注入到样式生成流程:

image.png

2.1 provider数据注入
2.1.1 provider嵌套示意图

image.png

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传入主题相关配置后,内部的处理逻辑:

  1. 注入token(这里的token为seedToken,说明见附录1)
    注意源码中token为mergedTheme,是因为在context层,antd支持嵌套Provider,这里mergedTheme为聚合后的token,这里将用户传入的token和默认的seedToken做了聚合。

  2. 生成主题对象themeObj
    algorith用户传入是一个函数, 通过createTheme转换函数为themeObj themeObj结构如下:{id: 1, derivative: [theme.darkAlgorithm], getDerivativeToken:()=> {}}

  3. components: 目的是将用户如传入的
    Input: { algorithm: true, colorBgContainer: '#f5f5f5', } 转换为 Input: { colorBgContainer: '#f5f5f5', // 作用:确保该组件严格使用全局配置的算法。(组件如果自身传入了算法也支持组件级算法函数) theme: themeObj },

2.2 context消费主题数据

数据消费到实际看到页面生效主题色:

image.png

消费数据: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内部实现步骤

image.png

插入样式到style标签节点:useStyleRegister

useStyleRegister插入样式逻辑实现步骤

image.png

image.png

image.png

数据消费context生成样式总结
  1. 第一步 通过useToken生成全局主题token变量(全局缓存,只要配置token和theme的算法没有改变,每次render都是缓存token对象)
  2. 第二步 每个组件比如Button、Card都从全局token变量中获取做为自身的样式变量,生成带token变量的css对象。
  3. 第三步 将带token变量的css对象注册到style标签中,这里有一个点注意如果开启了cssVar,那么组件也有组件级的cssVar,例如 fontSize: token.contrliOutlineWidth / 2 那么组件算出来就是一个动态的值,用户开启了cssVar,要将组件自身的css属性转换为cssVar,同时插入style组件级的css变量,比如--ant-btn-color前缀 全局变量--ant前缀

image.png

image.png

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;

效果如下:

image.png

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作用如下

  1. Button组件token的生成
  2. 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详解

  1. 创建主题对象
  // 创建主题对象
  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(),
  };
}
  1. 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)的缓存
}
  1. 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(); // 创建全局主题缓存

内部维护了一个全局缓存池,通过以下流程确保单例复用:

image.png

2. 缓存结构设计

ThemeCache 是一个专门为了解决 “多层级缓存键(Key)” 和 “LRU(最近最少使用)淘汰策略” 而设计的缓存类。

用户最开始 传入假设我们存 [FnA, FnB] -> Theme1 和 [FnA, FnC] -> Theme2,结构如下:

Map (Root)

└── FnA

└── { map: Map }

├── FnB

│ └── { value: [Theme1, timestamp] }

└── FnC

└── { value: [Theme2, timestamp] }

这是典型的 Trie 树(字典树)


为什么这么做?因为 JSMap 只能用单一对象做 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:[基础算法]

难点:JSMap 只能用单一引用做 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讲解

  1. 图解

image.png

  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的触发时机

image.png

useStyleRegister详解

image.png

  
  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性能测评

image.png

附录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。