Български

Овладейте новото изрично управление на ресурси в JavaScript с `using` и `await using`. Научете се да автоматизирате почистването, да предотвратявате изтичане на ресурси и да пишете по-чист и стабилен код.

Новата суперсила на JavaScript: Задълбочен поглед върху изричното управление на ресурси

В динамичния свят на разработката на софтуер, ефективното управление на ресурси е крайъгълен камък за изграждането на стабилни, надеждни и производителни приложения. В продължение на десетилетия JavaScript разработчиците разчитат на ръчни модели като try...catch...finally, за да гарантират, че критичните ресурси - като файлови манипулатори, мрежови връзки или сесии с бази данни - са правилно освободени. Макар и функционален, този подход често е многословен, склонен към грешки и бързо може да стане тромав, модел, понякога наричан "пирамида на обречеността" в сложни сценарии.

Влезте в смяна на парадигмата за езика: Изрично управление на ресурси (ERM). Финализирана в стандарта ECMAScript 2024 (ES2024), тази мощна функция, вдъхновена от подобни конструкции в езици като C#, Python и Java, въвежда декларативен и автоматизиран начин за обработка на почистването на ресурси. Чрез използването на новите ключови думи using и await using, JavaScript вече предоставя много по-елегантно и безопасно решение на едно вечно предизвикателство в програмирането.

Това изчерпателно ръководство ще ви отведе на пътешествие през изричното управление на ресурси в JavaScript. Ще проучим проблемите, които решава, ще анализираме основните му концепции, ще преминем през практически примери и ще разкрием разширени модели, които ще ви дадат възможност да пишете по-чист, по-устойчив код, без значение къде по света разработвате.

Старата гвардия: Предизвикателствата на ръчното почистване на ресурси

Преди да можем да оценим елегантността на новата система, първо трябва да разберем болезнените точки на старата. Класическият модел за управление на ресурси в JavaScript е блокът try...finally.

Логиката е проста: получавате ресурс в блока try и го освобождавате в блока finally. Блокът finally гарантира изпълнение, независимо дали кодът в блока try успее, се провали или се върне преждевременно.

Нека разгледаме един общ сценарий от страна на сървъра: отваряне на файл, запис на някои данни в него и след това гарантиране, че файлът е затворен.

Пример: Проста файлова операция с try...finally


const fs = require('fs/promises');

async function processFile(filePath, data) {
  let fileHandle;
  try {
    console.log('Отваряне на файл...');
    fileHandle = await fs.open(filePath, 'w');
    console.log('Запис във файл...');
    await fileHandle.write(data);
    console.log('Данните са записани успешно.');
  } catch (error) {
    console.error('Възникна грешка по време на обработката на файла:', error);
  } finally {
    if (fileHandle) {
      console.log('Затваряне на файл...');
      await fileHandle.close();
    }
  }
}

Този код работи, но разкрива няколко слабости:

Сега си представете да управлявате множество ресурси, като връзка с база данни и файлов манипулатор. Кодът бързо се превръща в вложено месиво:


async function logQueryResultToFile(query, filePath) {
  let dbConnection;
  try {
    dbConnection = await getDbConnection();
    const result = await dbConnection.query(query);

    let fileHandle;
    try {
      fileHandle = await fs.open(filePath, 'w');
      await fileHandle.write(JSON.stringify(result));
    } finally {
      if (fileHandle) {
        await fileHandle.close();
      }
    }
  } finally {
    if (dbConnection) {
      await dbConnection.release();
    }
  }
}

Тази влагане е трудна за поддръжка и мащабиране. Това е ясен сигнал, че е необходима по-добра абстракция. Това е точно проблемът, който изричното управление на ресурси е проектирано да реши.

Смяна на парадигмата: Принципите на изричното управление на ресурси

Изричното управление на ресурси (ERM) въвежда договор между обект на ресурс и JavaScript средата за изпълнение. Основната идея е проста: обектът може да декларира как трябва да бъде почистен и езикът предоставя синтаксис за автоматично извършване на това почистване, когато обектът излезе извън обхват.

