1. createStore()初始化方法
js
export function createStore<T extends DefaultRow>(
table: Table<T>,
props: TableProps<T>
) {
if (!table) {
throw new Error('Table is required.')
}
const store = useStore<T>()
// fix https://github.com/ElemeFE/element/issues/14075
// related pr https://github.com/ElemeFE/element/pull/14146
/**
* 原始方法:_toggleAllSelection 是执行全选/取消全选的逻辑
* 防抖包装:用 debounce 包装,延迟 10ms 执行
* 方法替换:将防抖后的方法赋值给 toggleAllSelection
*/
store.toggleAllSelection = debounce(store._toggleAllSelection, 10)
Object.keys(InitialStateMap).forEach((key) => {
/**
* props是Table组件的props,key是InitialStateMap的key
* 这段代码用于初始化 store 的状态:
* 遍历 InitialStateMap 的所有 key,从 props 中取值并同步到 store。
*/
handleValue(getArrKeysValue(props, key), key, store)
})
// 监听InitialStateMap中定义的所有属性
proxyTableProps(store, props)
return store
}
js
/**
* 原始方法:_toggleAllSelection 是执行全选/取消全选的逻辑
* 防抖包装:用 debounce 包装,延迟 10ms 执行
* 方法替换:将防抖后的方法赋值给 toggleAllSelection
*/
store.toggleAllSelection = debounce(store._toggleAllSelection, 10)
// 用户点击全选框时
store.toggleAllSelection() // 调用防抖后的方法
→ debounce 延迟 10ms
→ _toggleAllSelection() // 执行实际逻辑
→ 修改 selection 和 isAllSelected 状态
为什么需要防抖?
_toggleAllSelection方法会遍历所有行数据、更新每行的选择状态、触发事件,
如果用户快速连续点击,可能会导致状态不一致、性能问题、UI闪烁,而防抖可以避免这些问题
2. getArrKeysValue()
js
/**
* 从 props 中按路径取值,支持嵌套属性(如 'treeProps.hasChildren')
* @param props Table组件的props
* @param key InitialStateMap的key
* @returns
*/
function getArrKeysValue<T extends DefaultRow>(
props: TableProps<T>,
key: string
) {
if ((key as keyof typeof props).includes('.')) {
const keyList = (key as keyof typeof props).split('.')
let value: string | Record<string, any> = props
keyList.forEach((k) => {
value = (value as Record<string, any>)[k]
})
return value
} else {
return (props as any)[key] as boolean | string
}
}
3. handleValue()
js
/**
* 将props的值同步到store的状态中,并处理映射关系和默认值
* @param value 从props中按InitialStateMap的key取到的值,支持嵌套属性(如 'treeProps.hasChildren')
* @param propsKey InitialStateMap的key
* @param store TableStore
*/
function handleValue<T extends DefaultRow>(
value: string | boolean | Record<string, any>,
propsKey: string,
store: Store<T>
) {
// 保存从props中按InitialStateMap的key取到的原始值
let newVal = value
// 从InitialStateMap获取映射配置
// 可能是字符串(如 'rowKey')或对象(如 { key: 'lazyColumnIdentifier', default: 'hasChildren' })
let storeKey = InitialStateMap[propsKey as keyof typeof InitialStateMap]
if (isObject(storeKey)) {
// 如果newVal为空,则使用默认值
newVal = newVal || storeKey.default
storeKey = storeKey.key
}
; ((store.states as any)[storeKey] as any).value = newVal
}
4. proxyTableProps()
js
/**
* 用于监听 props 的变化,当 props 中的值改变时,自动同步到 store 的状态中
* @param store
* @param props
*/
function proxyTableProps<T extends DefaultRow>(
store: Store<T>,
props: TableProps<T>
) {
// 遍历 InitialStateMap 的所有 key,为每个 key 创建一个 watch 监听器
Object.keys(InitialStateMap).forEach((key) => {
watch(
// 监听 getArrKeysValue(props, key) 的返回值
() => getArrKeysValue(props, key),
(value) => {
// 当值变化时,调用 handleValue 同步到 store
handleValue(value, key, store)
}
)
})
}
核心编程思维提炼
1. 配置驱动编程(Configuration-Driven Programming)
思维:将变化的部分抽离为配置,用统一逻辑处理。
js
// ❌ 硬编码思维(你可能会这样写)
function syncPropsToStore(props, store) {
store.states.rowKey.value = props.rowKey
store.states.data.value = props.data
store.states.defaultExpandAll.value = props.defaultExpandAll
// ... 每个都要写一遍
}
// ✅ 配置驱动思维(Element Plus 的做法)
const config = {
rowKey: 'rowKey',
data: 'data',
defaultExpandAll: 'defaultExpandAll'
}
Object.keys(config).forEach(key => {
store.states[config[key]].value = props[key]
})
实际应用场景:
- API 字段映射:后端字段名 → 前端字段名
- 表单验证规则:统一配置,统一处理
- 权限控制:路由权限配置表
js
// 实际工作中的应用示例
const API_FIELD_MAP = {
'user_name': 'userName',
'create_time': 'createTime',
'user_info.avatar': 'avatar'
}
function transformApiData(apiData) {
const result = {}
Object.keys(API_FIELD_MAP).forEach(apiKey => {
const frontendKey = API_FIELD_MAP[apiKey]
result[frontendKey] = getNestedValue(apiData, apiKey)
})
return result
}
2. 映射层模式(Mapping Layer Pattern)
思维:在数据源和目标之间建立映射层,解耦命名差异。
js
// 映射层的作用
Props 命名(用户友好) → 映射层 → Store 命名(内部实现)
'treeProps.hasChildren' → InitialStateMap → 'lazyColumnIdentifier'
实际应用场景:
- 第三方 API 对接:外部 API 字段 → 内部数据模型
- 多语言支持:语言 key → 翻译文本
- 状态机转换:状态名 → 状态值
js
// 实际工作中的应用示例
const STATUS_MAP = {
'pending': { label: '待处理', color: 'orange', value: 0 },
'processing': { label: '处理中', color: 'blue', value: 1 },
'completed': { label: '已完成', color: 'green', value: 2 }
}
function getStatusInfo(status) {
return STATUS_MAP[status] || STATUS_MAP['pending']
}
3. 数据转换管道(Data Transformation Pipeline)
思维:将复杂的数据转换拆分为多个步骤,每个步骤职责单一。
解释 reduce 和数据管道的执行过程:
reduce 方法详解
1. reduce 的基本语法
javascript
array.reduce((accumulator, currentValue) => {
// 处理逻辑
return newAccumulator
}, initialValue)
accumulator(累加器):上一次处理的结果currentValue(当前值):当前处理的元素initialValue(初始值):第一次处理时的初始值
2. 数据管道的执行过程
javascript
const dataPipeline = [
(data) => transformApiFields(data), // 步骤1:字段转换
(data) => validateData(data), // 步骤2:数据验证
(data) => formatDates(data), // 步骤3:日期格式化
(data) => enrichData(data), // 步骤4:数据增强
]
function processData(rawData) {
return dataPipeline.reduce((data, transform) => transform(data), rawData)
}
3. 逐步执行过程(拆解)
等价写法:
javascript
function processData(rawData) {
// 初始值:rawData
let result = rawData
// 第1次循环:transform = transformApiFields
result = transformApiFields(result)
// 此时 result = transformApiFields(rawData)
// 第2次循环:transform = validateData
result = validateData(result)
// 此时 result = validateData(transformApiFields(rawData))
// 第3次循环:transform = formatDates
result = formatDates(result)
// 此时 result = formatDates(validateData(transformApiFields(rawData)))
// 第4次循环:transform = enrichData
result = enrichData(result)
// 此时 result = enrichData(formatDates(validateData(transformApiFields(rawData))))
return result
}
4. 用具体例子演示
javascript
// 假设原始数据
const rawData = {
user_name: '张三',
create_time: '2024-01-01',
age: 25
}
// 定义转换函数
const transformApiFields = (data) => {
return {
userName: data.user_name, // 下划线转驼峰
createTime: data.create_time,
age: data.age
}
}
const validateData = (data) => {
if (!data.userName) throw new Error('用户名不能为空')
return data
}
const formatDates = (data) => {
return {
...data,
createTime: new Date(data.createTime).toLocaleDateString()
}
}
const enrichData = (data) => {
return {
...data,
status: 'active',
id: Math.random().toString(36).substr(2, 9)
}
}
// 数据管道
const dataPipeline = [
transformApiFields,
validateData,
formatDates,
enrichData
]
// 执行过程
function processData(rawData) {
return dataPipeline.reduce((data, transform) => transform(data), rawData)
}
// 执行结果
const result = processData(rawData)
console.log(result)
// {
// userName: '张三',
// createTime: '2024/1/1',
// age: 25,
// status: 'active',
// id: 'abc123xyz'
// }
5. 执行流程图
css
原始数据: { user_name: '张三', create_time: '2024-01-01', age: 25 }
↓
[reduce 开始,初始值 = rawData]
↓
步骤1: transformApiFields(rawData)
→ { userName: '张三', createTime: '2024-01-01', age: 25 }
↓
步骤2: validateData(上一步结果)
→ { userName: '张三', createTime: '2024-01-01', age: 25 } (验证通过)
↓
步骤3: formatDates(上一步结果)
→ { userName: '张三', createTime: '2024/1/1', age: 25 }
↓
步骤4: enrichData(上一步结果)
→ { userName: '张三', createTime: '2024/1/1', age: 25, status: 'active', id: 'abc123xyz' }
↓
最终结果
6. 用 for 循环等价写法(更容易理解)
javascript
function processData(rawData) {
let result = rawData // 初始值
// 依次执行每个转换函数
for (let i = 0; i < dataPipeline.length; i++) {
const transform = dataPipeline[i]
result = transform(result) // 将上一步的结果作为下一步的输入
}
return result
}
7. 为什么用 reduce?
优势:
- 函数式编程:更简洁、声明式
- 链式处理:数据像流水线一样依次处理
- 易于扩展:添加新步骤只需在数组中添加函数
- 易于测试:每个转换函数可以独立测试
8. 实际工作中的应用场景
javascript
// 场景1:表单数据处理
const formDataPipeline = [
(data) => trimFields(data), // 去除空格
(data) => validateRequired(data), // 必填验证
(data) => validateFormat(data), // 格式验证
(data) => transformToApiFormat(data) // 转换为 API 格式
]
// 场景2:列表数据处理
const listDataPipeline = [
(data) => transformFields(data), // 字段转换
(data) => filterInvalid(data), // 过滤无效数据
(data) => sortByDate(data), // 按日期排序
(data) => paginate(data) // 分页
]
// 场景3:API 响应处理
const apiResponsePipeline = [
(data) => extractData(data), // 提取数据
(data) => handleError(data), // 错误处理
(data) => normalizeData(data), // 数据标准化
(data) => cacheData(data) // 缓存数据
]
9. 调试技巧
如果想看每一步的结果:
javascript
function processData(rawData) {
return dataPipeline.reduce((data, transform, index) => {
console.log(`步骤 ${index + 1}:`, data)
const result = transform(data)
console.log(`步骤 ${index + 1} 结果:`, result)
return result
}, rawData)
}
总结
reduce的作用:将数组中的每个函数依次执行,前一个函数的输出作为下一个函数的输入- 数据管道:像工厂流水线,数据依次经过每个处理步骤
- 优势:代码简洁、易于扩展、易于测试
这就是函数式编程中的"管道模式"(Pipeline Pattern)。