Suomi

Hallitse JavaScriptin uutta eksplisiittistä resurssienhallintaa käyttämällä `using` ja `await using`. Opi automatisoimaan siivous, estämään resurssivuotoja ja kirjoittamaan selkeämpää, vankempaa koodia.

JavaScriptin uusi supervoima: Syväsukellus eksplisiittiseen resurssienhallintaan

Ohjelmistokehityksen dynaamisessa maailmassa resurssien tehokas hallinta on perusta vankkojen, luotettavien ja suorituskykyisten sovellusten rakentamiselle. Vuosikymmeniä JavaScript-kehittäjät ovat turvautuneet manuaalisiin malleihin, kuten try...catch...finally, varmistaakseen, että kriittiset resurssit – kuten tiedostokahvat, verkkoyhteydet tai tietokantakannat – vapautetaan asianmukaisesti. Vaikka tämä lähestymistapa on toimiva, se on usein tilaavievä, virhealtis ja voi nopeasti muuttua hankalaksi, malli jota joskus kutsutaan "kuoleman pyramidiksi" monimutkaisissa skenaarioissa.

Tässä on paradigmaattinen muutos kielelle: Eksplisiittinen resurssienhallinta (ERM). ECMAScript 2024 (ES2024) -standardiin viimeistelty tämä tehokas ominaisuus, joka on saanut inspiraationsa vastaavista rakenteista kielissä kuten C#, Python ja Java, esittelee deklaratiivisen ja automatisoidun tavan käsitellä resurssien siivousta. Hyödyntämällä uusia using ja await using -avainsanoja JavaScript tarjoaa nyt paljon tyylikkäämmän ja turvallisemman ratkaisun ajattomaan ohjelmointihaasteeseen.

Tämä kattava opas vie sinut matkalle JavaScriptin eksplisiittisen resurssienhallinnan läpi. Tutustumme ongelmiin, joita se ratkaisee, puramme sen ydinkäsitteet, käymme läpi käytännön esimerkkejä ja paljastamme edistyneitä malleja, jotka antavat sinulle mahdollisuuden kirjoittaa selkeämpää, sitkeämpää koodia, missä tahansa maailmassa kehitätkin.

Vanha kaartti: Manuaalisen resurssien siivouksen haasteet

Ennen kuin voimme arvostaa uuden järjestelmän eleganssia, meidän on ensin ymmärrettävä vanhan kipupisteet. Klassinen malli resurssienhallinnalle JavaScriptissä on try...finally -lohko.

Logiikka on yksinkertainen: hankit resurssin try -lohkoon ja vapautat sen finally -lohkoon. finally -lohko takaa suorituksen, riippumatta siitä, onnistuuko, epäonnistuuko vai palautuuko try -lohkon koodi ennenaikaisesti.

Tarkastellaan yleistä palvelinpuolen skenaariota: tiedoston avaaminen, datan kirjoittaminen siihen ja sitten varmistaminen, että tiedosto suljetaan.

Esimerkki: Yksinkertainen tiedosto-operaatio try...finally -lauseella


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

Tämä koodi toimii, mutta se paljastaa useita heikkouksia:

Kuvittele nyt useiden resurssien hallintaa, kuten tietokantayhteyden ja tiedostokahvan. Koodista tulee nopeasti sisäkkäistä sotkua:


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

Tätä sisäkkäisyyttä on vaikea ylläpitää ja skaalata. Se on selvä merkki siitä, että parempi abstraktio on tarpeen. Tämä on juuri se ongelma, jonka eksplisiittinen resurssienhallinta on suunniteltu ratkaisemaan.

Paradigmaattinen muutos: Eksplisiittisen resurssienhallinnan periaatteet

Eksplisiittinen resurssienhallinta (ERM) esittelee sopimuksen resurssiobjektin ja JavaScript-ajoympäristön välillä. Ydinidea on yksinkertainen: objekti voi ilmoittaa, miten se tulisi siivota, ja kieli tarjoaa syntaksin tämän siivouksen automaattiseksi suorittamiseksi, kun objekti poistuu laajuudesta.

