Eesti

Õppige selgeks JavaScripti uus Explicit Resource Management `using` ja `await using` abil. Automatiseerige puhastus, ennetage ressursilekkeid ja kirjutage puhtamat ning robustsemat koodi.

JavaScripti uus supervõime: Sügavuti Explicit Resource Management'ist

Tarkvaraarenduse dünaamilises maailmas on ressursside tõhus haldamine robustsete, usaldusväärsete ja jõudlike rakenduste ehitamise nurgakivi. Aastakümneid on JavaScripti arendajad tuginenud käsitsi mustritele nagu try...catch...finally, et tagada kriitiliste ressursside – näiteks failikäepidemete, võrguühenduste või andmebaasiseansside – korrektne vabastamine. Kuigi see lähenemine on funktsionaalne, on see sageli liiga sõnaohtrane, vigaderohke ja võib keerulistes stsenaariumides kiiresti kohmakaks muutuda – mustrit, mida mõnikord nimetatakse "hukatuspüramiidiks" (pyramid of doom).

Sisenege keele paradigmavahetusse: Explicit Resource Management (ERM). See võimas funktsioon, mis on lõplikult vormistatud ECMAScript 2024 (ES2024) standardis ja inspireeritud sarnastest konstruktsioonidest keeltes nagu C#, Python ja Java, tutvustab deklaratiivset ja automatiseeritud viisi ressursside puhastamiseks. Kasutades uusi using ja await using võtmesõnu, pakub JavaScript nüüd palju elegantsemat ja turvalisemat lahendust ajatule programmeerimisprobleemile.

See põhjalik juhend viib teid rännakule läbi JavaScripti Explicit Resource Management'i. Uurime probleeme, mida see lahendab, analüüsime selle põhikontseptsioone, vaatame läbi praktilisi näiteid ja avastame täiustatud mustreid, mis annavad teile võimekuse kirjutada puhtamat ja vastupidavamat koodi, olenemata sellest, kus maailmas te arendate.

Vana kaardivägi: Käsitsi ressursipuhastuse väljakutsed

Enne kui saame hinnata uue süsteemi elegantsi, peame esmalt mõistma vana süsteemi valupunkte. Klassikaline muster ressursside haldamiseks JavaScriptis on try...finally plokk.

Loogika on lihtne: omandate ressursi try plokis ja vabastate selle finally plokis. finally plokk garanteerib täitmise, olenemata sellest, kas try ploki kood õnnestub, ebaõnnestub või tagastab enneaegselt.

Vaatleme tavalist serveripoolset stsenaariumi: faili avamine, sinna andmete kirjutamine ja seejärel faili sulgemise tagamine.

Näide: Lihtne failitoiming try...finally abil


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

async function processFile(filePath, data) {
  let fileHandle;
  try {
    console.log('Faili avamine...');
    fileHandle = await fs.open(filePath, 'w');
    console.log('Faili kirjutamine...');
    await fileHandle.write(data);
    console.log('Andmed edukalt kirjutatud.');
  } catch (error) {
    console.error('Faili töötlemisel ilmnes viga:', error);
  } finally {
    if (fileHandle) {
      console.log('Faili sulgemine...');
      await fileHandle.close();
    }
  }
}

See kood töötab, kuid see paljastab mitmeid nõrkusi:

Nüüd kujutage ette mitme ressursi haldamist, näiteks andmebaasiühendus ja failikäepide. Kood muutub kiiresti pesastatud segaduseks:


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

Seda pesastamist on raske hooldada ja skaleerida. See on selge märk, et vaja on paremat abstraktsiooni. Just selle probleemi lahendamiseks loodi Explicit Resource Management.

Paradigmamuutus: Explicit Resource Management'i põhimõtted

Explicit Resource Management (ERM) kehtestab lepingu ressursiobjekti ja JavaScripti käituskeskkonna vahel. Põhiidee on lihtne: objekt saab deklareerida, kuidas teda tuleks puhastada, ja keel pakub süntaksit selle puhastuse automaatseks teostamiseks, kui objekt väljub oma skoobist.

See saavutatakse kahe peamise komponendi abil:

  1. Vabastamisprotokoll (Disposable Protocol): Standardne viis, kuidas objektid saavad defineerida oma puhastusloogika, kasutades spetsiaalseid sümboleid: Symbol.dispose sünkroonseks puhastamiseks ja Symbol.asyncDispose asünkroonseks puhastamiseks.
  2. using ja await using deklaratsioonid: Uued võtmesõnad, mis seovad ressursi ploki skoopiga. Plokist väljumisel kutsutakse automaatselt välja ressursi puhastusmeetod.

