Dansk

Mestr JavaScripts nye eksplicitte ressourcestyring med `using` og `await using`. Lær at automatisere oprydning, forhindre ressource lækager og skrive renere, mere robust kode.

JavaScript's Nye Superkraft: Et Dybdegående Dyk Ned i Eksplicit Ressourcestyring

I den dynamiske verden af softwareudvikling er effektiv ressourcestyring en hjørnesten i opbygningen af robuste, pålidelige og performante applikationer. I årtier har JavaScript-udviklere været afhængige af manuelle mønstre som try...catch...finally for at sikre, at kritiske ressourcer – såsom filhåndtag, netværksforbindelser eller databasesessioner – frigives korrekt. Selvom denne tilgang er funktionel, er den ofte omstændelig, fejlbehæftet og kan hurtigt blive uhåndterlig, et mønster der undertiden omtales som \"doompyramiden\" i komplekse scenarier.

Indtræd et paradigmeskift for sproget: Eksplicit Ressourcestyring (ERM). Denne kraftfulde funktion, der blev færdiggjort i ECMAScript 2024 (ES2024) standarden og er inspireret af lignende konstruktioner i sprog som C#, Python og Java, introducerer en deklarativ og automatiseret måde at håndtere ressourceoprydning på. Ved at udnytte de nye using og await using nøgleord tilbyder JavaScript nu en langt mere elegant og sikker løsning på en tidløs programmeringsudfordring.

Denne omfattende guide vil tage dig med på en rejse gennem JavaScripts Eksplicitte Ressourcestyring. Vi vil udforske de problemer, den løser, dissekere dens kernekoncepter, gennemgå praktiske eksempler og afdække avancerede mønstre, der vil give dig mulighed for at skrive renere, mere modstandsdygtig kode, uanset hvor i verden du udvikler.

Den Gamle Garde: Udfordringerne ved Manuel Ressourceoprydning

Før vi kan værdsætte elegancen i det nye system, skal vi først forstå de problematiske punkter i det gamle. Det klassiske mønster for ressourcestyring i JavaScript er try...finally blokken.

Logikken er enkel: du erhverver en ressource i try-blokken, og du frigiver den i finally-blokken. finally-blokken garanterer udførelse, uanset om koden i try-blokken lykkes, fejler eller returnerer for tidligt.

Lad os overveje et almindeligt server-side scenario: at åbne en fil, skrive nogle data til den og derefter sikre, at filen lukkes.

Eksempel: En Simpel Filoperation med try...finally


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

async function processFile(filePath, data) {
  let fileHandle;
  try {
    console.log('Opening file...');
    fileHandle = await fs.open(filePath, 'w');
    console.log('Writing to file...');
    await fileHandle.write(data);
    console.log('Data written successfully.');
  } catch (error) {
    console.error('An error occurred during file processing:', error);
  } finally {
    if (fileHandle) {
      console.log('Closing file...');
      await fileHandle.close();
    }
  }
}

Denne kode fungerer, men den afslører flere svagheder:

Forestil dig nu at styre flere ressourcer, som en databaseforbindelse og et filhåndtag. Koden bliver hurtigt et indlejret rod:


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();
    }
  }
}

Denne indlejring er vanskelig at vedligeholde og skalere. Det er et klart signal om, at en bedre abstraktion er nødvendig. Dette er præcis det problem, som Eksplicit Ressourcestyring blev designet til at løse.

Et Paradigmeskift: Principperne for Eksplicit Ressourcestyring

Eksplicit Ressourcestyring (ERM) introducerer en kontrakt mellem et ressourceobjekt og JavaScript-runtime. Grundideen er enkel: et objekt kan erklære, hvordan det skal ryddes op, og sproget giver syntaks til automatisk at udføre denne oprydning, når objektet går ud af omfang.

