Svenska

Bemästra JavaScripts nya Explicit Resource Management med `using` och `await using`. Lär dig automatisera rensning, förhindra resursläckor och skriv renare, mer robust kod.

JavaScripts Nya Superkraft: En Djupdykning i Explicit Resource Management

I den dynamiska världen av mjukvaruutveckling är effektiv resurshantering en hörnsten i att bygga robusta, pålitliga och prestandaeffektiva applikationer. I årtionden har JavaScript-utvecklare förlitat sig på manuella mönster som try...catch...finally för att säkerställa att kritiska resurser – som filhandtag, nätverksanslutningar eller databassessioner – frigörs korrekt. Även om det är funktionellt är detta tillvägagångssätt ofta omständligt, felbenäget och kan snabbt bli otympligt, ett mönster som ibland kallas "pyramiden av fördömelse" i komplexa scenarier.

Gå in i ett paradigmskifte för språket: Explicit Resource Management (ERM). Denna kraftfulla funktion, som slutfördes i ECMAScript 2024 (ES2024) standarden, inspirerad av liknande konstruktioner i språk som C#, Python och Java, introducerar ett deklarativt och automatiserat sätt att hantera resursrensning. Genom att utnyttja de nya nyckelorden using och await using tillhandahåller JavaScript nu en mycket mer elegant och säker lösning på en tidlös programmeringsutmaning.

Denna omfattande guide tar dig med på en resa genom JavaScripts Explicit Resource Management. Vi kommer att utforska de problem det löser, dissektiera dess kärnkoncept, gå igenom praktiska exempel och avslöja avancerade mönster som ger dig möjlighet att skriva renare, mer motståndskraftig kod, oavsett var i världen du utvecklar.

Det Gamla Gardet: Utmaningarna med Manuell Resursrensning

Innan vi kan uppskatta elegans i det nya systemet måste vi först förstå smärtpunkterna i det gamla. Det klassiska mönstret för resurshantering i JavaScript är try...finally-blocket.

Logiken är enkel: du skaffar en resurs i try-blocket och du frigör den i finally-blocket. finally-blocket garanterar utförande, oavsett om koden i try-blocket lyckas, misslyckas eller returneras för tidigt.

Låt oss överväga ett vanligt serverscenario: att öppna en fil, skriva lite data till den och sedan säkerställa att filen stängs.

Exempel: En Enkel Filoperation med try...finally


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

async function processFile(filePath, data) {
  let fileHandle;
  try {
    console.log('Öppnar fil...');
    fileHandle = await fs.open(filePath, 'w');
    console.log('Skriver till fil...');
    await fileHandle.write(data);
    console.log('Data skrevs framgångsrikt.');
  } catch (error) {
    console.error('Ett fel uppstod under filbearbetningen:', error);
  } finally {
    if (fileHandle) {
      console.log('Stänger fil...');
      await fileHandle.close();
    }
  }
}

Denna kod fungerar, men den avslöjar flera svagheter:

Föreställ dig nu att hantera flera resurser, som en databasanslutning och ett filhandtag. Koden blir snabbt en kapslad röra:


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

Denna kapsling är svår att underhålla och skala. Det är en tydlig signal om att en bättre abstraktion behövs. Detta är precis det problem som Explicit Resource Management var utformat för att lösa.

Ett Paradigmskifte: Principerna för Explicit Resource Management

Explicit Resource Management (ERM) introducerar ett kontrakt mellan ett resurs objekt och JavaScript-runtime. Huvudidén är enkel: ett objekt kan deklarera hur det ska rensas upp, och språket tillhandahåller syntax för att automatiskt utföra den rensningen när objektet går utanför scope.

Detta uppnås genom två huvudkomponenter:

  1. Disposable Protocol: Ett standard sätt för objekt att definiera sin egen rensningslogik med hjälp av specialsymboler: Symbol.dispose för synkron rensning och Symbol.asyncDispose för asynkron rensning.
  2. using och await using-deklarationerna: Nya nyckelord som binder en resurs till ett block-scope. När blocket avslutas anropas resursens rensningsmetod automatiskt.