Põhikontseptsioonid: `Symbol.dispose` ja `Symbol.asyncDispose`

ERM-i keskmes on kaks uut tuntud sümbolit (well-known Symbols). Objekti, millel on meetod, mille võtmeks on üks neist sümbolitest, peetakse "vabastatavaks ressursiks" (disposable resource).

Sünkroonne vabastamine `Symbol.dispose` abil

Sümbol Symbol.dispose määrab sünkroonse puhastusmeetodi. See sobib ressurssidele, mille puhastamine ei nõua asünkroonseid operatsioone, näiteks failikäepideme sünkroonne sulgemine või mälus oleva luku vabastamine.

Loome ajutise faili jaoks ümbrise, mis puhastab ennast ise.


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(`Loodud ajutine fail: ${this.path}`);
  }

  // See on sünkroonne vabastamismeetod
  [Symbol.dispose]() {
    console.log(`Ajutise faili vabastamine: ${this.path}`);
    try {
      fs.unlinkSync(this.path);
      console.log('Fail edukalt kustutatud.');
    } catch (error) {
      console.error(`Faili kustutamine ebaõnnestus: ${this.path}`, error);
      // Oluline on käsitleda vigu ka dispose-meetodis!
    }
  }
}

Iga `TempFile` instants on nüüd vabastatav ressurss. Sellel on `Symbol.dispose` võtmega meetod, mis sisaldab loogikat faili kettalt kustutamiseks.

Asünkroonne vabastamine `Symbol.asyncDispose` abil

Paljud kaasaegsed puhastusoperatsioonid on asünkroonsed. Andmebaasiühenduse sulgemine võib hõlmata `QUIT`-käsu saatmist üle võrgu või sõnumijärjekorra klient võib vajada oma väljamineva puhvri tühjendamist. Nende stsenaariumide jaoks kasutame `Symbol.asyncDispose`.

Meetod, mis on seotud sümboliga `Symbol.asyncDispose`, peab tagastama `Promise`'i (või olema `async` funktsioon).

Modelleerime näidis-andmebaasiühenduse, mis tuleb asünkroonselt ühenduste kogusse (pool) tagasi vabastada.


// Andmebaasiühenduste näidiskogu (pool)
const mockDbPool = {
  getConnection: () => {
    console.log('Andmebaasiühendus omandatud.');
    return new MockDbConnection();
  }
};

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

  // See on asünkroonne vabastamismeetod
  async [Symbol.asyncDispose]() {
    console.log('Andmebaasiühenduse vabastamine tagasi kogusse...');
    // Simuleerime võrguviivitust ühenduse vabastamisel
    await new Promise(resolve => setTimeout(resolve, 50));
    console.log('Andmebaasiühendus vabastatud.');
  }
}

Nüüd on iga `MockDbConnection` instants asünkroonselt vabastatav ressurss. See teab, kuidas ennast asünkroonselt vabastada, kui teda enam ei vajata.

Uus süntaks: `using` ja `await using` praktikas

Kui meie vabastatavad klassid on defineeritud, saame nüüd kasutada uusi võtmesõnu nende automaatseks haldamiseks. Need võtmesõnad loovad ploki skoobiga deklaratsioone, täpselt nagu `let` ja `const`.

Sünkroonne puhastamine `using` abil

Võtmesõna `using` kasutatakse ressursside jaoks, mis implementeerivad `Symbol.dispose`. Kui koodi täitmine lahkub plokist, kus `using` deklaratsioon tehti, kutsutakse automaatselt välja `[Symbol.dispose]()` meetod.

Kasutame meie `TempFile` klassi:


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

  // Siin saate tempFile'iga töötada
  const content = fs.readFileSync(tempFile.path, 'utf8');
  console.log(`Loetud ajutisest failist: "${content}"`);

  // Siin pole puhastuskoodi vaja!
  console.log('...teen veel tööd...');
} // <-- tempFile.[Symbol.dispose]() kutsutakse siin automaatselt välja!

processDataWithTempFile();
console.log('Plokist on väljutud.');

Väljund oleks:

Plokki sisenemine...
Loodud ajutine fail: /path/to/temp_1678886400000.txt
Loetud ajutisest failist: "This is some important data."
...teen veel tööd...
Ajutise faili vabastamine: /path/to/temp_1678886400000.txt
Fail edukalt kustutatud.
Plokist on väljutud.

Vaadake, kui puhas see on! Ressursi kogu elutsükkel sisaldub plokis. Me deklareerime selle, kasutame seda ja unustame selle. Keel tegeleb puhastamisega. See on tohutu edasiminek loetavuses ja turvalisuses.

Mitme ressursi haldamine

Samas plokis võib olla mitu `using` deklaratsiooni. Need vabastatakse nende loomise vastupidises järjekorras (LIFO ehk "stack-like" käitumine).


{
  using resourceA = new MyDisposable('A'); // Loodud esimesena
  using resourceB = new MyDisposable('B'); // Loodud teisena
  console.log('Ploki sees, kasutan ressursse...');
} // esmalt vabastatakse resourceB, seejärel resourceA

Asünkroonne puhastamine `await using` abil

Võtmesõna `await using` on `using`'u asünkroonne vaste. Seda kasutatakse ressursside jaoks, mis implementeerivad `Symbol.asyncDispose`. Kuna puhastamine on asünkroonne, saab seda võtmesõna kasutada ainult `async` funktsiooni sees või mooduli tipptasemel (kui tipptaseme await on toetatud).

Kasutame meie `MockDbConnection` klassi:


async function performDatabaseOperation() {
  console.log('Asünkroonsesse funktsiooni sisenemine...');
  await using db = mockDbPool.getConnection();

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

  console.log('Andmebaasi operatsioon lõpetatud.');
} // <-- await db.[Symbol.asyncDispose]() kutsutakse siin automaatselt välja!

(async () => {
  await performDatabaseOperation();
  console.log('Asünkroonne funktsioon on lõpetanud.');
})();

Väljund demonstreerib asünkroonset puhastust:

Asünkroonsesse funktsiooni sisenemine...
Andmebaasiühendus omandatud.
Päringu täitmine: SELECT * FROM users
Andmebaasi operatsioon lõpetatud.
Andmebaasiühenduse vabastamine tagasi kogusse...
(ootab 50ms)
Andmebaasiühendus vabastatud.
Asünkroonne funktsioon on lõpetanud.

Täpselt nagu `using` puhul, haldab `await using` süntaks kogu elutsüklit, kuid see ootab korrektselt ära (`awaits`) asünkroonse puhastusprotsessi. See suudab isegi käsitleda ressursse, mis on ainult sünkroonselt vabastatavad – see lihtsalt ei oota neid ära.

Täiustatud mustrid: `DisposableStack` ja `AsyncDisposableStack`

Mõnikord ei ole `using`'u lihtne ploki skoop piisavalt paindlik. Mis siis, kui peate haldama rühma ressursse, mille eluiga ei ole seotud üheainsa leksikaalse plokiga? Või mis siis, kui integreerute vanema teegiga, mis ei tooda `Symbol.dispose`'iga objekte?

Nende stsenaariumide jaoks pakub JavaScript kahte abiklassi: `DisposableStack` ja `AsyncDisposableStack`.

`DisposableStack`: Paindlik puhastushaldur

`DisposableStack` on objekt, mis haldab puhastustoimingute kogumit. See on ise vabastatav ressurss, seega saate selle kogu eluea haldamiseks kasutada `using` plokki.

Sellel on mitu kasulikku meetodit:

Näide: Tingimuslik ressursside haldamine

Kujutage ette funktsiooni, mis avab logifaili ainult siis, kui teatud tingimus on täidetud, kuid soovite, et kogu puhastamine toimuks lõpus ühes kohas.


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

  const db = stack.use(getDbConnection()); // Kasuta alati andmebaasi

  if (shouldLog) {
    const logFileStream = fs.createWriteStream('app.log');
    // Lükka voo puhastamine edasi
    stack.defer(() => {
      console.log('Logifaili voo sulgemine...');
      logFileStream.end();
    });
    db.logTo(logFileStream);
  }

  db.doWork();

} // <-- Stack vabastatakse, kutsudes välja kõik registreeritud puhastusfunktsioonid LIFO-järjekorras.

`AsyncDisposableStack`: Asünkroonse maailma jaoks

Nagu võite arvata, on `AsyncDisposableStack` asünkroonne versioon. See suudab hallata nii sünkroonseid kui ka asünkroonseid vabastatavaid ressursse. Selle peamine puhastusmeetod on `.disposeAsync()`, mis tagastab `Promise`'i, mis laheneb, kui kõik asünkroonsed puhastustoimingud on lõpule viidud.

