callbackify, class adapter и async iterator
Контекст можно организовать через класс (alternative to closure). callbackify — обратная операция к promisify. async iterator над пользовательскими источниками. Создание Promise дорого — иногда thenable достаточно. Литеральные объекты in-place — критичный паттерн для V8-оптимизации.
Контекст не только через замыкания
Паттерны не только про функции. Люди привыкли, что паттерны это с классами. Контекст можно организовать при помощи класса.
Класс vs замыкание — два способа упаковать state + методы:
// Closure-style
const counter = (start = 0) => {
let count = start;
return {
inc: => ++count,
get: => count
};
};
// Class-style
class Counter {
#count;
constructor(start = 0) { this.#count = start; }
inc { return ++this.#count; }
get { return this.#count; }
}
Классы перестали быть сахаром
Очень долгое время классы в JS были синтаксическим сахаром над прототипами. Только не так давно классы в спецификации были выделены и реализованы в V8 как отдельная сущность.
В современном V8 классы оптимизируются лучше прототипного стиля.
Создание Promise дорого
Создание промисов в V8 — очень затратная по издержкам операция. Если есть возможность каким-либо образом обойти создание промисов — лучше это обойти.
За функционирование промиса во многом отвечает host-среда, а не сам V8. Это работает намного медленнее.
Thenable вместо Promise
function fastResolved(value) {
return {
then(resolve) {
resolve(value);
return this;
}
};
}
// Вместо Promise.resolve(value) — заметно быстрее на горячем пути
await fastResolved(42);
Если вам не нужна полная функциональность Promise, и можете обойтись методом then — это будет гораздо быстрее.
Thenable принимается везде, где принимаются промисы — async/await, Promise.all и т.д. Спецификация трактует их одинаково.
callbackify
Обратное к promisify:
const util = require('node:util');
const asyncFn = async (x) => x * 2;
const callbackFn = util.callbackify(asyncFn);
callbackFn(5, (err, result) => console.log(result)); // 10
Нужно когда: интегрируешь async-функцию в legacy API, ожидающий callback-стиль.
Async iterator над custom-источником
class Counter {
constructor(max) { this.max = max; this.current = 0; }
[Symbol.asyncIterator] {
return {
next: async => {
if (this.current < this.max) {
await new Promise(r => setTimeout(r, 100));
return { value: this.current++, done: false };
}
return { value: undefined, done: true };
}
};
}
}
for await (const n of new Counter(5)) console.log(n);
Symbol.asyncIterator + объект с async-методом next = async iterable.
Адаптер класса к callback
class FastResolver {
constructor(value) {
this.value = value;
}
then(resolve) {
queueMicrotask( => resolve(this.value));
return this;
}
}
await new FastResolver(42); // работает с await
Класс с методом then реализует thenable-контракт.
Литеральный объект in-place — оптимизация V8
// Хорошо: V8 видит, что объект локальный
this.resolve({ value, done: false });
// Хуже: объект через идентификатор
const item = { value, done: false };
this.resolve(item);
В this.resolve внутрь передаётся объект в литеральной форме. Это для V8 чрезвычайно важно — он сразу относится позитивно. Не нужно хранить объект или поддерживать дальше.
V8 может вообще не выделять объект на куче (scalar replacement), если видит литерал прямо в аргументе.
Bun на JavaScriptCore
Bun использовал в качестве основы JavaScriptCore, а не V8. Это сразу сделало его очень ограниченным с точки зрения оптимизаций. Изначальная идея — оптимальная реализация API Node.
Бенчмарки Bun хороши, но миграции с Node не происходит — слишком много несовместимостей. Узкое решение для маленьких приложений.
Node остаётся мейнстримом бэкенда на JS.
Связанные темы
- Promisify, callbackify, asyncify
- Thenable
- Асинхронные генераторы
- Callable Thenable Iterable Observable
- Async композиция и коллекторы
Ресурсы
- Лекция: fKvucLYtGu8