Това се постига чрез два основни компонента:

  1. Протоколът за изхвърляне: Стандартен начин за обектите да определят собствената си логика за почистване, използвайки специални символи: Symbol.dispose за синхронно почистване и Symbol.asyncDispose за асинхронно почистване.
  2. Декларациите `using` и `await using`: Нови ключови думи, които обвързват ресурс с обхват на блок. Когато блокът бъде напуснат, методът за почистване на ресурса се извиква автоматично.

Основните концепции: `Symbol.dispose` и `Symbol.asyncDispose`

В основата на ERM са два нови добре известни символа. Обект, който има метод с един от тези символи като негов ключ, се счита за "ресурс за изхвърляне".

Синхронно изхвърляне с `Symbol.dispose`

Символът Symbol.dispose указва синхронен метод за почистване. Това е подходящо за ресурси, където почистването не изисква никакви асинхронни операции, като например синхронно затваряне на файлов манипулатор или освобождаване на заключване в паметта.

Нека създадем обвивка за временен файл, който се почиства сам.


const fs = require('fs');
const path = require('path');

class TempFile {
  constructor(content) {
    this.path = path.join(__dirname, `temp_${Date.now()}.txt`);
    fs.writeFileSync(this.path, content);
    console.log(`Създаден е временен файл: ${this.path}`);
  }

  // Това е синхронният метод за изхвърляне
  [Symbol.dispose]() {
    console.log(`Изхвърляне на временен файл: ${this.path}`);
    try {
      fs.unlinkSync(this.path);
      console.log('Файлът е изтрит успешно.');
    } catch (error) {
      console.error(`Неуспешно изтриване на файл: ${this.path}`, error);
      // Важно е да обработвате грешки и в рамките на dispose!
    }
  }
}

Всеки екземпляр на `TempFile` вече е ресурс за изхвърляне. Той има метод, означен със `Symbol.dispose`, който съдържа логиката за изтриване на файла от диска.

Асинхронно изхвърляне с `Symbol.asyncDispose`

Много модерни операции за почистване са асинхронни. Затварянето на връзка с база данни може да включва изпращане на команда `QUIT` по мрежата или клиент на опашка за съобщения може да се наложи да изпразни изходящия си буфер. За тези сценарии използваме `Symbol.asyncDispose`.

Методът, свързан със `Symbol.asyncDispose`, трябва да върне `Promise` (или да бъде `async` функция).

Нека моделираме фиктивна връзка с база данни, която трябва да бъде освободена обратно към пул асинхронно.


// Фиктивен пул от бази данни
const mockDbPool = {
  getConnection: () => {
    console.log('DB връзка е получена.');
    return new MockDbConnection();
  }
};

class MockDbConnection {
  query(sql) {
    console.log(`Изпълнение на заявка: ${sql}`);
    return Promise.resolve({ success: true, rows: [] });
  }

  // Това е асинхронният метод за изхвърляне
  async [Symbol.asyncDispose]() {
    console.log('Освобождаване на DB връзка обратно към пула...');
    // Симулирайте мрежово забавяне за освобождаване на връзката
    await new Promise(resolve => setTimeout(resolve, 50));
    console.log('DB връзката е освободена.');
  }
}

Сега всеки екземпляр на `MockDbConnection` е асинхронен ресурс за изхвърляне. Той знае как да се освободи асинхронно, когато вече не е необходим.

Новият синтаксис: `using` и `await using` в действие

С дефинираните от нас класове за изхвърляне вече можем да използваме новите ключови думи, за да ги управляваме автоматично. Тези ключови думи създават декларации с обхват на блок, точно като `let` и `const`.

Синхронно почистване с `using`

Ключовата дума `using` се използва за ресурси, които прилагат `Symbol.dispose`. Когато изпълнението на кода напусне блока, където е направена декларацията `using`, методът `[Symbol.dispose]()` се извиква автоматично.

Нека използваме нашия клас `TempFile`:


function processDataWithTempFile() {
  console.log('Влизане в блок...');
  using tempFile = new TempFile('Това са някои важни данни.');

  // Можете да работите с tempFile тук
  const content = fs.readFileSync(tempFile.path, 'utf8');
  console.log(`Четене от временен файл: "${content}"`);

  // Не е необходим код за почистване тук!
  console.log('...извършване на още работа...');
} // <-- tempFile.[Symbol.dispose]() се извиква автоматично точно тук!

