graph TD
subgraph "外部 Props"
A1[dataSource]
A2[targetKeys]
A3[selectedKeys]
A4[onChange]
A5[onSelectChange]
end
subgraph "内部 Hooks"
B1[useData]
B2[useSelection]
B3[useMultipleSelect Left]
B4[useMultipleSelect Right]
end
subgraph "计算状态"
C1[mergedDataSource]
C2[leftDataSource]
C3[rightDataSource]
C4[sourceSelectedKeys]
C5[targetSelectedKeys]
end
subgraph "List 本地状态"
D1[filterValue Left]
D2[filterValue Right]
D3[filteredItems Left]
D4[filteredItems Right]
D5[checkStatus Left]
D6[checkStatus Right]
end
subgraph "ListBody 本地状态"
E1[current Left]
E2[pageSize Left]
E3[current Right]
E4[pageSize Right]
E5[memoizedItems Left]
E6[memoizedItems Right]
end
A1 --> B1
A2 --> B1
A3 --> B2
B1 --> C1
B1 --> C2
B1 --> C3
B2 --> C4
B2 --> C5
C2 --> D3
C3 --> D4
C4 --> D5
C5 --> D6
D1 --> D3
D2 --> D4
D3 --> E5
D4 --> E6
E1 --> E5
E2 --> E5
E3 --> E6
E4 --> E6
C4 -.->|onSelectChange| A5
C5 -.->|onSelectChange| A5
C2 -.->|onChange| A4
C3 -.->|onChange| A4
classDef propsStyle fill:#ffebee,stroke:#c62828
classDef hooksStyle fill:#e8f5e8,stroke:#2e7d32
classDef computedStyle fill:#e3f2fd,stroke:#1565c0
classDef localStyle fill:#fff8e1,stroke:#ef6c00
classDef paginationStyle fill:#fce4ec,stroke:#ad1457
class A1,A2,A3,A4,A5 propsStyle
class B1,B2,B3,B4 hooksStyle
class C1,C2,C3,C4,C5 computedStyle
class D1,D2,D3,D4,D5,D6 localStyle
class E1,E2,E3,E4,E5,E6 paginationStyle
Transfer 穿梭框组件源码解析(Ant Design)
1. 组件定位与结构概览
components/transfer/index.tsx:顶层容器,协调数据、状态与两个列表 + 操作区components/transfer/list.tsx:单侧列表容器(头部 + 搜索 + 列表主体 + 页脚)components/transfer/ListBody.tsx:列表主体(渲染项、分页、滚动、移除)components/transfer/operation.tsx:中间操作按钮(左右移动)components/transfer/search.tsx:搜索输入(带清空)components/transfer/hooks/useData.ts:将dataSource拆分为左右数据、保证右侧按targetKeys顺序components/transfer/hooks/useSelection.ts:维护左右选中 keys 的合并受控状态1.1 组件架构图
graph TB subgraph "Transfer 主组件" A[Transfer Container] A --> B[useData Hook] A --> C[useSelection Hook] A --> D[useMultipleSelect Hook] end subgraph "左侧列表" E[List Left] E --> F[Search Left] E --> G[List Header Left] E --> H[ListBody Left] G --> I[Checkbox] G --> J[Dropdown Menu] H --> K[ListItem...] H --> L[Pagination] end subgraph "操作区" M[Operation] M --> N[Right Arrow Button] M --> O[Left Arrow Button] end subgraph "右侧列表" P[List Right] P --> Q[Search Right] P --> R[List Header Right] P --> S[ListBody Right] R --> T[Checkbox] R --> U[Dropdown Menu] S --> V[ListItem...] S --> W[Pagination] end A --> E A --> M A --> P B -.->|leftDataSource| E B -.->|rightDataSource| P C -.->|sourceSelectedKeys| E C -.->|targetSelectedKeys| P E -.->|onItemSelect| A P -.->|onItemSelect| A M -.->|moveToLeft/Right| A classDef hookStyle fill:#e1f5fe,stroke:#0277bd classDef componentStyle fill:#f3e5f5,stroke:#7b1fa2 classDef subComponentStyle fill:#fff3e0,stroke:#ef6c00 class B,C,D hookStyle class A,E,M,P componentStyle class F,G,H,Q,R,S,I,J,K,L,N,O,T,U,V,W subComponentStyle2. 数据模型与关键类型
TransferItem:数据项结构(key?、title?、description?、disabled?,以及扩展字段)TransferKey:项主键类型KeyWise<T>:为记录补齐key字段TransferDirection:'left' | 'right'TransferLocale:标题、占位与菜单文案等PaginationType:分页配置(或布尔)3. 顶层容器 Transfer 的职责
文件:
components/transfer/index.tsxConfigContext获取prefixCls、direction、主题useStyle,并合并表单态status、hasFeedback。useData(dataSource, rowKey, targetKeys)返回:mergedDataSource:如提供rowKey,为每条记录补齐keyleftDataSource:未在targetKeys中的记录(保持dataSource顺序)rightDataSource:在targetKeys中的记录(按targetKeys顺序)useSelection(left, right, selectedKeys)将左右列表的选中合并维护:selectedKeyssetSourceSelectedKeys、setTargetSelectedKeysuseMultipleSelect支持按住 Shift 连续选择,内部通过“上次选择索引”实现区间选中。moveTo(direction):groupDisabledKeysMap)targetKeys:向右为追加,向左为排除onChange(newTargetKeys, direction, moveKeys)onItemSelectAll:支持追加/删除/替换('replace')三种模式,底层使用groupKeysMap做集合运算。onSearch('left' | 'right', value);清空时传空串。List,传入侧向、数据、选择、搜索、全选菜单、分页、图标等Operation(左右移动按钮),支持oneWay只显示单向按钮4. 列表容器 List 的职责
文件:
components/transfer/list.tsxfilterValue,支持受控初值showSearch.defaultValue;Search为空时会触发handleClear。render(item)→RenderedItem(renderedText/renderedEl),支持 plain object{ label, value }props.filterOption存在,则优先使用;否则renderedText.includes(filterValue)filteredItems与filteredRenderItemscheckedActiveItems:已过滤且选中的可用项checkStatus:'none' | 'part' | 'all',用于头部复选框checked/indeterminateDropdown:showRemove:提供“全选 / 当前页全选(分页时)/ 反选”等showRemove开启(通常右侧单向场景):提供“移除当前页 / 移除全部”getSelectAllLabel:动态显示“已选/总数 + 单位”的摘要,或自定义selectAllLabelrenderListBody渲染:renderList,包裹在-body-customize-wrapper容器内DefaultListBody(即ListBody.tsx)notFoundContent(支持数组形式按左右分开)showRemove与pagination会影响下拉菜单项,页内数据来自ListBody的ref.current.items5. 列表主体 ListBody 的职责
文件:
components/transfer/ListBody.tsxparsePagination合并默认配置(simple、showSizeChanger=false、showLessItems=false)current本地维护;pageSize支持受控值onItemSelect(key, !selected, e)onItemRemove([key])onScrollListItem(接收renderedEl、renderedText、checked、disabled、showRemove等)useImperativeHandle(ref, { items })暴露当前页展示的RenderedItem[],供头部“当前页操作”使用6. 操作区 Operation 的职责
文件:
components/transfer/operation.tsxdisabled || !rightActive时禁用oneWay时渲染,disabled || !leftActive时禁用7. 搜索框 Search 的职责
文件:
components/transfer/search.tsxInput,带allowClear与放大镜prefixonChange回调后,当值为空字符串时会触发handleClear,与List内部本地状态保持同步8. 选择与移动的完整数据流
用户在某侧点击项:
ListBody→ 调用onItemSelect(key, checked, e)index.tsx/onItemSelect:根据multiple(Shift)决定单项/区间选择 → 产生新的 holderSet → 同步onSelectChange回调 → 若非受控selectedKeys则更新内部合并选择状态用户点击"移动到右侧/左侧":
Operation→moveTo('right'|'left')targetKeys→ 清空对侧选择 → 回调onChange(newTargetKeys, direction, moveKeys)用户使用"全选/反选/当前页选择/反选"菜单:
List根据filteredItems与当前页ref.items计算目标 keysonItemSelectAll(keys, checkAll|'replace')→ 合并或替换当前选中集合搜索:
Search触发onChange与可能的handleClearList以本地filterValue与filterOption决定filteredItems8.1 交互时序图
单项选择流程
sequenceDiagram participant User as 用户 participant ListItem as ListItem participant ListBody as ListBody participant Transfer as Transfer主组件 participant Hooks as useSelection participant Parent as 父组件 User->>ListItem: 点击项目 ListItem->>ListBody: onClick(item, e) ListBody->>Transfer: onItemSelect(key, !selected, e) alt 按住Shift多选 Transfer->>Transfer: handleMultipleSelect() Transfer->>Transfer: 计算区间选择 else 单选 Transfer->>Transfer: handleSingleSelect() Transfer->>Transfer: 添加/移除key end Transfer->>Hooks: setSourceSelectedKeys/setTargetSelectedKeys Hooks->>Hooks: 合并左右选中状态 Transfer->>Parent: onSelectChange(sourceKeys, targetKeys) alt 非受控模式 Hooks-->>Transfer: 更新内部选中状态 else 受控模式 Parent-->>Transfer: 通过selectedKeys更新 end Transfer->>ListBody: 重新渲染选中状态项目移动流程
sequenceDiagram participant User as 用户 participant Operation as Operation按钮 participant Transfer as Transfer主组件 participant useData as useData Hook participant Parent as 父组件 User->>Operation: 点击移动按钮 Operation->>Transfer: moveToRight/moveToLeft() Transfer->>Transfer: moveTo(direction) Transfer->>Transfer: 获取当前选中keys Transfer->>Transfer: 过滤禁用项(groupDisabledKeysMap) alt 向右移动 Transfer->>Transfer: newTargetKeys = moveKeys.concat(targetKeys) else 向左移动 Transfer->>Transfer: newTargetKeys = targetKeys.filter(排除moveKeys) end Transfer->>Transfer: 清空对侧选中状态 Transfer->>Parent: onChange(newTargetKeys, direction, moveKeys) Transfer->>Parent: onSelectChange([], []) Parent->>Transfer: 更新targetKeys prop Transfer->>useData: 重新计算左右数据源 useData-->>Transfer: [leftDataSource, rightDataSource] Transfer->>Transfer: 重新渲染两侧列表搜索过滤流程
sequenceDiagram participant User as 用户 participant Search as Search输入框 participant List as List容器 participant Transfer as Transfer主组件 User->>Search: 输入搜索文本 Search->>Search: handleChange(e) alt 输入为空 Search->>List: handleClear() List->>List: setFilterValue('') else 有输入内容 Search->>List: onChange(e) List->>List: setFilterValue(e.target.value) end List->>Transfer: handleFilter(e) Transfer->>Transfer: onSearch?.(direction, value) List->>List: useMemo重新计算 List->>List: matchFilter(renderedText, item) alt 自定义filterOption List->>List: filterOption(filterValue, item, direction) else 默认过滤 List->>List: text.includes(filterValue) end List-->>List: 产生filteredItems和filteredRenderItems List->>List: 重新渲染过滤后的列表全选/反选流程
sequenceDiagram participant User as 用户 participant Checkbox as 头部复选框 participant Dropdown as 下拉菜单 participant List as List容器 participant Transfer as Transfer主组件 alt 点击头部复选框 User->>Checkbox: 点击全选框 Checkbox->>List: onChange() List->>Transfer: onItemSelectAll(filteredEnabledKeys, checkStatus !== 'all') else 使用下拉菜单 User->>Dropdown: 选择菜单项 Dropdown->>List: onClick(selectAll/selectInvert/selectCurrent) alt 全选/取消全选 List->>List: getEnabledItemKeys(filteredItems) List->>Transfer: onItemSelectAll(keys, checkAll) else 当前页选择 List->>List: 从listBodyRef.current.items获取 List->>Transfer: onItemSelectAll(pageKeys, true) else 反选 List->>List: 计算需要反选的keys List->>Transfer: onItemSelectAll(newKeys, 'replace') end end Transfer->>Transfer: 根据checkAll类型处理 alt checkAll === 'replace' Transfer->>Transfer: mergedCheckedKeys = keys else checkAll === true Transfer->>Transfer: 合并现有keys和新keys else checkAll === false Transfer->>Transfer: 从现有keys中移除指定keys end Transfer->>Transfer: handleSelectChange(direction, mergedCheckedKeys) Transfer->>Transfer: 更新选中状态并重新渲染分页交互流程
sequenceDiagram participant User as 用户 participant Pagination as 分页组件 participant ListBody as ListBody participant List as List容器 User->>Pagination: 切换页码/页大小 alt 切换页码 Pagination->>ListBody: onPageChange(page) ListBody->>ListBody: setCurrent(page) else 改变页大小 Pagination->>ListBody: onSizeChange(page, size) ListBody->>ListBody: setCurrent(page) + setPageSize(size) end ListBody->>ListBody: useMemo重新计算displayItems ListBody->>ListBody: 切片filteredRenderItems ListBody->>ListBody: useImperativeHandle更新ref.items ListBody->>List: 重新渲染当前页项目 Note over List: 头部下拉菜单的"当前页操作"<br/>会使用ref.current.items8.2 状态管理数据流图
graph TD subgraph "外部 Props" A1[dataSource] A2[targetKeys] A3[selectedKeys] A4[onChange] A5[onSelectChange] end subgraph "内部 Hooks" B1[useData] B2[useSelection] B3[useMultipleSelect Left] B4[useMultipleSelect Right] end subgraph "计算状态" C1[mergedDataSource] C2[leftDataSource] C3[rightDataSource] C4[sourceSelectedKeys] C5[targetSelectedKeys] end subgraph "List 本地状态" D1[filterValue Left] D2[filterValue Right] D3[filteredItems Left] D4[filteredItems Right] D5[checkStatus Left] D6[checkStatus Right] end subgraph "ListBody 本地状态" E1[current Left] E2[pageSize Left] E3[current Right] E4[pageSize Right] E5[memoizedItems Left] E6[memoizedItems Right] end A1 --> B1 A2 --> B1 A3 --> B2 B1 --> C1 B1 --> C2 B1 --> C3 B2 --> C4 B2 --> C5 C2 --> D3 C3 --> D4 C4 --> D5 C5 --> D6 D1 --> D3 D2 --> D4 D3 --> E5 D4 --> E6 E1 --> E5 E2 --> E5 E3 --> E6 E4 --> E6 C4 -.->|onSelectChange| A5 C5 -.->|onSelectChange| A5 C2 -.->|onChange| A4 C3 -.->|onChange| A4 classDef propsStyle fill:#ffebee,stroke:#c62828 classDef hooksStyle fill:#e8f5e8,stroke:#2e7d32 classDef computedStyle fill:#e3f2fd,stroke:#1565c0 classDef localStyle fill:#fff8e1,stroke:#ef6c00 classDef paginationStyle fill:#fce4ec,stroke:#ad1457 class A1,A2,A3,A4,A5 propsStyle class B1,B2,B3,B4 hooksStyle class C1,C2,C3,C4,C5 computedStyle class D1,D2,D3,D4,D5,D6 localStyle class E1,E2,E3,E4,E5,E6 paginationStyle9. 关键可扩展点
render(item):自定义每项的渲染;返回字符串、ReactNode,或{ label, value }(value 用于搜索匹配)children(renderList):完全自定义列表主体(仍会包裹在容器以适配布局)footer(props, { direction }):自定义列表底部filterOption(input, item, direction):接管筛选逻辑rowKey(record):为数据自定义key生成selectAllLabel/selectAllLabels:自定义头部“已选/总数”显示pagination:布尔或对象,结合ListBody的当前页暴露能力实现“当前页操作”selectionsIcon:自定义头部下拉触发图标10. 受控与非受控策略
selectedKeys受控:由外部完全控制所选 keys,组件内部仅回调onSelectChangeuseSelection维护合并选中状态targetKeys一般受控:移动后通过onChange通知外部更新(或在外部保持受控)11. 国际化与方向
useLocale('Transfer', defaultLocale.Transfer)获取默认上下文getLocale合并renderEmpty('Transfer')与props.localedir === 'rtl'时,列表方向与箭头图标自动调整12. 性能与可维护性
useMemo/useCallback以避免重复计算与不必要渲染groupKeysMap等)提升选择操作效率13. 易错点与边界处理
rowKey稳定,避免 key 抖动导致选中异常ListBody通过ref暴露的itemsoneWay模式下右侧提供“移除”菜单而非复选菜单selectedKeys时,内部不会主动 setState,需要外部同步回传14. 最小示例(API 侧重点)
15. 变更/扩展建议(可选)
onItemSelectAll('replace')ListBody的渲染(通过children自定义)filterOption内集中实现—— 完 ——