Dette opnås gennem to hovedkomponenter:

  1. Den Disposable Protokol: En standardmåde for objekter til at definere deres egen oprydningslogik ved hjælp af specielle symboler: Symbol.dispose for synkron oprydning og Symbol.asyncDispose for asynkron oprydning.
  2. using og await using Deklarationer: Nye nøgleord, der binder en ressource til et blokomfang. Når blokken forlades, kaldes ressourcens oprydningsmetode automatisk.

Kernekoncepterne: Symbol.dispose og Symbol.asyncDispose

Kernen i ERM er to nye velkendte Symboler. Et objekt, der har en metode med et af disse symboler som nøgle, betragtes som en \"disposable ressource.\"

Synkron Disposal med Symbol.dispose

Symbol.dispose symbolet specificerer en synkron oprydningsmetode. Dette er velegnet til ressourcer, hvor oprydning ikke kræver asynkrone operationer, som at lukke et filhåndtag synkront eller frigive en hukommelseslås.

Lad os oprette en wrapper til en midlertidig fil, der rydder sig selv op.


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(`Created temp file: ${this.path}`);
  }

  // This is the synchronous disposable method
  [Symbol.dispose]() {
    console.log(`Disposing temp file: ${this.path}`);
    try {
      fs.unlinkSync(this.path);
      console.log('File deleted successfully.');
    } catch (error) {
      console.error(`Failed to delete file: ${this.path}`, error);
      // It's important to handle errors within dispose, too!
    }
  }
}

Enhver instans af `TempFile` er nu en disposable ressource. Den har en metode navngivet med `Symbol.dispose`, der indeholder logikken til at slette filen fra disken.

Asynkron Disposal med Symbol.asyncDispose

Mange moderne oprydningsoperationer er asynkrone. Lukning af en databaseforbindelse kan involvere at sende en `QUIT`-kommando over netværket, eller en meddelelseskø-klient skal muligvis tømme sin udgående buffer. Til disse scenarier bruger vi `Symbol.asyncDispose`.

Metoden, der er forbundet med `Symbol.asyncDispose`, skal returnere en `Promise` (eller være en `async` funktion).

Lad os modellere en mock databaseforbindelse, der skal frigives tilbage til en pulje asynkront.


// A mock database pool
const mockDbPool = {
  getConnection: () => {
    console.log('DB connection acquired.');
    return new MockDbConnection();
  }
};

class MockDbConnection {
  query(sql) {
    console.log(`Executing query: ${sql}`);
    return Promise.resolve({ success: true, rows: [] });
  }

  // This is the asynchronous disposable method
  async [Symbol.asyncDispose]() {
    console.log('Releasing DB connection back to the pool...');
    // Simulate a network delay for releasing the connection
    await new Promise(resolve => setTimeout(resolve, 50));
    console.log('DB connection released.');
  }
}

Nu er enhver `MockDbConnection`-instans en asynkron disposable ressource. Den ved, hvordan den frigiver sig selv asynkront, når den ikke længere er nødvendig.

Den Nye Syntaks: using og await using i Aktion

Med vores disposable klasser defineret kan vi nu bruge de nye nøgleord til at administrere dem automatisk. Disse nøgleord opretter blok-scoped deklarationer, ligesom `let` og `const`.

Synkron Oprydning med using

Nøgleordet `using` bruges til ressourcer, der implementerer `Symbol.dispose`. Når kodeudførelsen forlader den blok, hvor `using`-deklarationen blev foretaget, kaldes metoden `[Symbol.dispose]()` automatisk.

Lad os bruge vores `TempFile`-klasse:


function processDataWithTempFile() {
  console.log('Entering block...');
  using tempFile = new TempFile('This is some important data.');

  // You can work with tempFile here
  const content = fs.readFileSync(tempFile.path, 'utf8');
  console.log(`Read from temp file: "${content}"`);

  // No cleanup code needed here!
  console.log('...doing more work...');
} // <-- tempFile.[Symbol.dispose]() is called automatically right here!

processDataWithTempFile();
console.log('Block has been exited.');

Outputtet ville være:

Entering block...
Created temp file: /path/to/temp_1678886400000.txt
Read from temp file: "This is some important data."
...doing more work...
Disposing temp file: /path/to/temp_1678886400000.txt
File deleted successfully.
Block has been exited.

Se, hvor rent det er! Ressourcens hele livscyklus er indeholdt i blokken. Vi erklærer den, vi bruger den, og vi glemmer alt om den. Sproget håndterer oprydningen. Dette er en massiv forbedring af læsbarhed og sikkerhed.

Administration af Flere Ressourcer

Du kan have flere `using`-deklarationer i den samme blok. De vil blive disposed i omvendt rækkefølge af deres oprettelse (en LIFO- eller \"stack-lignende\" adfærd).


{
  using resourceA = new MyDisposable('A'); // Created first
  using resourceB = new MyDisposable('B'); // Created second
  console.log('Inside block, using resources...');
} // resourceB is disposed of first, then resourceA

Asynkron Oprydning med await using

Nøgleordet `await using` er det asynkrone modstykke til `using`. Det bruges til ressourcer, der implementerer `Symbol.asyncDispose`. Da oprydningen er asynkron, kan dette nøgleord kun bruges inde i en `async` funktion eller på øverste niveau af et modul (hvis top-level await understøttes).

Lad os bruge vores `MockDbConnection`-klasse:


async function performDatabaseOperation() {
  console.log('Entering async function...');
  await using db = mockDbPool.getConnection();

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

  console.log('Database operation complete.');
} // <-- await db.[Symbol.asyncDispose]() is called automatically here!

(async () => {
  await performDatabaseOperation();
  console.log('Async function has completed.');
})();

Outputtet demonstrerer den asynkrone oprydning:

Entering async function...
DB connection acquired.
Executing query: SELECT * FROM users
Database operation complete.
Releasing DB connection back to the pool...
(waits 50ms)
DB connection released.
Async function has completed.

Ligesom med `using` håndterer `await using`-syntaksen hele livscyklussen, men den `awaits` korrekt den asynkrone oprydningsproces. Den kan endda håndtere ressourcer, der kun er synkront disposable – den vil simpelthen ikke vente på dem.

Avancerede Mønstre: DisposableStack og AsyncDisposableStack

Nogle gange er den simple blok-scoping af `using` ikke fleksibel nok. Hvad hvis du har brug for at administrere en gruppe ressourcer med en levetid, der ikke er bundet til en enkelt leksikalsk blok? Eller hvad hvis du integrerer med et ældre bibliotek, der ikke producerer objekter med `Symbol.dispose`?

Til disse scenarier leverer JavaScript to hjælperklasser: `DisposableStack` og `AsyncDisposableStack`.

`DisposableStack`: Den Fleksible Oprydningsmanager

En `DisposableStack` er et objekt, der administrerer en samling af oprydningsoperationer. Den er selv en disposable ressource, så du kan administrere hele dens levetid med en `using`-blok.

Den har flere nyttige metoder:

Eksempel: Betinget Ressourcestyring

Forestil dig en funktion, der åbner en logfil kun hvis en bestemt betingelse er opfyldt, men du ønsker, at al oprydning skal ske ét sted til sidst.


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

  const db = stack.use(getDbConnection()); // Always use the DB

  if (shouldLog) {
    const logFileStream = fs.createWriteStream('app.log');
    // Defer the cleanup for the stream
    stack.defer(() => {
      console.log('Closing log file stream...');
      logFileStream.end();
    });
    db.logTo(logFileStream);
  }

  db.doWork();

} // <-- The stack is disposed, calling all registered cleanup functions in LIFO order.

`AsyncDisposableStack`: Til den Asynkrone Verden

Som du måske gætter, er `AsyncDisposableStack` den asynkrone version. Den kan administrere både synkrone og asynkrone disposables. Dens primære oprydningsmetode er `.disposeAsync()`, som returnerer en `Promise`, der løses, når alle asynkrone oprydningsoperationer er fuldført.