processDataWithTempFile();
console.log('Блокът е напуснат.');

Резултатът ще бъде:

Влизане в блок...
Създаден е временен файл: /path/to/temp_1678886400000.txt
Четене от временен файл: "Това са някои важни данни."
...извършване на още работа...
Изхвърляне на временен файл: /path/to/temp_1678886400000.txt
Файлът е изтрит успешно.
Блокът е напуснат.

Вижте колко е чисто! Целият жизнен цикъл на ресурса е съдържащ се в рамките на блока. Ние го декларираме, използваме го и забравяме за него. Езикът се занимава с почистването. Това е огромно подобрение в четливостта и безопасността.

Управление на множество ресурси

Можете да имате множество декларации `using` в един и същ блок. Те ще бъдат изхвърлени в обратен ред на тяхното създаване (LIFO или "подобно на стек" поведение).


{
  using resourceA = new MyDisposable('A'); // Създаден първи
  using resourceB = new MyDisposable('B'); // Създаден втори
  console.log('Вътре в блок, използване на ресурси...');
} // resourceB се изхвърля първо, след това resourceA

Асинхронно почистване с `await using`

Ключовата дума `await using` е асинхронният аналог на `using`. Използва се за ресурси, които прилагат `Symbol.asyncDispose`. Тъй като почистването е асинхронно, тази ключова дума може да се използва само вътре в `async` функция или на най-високо ниво на модул (ако се поддържа очакване на най-високо ниво).

Нека използваме нашия клас `MockDbConnection`:


async function performDatabaseOperation() {
  console.log('Влизане в асинхронна функция...');
  await using db = mockDbPool.getConnection();

  await db.query('SELECT * FROM users');

  console.log('Операцията с базата данни е завършена.');
} // <-- await db.[Symbol.asyncDispose]() се извиква автоматично тук!

(async () => {
  await performDatabaseOperation();
  console.log('Асинхронната функция е завършена.');
})();

Резултатът демонстрира асинхронното почистване:

Влизане в асинхронна функция...
DB връзка е получена.
Изпълнение на заявка: SELECT * FROM users
Операцията с базата данни е завършена.
Освобождаване на DB връзка обратно към пула...
(изчаква 50ms)
DB връзката е освободена.
Асинхронната функция е завършена.

Точно както при `using`, синтаксисът `await using` обработва целия жизнен цикъл, но правилно `изчаква` асинхронния процес на почистване. Той може дори да обработва ресурси, които са само синхронно изхвърляеми - той просто няма да ги изчаква.

Разширени модели: `DisposableStack` и `AsyncDisposableStack`

Понякога простото определяне на обхвата на блокове на `using` не е достатъчно гъвкаво. Какво ще стане, ако трябва да управлявате група ресурси с живот, който не е обвързан с един лексикален блок? Или какво ще стане, ако се интегрирате със стара библиотека, която не произвежда обекти със `Symbol.dispose`?

За тези сценарии JavaScript предоставя два помощни класа: `DisposableStack` и `AsyncDisposableStack`.

`DisposableStack`: Гъвкавият мениджър за почистване

`DisposableStack` е обект, който управлява колекция от операции за почистване. Той сам по себе си е ресурс за изхвърляне, така че можете да управлявате целия му живот с блок `using`.

Той има няколко полезни метода:

Пример: Условно управление на ресурси

Представете си функция, която отваря лог файл само ако е изпълнено определено условие, но искате цялото почистване да се извърши на едно място в края.


function processWithConditionalLogging(shouldLog) {
  using stack = new DisposableStack();

  const db = stack.use(getDbConnection()); // Винаги използвайте DB

  if (shouldLog) {
    const logFileStream = fs.createWriteStream('app.log');
    // Отложете почистването за потока
    stack.defer(() => {
      console.log('Затваряне на поток от лог файлове...');
      logFileStream.end();
    });
    db.logTo(logFileStream);
  }

  db.doWork();

} // <-- Стекът се изхвърля, извиквайки всички регистрирани функции за почистване в LIFO ред.

`AsyncDisposableStack`: За асинхронния свят

