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

相关推荐
程序员修心2 小时前
CSS文本样式全解析:11个核心属性详解
前端·css
旧梦吟2 小时前
脚本网站 开源项目
前端·web安全·网络安全·css3·html5
我有一棵树2 小时前
解决 highlight.js 不支持语言的方法
开发语言·javascript·ecmascript
北极糊的狐3 小时前
按钮绑定事件达成跳转效果并将树结构id带入子页面形成参数完成查询功能并将返回的数据渲染到页面上2022.5.29
前端·javascript·vue.js
幽络源小助理3 小时前
幽络源二次元分享地址发布页源码(HTML) – 源码网免费分享
前端·html
全栈前端老曹3 小时前
【ReactNative】页面跳转与参数传递 - navigate、push 方法详解
前端·javascript·react native·react.js·页面跳转·移动端开发·页面导航
用泥种荷花3 小时前
【前端学习AI】Python环境搭建
前端
老华带你飞3 小时前
考试管理系统|基于java+ vue考试管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·后端
_Kayo_3 小时前
React上绑定全局方法
前端·javascript·react.js