Eksempel: Administration af en Blanding af Ressourcer

Lad os oprette en webserver request handler, der har brug for en databaseforbindelse (asynkron oprydning) og en midlertidig fil (synkron oprydning).


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

  // Manage an async disposable resource
  const dbConnection = await stack.use(getAsyncDbConnection());

  // Manage a sync disposable resource
  const tempFile = stack.use(new TempFile('request data'));

  // Adopt a resource from an old API
  const legacyResource = getLegacyResource();
  stack.adopt(legacyResource, () => legacyResource.shutdown());

  console.log('Processing request...');
  await doWork(dbConnection, tempFile.path);

} // <-- stack.disposeAsync() is called. It will correctly await async cleanup.

`AsyncDisposableStack` er et kraftfuldt værktøj til at orkestrere kompleks opsætning og nedrivningslogik på en ren, forudsigelig måde.

Robust Fejlhåndtering med `SuppressedError`

En af de mest subtile, men betydningsfulde forbedringer af ERM er, hvordan den håndterer fejl. Hvad sker der, hvis en fejl kastes inde i `using`-blokken, og *en anden* fejl kastes under den efterfølgende automatiske disposal?

I den gamle `try...finally`-verden ville fejlen fra `finally`-blokken typisk overskrive eller \"undertrykke\" den oprindelige, vigtigere fejl fra `try`-blokken. Dette gjorde ofte debugging utroligt vanskelig.

ERM løser dette med en ny global fejltype: `SuppressedError`. Hvis en fejl opstår under disposal, mens en anden fejl allerede propagerer, \"undertrykkes\" disposal-fejlen. Den oprindelige fejl kastes, men den har nu en `suppressed`-egenskab, der indeholder disposal-fejlen.


class FaultyResource {
  [Symbol.dispose]() {
    throw new Error('Error during disposal!');
  }
}

try {
  using resource = new FaultyResource();
  throw new Error('Error during operation!');
} catch (e) {
  console.log(`Caught error: ${e.message}`); // Error during operation!
  if (e.suppressed) {
    console.log(`Suppressed error: ${e.suppressed.message}`); // Error during disposal!
    console.log(e instanceof SuppressedError); // false
    console.log(e.suppressed instanceof Error); // true
  }
}

Denne adfærd sikrer, at du aldrig mister konteksten af den oprindelige fejl, hvilket fører til meget mere robuste og debuggbare systemer.

Praktiske Anvendelsestilfælde På Tværs af JavaScript-Økosystemet

Anvendelserne af Eksplicit Ressourcestyring er omfattende og relevante for udviklere over hele kloden, uanset om de arbejder med back-end, front-end eller i test.

Browser- og Runtime-understøttelse

Som en moderne funktion er det vigtigt at vide, hvor du kan bruge Eksplicit Ressourcestyring. Fra slutningen af 2023 / begyndelsen af 2024 er understøttelsen udbredt i de nyeste versioner af store JavaScript-miljøer:

For ældre miljøer skal du ty til transpilers som Babel med de passende plugins for at transformere `using`-syntaksen og polyfill de nødvendige symboler og stack-klasser.

Konklusion: En Ny Æra af Sikkerhed og Klarhed

JavaScript's Eksplicitte Ressourcestyring er mere end bare syntaktisk sukker; det er en fundamental forbedring af sproget, der fremmer sikkerhed, klarhed og vedligeholdelighed. Ved at automatisere den kedelige og fejlbehæftede proces med ressourceoprydning frigør det udviklere til at fokusere på deres primære forretningslogik.

De vigtigste punkter er:

Når du påbegynder nye projekter eller refactorerer eksisterende kode, overvej at vedtage dette kraftfulde nye mønster. Det vil gøre dit JavaScript renere, dine applikationer mere pålidelige og dit liv som udvikler bare en lille smule lettere. Det er en sand global standard for at skrive moderne, professionel JavaScript.