Привет, друзья!
Radash — это современная альтернатива Lodash, библиотека, предоставляющая набор часто используемых утилит (вспомогательных функций), реализованных на TypeScript. В данной статье мы вместе с вами разберем исходный код нескольких наиболее интересных утилит.
Репозиторий с кодом библиотеки находится здесь.
Обратите внимание: я позволил себе немного модифицировать отдельные утилиты для повышения читаемости и сокращения шаблонного кода. Также в нескольких местах пришлось поправить типы.
Для тех, кому интересно, вот большая коллекция сниппетов JavaScript.
Начнем с чего-нибудь попроще.
Генерации и извлечение произвольных значений
Функция для генерации произвольного целого числа в заданном диапазоне
const randomInt = (min: number, max: number) => ~~(Math.random() * (max - min + 1) + min);
Пример использования:
const randomInt = randomInt(1, 10); console.log(randomInt); // 6
Классика.
Функция для извлечения произвольного элемента из массива
const draw = <T>(arr: T[]): T | null => { // длина массива const len = arr.length; // если массив является пустым if (len === 0) { return null; } // генерируем произвольное целое число от первого до последнего индекса массива const i = random(0, len - 1); // возвращаем произвольный элемент return arr[i]; };
Пример использования:
const arr = [1, 2, 3, 4, 5]; const randomItem = draw(arr); console.log(randomItem); // 4
Если требуется возвращать только уникальные элементы, можно мутировать исходный массив следующим образом:
const draw = <T>(arr: T[], mutate?: boolean): T | null => { const len = arr.length; if (len === 0) { return null; } const i = random(0, len - 1); // метод `splice` мутирует исходный массив и возвращает массив извлеченных элементов return mutate ? arr.splice(i, 1)[0] : arr[i]; };
Пример использования:
const arr = [1, 2, 3, 4, 5]; const randomItems = []; while (arr.length) { const randomItem = draw(arr, true); randomItems.push(randomItem); } // получается своего рода перемешивание элементов исходного массива console.log(randomItems); // [2, 5, 1, 4, 3]
Это приводит нас к следующей функции.
Функция для перемешивания элементов массива
export const shuffle = <T>(arr: T[]): T[] => { return arr // преобразуем исходный массив в массив объектов со свойствами `random` и `value` .map((a) => ({ random: Math.random(), value: a })) // сортируем массив по полю `random` .sort((a, b) => a.random - b.random) // возвращаем оригинальные значения .map((a) => a.value); };
Пример использования:
const arr = [1, 2, 3, 4, 5]; const randomItems = shuffle(arr); console.log(randomItems); // [4, 2, 5, 1, 3]
Простейшая, но не очень правильная реализация такой функции выглядит следующим образом:
const shuffle = <T>(arr: T[]): T[] => arr.slice().sort(() => Math.random() - 0.5);
Более правильный вариант — Тасование Фишера-Йетса:
const shuffle = <T>(arr: T[]): T[] => { let len = arr.length; while (len) { const i = ~~(Math.random() * len--); [arr[len], arr[i]] = [arr[i], arr[len]]; } return arr; };
Функция для генерации произвольной строки заданной длины
export const uid = (length: number, symbols: string = "") => { // символы, используемые для генерации строки const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + symbols; // результат let _uid = ""; for (let i = 1; i <= length; i++) { // извлекаем случайный символ const i = random(0, chars.length - 1); // и добавляем его к результату _uid += characters[i]; } // возвращаем результат return _uid; };
Пример использования:
const randomStr = uid(10); console.log(randomStr); // xQZc1hzSqa
Простейшая реализация такой функции выглядит следующим образом:
// 10-11 символов // преобразуем число в строку и удаляем первые 2 символа - `0.` const uid = () => Math.random().toString(36).slice(2);
Обратите внимание: если значения, генерируемые такими функциями, планируется использовать в качестве идентификаторов DOM-элементов, то следует помнить, что id элемента не может начинаться с числа. Возможно, это как-то связано с тем, что такие элементы становятся свойствами глобального объекта window. Для решения данной задачи достаточно заменить первое число в строке на какую-нибудь букву, например, x:
// заменяем первое число буквой `x` const uid = () => Math.random().toString(36).slice(2).replace(/\d/, "x");
Двигаемся дальше.
Работа с массивами и объектами
Функция-генератор для формирования диапазона целых чисел
function* range( // начало диапазона start: number, // конец диапазона end: number, // шаг step: number = 1 ): Generator<number> { for (let i = start; i <= end; i += step) { yield i; // останавливаем генератор, если текущее значение плюс шаг больше конца диапазона if (i + step > end) break; } }
Пример использования:
const numsRange = range(1, 10, 2); console.log(numsRange.next().value); // 1 console.log(numsRange.next().value); // 3 console.log(...numsRange); // 5 7 9
Функция для генерации массива с диапазоном целых чисел
// функция возвращает массив из генератора const list = (start: number, end: number, step: number = 1): number[] => Array.from(range(start, end, step));
Пример использования:
const arrWithNumsRange = list(1, 10, 2); console.log(arrWithNumsRange); // [1, 3, 5, 7, 9]
Для генерации массива с числами двойной точности можно воспользоваться следующей функцией:
const list = ( start: number = 0, stop: number = 1, step: number = 0.1, // точность округления precision: number = 1 ) => Array.from({ length: (stop - start) / step + 1 }, (_, i) => // метод `toFixed` возвращает строку // конвертируем ее в число Number((start + i * step).toFixed(precision)) );
Пример использования:
const arrWithNumsRange = list(); console.log(arrWithNumsRange); // [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]
Функция для разделение массива на части
const chunk = <T>(arr: T[], size: number = 2): T[][] => { // определяем количество частей посредством деления длины массива // на указанный размер с округлением в большую сторону const chunks = Math.ceil(arr.length / size); // создаем новый массив с длиной, равной количеству частей // перебираем его элементы и возвращаем копии частей исходного массива указанного размера return Array.from({ length: chunks }, (_, i) => arr.slice(i * size, i * size + size) ); };
Пример использования:
const arr = [1, 2, 3, 4, 5]; const chunked = chunk(arr, 2); console.log(chunked); // [ [1, 2], [3, 4], [5] ]
Функция для преобразования массива в объект
const objectify = <T, Key extends string | number | symbol, Value = T>( arr: T[], // геттер ключей getKey: (i: T) => Key, // геттер значений, по умолчанию возвращающий элемент массива - объект getValue: (i: T) => Value = (i) => i as unknown as Value ): Record<Key, Value> => arr.reduce( // возвращается объект (acc, item) => ({ ...acc, // динамическое свойство [getKey(item)]: getValue(item), }), {} as Record<Key, Value> );
Пример использования:
const usersArr = [ { name: "Alice", age: 23 }, { name: "Bob", age: 32 }, ]; // геттер ключей const usersObj = objectify(usersArr, (u) => u.name); console.log(usersObj); /* { Alice: { name: 'Alice', age: 23 }, Bob: { name: 'Bob', age: 32 } } */ // геттеры ключей и значений const usersObj2 = objectify( usersArr, (u) => u.name, (u) => u.age ); console.log(usersObj2); // { Alice: 23, Bob: 32 }
Функция для преобразования объекта в массив
const listify = <TVal, TKey extends string | number | symbol, KRes>( obj: Record<TKey, TVal>, // функция-преобразователь toItem: (key: TKey, val: TVal) => KRes ) => { const entries = Object.entries(obj); if (entries.length === 0) return []; return entries.reduce((acc, entry) => { return [...acc, toItem(entry[0] as TKey, entry[1] as TVal)]; }, [] as KRes[]); };
Пример использования:
const usersObj = { alice: { age: 23, }, bob: { age: 32, }, }; // `key` - ключ/имя пользователя в нижнем регистре // `val` - объект пользователя: `{ age: number }` const usersArr = listify(usersObj, (key, val) => ({ // разворачиваем объект ...val, // "капитализируем" имя name: key[0].toUpperCase() + key.slice(1), })); console.log(usersArr); /* [ { age: 23, name: "Alice" }, { age: 32, name: "Bob" } ] */
Теперь кое-что более интересное.
Работа с функциями
Частичное применение функции
const partial = // (функция, основные параметры) (fn: Function, ...args: any[]) => // (дополнительные параметры) (...rest: any[]) => fn(...args, ...rest);
Пример использования:
// скидка const discount = 0.1; // функция для получение цены со скидкой, равной 10% стоимости товара const getPriceWithDiscount = partial( // функция для получения цены со скидкой (d: number, p: number) => p - p * d, // скидка discount ); // цена const price = 100; // цена со скидкой const priceWithDiscount = getPriceWithDiscount(price); console.log(priceWithDiscount); // 90
Функция для проксирования свойств объекта
Данная функция позволяет выполнять определенные операции при доступе к свойству объекта (реализовано с помощью объекта Proxy):
export const proxied = <T, K>( // обработчик, вызываемый при доступе к свойству handler: (prop: T) => K ): Record<string, K> => new Proxy( {}, { get: (_, prop: any) => handler(prop), } );
Пример использования:
const person = { firstName: "Harry", lastName: "Heman", }; const proxiedPerson = proxied((prop: keyof typeof person) => // переводим значение свойства в верхний регистр person[prop].toUpperCase() ); console.log(proxiedPerson.firstName); // HARRY
Функция мемоизации
Данная функция позволяет мемоизировать (memoize) результаты вызова другой функции:
// тип функции, передаваемой в качестве параметра функции мемоизации type Func<TArgs = any, KReturn = any | void> = (...args: TArgs[]) => KReturn; // тип кеша - объект со свойствами `exp` и `value` type Cache<T> = Record<string, { exp: number; value: T }>; // функция кеширования const memoize = <T>( // кеш - объект cache: Cache<T>, // кешируемая функция fn: Func<any, T>, // геттер ключа для доступа к кешу keyFunc: Func<string> | null, // время жизни кеша - срок, в течение которого кеш считается валидным ttl: number ) => { return function callWithMemo(...args: any): T { // ключ для доступа к кешу const key = keyFunc ? keyFunc(...args) : JSON.stringify({ args }); // имеется ли значения в кеше? const existing = cache[key]; // если имеется if (existing !== undefined) { // и время жизни кеша не истекло if (existing.exp > new Date().getTime()) { // возвращаем значение return existing.value; } } // вычисляем значение const result = fn(...args); // записываем его в кеш cache[key] = { exp: new Date().getTime() + ttl, value: result, }; // возвращаем значение return result; }; }; const memo = <TFunc extends Function>( // кешируемая функция fn: TFunc, // настройки { // геттер ключа для доступа к кешу key = null, // время жизни кеша ttl = 300, }: { key?: Func<any, string> | null; ttl?: number; } = {} ) => memoize({}, fn as any, key, ttl) as any as TFunc;
Пример использования:
const factorial = (n: number): number => (n <= 1 ? 1 : n * factorial(n - 1)); const memoizedFactorial = memo(factorial); console.time("t1"); // первый вызов мемоизированной функции - значение вычисляется memoizedFactorial(150); console.timeEnd("t1"); // 0.10... console.time("t2"); // второй вызов мемоизированной функции с тем же аргументом - значение доставляется из кеша memoizedFactorial(150); console.timeEnd("t2"); // 0.01...
Дебаунсинг и троттлинг
Простыми словами: дебаунсинг (debouncing) — это когда функция выполняется один раз по истечении указанного времени с момента последнего вызова, независимо от количества ее вызовов, а троттлинг (throttling) — это когда в течение определенного времени функция выполняется только один раз, несмотря на количество ее вызовов (обычно функция выполняется в начале указанного периода).
Начнем с дебаунсинга:
// функция принимает коллбек, вызываемый по истечении указанного времени, // и задержку в мс export const debounce = (fn: Function, ms: number) => { let timer: any = null; const debounced = (...args: any[]) => { // очищаем таймер при каждом вызове функции clearTimeout(timer); timer = setTimeout(() => { // выполняем коллбек fn(...args); // очищаем таймер clearTimeout(timer); }, ms); }; return debounced; };
Пример использования:
<p id="par">0</p> <button id="btn">click</button>
// получаем ссылку на параграф const par = document.getElementById("par"); // счетчик let clicks = 0; // обработчик клика const onClick = () => { // увеличиваем значение счетчика clicks += 1; // выводим значение счетчика в качестве текста параграфа (par as HTMLParagraphElement).textContent = clicks.toString(); }; // дебаунсинг const debouncedOnClick = debounce(onClick, 1000); // получаем ссылку на кнопку const btn = document.getElementById("btn"); // регистрируем обработчик (btn as HTMLButtonElement).onclick = throttledOnClick;
Сколько бы раз мы не нажали кнопку, значение счетчика увеличится только на 1 по истечении 1 сек с момента последнего нажатия. Как правило, дебаунсинг применяется в отношении обработчиков таких событий, как scroll и mousemove (или touchmove).
Троттлинг:
// функция принимает коллбек, вызываемый один раз в течение указанного времени, // и интервал в мс export const throttle = (fn: Function, ms: number) => { // индикатор готовности let ready = true; const throttled = (...args: any[]) => { // если индикатор готовности имеет значение `false` if (!ready) return; // выполняем коллбек fn(...args); // обновляем индикатор ready = false; const timer = setTimeout(() => { // обновляем индикатор по истечении указанного времени ready = true; // очищаем таймер clearTimeout(timer); }, ms); }; return throttled; };
Пример использования:
// перепишем последний пример const throttledOnClick = throttle(onClick, 1000); const btn = document.getElementById("btn"); (btn as HTMLButtonElement).onclick = throttledOnClick;
Теперь сколько бы раз мы не нажимали кнопку, значение счетчика будет увеличиваться на 1 не чаще одного раза в сек. Троттлинг может применяться в отношении обработчиков таких событий, как keydown или mousedown.
Напоследок самое интересное.
Работа с асинхронными функциями
Функция для выполнения асинхронной функции
// тип аргументов type ArgumentsType<T> = T extends (...args: infer U) => any ? U : never; // тип результата выполнения промиса type UnwrapPromisify<T> = T extends Promise<infer U> ? U : T; // функция возвращает [ ошибка, результат ] // ошибка и результат могут иметь значение `null` export const tryit = <TFunction extends (...args: any) => any>( fn: TFunction ) => { return async ( ...args: ArgumentsType<TFunction> ): Promise<[Error | null, UnwrapPromisify<ReturnType<TFunction>> | null]> => { try { return [null, await fn(...(args as any))]; } catch (err) { return [err as any, null]; } }; };
Пример использования:
const getUsers = tryit(() => fetch("https://jsonplaceholder.typicode.com/users?_limit=2").then((r) => r.json() ) ); const [error, users] = await getUsers(); console.log(error); // null console.log(users); // [ [user], [user] ] // ошибка в урле const getUsers2 = tryit(() => fetch("https://jsonplaceholder.typicod.com/users?_limit=2").then((r) => r.json() ) ); const [error2, users2] = await getUsers2(); console.log(error2?.message); // Failed to fetch console.log(users2); // null
Функция для повторного выполнения асинхронной операции
Данная функция позволяет предпринимать несколько попыток выполнения асинхронной операции:
export const retry = async <TResponse>( // выполняемая операция - промис fn: (exit: (err: any) => void) => Promise<TResponse>, options: { // количество попыток times?: number; // задержка между попытками delay?: number | null; // экспоненциальная задержка backoff?: (count: number) => number; } ): Promise<TResponse | void> => { // по умолчанию предпринимается 3 попытки const times = options?.times ?? 3; const delay = options?.delay; const backoff = options?.backoff ?? null; for (const i of list(1, times)) { const [err, result] = (await tryit(fn)((err: any) => { throw { _exited: err }; })) as [any, TResponse]; // если ошибки нет, возвращаем результат if (!err) return result; // если возникла ошибка, выбрасываем ее if (err._exited) throw err._exited; // если количество попыток исчерпано, выбрасываем исключение if (i === times) throw err; // задержка между попытками if (delay) await sleep(delay); // экспоненциальная задержка if (backoff) await sleep(backoff(i)); } };
Пример использования:
// ошибка в урле const getUsers = () => fetch("https://jsonplaceholder.typicod.com/users?_limit=2").then((r) => r.json() ); await retry(getUsers, { delay: 1000 }); // после 3 попыток с задержкой в 1 сек выбрасывается исключение `Uncaught TypeError: Failed to fetch`
Функция для одновременного выполнения нескольких асинхронных операций
Данная функция позволяет выполнять несколько асинхронных операций за один раз (реализовано с помощью Promise.all()):
// тип результата выполнения асинхронной операции type WorkItemResult<K> = { index: number; result: K; error: any; }; // класс кастомной ошибки class AggregateError extends Error { errors: Error[]; constructor(errors: Error[]) { super(); this.errors = errors; } } // вспомогательная функция сортировки const sort = <T>( arr: T[], getter: (item: T) => number, desc = false ) => { if (!arr) return []; const asc = (a: T, b: T) => getter(a) - getter(b); const dsc = (a: T, b: T) => getter(b) - getter(a); return arr.slice().sort(desc === true ? dsc : asc); }; // вспомогательная функция разделения массива пополам // в зависимости от логического значения, возвращаемого переданной функцией `condition` const fork = <T>( arr: T[], condition: (item: T) => boolean ): [T[], T[]] => { if (!arr) return [[], []]; return arr.reduce( (acc, item) => { const [a, b] = acc; if (condition(item)) { return [[...a, item], b]; } else { return [a, [...b, item]]; } }, [[], []] as [T[], T[]] ); }; // основная функция const parallel = async <T, K>( // количество одновременно выполняемых асинхронных операций limit: number, // массив параметров для операции arr: T[], // операция fn: (item: T) => Promise<K> ): Promise<K[]> => { // преобразуем массив параметров в массив объектов const work = arr.map((item, index) => ({ index, item, })); // обрабатываем этот массив const processor = async (res: (value: WorkItemResult<K>[]) => void) => { // массив результатов const results: WorkItemResult<K>[] = []; while (true) { // берем последний элемент массива - метод `pop` мутирует исходный массив const next = work.pop(); // если элементы кончились, возвращаем результат if (!next) return res(results); // выполняем операцию, получаем результаты const [error, result] = await tryit(fn)(next.item); // помещаем результаты в массив results.push({ error, result: result as K, index: next.index, }); } }; // создаем очередь const queues = list(1, limit).map(() => new Promise(processor)); // ждем завершения всех операций const itemResults = (await Promise.all(queues)) as WorkItemResult<K>[][]; // сортируем массив результатов по индексам // и делим его по наличию ошибок const [errors, results] = fork( sort(itemResults.flat(), (r) => r.index), (x) => !!x.error ); // если имеются ошибки if (errors.length > 0) { // выбрасываем кастомное исключение throw new AggregateError(errors.map((error) => error.error)); } // иначе возвращаем массив результатов return results.map((r) => r.result); };
Пример использования:
// массив путей const urls = [ "https://jsonplaceholder.typicode.com/users/1", "https://jsonplaceholder.typicode.com/posts/1", "https://jsonplaceholder.typicode.com/todos/1", ]; // функция для отправки запроса по указанному урлу const fetcher = (url: string) => fetch(url).then((r) => r.json()); // данные const data = await parallel(3, urls, async (url) => await fetcher(url)); console.log(data); // [ [user], [post], [todo] ] const urls2 = [ "https://jsonplaceholder.typicode.com/users/1", // ошибка в урле "https://jsonplaceholder.typicod.com/posts/1", "https://jsonplaceholder.typicode.com/todos/1", ]; const [err, data2] = await tryit(parallel)( 3, urls2, // не хватает типа async (url) => await fetcher(url as string) ); console.log(data2); // null // не хватает типа console.log((err as AggregateError).errors); // [TypeError: Failed to fetch...] console.log((err as AggregateError).errors[0].message); // Failed to fetch
Пожалуй, это все, чем я хотел поделиться с вами в этой статье. Мы рассмотрели примерно половину утилит, предоставляемых Radash, остальные функции показались мне не такими интересными. Надеюсь, вы нашли для себя что-то полезное и не зря потратили время.
Благодарю за внимание и happy coding!
ссылка на оригинал статьи https://habr.com/ru/company/timeweb/blog/686824/

Добавить комментарий