Read OSS

深入解析 styled():createStyled 流水线的工作原理

高级

前置知识

  • 第一篇:架构与包层级
  • Emotion styled() API
  • CSS 优先级与层叠机制
  • JavaScript 闭包与工厂函数

深入解析 styled():createStyled 流水线的工作原理

在第一篇中,我们梳理了四层依赖金字塔,并了解到 Material UI 的 styled 导出是通过向 createStyled() 传入配置对象来创建的。本篇将打开这个工厂函数,逐一追踪其中的每一个表达式、每一处优化以及每一个设计决策。这段代码在库中的每一个 styled 组件上都会执行——理解它,就等于理解了 MUI 样式系统的全貌。

核心实现集中在一个文件中:packages/mui-system/src/createStyled/createStyled.js。全文 351 行,代码密度较高,但只要理清结构,便不难读懂。

createStyled:工厂函数

createStyled 是一个返回 styled 函数的工厂函数。工厂接收的配置会被闭包捕获,并在所有由该 styled 创建的组件之间共享:

export default function createStyled(input = {}) {
  const {
    themeId,
    defaultTheme = systemDefaultTheme,
    rootShouldForwardProp = shouldForwardProp,
    slotShouldForwardProp = shouldForwardProp,
  } = input;

  const styled = (tag, inputOptions = {}) => {
    // ...returns muiStyledResolver
  };

  return styled;
}

正如第一篇所示,Material UI 以 { themeId: '$$material', defaultTheme, rootShouldForwardProp } 调用该工厂函数一次,并将结果导出。这一次调用,便创建了整个库中所有组件共用的 styled 函数。

flowchart TD
    Factory["createStyled({ themeId, defaultTheme, rootShouldForwardProp })"]
    Factory --> StyledFn["styled() — closed over config"]
    StyledFn --> |"styled(ButtonBase, { name: 'MuiButton', slot: 'Root' })"|Resolver["muiStyledResolver"]
    Resolver --> |"muiStyledResolver(styles...)"|Component["React Component"]

返回的 styled 函数接收两个参数:tag(HTML 元素字符串或 React 组件)和一个携带 MUI 特有元数据的 options 对象:

  • name:组件名称(如 'MuiButton')——用于查找主题覆盖配置
  • slot:子组件插槽(如 'Root''StartIcon')——控制 shouldForwardProp 的行为
  • overridesResolver:将主题的 styleOverrides 键映射到当前组件的函数
  • skipVariantsResolverskipSx:用于跳过流水线中特定阶段的开关

三区表达式流水线

createStyled 的核心是 muiStyledResolver 函数,定义于第 216 行。当你写下 styled('div')({ color: 'red' }) 时,传入的样式对象并非唯一被送入 Emotion 的表达式。MUI 将它们包裹在一条三区流水线中处理:

const muiStyledResolver = (...expressionsInput) => {
  const expressionsHead = [];
  const expressionsBody = expressionsInput.map(transformStyle);
  const expressionsTail = [];

  // HEAD: Theme attachment — must run first
  expressionsHead.push(styleAttachTheme);

  // TAIL: Theme overrides
  if (componentName && overridesResolver) {
    expressionsTail.push(styleThemeOverrides);
  }

  // TAIL: Theme variants
  if (componentName && !skipVariantsResolver) {
    expressionsTail.push(styleThemeVariants);
  }

  // TAIL: sx prop — always last
  if (!skipSx) {
    expressionsTail.push(styleFunctionSx);
  }

  const expressions = [...expressionsHead, ...expressionsBody, ...expressionsTail];
  const Component = defaultStyledResolver(...expressions);
  return Component;
};
sequenceDiagram
    participant Dev as Developer
    participant MSR as muiStyledResolver
    participant Emotion as Emotion styled

    Dev->>MSR: styled('div')(style1, style2)
    MSR->>MSR: expressionsHead = [attachTheme]
    MSR->>MSR: expressionsBody = [transform(style1), transform(style2)]
    MSR->>MSR: expressionsTail = [overrides, variants, sx]
    MSR->>Emotion: styled('div')(attachTheme, style1', style2', overrides, variants, sx)
    Emotion-->>Dev: React Component

