深入解析 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键映射到当前组件的函数skipVariantsResolver、skipSx:用于跳过流水线中特定阶段的开关
三区表达式流水线
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
这种排列顺序是经过深思熟虑的,它直接决定了样式的优先级:
- Head——
attachTheme最先执行,确保后续所有表达式都能访问到正确作用域的 theme - Body——开发者传入的样式函数或对象,经
transformStyle转换以支持变体和 CSS 层包裹 - Tail——主题覆盖、主题变体,最后是
sx,优先级依次递增
因此,sx 始终优先于主题覆盖,主题覆盖始终优先于组件默认样式,组件默认样式始终优先于基础样式。CSS 层叠规则在此通过代码逻辑进行了精确管理。
processStyle 与变体匹配
第 48 行的 processStyle 函数是样式解析引擎,负责处理以下五种输入类型:
- 函数——以
props为参数调用,结果递归传入processStyle - 数组——通过
processStyle递归展平 - 含
variants的对象——提取根级样式,再进行变体匹配 - 已处理对象(
isProcessed: true)——样式已完成序列化,直接返回 - 普通对象或字符串——直接返回(可能会被包裹在 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 表达式(styleOverrides、variants) |
主题自定义 |
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 }}> 渲染时的完整过程:
- Emotion 按顺序以
{ theme, ownerState, ... }调用所有表达式 attachTheme(head)从 context 中解析theme['$$material']- 经
memoTheme包裹的 body 表达式返回预序列化的基础样式,以及匹配contained+primary的变体样式 styleThemeOverrides(tail)检查theme.components.MuiButton.styleOverrides——若存在,processStyle解析root插槽的覆盖样式styleThemeVariants(tail)检查theme.components.MuiButton.variants——将所有匹配的{props, style}条目追加进来styleFunctionSx(tail)将{ mt: 2 }处理为{ marginTop: '16px' },若启用了 CSS layers 则包裹在@layer sx中
优先级顺序有保证:基础样式 → 覆盖样式 → 变体样式 → sx。
提示: 调试样式未生效的问题时,首先确认该样式处于流水线的哪个区。如果
sxprop 没有覆盖主题覆盖样式,可能是因为sx处理被跳过了(skipSx: true)。如果变体没有匹配,检查ownerState上的 prop 值是否与变体props完全一致(严格相等,不做类型转换)。
下一步
现在我们已经理解了驱动每个 MUI 组件的样式流水线。第三篇将聚焦一个具体组件——Button——深入了解它的五文件目录约定、从 JSX 到最终渲染的 props 解析链、ownerState 模式,以及 CSS 类名生成系统。我们将从抽象的流水线走向具体的组件解剖。