Tämä saavutetaan kahdella pääkomponentilla:

  1. Disposable-protokolla: Standardi tapa objekteille määritellä omat siivouslogiikkansa käyttämällä erityisiä symboleja: Symbol.dispose synkroniseen siivoukseen ja Symbol.asyncDispose asynkroniseen siivoukseen.
  2. `using`- ja `await using` -määritykset: Uudet avainsanat, jotka sitovat resurssin lohkon laajuuteen. Kun lohko poistuu, resurssin siivousmetodia kutsutaan automaattisesti.

Ydin käsitteet: `Symbol.dispose` ja `Symbol.asyncDispose`

ERM:n ytimessä ovat kaksi uutta tunnettua symbolia. Objekti, jolla on metodi, jonka avaimena on yksi näistä symboleista, on "disposaabeli resurssi".

Synkroninen siivous `Symbol.dispose` -symbolilla

Symbol.dispose -symboli määrittää synkronisen siivousmetodin. Tämä sopii resursseille, joiden siivous ei vaadi asynkronisia operaatioita, kuten tiedostokahvan synkroninen sulkeminen tai muistilukon vapauttaminen.

Luodaanpa kääre väliaikaiselle tiedostolle, joka siivoaa itsensä.


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

  // Tämä on synkroninen disposaabeli metodi
  [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);
      // On tärkeää käsitellä virheitä myös dispose-metodissa!
    }
  }
}

Mikä tahansa `TempFile`-luokan ilmentymä on nyt disposaabeli resurssi. Sillä on metodi, jonka avaimena on `Symbol.dispose`, ja joka sisältää tiedoston poistamisen levyltä.

Asynkroninen siivous `Symbol.asyncDispose` -symbolilla

Monet modernit siivousoperaatiot ovat asynkronisia. Tietokantayhteyden sulkeminen voi sisältää `QUIT`-komennon lähettämisen verkon yli, tai viestijonon asiakas voi joutua tyhjentämään lähtöpuskurinsa. Näihin skenaarioihin käytämme `Symbol.asyncDispose`.

Symbol.asyncDispose -symboliin liitetyn metodin on palautettava `Promise` (tai oltava `async`-funktio).

Mallinnetaanpa väärennetty tietokantayhteys, joka täytyy vapauttaa takaisin pooliin asynkronisesti.


// Väärennetty tietokantapooli
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: [] });
  }

  // Tämä on asynkroninen disposaabeli metodi
  async [Symbol.asyncDispose]() {
    console.log('Releasing DB connection back to the pool...');
    // Simuloidaan viivettä yhteyden vapauttamiseen
    await new Promise(resolve => setTimeout(resolve, 50));
    console.log('DB connection released.');
  }
}

Nyt mikä tahansa `MockDbConnection`-ilmentymä on asynkronisesti disposaabeli resurssi. Se tietää, miten vapauttaa itsensä asynkronisesti, kun sitä ei enää tarvita.

Uusi syntaksi: `using` ja `await using` toiminnassa

Kun disposaabelit luokkamme on määritelty, voimme nyt käyttää uusia avainsanoja niiden automaattiseen hallintaan. Nämä avainsanat luovat lohkon laajuuden mukaisia määrityksiä, aivan kuten `let` ja `const`.

Synkroninen siivous `using`-avainsanalla

using -avainsanaa käytetään resursseille, jotka toteuttavat `Symbol.dispose`. Kun koodin suoritus poistuu lohkosta, jossa `using`-määritys tehtiin, `[Symbol.dispose]()` -metodia kutsutaan automaattisesti.

Käytetäänpä `TempFile`-luokkaamme:


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

  // Voit käsitellä tempFile:a täällä
  const content = fs.readFileSync(tempFile.path, 'utf8');
  console.log(`Read from temp file: "${content}"`);

  // Ei siivouskoodia tarvita tässä!
  console.log('...doing more work...');
}
// <-- tempFile.[Symbol.dispose]() kutsutaan automaattisesti juuri tässä!

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

