上一主题 下一主题
ScriptCat,新一代的脚本管理器脚本站,与全世界分享你的用户脚本油猴脚本开发指南教程目录
返回列表 发新帖

[radash 源码解析#3] `radash.group` 的使用和源码实现

[复制链接]
  • TA的每日心情
    难过
    2024-4-24 18:57
  • 签到天数: 13 天

    [LV.3]偶尔看看II

    17

    主题

    26

    回帖

    248

    积分

    荣誉开发者

    积分
    248

    荣誉开发者油中2周年生态建设者喜迎中秋

    发表于 2024-4-26 00:04:06 | 显示全部楼层 | 阅读模式

    [radash 源码解析#3] radash.group 的使用和源码实现

    0. 前言

    内容先行预览:

    • radash.group() 函数的说明和使用方法
    • TypeScript 类型体操之对象属性名的索引类型, 类型 in 运算符, 类型工具 Record<Keys, Type>, 类型工具 Partial<Type>
    • Object.groupBy() 方法的使用

    1. 说明

    函数说明

    radash.group() 函数是一个将数组分类/分组, 输出为一个包含分类/分组结果对象.

    这种分组方法在原生其实是存在的, Object.groupBy() 也同样实现了数组分组的方法. 而且它的用法更加强大, 在本文的后面章节也会简单介绍 Object.groupBy() 的使用. radash.group() 可以当成 Object.groupBy() 全面兼容之前的过渡方法使用.

    Object.groupBy() 方法是一项实验性内容, 于 2023 年开始支持, 并在 2024 年在主流浏览器全面支持. 不过如果使用的 node 版本过低 (<= 20, node@21 若不支持需更新到最新版), 是无法在 node 环境中使用的.

    TypeScript 打开 Object.groupBy() 支持, 需要在 tsconfig.json 文件中的 lib 选项中的 ES 版本支持更改为 ESNext , 2025年开始可以改成 ES2024 .

    // tsconfig.json
    {
     "lib": [
         "ESNext"
     ]
    }

    2. 使用

    用法

    引入 radash.group()

    import { group } from 'radash';

    使用 radash.group()

    const groupObj = group( list, getGroupIdFn );

    参数

    • list
      一个将进行元素分组的数组.
    • getGroupIdFn:
      对数组中的每个元素执行的函数. 它应该返回一个值, 可以被强制转换成属性键 (字符串, 数字或 symbol) , 用于指示当前元素所属的分组. 该函数被调用时将传入以下参数:

    item: 数组中当前正在处理的元素.

    返回值

    一个带有所有分组属性的对象, 每个属性都分配了一个包含相关组元素的数组.

    示例

    单属性分组:

    import { group } from 'radash';
    
    const inventory = [
        { name: '芦笋', type: '蔬菜', quantity: 5 },
        { name: '香蕉', type: '水果', quantity: 0 },
        { name: '山羊', type: '肉', quantity: 23 },
        { name: '樱桃', type: '水果', quantity: 5 },
        { name: '鱼', type: '肉', quantity: 22 },
    ];
    
    const result = group( inventory, ( item ) => item.type );
    
    console.log( result );
    /* 结果是:
    {
      蔬菜: [
        { name: "芦笋", type: "蔬菜", quantity: 5 },
      ],
      水果: [
        { name: "香蕉", type: "水果", quantity: 0 },
        { name: "樱桃", type: "水果", quantity: 5 }
      ],
      肉: [
        { name: "山羊", type: "肉", quantity: 23 },
        { name: "鱼", type: "肉", quantity: 22 }
      ]
    }
    */

    多属性分组

    const inventory = [
        { name: '芦笋', type: '蔬菜', quantity: 5 },
        { name: '香蕉', type: '水果', quantity: 0 },
        { name: '山羊', type: '肉', quantity: 23 },
        { name: '樱桃', type: '水果', quantity: 5 },
        { name: '鱼', type: '肉', quantity: 22 },
        { name: '鱼', type: '肉', quantity: 55 },
    ];
    
    const result = group( inventory, ( { name,type } ) => `${ name }-${ type }` );
    
    console.log( result );
    /* 结果是:
    {
      '芦笋-蔬菜': [ { name: '芦笋', type: '蔬菜', quantity: 5 } ],
      '香蕉-水果': [ { name: '香蕉', type: '水果', quantity: 0 } ],
      '山羊-肉': [ { name: '山羊', type: '肉', quantity: 23 } ],
      '樱桃-水果': [ { name: '樱桃', type: '水果', quantity: 5 } ],
      '鱼-肉': [
        { name: '鱼', type: '肉', quantity: 22 },
        { name: '鱼', type: '肉', quantity: 55 }
      ]
    }
    */

    3. 源码实现 - TypeScript

    Step - 1: 通过 JavaScript 实现数组分类

    const group = ( items, getGroupIdFn ) => {
        // 定义一个空对象result
        const result = {};
        // 遍历items数组
        for ( let item of items ) {
            // 获取callback函数的返回值key
            const key = callback( item );
    
            // 如果result中不存在key属性,则赋值为空数组
            result[ key ] ||= [];
    
            // 将item添加到result[key]中
            result[ key ].push( item );
        }
        // 返回result对象
        return result;
    };

    Step - 2: 添加 TypeScript 类型描述

    1. 定义参数的类型

    首先由于不知道传入数组是什么类型的数组, 所以给数组 items 传入一个泛型 T 约束约束数组的类型.

    由于在函数进行过程中, 数组 items 的值是不会改变了, 所以我们可以给他添加上一个 readonly 标示符表示该属性是只读的.

    因为约束的类型, 所以用于传入类型的回调函数 getGroupIdFn 的参数的类型也明确了, 就是泛型 T . getGroupIdFn() 函数将返回一个字符串(string) / 数字(number) / symbol 用于传入用于分组的键名.

    const group = <T>( 
        items: readonly T[], 
        getGroupIdFn: ( item: T ) => string | number | symbol 
    ) => {
        /* code */
    }

    对于我前面文章讲过的类型通常我会一笔略过, 比如上文提到的泛型, 如果不了解可以去看我之前的文章, 或者看文末的附页中给出的参考资料.


    2. 定义函数的返回值

    group 函数的返回值是一个对象, 键(key)是第一步中 getGroupIdFn() 的返回值, 值(value)是一个新的 T[] . 所以这里添加了一个新的泛型 Key , 并将其的类型约束为 string | number | symbol .

    const group = <T, Key extends string | number | symbol>(
        items: readonly T[],
        getGroupIdFn: ( item: T ) => Key,
    ) => {
        /* code */
    };

    然后我们就可以声明返回值类型了: { [prop in string | symbol | number]?: T[] }.

    首先, 因为我们不知道到底有多少个属性, 所以我们需要使用到对象的属性名索引类型声明.
    也就是定义一个对象 { } , 里面的属性不写入具体的名称, 而是使用一对方括号表示, 在方括号里面就可以写 [ 任意属性名: 属性类型 ]. 如: { [prop: string]: any } 就表示任意类型为字符串(string)的属性名, 并且其值为 any .
    如果是方括号里的值是复合类型, 比如 Key 就是一个 string | number | symbol 复合类型, 就不能直接使用冒号, 而要使用 in 运算符, 表示将复合类型中的类型依次取出进行类型声明.
    所以类型声明就是: { [prop in Key]: T[] } .

    因为传出的对象可能是一个空对象, 所以我们将对象值添加上可选声明 ? : { [prop in Key]?: T[] } .

    const group = <T, Key extends string | number | symbol>(
        items: readonly T[],
        getGroupIdFn: ( item: T ) => Key,
    ): { [prop in Key]?: T[] } => {
        /* code */
    };

    如果返回值写成这样, 类型声明可读性不高, 所以使用类型工具重写一下:

    类型工具: Record<Keys, Type> 返回一个对象, 对象的键的类型是 Keys , 值的类型是 Type .
    所以 { [prop in Key]: T[] } 可以重写成 Record<Key, T[]>

    类型工具: Partial<Type> 返回一个新类型, 新类型中将 Type 中的所有属性变为可选属性.
    所以 { [prop in Key]?: T[] } 可以重写成 Partial<Record<Key, T[]>> .

    const group = <T, Key extends string | number | symbol>(
        items: readonly T[],
        getGroupIdFn: ( item: T ) => Key,
    ): Partial<Record<Key, T[]>> => {
        /* code */
    };


    3. 在实际返回值中注明类型

    result 标注上类型 Partial<Record<Key, T[]>> 即可.

    const group = <T, Key extends string | number | symbol>(
        items: readonly T[],
        getGroupIdFn: ( item: T ) => Key,
    ): Partial<Record<Key, T[]>> => {
        // 定义一个空对象result, 并声明其类型为 `Partial<Record<Key, T[]>>`
        const result: Partial<Record<Key, T[]>> = {};
    
        for ( let item of items ) {
            const key = getGroupIdFn( item );
    
            result[ key ] ||= [];
    
            // 这里会报错 `result[ key ]` 可能为空, 但是因为上面已经写入数组了, 不可能为空, 所以直接用类型断言就可以了
            ( <T[]> result[ key ] ).push( item );
        }
    
        // 返回result对象
        return result;
    };

    radash 源码是使用了 Array.reduce() 直接将对象视为 Record<Key, T[]> , 所以不用进行额外的类型断言.

    4. radash 源码

    TypeScript

    /**
     * Sorts an array of items into groups. The return value is a map where the keys are
     * the group ids the given getGroupId function produced and the value is an array of
     * each item in that group.
     */
    export const group = <T, Key extends string | number | symbol>(
        array: readonly T[],
        getGroupId: ( item: T ) => Key,
    ): Partial<Record<Key, T[]>> => {
        return array.reduce( ( acc, item ) => {
            const groupId = getGroupId( item );
            if ( !acc[ groupId ] ) acc[ groupId ] = [];
            acc[ groupId ].push( item );
            return acc;
        }, {} as Record<Key, T[]> );
    };

    JavaScript

    const group = ( array, getGroupId ) => {
        return array.reduce( ( acc, item ) => {
            const groupId = getGroupId( item );
            if ( !acc[ groupId ] )
                acc[ groupId ] = [];
            acc[ groupId ].push( item );
            return acc;
        }, {} );
    };

    5. Object.groupBy()

    Object.groupBy() 的参数和返回值和 radash.group() 一样, 区别在于:

    • 第一个参数不仅可以接受数组, 只要是可迭代对象就都可以接收, 比如 Set / Map 等.
    • 第二个参数具有第二个参数, index 索引, 其行为和 Array.forEach() 类似.

    修改稍微一下第三部分的代码实现就可以实现 Object.groupBy() 的效果:

    const groupBy = <Key extends PropertyKey, T>(
        items: Iterable<T>,
        getGroupIdFn: ( item: T, index: number ) => Key,
    ): Partial<Record<Key, T[]>> => {
        // 定义一个空对象result, 并声明其类型为 `Partial<Record<Key, T[]>>`
        const result: Partial<Record<Key, T[]>> = {};
    
        let index = 0;
        for ( let item of items ) {
            const key = getGroupIdFn( item, index++ );
    
            result[ key ] ||= [];
    
            // 这里会报错 `result[ key ]` 可能为空, 但是因为上面已经写入数组了, 不可能为空, 所以直接用类型断言就可以了
            ( <T[]> result[ key ] ).push( item );
        }
    
        // 返回result对象
        return result;
    };

    附页




    已有1人评分好评 油猫币 理由
    王一之 + 1 + 4 赞一个!

    查看全部评分 总评分:好评 +1  油猫币 +4 

  • TA的每日心情
    开心
    2024-3-13 10:14
  • 签到天数: 211 天

    [LV.7]常住居民III

    296

    主题

    3925

    回帖

    3844

    积分

    管理员

    积分
    3844

    管理员荣誉开发者油中2周年生态建设者喜迎中秋油中3周年挑战者 lv2

    发表于 2024-4-26 10:55:26 | 显示全部楼层
    感谢分享,学习了
    上不慕古,下不肖俗。为疏为懒,不敢为狂。为拙为愚,不敢为恶。/ 微信公众号:一之哥哥
    回复

    使用道具 举报

    发表回复

    本版积分规则

    快速回复 返回顶部 返回列表