Kärnkoncepten: Symbol.dispose och Symbol.asyncDispose

Kärnan i ERM är två nya välkända symboler. Ett objekt som har en metod med en av dessa symboler som sin nyckel betraktas som en "disponibel resurs".

Synkron Avyttring med Symbol.dispose

Symbolen Symbol.dispose anger en synkron rensningsmetod. Detta är lämpligt för resurser där rensning inte kräver några asynkrona operationer, som att stänga ett filhandtag synkront eller frigöra ett minneslås.

Låt oss skapa en wrapper för en temporär fil som rensar sig själv.


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(`Skapade temporär fil: ${this.path}`);
  }

  // Detta är den synkrona disponibla metoden
  [Symbol.dispose]() {
    console.log(`Avyttrar temporär fil: ${this.path}`);
    try {
      fs.unlinkSync(this.path);
      console.log('Filen raderades framgångsrikt.');
    } catch (error) {
      console.error(`Kunde inte radera filen: ${this.path}`, error);
      // Det är också viktigt att hantera fel inom dispose!
    }
  }
}

Alla instanser av `TempFile` är nu en disponibel resurs. Den har en metod som är nycklad av `Symbol.dispose` som innehåller logiken för att ta bort filen från disken.

Asynkron Avyttring med Symbol.asyncDispose

Många moderna rensningsoperationer är asynkrona. Att stänga en databasanslutning kan innebära att skicka ett `QUIT`-kommando över nätverket, eller en meddelandeköklient kan behöva tömma sin utgående buffert. För dessa scenarier använder vi `Symbol.asyncDispose`.

Metoden som är associerad med `Symbol.asyncDispose` måste returnera ett `Promise` (eller vara en `async`-funktion).

Låt oss modellera en mockdatabasanslutning som behöver släppas tillbaka till en pool asynkront.


// En mockdatabaspool
const mockDbPool = {
  getConnection: () => {
    console.log('DB-anslutning förvärvad.');
    return new MockDbConnection();
  }
};

class MockDbConnection {
  query(sql) {
    console.log(`Kör fråga: ${sql}`);
    return Promise.resolve({ success: true, rows: [] });
  }

  // Detta är den asynkrona disponibla metoden
  async [Symbol.asyncDispose]() {
    console.log('Släpper DB-anslutning tillbaka till poolen...');
    // Simulera en nätverksfördröjning för att släppa anslutningen
    await new Promise(resolve => setTimeout(resolve, 50));
    console.log('DB-anslutning släppt.');
  }
}

Nu är alla `MockDbConnection`-instanser en asynkron disponibel resurs. Den vet hur man släpper sig själv asynkront när den inte längre behövs.

Den Nya Syntaxen: using och await using i Praktiken

Med våra disponibla klasser definierade kan vi nu använda de nya nyckelorden för att hantera dem automatiskt. Dessa nyckelord skapar block-scoped deklarationer, precis som `let` och `const`.

Synkron Rensning med using

Nyckelordet using används för resurser som implementerar Symbol.dispose. När kodutförandet lämnar blocket där using-deklarationen gjordes, anropas metoden [Symbol.dispose]() automatiskt.

Låt oss använda vår `TempFile`-klass:


function processDataWithTempFile() {
  console.log('Går in i blocket...');
  using tempFile = new TempFile('Detta är viktig data.');

  // Du kan arbeta med tempFile här
  const content = fs.readFileSync(tempFile.path, 'utf8');
  console.log(`Läste från tempfil: "${content}"`);

  // Ingen rensningskod behövs här!
  console.log('...gör mer arbete...');
} // <-- tempFile.[Symbol.dispose]() anropas automatiskt här!

processDataWithTempFile();
console.log('Blocket har avslutats.');

Utdata skulle vara:

