Reach out, please jump directly to 【Method Collection】~
List List
(including arrays and Set
or various array-like structures can be lists) data as the carrier of series data can be seen everywhere, it must be in the project development The processing of lists is indispensable, and there are more or less encapsulation of list processing methods. This time we will take a look at the common list processing functions.
If you think the article is helpful to you, I hope you can generously give a like~
List structure to Map structure
This can be said to be the most conventional and the most frequently processed. When we have a series of objects, we often encounter the corresponding objects based on the id
to find the corresponding objects. When the list is relatively large or the number of searches is relatively high多的时候, Array.find
来查找成本就会很高,于是将其转成id
key
,对象本身作为vulue
the Map structure.
// 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
}
However, at first glance, this method seems to be thin and cannot cover some relatively complex situations.
For example, when some combined values or calculated values are required as key
, then a single prop
cannot meet the situation.
For another example, when the part that needs to be used as value
is not the object itself, but some specific attributes or a combination or calculation of some attributes, then obviously the current parameters cannot be supported.
So we add 100 million more details to improve this function:
// 这是上一期写的 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
}
Remove a specific element from a List
I will post a piece of code first, I believe everyone should have written:
const list: any[] = [/* ... */]
const removedId = 'removedId'
const index = list.findIndex(item => item.id === removedId)
if (index !== -1) {
list.splice(index, 1)
}
Yes, it is also very common to delete specific elements in a list based on conditions, so we can also encapsulate it:
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
}
However, sometimes, we may need to delete multiple elements at the same time, and the above method cannot be overridden, so we need to modify it again:
// 在处理上,会直接操作源列表,并返回被移除的元素集合
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 []
}
The processing of the second half of the function may have some abstractions, which can be repeated slowly.
Although this function covers the case of multi-element deletion, when using a custom function to delete, you may only want to delete one element, but it will perform a complete traversal of the entire list, thus losing some performance.
Sort the elements in the List (GroupBy)
For example, there is the following set of data:
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 }
]
Now we need to sum the numbers of the same type
and the same name
, then we need to put the data according to type
and name
Two attributes are classified, which is a very classic GroupBy problem.
In fact, the first case of converting List to Map structure is essentially a GroupBy problem, but it is the simplest one-dimensional classification.
Of course, if we know that we can only classify according to two attributes, it is no problem to directly use a two-layer Map to store the results:
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
However, when we encapsulate general utility functions, we must consider covering possible situations as much as possible (the tenfold principle), so our starting point is to support infinite levels of grouping (as long as the memory is sufficient), and here we go directly to the complete code. :
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
}
The type inference of the result returned by this function has not yet thought of a particularly good way. It can only be processed first with Record<string, any>
.
Sort the elements of a List based on a condition
This is the last function this time ( not ), which is also a scene with high frequencies.
According to my personal past experience, whenever there is a need to display data in a table, there will be a need to sort the data according to a certain column.
For sorting only on a single attribute, I believe you should write upside down It can be written. Of course, for a general function, it needs to support the sorting of multiple columns (as the last function, I will directly read the complete code for everyone to read):
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
}
Interchange between List structure and Tree structure
Here is a quote from an article I wrote more than two years ago: js converts flat structure data into tree structure
It analyzes several ways to convert list data to tree structure, but it is written in js, and the final collection will be pasted with the ts version.
Then, the method of flattening the tree structure into a list structure will be added in the collection, which adopts the method of looping instead of recursion. There are relatively few usage scenarios of tree flattening, so I won't go into details.
Collection of methods
There is no detailed proofreading, if there are any small mistakes, please fix them by yourself~
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
}
Past Portals:
[Packaging Tips] Packaging of is series methods
renew:
[Encapsulation Tips] Encapsulation of digital processing functions
Finally, let me recommend my personal open source project Vexip UI - GitHub
A relatively complete Vue3 component library that supports comprehensive css variables, built-in dark theme, full TypeScript and combined Api, its feature is that almost every property of all components supports modifying its default value through configuration (passing an object). It should be a feature that other component libraries do not have at present~
I am currently recruiting small partners to use or participate in the maintenance and development of this project. My strength is very limited. Documentation, unit testing, server-side rendering support, peripheral plug-ins, use cases, etc., as long as you are interested, you can start from each Click to participate, very welcome~
The content source code of these issues of [Packaging Tips] is included in the @vexip-ui/utils
package, GitHub , this package is also released separately, but there is no Api documentation yet, you may need to check the source code directly to eat~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。