上期分享了,Tag组件的分析情况,本期进行
Select组件
的探索;在开始之前,也同样希望能够同时把源代码打开对照阅读。(PS:Select组件是目前来遇到的第一个非常复杂的组件,被虐得体无完肤)
这是我做的一个系列文章,我会逐渐把
Ant Design
中的组件以及其他相关内容逐步更新完,有需要可以关注这个专栏OK,那么正文开始:
概述
Ant Design React 的 Select 组件是一个功能强大、设计精良的选择器组件,它不仅提供了丰富的交互功能,还展现了现代 React 组件库的最佳实践。本文将从架构设计、核心实现、样式系统等多个维度深入分析这个组件。
1. 整体架构设计
1.1 组件层次结构
Select 组件采用了分层架构设计:
typescript
// 核心层次
Select (Ant Design 封装层)
↓
RcSelect (rc-select 基础组件)
↓
DOM 渲染层
1.2 核心文件结构
csharp
select/
├── index.tsx # 主组件入口
├── useIcons.tsx # 图标处理逻辑
├── useShowArrow.ts # 箭头显示逻辑
├── mergedBuiltinPlacements.ts # 弹出位置配置
├── style/ # 样式系统
│ ├── index.ts # 样式入口
│ ├── token.ts # 设计令牌
│ ├── single.ts # 单选样式
│ ├── multiple.ts # 多选样式
│ ├── dropdown.ts # 下拉菜单样式
│ └── variants.ts # 变体样式
└── demo/ # 示例代码
2. 核心实现分析
2.1 主组件实现
/select/index.tsx
是 Select 组件的核心实现,让我们分析其关键设计:
tsx
const InternalSelect = <
ValueType = any,
OptionType extends BaseOptionType | DefaultOptionType = DefaultOptionType,
>(
props: SelectProps<ValueType, OptionType>,
ref: React.Ref<BaseSelectRef>,
) => {
// 配置上下文获取
const {
getPopupContainer: getContextPopupContainer,
getPrefixCls,
renderEmpty,
direction: contextDirection,
virtual,
popupMatchSelectWidth: contextPopupMatchSelectWidth,
popupOverflow,
} = React.useContext(ConfigContext);
// 组件配置获取
const {
showSearch,
style: contextStyle,
styles: contextStyles,
className: contextClassName,
classNames: contextClassNames,
} = useComponentConfig('select');
// ... 其他逻辑
};
设计亮点:
- 泛型设计 :支持自定义
ValueType
和OptionType
,提供强类型支持 - 上下文集成:深度集成 ConfigProvider,支持全局配置
- 组件配置 :通过
useComponentConfig
获取组件级别的配置
2.2 模式处理逻辑
tsx
const mode = React.useMemo(() => {
const { mode: m } = props as InternalSelectProps<OptionType>;
if (m === 'combobox') {
return undefined;
}
if (m === SECRET_COMBOBOX_MODE_DO_NOT_USE) {
return 'combobox';
}
return m;
}, [props.mode]);
const isMultiple = mode === 'multiple' || mode === 'tags';
设计思路:
- 使用
useMemo
优化模式计算 - 处理废弃的
combobox
模式 - 提供内部的
SECRET_COMBOBOX_MODE_DO_NOT_USE
用于特殊场景
2.3 图标系统设计
/select/useIcons.tsx
实现了灵活的图标处理逻辑:
tsx
export default function useIcons({
suffixIcon,
clearIcon,
menuItemSelectedIcon,
removeIcon,
loading,
multiple,
hasFeedback,
prefixCls,
showSuffixIcon,
feedbackIcon,
showArrow,
componentName,
}) {
// 清除图标处理
const mergedClearIcon = clearIcon ?? <CloseCircleFilled />;
// 后缀图标动态处理
const getSuffixIconNode = (arrowIcon?: ReactNode) => {
if (suffixIcon === null && !hasFeedback && !showArrow) {
return null;
}
return (
<>
{showSuffixIcon !== false && arrowIcon}
{hasFeedback && feedbackIcon}
</>
);
};
// 动态后缀图标
let mergedSuffixIcon = null;
if (suffixIcon !== undefined) {
mergedSuffixIcon = getSuffixIconNode(suffixIcon);
} else if (loading) {
mergedSuffixIcon = getSuffixIconNode(<LoadingOutlined spin />);
} else {
const iconCls = `${prefixCls}-suffix`;
mergedSuffixIcon = ({ open, showSearch }: { open: boolean; showSearch: boolean }) => {
if (open && showSearch) {
return getSuffixIconNode(<SearchOutlined className={iconCls} />);
}
return getSuffixIconNode(<DownOutlined className={iconCls} />);
};
}
// 选中项图标
let mergedItemIcon = null;
if (menuItemSelectedIcon !== undefined) {
mergedItemIcon = menuItemSelectedIcon;
} else if (multiple) {
mergedItemIcon = <CheckOutlined />;
} else {
mergedItemIcon = null;
}
return {
clearIcon: mergedClearIcon,
suffixIcon: mergedSuffixIcon,
itemIcon: mergedItemIcon,
removeIcon: mergedRemoveIcon,
};
}
设计亮点:
- 状态驱动:根据组件状态(loading、open、showSearch)动态切换图标
- 优先级处理:自定义图标 > 状态图标 > 默认图标
- 多选支持:为多选模式提供专门的选中图标
2.4 箭头显示逻辑
/select/useShowArrow.ts
提供了简洁的箭头显示逻辑:
ts
export default function useShowArrow(suffixIcon?: ReactNode, showArrow?: boolean) {
return showArrow !== undefined ? showArrow : suffixIcon !== null;
}
设计原则:
- 显式的
showArrow
属性优先 - 当
suffixIcon
不为null
时默认显示箭头 - 简单而清晰的逻辑
3. 样式系统设计
3.1 设计令牌系统
/select/style/token.ts
定义了完整的设计令牌:
ts
export interface MultipleSelectorToken {
/** 多选标签背景色 */
multipleItemBg: string;
/** 多选标签边框色 */
multipleItemBorderColor: string;
/** 多选标签高度 */
multipleItemHeight: number;
/** 小号多选标签高度 */
multipleItemHeightSM: number;
/** 大号多选标签高度 */
multipleItemHeightLG: number;
/** 多选框禁用背景 */
multipleSelectorBgDisabled: string;
/** 多选标签禁用文本颜色 */
multipleItemColorDisabled: string;
/** 多选标签禁用边框色 */
multipleItemBorderColorDisabled: string;
}
export interface ComponentToken extends MultipleSelectorToken {
/** 下拉菜单 z-index */
zIndexPopup: number;
/** 选项选中时文本颜色 */
optionSelectedColor: string;
/** 选项选中时文本字重 */
optionSelectedFontWeight: CSSProperties['fontWeight'];
/** 选项选中时背景色 */
optionSelectedBg: string;
/** 选项激活态时背景色 */
optionActiveBg: string;
/** 选项内间距 */
optionPadding: CSSProperties['padding'];
/** 选项字体大小 */
optionFontSize: number;
/** 选项行高 */
optionLineHeight: CSSProperties['lineHeight'];
/** 选项高度 */
optionHeight: number;
/** 选框背景色 */
selectorBg: string;
}
设计特点:
- 语义化命名:每个令牌都有明确的语义
- 多语言文档:提供中英文注释
- 分层设计:基础令牌 + 组件令牌
- 类型安全:完整的 TypeScript 类型定义
3.2 多选样式实现
/select/style/multiple.ts
实现了复杂的多选布局计算:
ts
/**
* Get multiple selector needed style. The calculation:
*
* ContainerPadding = BasePadding - ItemMargin
*
* Border: ╔═══════════════════════════╗ ┬
* ContainerPadding: ║ ║ │
* ╟───────────────────────────╢ ┬ │
* Item Margin: ║ ║ │ │
* ║ ┌──────────┐ ║ │ │
* Item(multipleItemHeight): ║ BasePadding │ Item │ ║ Overflow Container(ControlHeight)
* ║ └──────────┘ ║ │ │
* Item Margin: ║ ║ │ │
* ╟───────────────────────────╢ ┴ │
* ContainerPadding: ║ ║ │
* Border: ╚═══════════════════════════╝ ┴
*/
export const getMultipleSelectorUnit = (
token: Pick<
SelectToken,
| 'max'
| 'calc'
| 'multipleSelectItemHeight'
| 'paddingXXS'
| 'lineWidth'
| 'INTERNAL_FIXED_ITEM_MARGIN'
>,
) => {
const { multipleSelectItemHeight, paddingXXS, lineWidth, INTERNAL_FIXED_ITEM_MARGIN } = token;
const basePadding = token.max(token.calc(paddingXXS).sub(lineWidth).equal(), 0);
const containerPadding = token.max(
token.calc(basePadding).sub(INTERNAL_FIXED_ITEM_MARGIN).equal(),
0,
);
return {
basePadding,
containerPadding,
itemHeight: unit(multipleSelectItemHeight),
itemLineHeight: unit(
token.calc(multipleSelectItemHeight).sub(token.calc(token.lineWidth).mul(2)).equal(),
),
};
};
设计亮点:
- 精确计算:使用数学计算确保像素级精确
- 可视化文档:ASCII 图表清晰展示布局逻辑
- 响应式设计:支持不同尺寸的自适应
3.3 下拉菜单样式
/select/style/dropdown.ts
实现了丰富的下拉菜单样式:
ts
const genSingleStyle: GenerateStyle<SelectToken> = (token) => {
const { antCls, componentCls } = token;
const selectItemCls = `${componentCls}-item`;
const slideUpEnterActive = `&${antCls}-slide-up-enter${antCls}-slide-up-enter-active`;
const slideUpAppearActive = `&${antCls}-slide-up-appear${antCls}-slide-up-appear-active`;
const slideUpLeaveActive = `&${antCls}-slide-up-leave${antCls}-slide-up-leave-active`;
const dropdownPlacementCls = `${componentCls}-dropdown-placement-`;
const selectedItemCls = `${selectItemCls}-option-selected`;
return [
{
[`${componentCls}-dropdown`]: {
// ========================== Popup ==========================
...resetComponent(token),
position: 'absolute',
top: -9999,
zIndex: token.zIndexPopup,
boxSizing: 'border-box',
padding: token.paddingXXS,
overflow: 'hidden',
fontSize: token.fontSize,
fontVariant: 'initial',
backgroundColor: token.colorBgElevated,
borderRadius: token.borderRadiusLG,
outline: 'none',
boxShadow: token.boxShadowSecondary,
// 动画处理
[`${slideUpEnterActive}${dropdownPlacementCls}bottomLeft,
${slideUpAppearActive}${dropdownPlacementCls}bottomLeft`]: {
animationName: slideUpIn,
},
// 选项样式
[selectItemCls]: {
...genItemStyle(token),
cursor: 'pointer',
transition: `background ${token.motionDurationSlow} ease`,
borderRadius: token.borderRadiusSM,
'&-option': {
display: 'flex',
'&-content': {
flex: 'auto',
...textEllipsis,
},
'&-state': {
flex: 'none',
display: 'flex',
alignItems: 'center',
},
[`&-active:not(${selectItemCls}-option-disabled)`]: {
backgroundColor: token.optionActiveBg,
},
[`&-selected:not(${selectItemCls}-option-disabled)`]: {
color: token.optionSelectedColor,
fontWeight: token.optionSelectedFontWeight,
backgroundColor: token.optionSelectedBg,
[`${selectItemCls}-option-state`]: {
color: token.colorPrimary,
},
},
},
},
},
},
// 动画定义
initSlideMotion(token, 'slide-up'),
initSlideMotion(token, 'slide-down'),
initMoveMotion(token, 'move-up'),
initMoveMotion(token, 'move-down'),
];
};
设计特色:
- 动画系统:完整的进入/退出动画
- 状态管理:hover、active、selected、disabled 等状态
- 布局灵活:Flexbox 布局支持复杂内容
4. 功能特性分析
4.1 多选模式
多选模式是 Select 组件的核心功能之一:
typescript
// 多选模式判断
const isMultiple = mode === 'multiple' || mode === 'tags';
// 多选相关属性处理
maxCount={isMultiple ? maxCount : undefined}
tagRender={isMultiple ? tagRender : undefined}
功能特点:
- 支持
multiple
和tags
两种多选模式 tags
模式支持用户输入自定义选项- 提供
maxCount
限制选择数量 - 支持自定义
tagRender
渲染标签
4.2 搜索功能
typescript
// 搜索配置
const { showSearch } = useComponentConfig('select');
// 搜索图标处理
if (open && showSearch) {
return getSuffixIconNode(<SearchOutlined className={iconCls} />);
}
实现特点:
- 支持全局和组件级别的搜索配置
- 动态切换搜索/下拉图标
- 支持自定义过滤逻辑
4.3 虚拟滚动
typescript
<RcSelect
virtual={virtual}
listHeight={listHeight}
listItemHeight={listItemHeight}
// ...
/>
性能优化:
- 默认启用虚拟滚动
- 支持大数据量渲染
- 可配置列表高度和项目高度
4.4 无障碍支持
组件内置了完整的无障碍功能:
- 键盘导航支持
- 屏幕阅读器支持
- ARIA 属性完整
- 焦点管理
5. 设计模式与最佳实践
5.1 组合模式
typescript
// 组件组合
Select.Option = Option;
Select.OptGroup = OptGroup;
Select._InternalPanelDoNotUseOrYouWillBeFired = PurePanel;
5.2 Hook 模式
typescript
// 自定义 Hook
const { suffixIcon, itemIcon, removeIcon, clearIcon } = useIcons({
// ...
});
const showSuffixIcon = useShowArrow(props.suffixIcon, props.showArrow);
5.3 配置驱动
typescript
// 全局配置
const { showSearch, style: contextStyle } = useComponentConfig('select');
// 主题配置
const [variant, enableVariantCls] = useVariants('select', customizeVariant, bordered);
5.4 类型安全
typescript
// 泛型约束
export interface SelectProps<
ValueType = any,
OptionType extends BaseOptionType | DefaultOptionType = DefaultOptionType,
> extends Omit<InternalSelectProps<ValueType, OptionType>, 'mode' | 'getInputElement'> {
// ...
}
6. 性能优化策略
6.1 记忆化优化
typescript
// 模式计算优化
const mode = React.useMemo(() => {
const { mode: m } = props as InternalSelectProps<OptionType>;
// 计算逻辑
}, [props.mode]);
// 位置计算优化
const memoPlacement = React.useMemo<SelectCommonPlacement>(() => {
if (placement !== undefined) {
return placement;
}
return direction === 'rtl' ? 'bottomRight' : 'bottomLeft';
}, [placement, direction]);
6.2 样式优化
typescript
// CSS-in-JS 优化
const rootCls = useCSSVarCls(prefixCls);
const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls, rootCls);
6.3 虚拟化
- 大数据量场景下的虚拟滚动
- 按需渲染选项
- 内存占用优化
7. 扩展性设计
7.1 插件化图标
typescript
// 支持自定义各种图标
suffixIcon?: React.ReactNode;
clearIcon?: RenderNode;
menuItemSelectedIcon?: RenderNode;
removeIcon?: RenderNode;
7.2 样式定制
typescript
// 多层次样式定制
styles?: Partial<Record<SemanticName, React.CSSProperties>> & {
popup?: Partial<Record<PopupSemantic, React.CSSProperties>>;
};
classNames?: Partial<Record<SemanticName, string>> & {
popup?: Partial<Record<PopupSemantic, string>>;
};
7.3 渲染定制
typescript
// 支持自定义渲染
tagRender?: (props) => ReactNode;
labelRender?: (props: LabelInValueType) => ReactNode;
popupRender?: (menu: React.ReactElement) => React.ReactElement;
8. 总结
这个组件比较有意思的一点就是,这个组件是包装的RcSelect
组件,Select
组件做的其实其实就是把RcSelect
组件需要的属性一一补齐而已,真正的「下拉框」实现在RcSelect
中。
但是光是分析Select
就够喝一壶的了,尤其是其中的样式处理、TypeScript处理等等,总结来说就是一个非常复杂、高度集中的组件。
在分析这个组件的时候产生了深深地挫败感,太难了、太复杂了、考虑的东西太多了,希望读者在分析这个组件的时候不要抱着一下子就处理好的打算,保持耐心,慢慢搞。
OK,我是李仲轩,下一篇再见吧!👋