Както може би се досещате, `AsyncDisposableStack` е асинхронната версия. Той може да управлява както синхронни, така и асинхронни изхвърляеми. Основният му метод за почистване е `.disposeAsync()`, който връща `Promise`, който се разрешава, когато всички асинхронни операции за почистване са завършени.

Пример: Управление на смес от ресурси

Нека създадем манипулатор на заявки към уеб сървър, който се нуждае от връзка с база данни (асинхронно почистване) и временен файл (синхронно почистване).


async function handleRequest() {
  await using stack = new AsyncDisposableStack();

  // Управлявайте асинхронен изхвърляем ресурс
  const dbConnection = await stack.use(getAsyncDbConnection());

  // Управлявайте синхронен изхвърляем ресурс
  const tempFile = stack.use(new TempFile('данни за заявката'));

  // Приемете ресурс от стар API
  const legacyResource = getLegacyResource();
  stack.adopt(legacyResource, () => legacyResource.shutdown());

  console.log('Обработка на заявка...');
  await doWork(dbConnection, tempFile.path);

} // <-- stack.disposeAsync() се извиква. Той правилно ще изчака асинхронното почистване.

`AsyncDisposableStack` е мощен инструмент за организиране на сложна логика за настройка и разглобяване по чист и предвидим начин.

Стабилна обработка на грешки със `SuppressedError`

Едно от най-фините, но значими подобрения на ERM е как обработва грешки. Какво се случва, ако грешка бъде хвърлена вътре в блока `using` и *друга* грешка бъде хвърлена по време на последващото автоматично изхвърляне?

В стария свят `try...finally` грешката от блока `finally` обикновено би презаписала или "потиснала" оригиналната, по-важна грешка от блока `try`. Това често правеше отстраняването на грешки невероятно трудно.

ERM решава това с нов глобален тип грешка: `SuppressedError`. Ако възникне грешка по време на изхвърляне, докато друга грешка вече се разпространява, грешката при изхвърляне е "потисната". Оригиналната грешка се хвърля, но сега има свойство `suppressed`, съдържащо грешката при изхвърляне.


class FaultyResource {
  [Symbol.dispose]() {
    throw new Error('Грешка по време на изхвърляне!');
  }
}

try {
  using resource = new FaultyResource();
  throw new Error('Грешка по време на операция!');
} catch (e) {
  console.log(`Захваната грешка: ${e.message}`); // Грешка по време на операция!
  if (e.suppressed) {
    console.log(`Потисната грешка: ${e.suppressed.message}`); // Грешка по време на изхвърляне!
    console.log(e instanceof SuppressedError); // false
    console.log(e.suppressed instanceof Error); // true
  }
}

Това поведение гарантира, че никога не губите контекста на първоначалния отказ, което води до много по-стабилни и лесни за отстраняване системи.

Практически случаи на употреба в екосистемата на JavaScript

Приложенията на изричното управление на ресурси са огромни и релевантни за разработчиците по целия свят, независимо дали работят в бекенда, фронтенда или в тестването.

Поддръжка на браузъри и среди за изпълнение

Като модерна функция, важно е да знаете къде можете да използвате изрично управление на ресурси. Към края на 2023 г. / началото на 2024 г. поддръжката е широко разпространена в най-новите версии на основните JavaScript среди:

За по-стари среди ще трябва да разчитате на транспайлери като Babel със съответните плъгини, за да трансформирате синтаксиса `using` и да полифилирате необходимите символи и класове на стека.

Заключение: Нова ера на безопасност и яснота

Изричното управление на ресурси в JavaScript е повече от просто синтактична захар; то е фундаментално подобрение на езика, което насърчава безопасността, яснотата и поддръжката. Чрез автоматизиране на досадния и склонен към грешки процес на почистване на ресурси, то освобождава разработчиците да се съсредоточат върху основната си бизнес логика.

Основните изводи са:

Докато започвате нови проекти или преработвате съществуващ код, помислете за приемане на този мощен нов модел. Това ще направи вашия JavaScript по-чист, вашите приложения по-надеждни и живота ви като разработчик малко по-лесен. Това е наистина глобален стандарт за писане на модерен, професионален JavaScript.