Tuloste olisi:

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.

Katso, kuinka siistiä se on! Resurssin koko elinkaari sisältyy lohkoon. Määritämme sen, käytämme sitä ja unohdamme sen. Kieli hoitaa siivouksen. Tämä on valtava parannus luettavuudessa ja turvallisuudessa.

Useiden resurssien hallinta

Voit käyttää useita `using`-määrityksiä samassa lohkossa. Niitä siivotaan käänteisessä järjestyksessä luomisensa suhteen (LIFO eli "pinomainen" käyttäytyminen).


{
  using resourceA = new MyDisposable('A'); // Luotu ensin
  using resourceB = new MyDisposable('B'); // Luotu toisena
  console.log('Inside block, using resources...');
}
// <-- resourceB siivotaan ensin, sitten resourceA

Asynkroninen siivous `await using` -avainsanalla

await using -avainsana on `using`-avainsanan asynkroninen vastine. Sitä käytetään resursseille, jotka toteuttavat `Symbol.asyncDispose`. Koska siivous on asynkroninen, tätä avainsanaa voidaan käyttää vain `async`-funktion sisällä tai moduulin ylimmällä tasolla (jos ylimmän tason await on tuettu).

Käytetäänpä `MockDbConnection`-luokkaamme:


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]() kutsutaan automaattisesti tässä!

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

Tuloste näyttää asynkronisen siivouksen:

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.

Aivan kuten `using`-avainsanassa, `await using`-syntaksi hoitaa koko elinkaaren, mutta se `awaittaa` oikein asynkronisen siivousprosessin. Se voi jopa käsitellä resursseja, jotka ovat vain synkronisesti disposaabeleja – se ei yksinkertaisesti odota niitä.

Edistyneet mallit: `DisposableStack` ja `AsyncDisposableStack`

Joskus `using`-lohkon yksinkertainen laajuus ei ole riittävän joustava. Entä jos sinun on hallittava resurssiryhmää, jonka elinkaari ei ole sidottu yksittäiseen leksikaaliseen lohkoon? Tai entä jos integroi vanhaan kirjastoon, joka ei tuota objekteja `Symbol.dispose` -symbolilla?

Näihin skenaarioihin JavaScript tarjoaa kaksi apuluokkaa: `DisposableStack` ja `AsyncDisposableStack`.

`DisposableStack`: Joustava siivoushallitsija

`DisposableStack` on objekti, joka hallitsee siivousoperaatioiden kokoelmaa. Se on itse disposaabeli resurssi, joten voit hallita sen koko elinkaarta `using`-lohkolla.

Sillä on useita hyödyllisiä metodeja:

Esimerkki: Ehdollinen resurssienhallinta

Kuvittele funktio, joka avaa lokitiedoston vain, jos tietty ehto täyttyy, mutta haluat kaiken siivouksen tapahtuvan yhdessä paikassa lopussa.


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

  const db = stack.use(getDbConnection()); // Käytä aina tietokantaa

  if (shouldLog) {
    const logFileStream = fs.createWriteStream('app.log');
    // Siirrä virran siivous
    stack.defer(() => {
      console.log('Closing log file stream...');
      logFileStream.end();
    });
    db.logTo(logFileStream);
  }

  db.doWork();

} // <-- Pino siivotaan, kutsuen kaikkia rekisteröityjä siivousfunktioita LIFO-järjestyksessä.

`AsyncDisposableStack`: Asynkroniselle maailmalle

Kuten arvaatkin, `AsyncDisposableStack` on asynkroninen versio. Se voi hallita sekä synkronisia että asynkronisia disposaabeleja. Sen pääasiallinen siivousmetodi on `.disposeAsync()`, joka palauttaa `Promise`-objektin, joka ratkeaa, kun kaikki asynkroniset siivousoperaatiot ovat valmiita.

Esimerkki: Erilaisten resurssien hallinta

