el-table源码解读2-2——createStore()初始化方法

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

优势:

  1. 函数式编程:更简洁、声明式
  2. 链式处理:数据像流水线一样依次处理
  3. 易于扩展:添加新步骤只需在数组中添加函数
  4. 易于测试:每个转换函数可以独立测试

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)。

相关推荐
竹林81814 小时前
用 wagmi v2 + viem 监听链上事件,我踩了三天坑终于搞懂了实时日志与历史补全
javascript
Momo__14 小时前
VueUse createReusableTemplate —— 单文件组件内的模板复用神器
前端·vue.js
只一14 小时前
😭从回调地狱到 async/await:一文打通 Ajax 与 JS 异步编程
javascript
程序员小富14 小时前
我开源了一个开发者专属的智能 JSON 工具,得到了媳妇高度认可
前端·vue.js·后端
小小小小宇14 小时前
程序员如何给 LLM 装工具以及看懂推理过程
前端
写代码的皮筏艇14 小时前
React中的forwardRef
前端·react.js·面试
槑有老呆14 小时前
花三个月工资请了个 AI 程序员,结果它连青岛啤酒股价都查不了
前端
风骏时光牛马14 小时前
Verilog开发常见问题汇总解析
前端
子兮曰14 小时前
AI Coding Method Map:一张图看懂 AI 编程的完整链路
前端·人工智能·后端