Går in i blocket...
Skapade temporär fil: /path/to/temp_1678886400000.txt
Läste från tempfil: "Detta är viktig data."
...gör mer arbete...
Avyttrar temporär fil: /path/to/temp_1678886400000.txt
Filen raderades framgångsrikt.
Blocket har avslutats.

Titta på hur rent det är! Resursens hela livscykel finns i blocket. Vi deklarerar det, vi använder det och vi glömmer det. Språket hanterar rensningen. Detta är en enorm förbättring av läsbarhet och säkerhet.

Hantera Flera Resurser

Du kan ha flera using-deklarationer i samma block. De kommer att avyttras i omvänd ordning av deras skapande (ett LIFO- eller "stackliknande" beteende).


{
  using resourceA = new MyDisposable('A'); // Skapades först
  using resourceB = new MyDisposable('B'); // Skapades i andra hand
  console.log('Inuti blocket, använder resurser...');
} // resourceB avyttras först, sedan resourceA

Asynkron Rensning med await using

Nyckelordet await using är den asynkrona motsvarigheten till using. Den används för resurser som implementerar Symbol.asyncDispose. Eftersom rensningen är asynkron kan detta nyckelord endast användas inuti en async-funktion eller på toppnivå i en modul (om await på toppnivå stöds).

Låt oss använda vår `MockDbConnection`-klass:


async function performDatabaseOperation() {
  console.log('Går in i asynkron funktion...');
  await using db = mockDbPool.getConnection();

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

  console.log('Databasåtgärden är klar.');
} // <-- await db.[Symbol.asyncDispose]() anropas automatiskt här!

(async () => {
  await performDatabaseOperation();
  console.log('Asynkron funktion har slutförts.');
})();

Utdata visar den asynkrona rensningen:

Går in i asynkron funktion...
DB-anslutning förvärvad.
Kör fråga: SELECT * FROM users
Databasåtgärden är klar.
Släpper DB-anslutning tillbaka till poolen...
(väntar 50ms)
DB-anslutning släppt.
Asynkron funktion har slutförts.

Precis som med `using` hanterar syntaxen await using hela livscykeln, men den awaitar korrekt den asynkrona rensningsprocessen. Den kan till och med hantera resurser som bara är synkront disponibla – den kommer helt enkelt inte att avvakta dem.

Avancerade Mönster: DisposableStack och AsyncDisposableStack

Ibland är den enkla block-scoping av using inte tillräckligt flexibel. Tänk om du behöver hantera en grupp resurser med en livstid som inte är bunden till ett enda lexikalt block? Eller tänk om du integrerar med ett äldre bibliotek som inte producerar objekt med Symbol.dispose?

För dessa scenarier tillhandahåller JavaScript två hjälpklasser: DisposableStack och AsyncDisposableStack.

DisposableStack: Den Flexibla Rensningshanteraren

En DisposableStack är ett objekt som hanterar en samling rensningsåtgärder. Det är i sig en disponibel resurs, så du kan hantera hela dess livstid med ett using-block.

Den har flera användbara metoder:

Exempel: Villkorlig Resurshantering

Föreställ dig en funktion som öppnar en loggfil bara om ett visst villkor är uppfyllt, men du vill att all rensning ska ske på ett ställe i slutet.


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

  const db = stack.use(getDbConnection()); // Använd alltid DB

  if (shouldLog) {
    const logFileStream = fs.createWriteStream('app.log');
    // Skjut upp rensningen för strömmen
    stack.defer(() => {
      console.log('Stänger loggfilsström...');
      logFileStream.end();
    });
    db.logTo(logFileStream);
  }

  db.doWork();

} // <-- Stacken avyttras och anropar alla registrerade rensningsfunktioner i LIFO-ordning.

AsyncDisposableStack: För Den Asynkrona Världen