Luodaanpa web-palvelimen pyyntökäsittelijä, joka tarvitsee tietokantayhteyden (asynkroninen siivous) ja väliaikaisen tiedoston (synkroninen siivous).


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

  // Hallitse asynkronisesti disposaabelia resurssia
  const dbConnection = await stack.use(getAsyncDbConnection());

  // Hallitse synkronisesti disposaabelia resurssia
  const tempFile = stack.use(new TempFile('request data'));

  // Hyväksy vanhan API:n resurssi
  const legacyResource = getLegacyResource();
  stack.adopt(legacyResource, () => legacyResource.shutdown());

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

} // <-- stack.disposeAsync() kutsutaan. Se odottaa oikein asynkronista siivousta.

`AsyncDisposableStack` on tehokas työkalu monimutkaisen asetuksen ja purkamisen logiikan orkestrointiin puhtaalla, ennustettavalla tavalla.

Vankka virheiden käsittely `SuppressedError`-virheellä

Yksi hienovaraisimmista, mutta merkittävimmistä ERM:n parannuksista on tapa, jolla se käsittelee virheitä. Mitä tapahtuu, jos virhe heitetään `using`-lohkon sisällä ja *toinen* virhe heitetään sen jälkeisen automaattisen siivouksen aikana?

Vanhan `try...finally` -maailman virhe syrjäyttäisi tai "tukahduttaisi" alkuperäisen, tärkeämmän virheen `try`-lohkosta. Tämä teki virheenkorjauksesta usein uskomattoman vaikeaa.

ERM ratkaisee tämän uudella globaalilla virhetyypillä: `SuppressedError`. Jos siivouksen aikana ilmenee virhe, kun toinen virhe on jo leviämässä, siivousvirhe "tukahdutetaan". Alkuperäinen virhe heitetään, mutta sillä on nyt `suppressed`-ominaisuus, joka sisältää siivousvirheen.


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
  }
}

Tämä käyttäytyminen varmistaa, että et koskaan menetä alkuperäisen vian kontekstia, mikä johtaa paljon vankempiin ja helpommin korjattaviin järjestelmiin.

Käytännön käyttökohteet JavaScript-ekosysteemissä

Eksplisiittisen resurssienhallinnan sovellukset ovat valtavat ja relevantteja kehittäjille ympäri maailmaa, olivatpa he sitten backendissä, frontendissä tai testaustoiminnassa.

Selaimen ja ajoympäristön tuki

Modernina ominaisuutena on tärkeää tietää, missä voit käyttää eksplisiittistä resurssienhallintaa. Loppuvuodesta 2023 / alkuvuodesta 2024 alkaen tuki on laajaa uusimmissa versioissa yleisimmistä JavaScript-ympäristöistä:

Vanhemmissa ympäristöissä sinun on käytettävä transpilereita, kuten Babel, asianmukaisilla lisäosilla muuntaaksesi `using`-syntaksin ja polyfillataksesi tarvittavat symbolit ja pinoluokat.

Yhteenveto: Uusi aikakausi turvallisuutta ja selkeyttä

JavaScriptin eksplisiittinen resurssienhallinta on enemmän kuin pelkkää syntaktista sokeria; se on perustavanlaatuinen parannus kieleen, joka edistää turvallisuutta, selkeyttä ja ylläpidettävyyttä. Automatisoimalla työlään ja virhealtin resurssien siivousprosessin se vapauttaa kehittäjät keskittymään ensisijaiseen liiketoimintalogiikkaansa.

Keskeiset opit ovat:

Kun aloitat uusia projekteja tai refaktoroit olemassa olevaa koodia, harkitse tämän tehokkaan uuden mallin käyttöönottoa. Se tekee JavaScriptistäsi selkeämpää, sovelluksistasi luotettavampia ja elämästäsi kehittäjänä vain hieman helpompaa. Se on todella maailmanlaajuinen standardi modernin, ammattimaisen JavaScriptin kirjoittamiseen.