这种排列顺序是经过深思熟虑的,它直接决定了样式的优先级:

  1. Head——attachTheme 最先执行,确保后续所有表达式都能访问到正确作用域的 theme
  2. Body——开发者传入的样式函数或对象,经 transformStyle 转换以支持变体和 CSS 层包裹
  3. Tail——主题覆盖、主题变体,最后是 sx,优先级依次递增

因此,sx 始终优先于主题覆盖,主题覆盖始终优先于组件默认样式,组件默认样式始终优先于基础样式。CSS 层叠规则在此通过代码逻辑进行了精确管理。

processStyle 与变体匹配

第 48 行processStyle 函数是样式解析引擎,负责处理以下五种输入类型:

  1. 函数——以 props 为参数调用,结果递归传入 processStyle
  2. 数组——通过 processStyle 递归展平
  3. variants 的对象——提取根级样式,再进行变体匹配
  4. 已处理对象isProcessed: true)——样式已完成序列化,直接返回
  5. 普通对象或字符串——直接返回(可能会被包裹在 CSS 层中)

变体匹配逻辑位于第 85 行processStyleVariants 函数中:

function processStyleVariants(props, variants, results = [], layerName = undefined) {
  let mergedState;

  variantLoop: for (let i = 0; i < variants.length; i += 1) {
    const variant = variants[i];

    if (typeof variant.props === 'function') {
      mergedState ??= { ...props, ...props.ownerState, ownerState: props.ownerState };
      if (!variant.props(mergedState)) {
        continue;
      }
    } else {
      for (const key in variant.props) {
        if (props[key] !== variant.props[key] &&
            props.ownerState?.[key] !== variant.props[key]) {
          continue variantLoop;
        }
      }
    }

    // Variant matched — add its style
    results.push(/* resolved style */);
  }

  return results;
}

这里有两处值得关注的性能细节。其一,variantLoop: 标签配合 continue variantLoop——这是 JavaScript 中罕见的标签循环语法,它让内层的 for...in 循环在某个 prop 不匹配时,可以直接跳至下一个变体,无需创建中间的 Object.entries() 数组,也无需调用 Object.keys().every()。对于一个在每次渲染中都会执行的函数来说,这一点至关重要。

其二,mergedState 使用空值合并赋值(??=)实现惰性初始化,只有当变体使用函数形式的 props 时才会分配内存,而大多数变体并不需要。

提示: 在主题中定义变体时,建议优先使用对象形式({ props: { variant: 'contained', color: 'primary' } })而非函数形式,以获得更好的性能。对象形式采用逐属性比对的快速检查;而函数形式需要分配一个合并后的状态对象。

性能优化:memoTheme 与 preprocessStyles

Material UI 中每个 styled 组件的样式函数都被 memoTheme 包裹。以 Button 的根节点样式为例:

const ButtonRoot = styled(ButtonBase, { name: 'MuiButton', slot: 'Root' })(
  memoTheme(({ theme }) => {
    return { /* hundreds of lines of styles */ };
  })
);

packages/mui-system/src/memoTheme.ts 的实现是一个基于闭包的引用相等性检查:

export default function unstable_memoTheme<T>(styleFn: ThemeStyleFunction<T>) {
  let lastValue: CSSInterpolation;
  let lastTheme: T;

  return function styleMemoized(props: { theme: T }) {
    let value = lastValue;
    if (value === undefined || props.theme !== lastTheme) {
      arg.theme = props.theme;
      value = preprocessStyles(styleFn(arg));
      lastValue = value;
      lastTheme = props.theme;
    }
    return value;
  };
}

由于 theme 对象在多次渲染之间通常保持引用稳定,这一记忆化机制意味着样式函数体——其中可能包含数十个变体定义和主题取值——只会在 theme 发生变化时执行一次,而不是每次渲染都执行。

flowchart TD
    Render1["Render 1: theme = T1"] --> Check1{"lastTheme === T1?"}
    Check1 -->|"No (first render)"| Compute["Call styleFn → preprocessStyles → cache"]
    Check1 -->|"Yes"| Return["Return cached value"]
    Compute --> Return
    Render2["Render 2: theme = T1"] --> Check2{"lastTheme === T1?"}
    Check2 -->|"Yes"| Return2["Return cached value ⚡"]