Som du kanske gissar är AsyncDisposableStack den asynkrona versionen. Den kan hantera både synkrona och asynkrona disposables. Dess primära rensningsmetod är .disposeAsync(), som returnerar ett Promise som löses när alla asynkrona rensningsåtgärder är slutförda.

Exempel: Hantera en Blandning av Resurser

Låt oss skapa en webbserverbegärandehanterare som behöver en databasanslutning (asynkron rensning) och en temporär fil (synkron rensning).


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

  // Hantera en asynkron disponibel resurs
  const dbConnection = await stack.use(getAsyncDbConnection());

  // Hantera en synkron disponibel resurs
  const tempFile = stack.use(new TempFile('begärandedata'));

  // Använd en resurs från ett gammalt API
  const legacyResource = getLegacyResource();
  stack.adopt(legacyResource, () => legacyResource.shutdown());

  console.log('Bearbetar begäran...');
  await doWork(dbConnection, tempFile.path);

} // <-- stack.disposeAsync() anropas. Den kommer korrekt att avvakta asynkron rensning.

AsyncDisposableStack är ett kraftfullt verktyg för att orkestrera komplex installation och rivningslogik på ett rent, förutsägbart sätt.

Robust Felhantering med SuppressedError

En av de mest subtila men betydande förbättringarna av ERM är hur den hanterar fel. Vad händer om ett fel kastas i using-blocket, och *ett annat* fel kastas under den efterföljande automatiska avyttringen?

I den gamla try...finally-världen skulle felet från finally-blocket typiskt skriva över eller "undertrycka" det ursprungliga, viktigare felet från try-blocket. Detta gjorde ofta felsökning otroligt svår.

ERM löser detta med en ny global feltyp: SuppressedError. Om ett fel uppstår under avyttring medan ett annat fel redan fortplantas, "undertrycks" avyttringsfelet. Det ursprungliga felet kastas, men det har nu en suppressed-egenskap som innehåller avyttringsfelet.


class FaultyResource {
  [Symbol.dispose]() {
    throw new Error('Fel under avyttring!');
  }
}

try {
  using resource = new FaultyResource();
  throw new Error('Fel under operation!');
} catch (e) {
  console.log(`Fångade fel: ${e.message}`); // Fel under operation!
  if (e.suppressed) {
    console.log(`Undertryckt fel: ${e.suppressed.message}`); // Fel under avyttring!
    console.log(e instanceof SuppressedError); // false
    console.log(e.suppressed instanceof Error); // true
  }
}

Detta beteende säkerställer att du aldrig förlorar kontexten för det ursprungliga felet, vilket leder till mycket mer robusta och felsökningsbara system.

Praktiska Användningsfall Över JavaScript-Ekosystemet

Tillämpningarna av Explicit Resource Management är omfattande och relevanta för utvecklare över hela världen, oavsett om de arbetar med backend, frontend eller i testning.

Webbläsare och Runtime-Stöd

Som en modern funktion är det viktigt att veta var du kan använda Explicit Resource Management. Från och med slutet av 2023 / början av 2024 är stödet utbrett i de senaste versionerna av stora JavaScript-miljöer:

För äldre miljöer måste du förlita dig på transpiler som Babel med lämpliga plugins för att transformera using-syntaxen och polyfylla de nödvändiga symbolerna och stackklasserna.

Slutsats: En Ny Era av Säkerhet och Tydlighet

JavaScripts Explicit Resource Management är mer än bara syntaktiskt socker; det är en grundläggande förbättring av språket som främjar säkerhet, klarhet och underhållbarhet. Genom att automatisera den mödosamma och felbenägna processen för resursrensning frigörs utvecklare för att fokusera på sin primära affärslogik.

Huvudpunkterna är:

När du påbörjar nya projekt eller refaktorera befintlig kod, överväg att anta detta kraftfulla nya mönster. Det kommer att göra din JavaScript renare, dina applikationer mer pålitliga och ditt liv som utvecklare bara lite lättare. Det är en verkligt global standard för att skriva modern, professionell JavaScript.