Näide: Erinevate ressursside segu haldamine

Loome veebiserveri päringukäsitleja, mis vajab andmebaasiühendust (asünkroonne puhastus) ja ajutist faili (sünkroonne puhastus).


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

  // Halda asünkroonset vabastatavat ressurssi
  const dbConnection = await stack.use(getAsyncDbConnection());

  // Halda sünkroonset vabastatavat ressurssi
  const tempFile = stack.use(new TempFile('request data'));

  // Võta üle ressurss vanast API-st
  const legacyResource = getLegacyResource();
  stack.adopt(legacyResource, () => legacyResource.shutdown());

  console.log('Päringu töötlemine...');
  await doWork(dbConnection, tempFile.path);

} // <-- stack.disposeAsync() kutsutakse välja. See ootab korrektselt ära asünkroonse puhastuse.

`AsyncDisposableStack` on võimas tööriist keeruka seadistus- ja lõpetamisloogika orkestreerimiseks puhtal ja ettearvataval viisil.

Robustne veatöötlus `SuppressedError`'iga

Üks peenemaid, kuid olulisemaid ERM-i täiustusi on see, kuidas see vigu käsitleb. Mis juhtub, kui `using` ploki sees visatakse viga ja *teine* viga visatakse järgneva automaatse vabastamise käigus?

Vanas `try...finally` maailmas kirjutaks `finally` plokist tulnud viga tavaliselt üle või "suruks alla" algse, olulisema vea `try` plokist. See muutis silumise sageli uskumatult keeruliseks.

ERM lahendab selle uue globaalse veatüübiga: `SuppressedError`. Kui vabastamise ajal tekib viga, samal ajal kui teine viga juba levib, siis vabastamisviga "surutakse alla". Algne viga visatakse, kuid sellel on nüüd `suppressed` omadus, mis sisaldab vabastamisviga.


class FaultyResource {
  [Symbol.dispose]() {
    throw new Error('Viga vabastamise ajal!');
  }
}

try {
  using resource = new FaultyResource();
  throw new Error('Viga operatsiooni ajal!');
} catch (e) {
  console.log(`Püütud viga: ${e.message}`); // Viga operatsiooni ajal!
  if (e.suppressed) {
    console.log(`Allasurutud viga: ${e.suppressed.message}`); // Viga vabastamise ajal!
    console.log(e instanceof SuppressedError); // false
    console.log(e.suppressed instanceof Error); // true
  }
}

See käitumine tagab, et te ei kaota kunagi algse ebaõnnestumise konteksti, mis viib palju robustsemate ja paremini silutavate süsteemideni.

Praktilised kasutusjuhud üle JavaScripti ökosüsteemi

Explicit Resource Management'i rakendused on laiaulatuslikud ja asjakohased arendajatele üle maailma, olenemata sellest, kas nad töötavad back-endis, front-endis või testimisel.

Brauseri ja käituskeskkonna tugi

Kuna tegemist on kaasaegse funktsiooniga, on oluline teada, kus saate Explicit Resource Management'i kasutada. 2023. aasta lõpu / 2024. aasta alguse seisuga on tugi laialt levinud peamiste JavaScripti keskkondade uusimates versioonides:

Vanemate keskkondade jaoks peate tuginema transpileritele nagu Babel koos vastavate pluginatega, et teisendada using süntaksit ja polütäita vajalikud sümbolid ning stack-klassid.

Kokkuvõte: Uus ohutuse ja selguse ajastu

JavaScripti Explicit Resource Management on rohkem kui lihtsalt süntaktiline suhkur; see on keele fundamentaalne täiustus, mis edendab ohutust, selgust ja hooldatavust. Automatiseerides tüütu ja vigaderohke ressursipuhastuse protsessi, vabastab see arendajad keskenduma oma peamisele äriloogikale.

Peamised järeldused on:

Kui alustate uusi projekte või refaktoriteerite olemasolevat koodi, kaaluge selle võimsa uue mustri kasutuselevõttu. See muudab teie JavaScripti puhtamaks, teie rakendused usaldusväärsemaks ja teie kui arendaja elu lihtsalt natuke lihtsamaks. See on tõeliselt globaalne standard kaasaegse, professionaalse JavaScripti kirjutamiseks.