记忆化的结果随后会传入 packages/mui-system/src/preprocessStyles.ts 中的 preprocessStyles 处理:

export default function preprocessStyles(input: any) {
  const { variants, ...style } = input;
  const result = {
    variants,
    style: internal_serializeStyles(style) as any,
    isProcessed: true,
  };

  if (variants) {
    variants.forEach((variant: any) => {
      if (typeof variant.style !== 'function') {
        variant.style = internal_serializeStyles(variant.style);
      }
    });
  }
  return result;
}

Emotion 的 serializeStyles 正是在这里被调用的——它将样式对象转换为带有确定性类名的预哈希 CSS 字符串。通过在记忆化阶段完成这一操作,而非在每次渲染时执行,CSS 序列化的开销便只在 theme 变化时产生一次。

通过 CSS Layers 控制优先级

第 184 行transformStyle 函数会检查 props.theme.modularCssLayers,若启用,则将样式包裹在 @layer 指令中。层的名称由上下文决定:

const layerName =
  (componentName && componentName.startsWith('Mui')) || !!componentSlot
    ? 'components'
    : 'custom';

整条流水线共使用三个层:

使用位置 用途
components Body 表达式(MUI 组件样式) 组件基础样式
theme Tail 表达式(styleOverridesvariants 主题自定义
sx styleFunctionSx(tail,最末位) 内联样式覆盖
flowchart BT
    L1["@layer components — Base component styles"]
    L2["@layer theme — styleOverrides + variants"]
    L3["@layer sx — sx prop styles"]

    L1 --> L2 --> L3

    style L3 fill:#c62828,color:#fff
    style L2 fill:#e65100,color:#fff
    style L1 fill:#1565c0,color:#fff

shallowLayer 工具函数负责包裹已序列化的 CSS:

function shallowLayer(serialized, layerName) {
  if (layerName && serialized?.styles && !serialized.styles.startsWith('@layer')) {
    serialized.styles = `@layer ${layerName}{${String(serialized.styles)}}`;
  }
  return serialized;
}

与依赖注入顺序来控制优先级的传统方式相比,这是一项重要的架构改进。有了 CSS layers,层叠规则不再依赖样式的加载先后——无论哪部分样式先加载,结果都是确定的,这使 SSR 水合与代码分割的行为更加可预期。

串联全流程

让我们追踪一下 <Button variant="contained" color="primary" sx={{ mt: 2 }}> 渲染时的完整过程:

  1. Emotion 按顺序以 { theme, ownerState, ... } 调用所有表达式
  2. attachTheme(head)从 context 中解析 theme['$$material']
  3. memoTheme 包裹的 body 表达式返回预序列化的基础样式,以及匹配 contained + primary 的变体样式
  4. styleThemeOverrides(tail)检查 theme.components.MuiButton.styleOverrides——若存在,processStyle 解析 root 插槽的覆盖样式
  5. styleThemeVariants(tail)检查 theme.components.MuiButton.variants——将所有匹配的 {props, style} 条目追加进来
  6. styleFunctionSx(tail)将 { mt: 2 } 处理为 { marginTop: '16px' },若启用了 CSS layers 则包裹在 @layer sx

优先级顺序有保证:基础样式 → 覆盖样式 → 变体样式 → sx。

提示: 调试样式未生效的问题时,首先确认该样式处于流水线的哪个区。如果 sx prop 没有覆盖主题覆盖样式,可能是因为 sx 处理被跳过了(skipSx: true)。如果变体没有匹配,检查 ownerState 上的 prop 值是否与变体 props 完全一致(严格相等,不做类型转换)。

下一步

现在我们已经理解了驱动每个 MUI 组件的样式流水线。第三篇将聚焦一个具体组件——Button——深入了解它的五文件目录约定、从 JSX 到最终渲染的 props 解析链、ownerState 模式,以及 CSS 类名生成系统。我们将从抽象的流水线走向具体的组件解剖。