伸手请直接跳到【方法合集】~
列表 List
(包括数组和 Set
或者各种类数组结构都可以是列表)数据的作为系列数据的载体可以说是随处可见了,必然在项目开发中是少不了对列表的处理,或多或少的也会有对列表处理方法的封装,这次我们就来看看有那些常见的列表处理函数。
如果你觉得文章对你有所帮助,希望你可以慷慨解囊地给一个赞~
List 结构转 Map 结构
这个可以说是最常规的,也是最频繁的处理了,当我们有一系列对象时,经常会遇到根据对象的 id
来查找对应的对象,当列表比较大或者查找次数比较多的时候,直接使用 Array.find
来查找成本就会很高,于是将其转成 id
作为 key
,对象本身作为 vulue
的 Map 结构了。
// prop 指定使用哪个属性的值作为 key
function transformListToMap<T = any>(list: T[], prop: keyof T) {
const map = {} as Record<string, T>
if (!prop) return map
list.forEach(item => {
// 这里丢到 String 里规避一下 ts 的类型限制
map[String(item[prop])] = item
})
return map
}
不过咋一看,这方法好像单薄的一些,不能覆盖一些相对复杂的情况。
比如当需要一些组合值或者计算值作为 key
时,那单传一个 prop
是不能满足情况的。
再比如,当需要作为 value
的部分不是对象本身,而是一些特定的属性或者一些属性的组合或计算,那显然目前的参数也是无法支持的。
于是我们再加亿点细节,完善一下这个函数:
// 这是上一期写的 is 系列函数,在文章最底部有链接
import { isDefined, isFunction } from './is'
// 第二个参数同时支持传入一个函数,以支持返回任意处理的值作为 key
// 增加第三个参数,拓展支持传入一个函数以处理任意的值作为 value
function transformListToMap<T = any, K = T>(
list: T[],
prop: keyof T | ((item: T) => any),
accessor: (item: T) => K = v => v as any
): Record<string, K> {
const map = {} as Record<string, any>
if (!isDefined(prop)) return map
// 统一处理成读取函数
const propAccessor = isFunction(prop) ? prop : (item: T) => item[prop]
list.forEach(item => {
const key = propAccessor(item)
// 防止传入不规范函数出现 null 或 undefined,让其静默失效
if (isDefined(key)) {
map[key] = accessor(item)
}
})
return map
}
移除 List 中的特定元素
我先贴一段代码,我相信大伙应该没少写过:
const list: any[] = [/* ... */]
const removedId = 'removedId'
const index = list.findIndex(item => item.id === removedId)
if (index !== -1) {
list.splice(index, 1)
}
没错,根据条件删除列表中的特定元素也是很常见的需求了,于是我们也可以来封装一下:
function removeArrayItem<T = any>(
array: T[],
item: T | ((item: T) => boolean), // 老样子支持传入一个函数适配复杂情况
isFn = false // 用来指示列表里的元素是否是函数,以适配极少数情况
): T | null {
let index = -1
if (isFn || typeof item !== 'function') {
index = array.findIndex(current => current === item)
} else {
index = array.findIndex(item as (item: T) => boolean)
}
if (~index) {
return array.splice(index, 1)[0]
}
return null
}
不过有时候,我们可能需要同时删除多个元素,那上面的方法是无法覆盖的,于是我们还需要再改造一下:
// 在处理上,会直接操作源列表,并返回被移除的元素集合
function removeArrayItems<T = any>(
array: T[],
items: T | T[] | ((item: T) => boolean),
isFn = false
): T[] {
const multiple = Array.isArray(items)
// 针对删除单个元素单独处理
if (!multiple && (isFn || typeof items !== 'function')) {
const index = array.findIndex(current => current === items)
if (~index) {
return array.splice(index, 1)
}
} else {
let filterFn: (item: T) => boolean
if (multiple) {
const removedSet = new Set(items)
filterFn = item => removedSet.has(item)
} else {
filterFn = items as (item: T) => boolean
}
// 浅克隆源列表,用来遍历处理
const originArray = Array.from(array)
const removedItems: T[] = []
// 用源列表来储存删除后的结果以达到直接操作源列表的目的
array.length = 0
originArray.forEach(item => (filterFn(item) ? removedItems : array).push(item))
return removedItems
}
return []
}
函数的后半部分的处理可能有一些抽象,可以慢慢屡一下。
这个函数虽然涵盖了多元素删除的情况,不过当使用自定义函数来进行删除时,可能原本只是希望删除一个元素,但却会对整个列表进行完整的遍历,从而损失了一些性能。
对 List 中的元素进行归类(GroupBy)
例如有下面这样一组数据:
const list = [
{ type: 'a', name: 'x', count: 10 },
{ type: 'a', name: 'y', count: 11 },
{ type: 'a', name: 'x', count: 12 },
{ type: 'a', name: 'y', count: 13 },
{ type: 'b', name: 'x', count: 14 },
{ type: 'b', name: 'y', count: 15 }
]
现在需要针对同 type
且同 name
的数量进行求和,那这里就会需要我们把数据按照 type
和 name
两个属性进行归类,也就是很经典的 GroupBy 问题了。
其实第一个案例的将 List 转 Map 结构本质也是一个 GroupBy 问题,只不过是最简单的一维归类。
当然如果我们知道只会根据两个属性进行归类的话,直接用一个两层的 Map 来储存结果是没问题的:
const record = {}
arr.forEach(({ type, name, count }) => {
if (!record[type]) {
record[type] = {}
}
const typeRecord = record[type]
if (!typeRecord[name]) {
typeRecord[name] = 0
}
typeRecord[name] += count
})
record.a.x // 22
不过我们封装通用的工具函数,肯定是要考虑尽量覆盖可能出现的情况的(十倍原则),所以我们出发点是要支持无限层级的分组(只要内存够用),这里就直接上完全体代码了:
function groupByProps<T = any>(
list: T[],
// 可以传入一个数组按顺序指定要 groupBy 的属性
props: Array<string | ((item: T) => any)> | string | ((item: T) => any) = []
) {
// 如果传入了单个属性或者函数,先统一处理成数组
if (typeof props === 'string' || typeof props === 'function') {
props = [props]
}
const propCount = props.length
const zipData: Record<string, any> = {}
for (const item of list) {
// 需要一个变量用来记录当前属性对应的分组层级的 record 对象
// 这里的类型推断需要额外定义不少变量,省事来个 any
let data: any
for (let i = 0; i < propCount; ++i) {
const isLast = i === propCount - 1
const prop = props[i]
const value = typeof prop === 'function' ? prop(item) : item[prop as keyof T]
if (!data) {
if (!zipData[value]) {
// 如果到最后一层时,应该初始化一个数组来储存分组后的结果
zipData[value] = isLast ? [] : {}
}
data = zipData[value]
} else {
if (!data[value]) {
data[value] = isLast ? [] : {}
}
data = data[value]
}
}
data.push(item)
}
return zipData
}
这个函数返回结果的类型推断目前没想到特别好的办法,只能先用 Record<string, any>
处理。
根据条件对 List 的元素进行排序
这是这次的最后一个函数了(并不是),也是一个跟高频的场景。
根据我个人以往的经验,但凡遇到需要用表格展示数据的场合,都会出现根据某列对数据进行排序的需求。
对于只针对单一属性的排序,我相信大家应该倒着写都能写出来了,对于一个通用函数当然是需要支持多列的排序了(作为最后一个函数,我直接上完整代码给大家自己读一读):
import { isObject } from './is'
// 支持细粒度定制某个属性的排序规则
interface SortOptions<T = string> {
key: T,
method?: (prev: any, next: any) => number, // 排序的方法
accessor?: (...args: any[]) => any, // 读取属性的方法
type?: 'asc' | 'desc',
params?: any[] // 传入读取器的额外参数
}
// 默认的排序方法
const defaultSortMethod = (prev: any, next: any) => {
if (Number.isNaN(Number(prev) - Number(next))) {
return String(prev).localeCompare(next)
}
return prev - next
}
function sortByProps<T = any>(
list: T[],
props: keyof T | SortOptions<keyof T> | (keyof T | SortOptions<keyof T>)[]
) {
if (
!list.sort ||
(isObject<SortOptions>(props) && !props.key) ||
!(props as string | SortOptions[]).length
) {
return list
}
const sortedList = Array.from(list)
if (!Array.isArray(props)) {
props = [props]
}
const formattedProps = props
.map(
value =>
(typeof value === 'string'
? {
key: value,
method: defaultSortMethod,
type: 'asc'
}
: value) as SortOptions<keyof T>
)
.map(value => {
if (typeof value.accessor !== 'function') {
value.accessor = (data: T) => data[value.key]
}
if (typeof value.method !== 'function') {
value.method = defaultSortMethod
}
value.params = Array.isArray(value.params) ? value.params : []
return value as Required<SortOptions>
})
sortedList.sort((prev, next) => {
let lastResult = 0
for (const prop of formattedProps) {
const { method, type, accessor, params } = prop
const desc = type === 'desc'
const result = method(accessor(prev, ...params), accessor(next, ...params))
lastResult = desc ? -result : result
// 若不为0则无需进行下一层排序
if (lastResult) break
}
return lastResult
})
return sortedList
}
List 结构与 Tree 结构的互转
这里引用一下我在两年多前的一篇文章:js将扁平结构数据转换为树形结构
里面解析了将列表数据转树形结构的几种方式,不过是 js 写的,最后的合集会贴上 ts 版本。
然后在合集里会付上将树形结构展平成列表结构的方法,采用的是循环取代递归的方式,树展平的使用场景相对较少,就不细说了。
方法合集
没有细致校对,如果有一丢丢小错自行修复一下~
import { isDefined, isObject, isFunction } from './is'
/**
* 根据数组元素中某个或多个属性的值转换为映射
* @param list - 需要被转换的数组
* @param prop - 需要被转换的属性或提供一个读取方法
* @param accessor - 映射的值的读取方法,默认返回元素本身
*/
export function transformListToMap<T = any, K = T>(
list: T[],
prop: keyof T | ((item: T) => any),
accessor: (item: T) => K = v => v as any
): Record<string, K> {
const map = {} as Record<string, any>
if (!isDefined(prop)) return map
const propAccessor = isFunction(prop) ? prop : (item: T) => item[prop]
list.forEach(item => {
const key = propAccessor(item)
if (isDefined(key)) {
map[key] = accessor(item)
}
})
return map
}
/**
* 移除数组中的某个元素
* @param array - 需要被移除元素的数组
* @param item - 需要被移除的元素, 或一个查找方法,如果元素为函数时则需要做一层简单包装
* @param isFn - 标记数组的元素是否为函数
*/
export function removeArrayItem<T = any>(
array: T[],
item: T | ((item: T) => boolean),
isFn = false
): T | null {
let index = -1
if (isFn || typeof item !== 'function') {
index = array.findIndex(current => current === item)
} else {
index = array.findIndex(item as (item: T) => boolean)
}
if (~index) {
return array.splice(index, 1)[0]
}
return null
}
/**
* 移除数组中的某个或多个元素
* @param array - 需要被移除元素的数组
* @param items - 需要被移除的元素, 或一个查找方法
* @param isFn - 标记数组的元素是否为函数
*/
function removeArrayItems<T = any>(
array: T[],
items: T | T[] | ((item: T) => boolean),
isFn = false
): T[] {
const multiple = Array.isArray(items)
if (!multiple && (isFn || typeof items !== 'function')) {
const index = array.findIndex(current => current === items)
if (~index) {
return array.splice(index, 1)
}
} else {
let filterFn: (item: T) => boolean
if (multiple) {
const removedSet = new Set(items)
filterFn = item => removedSet.has(item)
} else {
filterFn = items as (item: T) => boolean
}
const originArray = Array.from(array)
const removedItems: T[] = []
array.length = 0
originArray.forEach(item => (filterFn(item) ? removedItems : array).push(item))
return removedItems
}
return []
}
/**
* 按照一定顺序的属性对数据进行分组
* @param list - 需要分数的数据
* @param props - 需要按顺序分组的属性
*/
export function groupByProps<T = any>(
list: T[],
props: Array<string | ((item: T) => any)> | string | ((item: T) => any) = []
): Record<string, T[]> {
if (typeof props === 'string' || typeof props === 'function') {
props = [props]
}
const propCount = props.length
const zipData: Record<string, any> = {}
for (const item of list) {
let data
for (let i = 0; i < propCount; ++i) {
const isLast = i === propCount - 1
const prop = props[i]
const value = typeof prop === 'function' ? prop(item) : item[prop as keyof T]
if (!data) {
if (!zipData[value]) {
zipData[value] = isLast ? [] : {}
}
data = zipData[value]
} else {
if (!data[value]) {
data[value] = isLast ? [] : {}
}
data = data[value]
}
}
data.push(item)
}
return zipData
}
export interface TreeOptions<T = string> {
keyField?: T,
childField?: T,
parentField?: T,
rootId?: any
}
/**
* 转换扁平结构为树形结构
* @param list - 需要转换的扁平数据
* @param options - 转化配置项
*/
export function transformTree<T = any>(list: T[], options: TreeOptions<keyof T> = {}) {
const {
keyField = 'id' as keyof T,
childField = 'children' as keyof T,
parentField = 'parent' as keyof T,
rootId = null
} = options
const hasRootId = isDefined(rootId) && rootId !== ''
const tree: T[] = []
const record = new Map<T[keyof T], T[]>()
for (let i = 0, len = list.length; i < len; ++i) {
const item = list[i]
const id = item[keyField]
if (hasRootId ? id === rootId : !id) {
continue
}
if (record.has(id)) {
(item as any)[childField] = record.get(id)!
} else {
(item as any)[childField] = []
record.set(id, (item as any)[childField])
}
if (item[parentField] && (!hasRootId || item[parentField] !== rootId)) {
const parentId = item[parentField]
if (!record.has(parentId)) {
record.set(parentId, [])
}
record.get(parentId)!.push(item)
} else {
tree.push(item)
}
}
return tree
}
/**
* 转换树形结构为扁平结构
* @param tree - 需要转换的树形数据
* @param options - 转化配置项
*/
export function flatTree<T = any>(tree: T[], options: TreeOptions<keyof T> = {}) {
const {
keyField = 'id' as keyof T,
childField = 'children' as keyof T,
parentField = 'parent' as keyof T,
rootId = null
} = options
const hasRootId = isDefined(rootId) && rootId !== ''
const list: T[] = []
const loop = [...tree]
let idCount = 1
while (loop.length) {
const item = loop.shift()!
let id
let children: any[] = []
const childrenValue = item[childField]
if (Array.isArray(childrenValue) && childrenValue.length) {
children = childrenValue
}
if (item[keyField]) {
id = item[keyField]
} else {
id = idCount++
}
if (hasRootId ? item[parentField] === rootId : !item[parentField]) {
(item as any)[parentField] = rootId
}
for (let i = 0, len = children.length; i < len; ++i) {
const child = children[i]
child[parentField] = id
loop.push(child)
}
list.push(item)
}
return list
}
export interface SortOptions<T = string> {
key: T,
method?: (prev: any, next: any) => number,
accessor?: (...args: any[]) => any,
type?: 'asc' | 'desc',
params?: any[] // 传入读取器的额外参数
}
const defaultSortMethod = (prev: any, next: any) => {
if (Number.isNaN(Number(prev) - Number(next))) {
return String(prev).localeCompare(next)
}
return prev - next
}
/**
* 根据依赖的属性逐层排序
* @param list - 需要排序的数组
* @param props - 排序依赖的属性 key-属性名 method-排序方法 accessor-数据获取方法 type-升降序
*/
export function sortByProps<T = any>(
list: T[],
props: keyof T | SortOptions<keyof T> | (keyof T | SortOptions<keyof T>)[]
) {
if (
!list.sort ||
(isObject<SortOptions>(props) && !props.key) ||
!(props as string | SortOptions[]).length
) {
return list
}
const sortedList = Array.from(list)
if (!Array.isArray(props)) {
props = [props]
}
const formattedProps = props
.map(
value =>
(typeof value === 'string'
? {
key: value,
method: defaultSortMethod,
type: 'asc'
}
: value) as SortOptions<keyof T>
)
.map(value => {
if (typeof value.accessor !== 'function') {
value.accessor = (data: T) => data[value.key]
}
if (typeof value.method !== 'function') {
value.method = defaultSortMethod
}
value.params = Array.isArray(value.params) ? value.params : []
return value as Required<SortOptions>
})
sortedList.sort((prev, next) => {
let lastResult = 0
for (const prop of formattedProps) {
const { method, type, accessor, params } = prop
const desc = type === 'desc'
const result = method(accessor(prev, ...params), accessor(next, ...params))
lastResult = desc ? -result : result
// 若不为0则无需进行下一层排序
if (lastResult) break
}
return lastResult
})
return sortedList
}
往期传送门:
更新:
最后来推荐一下我的个人开源项目 Vexip UI - GitHub
一个比较齐全的 Vue3 组件库,支持全面的 css 变量,内置暗黑主题,全量 TypeScript 和组合式 Api,其特点是所有组件几乎每个属性都支持通过配置(传一个对象)来修改其默认值,这应该是目前其他组件库不具备的特性~
现正招募小伙伴来使用或者参与维护与发展这个项目,我一个人的力量非常有限,文档、单元测试、服务端渲染支持、周边插件、使用案例等等,只要你有兴趣都可以从各个切入点参与进来,非常欢迎~
这几期【封装小技巧】的内容源码都包含在了 @vexip-ui/utils
包下面,GitHub,这个包也有单独发布,不过目前还没有 Api 文档,可能需要直接查阅源码食用~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。