Slovenščina

Obvladajte novo eksplicitno upravljanje z viri v JavaScriptu z uporabo `using` in `await using`. Avtomatizirajte čiščenje, preprečite uhajanje virov in pišite čistejšo, robustnejšo kodo.

Nova supermoč JavaScripta: Poglobljen pogled v eksplicitno upravljanje z viri

V dinamičnem svetu razvoja programske opreme je učinkovito upravljanje z viri temelj gradnje robustnih, zanesljivih in zmogljivih aplikacij. Na desetletja so se razvijalci JavaScripta zanašali na ročne vzorce, kot je try...catch...finally, da bi zagotovili pravilno sproščanje kritičnih virov - kot so ročaji datotek, omrežne povezave ali seje z bazami podatkov. Čeprav je ta pristop funkcionalen, je pogosto obsežen, nagnjen k napakam in lahko hitro postane neroden, vzorec, ki ga v zapletenih scenarijih včasih imenujemo "piramida pogubljenja".

Vstopite v spremembo paradigme za jezik: Eksplicitno upravljanje z viri (ERM). Ta zmogljiva funkcija, dokončana v standardu ECMAScript 2024 (ES2024) in navdihnjena s podobnimi konstrukti v jezikih, kot so C#, Python in Java, uvaja deklarativni in avtomatiziran način za ravnanje s čiščenjem virov. Z izkoriščanjem novih ključnih besed using in await using zdaj JavaScript zagotavlja veliko bolj elegantno in varnejšo rešitev za brezčasni programski izziv.

Ta obsežen vodnik vas bo popeljal na potovanje skozi eksplicitno upravljanje z viri v JavaScriptu. Raziskali bomo probleme, ki jih rešuje, razčlenili njegove osnovne koncepte, šli skozi praktične primere in razkrili napredne vzorce, ki vam bodo omogočili pisanje čistejše, odpornejše kode, ne glede na to, kje v svetu razvijate.

Stara garda: Izzivi ročnega čiščenja virov

Preden lahko cenimo eleganco novega sistema, moramo najprej razumeti boleče točke starega. Klasičen vzorec za upravljanje z viri v JavaScriptu je blok try...finally.

Logika je preprosta: pridobite vir v bloku try in ga sprostite v bloku finally. Blok finally zagotavlja izvedbo, ne glede na to, ali koda v bloku try uspe, ne uspe ali se predčasno vrne.

Razmislite o pogostem scenariju na strani strežnika: odpiranje datoteke, pisanje nekaterih podatkov vanjo in nato zagotavljanje, da je datoteka zaprta.

Primer: Preprosta operacija z datoteko z try...finally


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

async function processFile(filePath, data) {
  let fileHandle;
  try {
    console.log('Odpiranje datoteke...');
    fileHandle = await fs.open(filePath, 'w');
    console.log('Pisanje v datoteko...');
    await fileHandle.write(data);
    console.log('Podatki uspešno zapisani.');
  } catch (error) {
    console.error('Pri obdelavi datoteke je prišlo do napake:', error);
  } finally {
    if (fileHandle) {
      console.log('Zapiranje datoteke...');
      await fileHandle.close();
    }
  }
}

Ta koda deluje, vendar razkriva več pomanjkljivosti:

Zdaj si predstavljajte upravljanje z več viri, kot sta povezava z bazo podatkov in ročaj datoteke. Koda hitro postane zapletena:


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

To gnezdenje je težko vzdrževati in prilagajati. Je jasen signal, da je potrebna boljša abstrakcija. Točno ta problem je bil zasnovan za rešitev eksplicitnega upravljanja z viri.

Sprememba paradigme: načela eksplicitnega upravljanja z viri

Eksplicitno upravljanje z viri (ERM) uvaja pogodbo med predmetom vira in izvajalnim okoljem JavaScript. Osnovna ideja je preprosta: objekt lahko deklarira, kako naj se očisti, jezik pa zagotavlja sintakso za samodejno izvedbo tega čiščenja, ko objekt zapusti obseg.

To je doseženo z dvema glavnima komponentama:

  1. Protokol Disposable: Standardni način, da predmeti določijo svojo lastno logiko čiščenja z uporabo posebnih simbolov: Symbol.dispose za sinhrono čiščenje in Symbol.asyncDispose za asinhrono čiščenje.
  2. Deklaracije `using` in `await using`: Nove ključne besede, ki povežejo vir z blokovskim obsegom. Ko blok zapustite, se samodejno prikliče metoda čiščenja vira.

Osnovni koncepti: `Symbol.dispose` in `Symbol.asyncDispose`

V središču ERM sta dva nova dobro znana simbola. Objekt, ki ima metodo s tem simbolom kot njen ključ, se šteje za "razpoložljiv vir".

Sinhrono odstranjevanje s `Symbol.dispose`

Simbol Symbol.dispose določa sinhrono metodo čiščenja. To je primerno za vire, kjer čiščenje ne zahteva nobenih asinhronih operacij, kot je sinhrono zapiranje ročaja datoteke ali sprostitev ključavnice v pomnilniku.

Ustvarimo ovojnico za začasno datoteko, ki se sama očisti.


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(`Ustvarjena začasna datoteka: ${this.path}`);
  }

  // To je sinhrona metoda za odstranjevanje
  [Symbol.dispose]() {
    console.log(`Odstranjevanje začasne datoteke: ${this.path}`);
    try {
      fs.unlinkSync(this.path);
      console.log('Datoteka je bila uspešno izbrisana.');
    } catch (error) {
      console.error(`Brisanje datoteke ni uspelo: ${this.path}`, error);
      // Pomembno je, da obravnavate tudi napake v dispose!
    }
  }
}

Vsaka instanca `TempFile` je zdaj razpoložljiv vir. Ima metodo, ki jo zaklene `Symbol.dispose`, ki vsebuje logiko za brisanje datoteke z diska.

Asinhrono odstranjevanje s `Symbol.asyncDispose`

Številne sodobne operacije čiščenja so asinhroni. Zapiranje povezave z bazo podatkov lahko vključuje pošiljanje ukaza `QUIT` prek omrežja, ali odjemalec čakalne vrste sporočil bo morda moral izprazniti svoj odhodni medpomnilnik. Za te scenarije uporabljamo `Symbol.asyncDispose`.

Metoda, povezana s `Symbol.asyncDispose`, mora vrniti `Promise` (ali biti funkcija `async`).

Modelirajmo lažno povezavo z bazo podatkov, ki jo je treba asinhrono sprostiti v sklad.


// Lažni sklad baz podatkov
const mockDbPool = {
  getConnection: () => {
    console.log('Povezava z bazo podatkov pridobljena.');
    return new MockDbConnection();
  }
};

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

  // To je asinhrona metoda za odstranjevanje
  async [Symbol.asyncDispose]() {
    console.log('Sproščanje povezave z bazo podatkov nazaj v sklad...');
    // Simulirajte omrežno zakasnitev za sprostitev povezave
    await new Promise(resolve => setTimeout(resolve, 50));
    console.log('Povezava z bazo podatkov sproščena.');
  }
}

Zdaj je vsaka instanca `MockDbConnection` asinhroni razpoložljiv vir. Ve, kako se asinhrono sprostiti, ko ni več potreben.

Nova sintaksa: `using` in `await using` v akciji

Z opredeljenimi razpoložljivimi razredi lahko zdaj uporabimo nove ključne besede za njihovo samodejno upravljanje. Te ključne besede ustvarijo deklaracije v obsegu bloka, tako kot `let` in `const`.

Sinhrono čiščenje z `using`

Ključna beseda `using` se uporablja za vire, ki izvajajo `Symbol.dispose`. Ko se izvedba kode oddalji od bloka, kjer je bila narejena deklaracija `using`, se samodejno prikliče metoda `[Symbol.dispose]()`.

Uporabimo naš razred `TempFile`:


function processDataWithTempFile() {
  console.log('Vstop v blok...');
  using tempFile = new TempFile('To so pomembni podatki.');

  // Tukaj lahko delate s tempFile
  const content = fs.readFileSync(tempFile.path, 'utf8');
  console.log(`Prebrano iz začasne datoteke: "${content}"`);

  // Tukaj ni potrebna nobena koda za čiščenje!
  console.log('...delam več...');
} // <-- tempFile.[Symbol.dispose]() se samodejno prikliče tukaj!

processDataWithTempFile();
console.log('Blok je bil zapuščen.');

Izhod bi bil:

Vstop v blok...
Ustvarjena začasna datoteka: /path/to/temp_1678886400000.txt
Prebrano iz začasne datoteke: "To so pomembni podatki."
...delam več...
Odstranjevanje začasne datoteke: /path/to/temp_1678886400000.txt
Datoteka je bila uspešno izbrisana.
Blok je bil zapuščen.

Poglejte, kako čisto je to! Celotni življenjski cikel vira je vsebovan v bloku. Deklariramo ga, uporabljamo in pozabimo. Jezik obravnava čiščenje. To je ogromno izboljšanje berljivosti in varnosti.

Upravljanje več virov

V istem bloku imate lahko več deklaracij `using`. Odstranili se bodo v obratnem vrstnem redu njihovega ustvarjanja (vedenje LIFO ali "podobno skladu").


{
  using resourceA = new MyDisposable('A'); // Ustvarjeno najprej
  using resourceB = new MyDisposable('B'); // Ustvarjeno drugič
  console.log('Znotraj bloka, z uporabo virov...');
} // resourceB se odstrani najprej, nato resourceA

Asinhrono čiščenje z `await using`

Ključna beseda `await using` je asinhroni ekvivalent `using`. Uporablja se za vire, ki izvajajo `Symbol.asyncDispose`. Ker je čiščenje asinhrono, se ta ključna beseda lahko uporablja samo znotraj funkcije `async` ali na najvišji ravni modula (če je podprt `top-level await`).

Uporabimo naš razred `MockDbConnection`:


async function performDatabaseOperation() {
  console.log('Vstop v asinhrono funkcijo...');
  await using db = mockDbPool.getConnection();

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

  console.log('Operacija z bazo podatkov dokončana.');
} // <-- await db.[Symbol.asyncDispose]() se samodejno prikliče tukaj!

(async () => {
  await performDatabaseOperation();
  console.log('Asinhrona funkcija je dokončana.');
})();

Izhod prikazuje asinhrono čiščenje:

Vstop v asinhrono funkcijo...
Pridobljena povezava z bazo podatkov.
Izvajanje poizvedbe: SELECT * FROM users
Operacija z bazo podatkov dokončana.
Sproščanje povezave z bazov podatkov nazaj v sklad...
(čaka 50ms)
Povezava z bazov podatkov sproščena.
Asinhrona funkcija je dokončana.

Tako kot pri `using` sintaksa `await using` obravnava celoten življenjski cikel, vendar pravilno `čaka` asinhroni postopek čiščenja. Lahko celo obravnava vire, ki so le sinhrono odstranljivi - preprosto jih ne bo čakal.

Napredni vzorci: `DisposableStack` in `AsyncDisposableStack`

Včasih preprosto blokovno obseganje `using` ni dovolj prilagodljivo. Kaj pa, če morate upravljati skupino virov z življenjsko dobo, ki ni povezana z enim samim leksikalnim blokom? Ali kaj pa, če se integrirate s starejšo knjižnico, ki ne ustvarja objektov s `Symbol.dispose`?

Za te scenarije JavaScript zagotavlja dva pomožna razreda: `DisposableStack` in `AsyncDisposableStack`.

`DisposableStack`: Upravljalnik prilagodljivega čiščenja

`DisposableStack` je objekt, ki upravlja zbirko operacij čiščenja. Sam po sebi je razpoložljiv vir, tako da lahko njegovo celotno življenjsko dobo upravljate z blokom `using`.

Ima več uporabnih metod:

Primer: Pogojno upravljanje z viri

Predstavljajte si funkcijo, ki odpre datoteko dnevnika samo, če je izpolnjen določen pogoj, vendar želite, da se vse čiščenje zgodi na enem mestu na koncu.


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

  const db = stack.use(getDbConnection()); // Vedno uporabi bazo podatkov

  if (shouldLog) {
    const logFileStream = fs.createWriteStream('app.log');
    // Odloži čiščenje za tok
    stack.defer(() => {
      console.log('Zapiranje toka datoteke dnevnika...');
      logFileStream.end();
    });
    db.logTo(logFileStream);
  }

  db.doWork();

} // <-- Sklad je odstranjen in pokliče vse registrirane funkcije čiščenja v vrstnem redu LIFO.

`AsyncDisposableStack`: Za asinhroni svet

Kot bi si lahko predstavljali, je `AsyncDisposableStack` asinhrona različica. Lahko upravlja sinhrono in asinhrono odstranjevanje. Njena glavna metoda čiščenja je `.disposeAsync()`, ki vrne `Promise`, ki se razreši, ko so dokončane vse asinhroni operacije čiščenja.

Primer: Upravljanje mešanice virov

Ustvarimo ravnalnik zahteve spletnega strežnika, ki potrebuje povezavo z bazo podatkov (asinhrono čiščenje) in začasno datoteko (sinhrono čiščenje).


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

  // Upravljanje asinhronim razpoložljivim virom
  const dbConnection = await stack.use(getAsyncDbConnection());

  // Upravljanje sinhronim razpoložljivim virom
  const tempFile = stack.use(new TempFile('request data'));

  // Sprejmi vir iz starega API-ja
  const legacyResource = getLegacyResource();
  stack.adopt(legacyResource, () => legacyResource.shutdown());

  console.log('Obdelava zahteve...');
  await doWork(dbConnection, tempFile.path);

} // <-- stack.disposeAsync() se prikliče. Pravilno bo čakal asinhrono čiščenje.

`AsyncDisposableStack` je zmogljivo orodje za orkestracijo zapletene logike nastavitve in razgradnje na čist, predvidljiv način.

Robustno obravnavanje napak z `SuppressedError`

Ena od najbolj subtilnih, a pomembnih izboljšav ERM je način obravnave napak. Kaj se zgodi, če je napaka vržena znotraj bloka `using` in se *druga* napaka vrže med naknadnim samodejnim odstranjevanjem?

V starem svetu `try...finally` bi napaka iz bloka `finally` običajno prepisala ali "zadušila" prvotno, pomembnejšo napako iz bloka `try`. Zaradi tega je bilo odpravljanje napak pogosto neverjetno težko.

ERM to rešuje z novo globalno vrsto napake: `SuppressedError`. Če se med odstranjevanjem pojavi napaka, medtem ko se že širi druga napaka, se napaka pri odstranjevanju "zatre". Prvotna napaka se vrže, vendar ima zdaj lastnost `suppressed`, ki vsebuje napako pri odstranjevanju.


class FaultyResource {
  [Symbol.dispose]() {
    throw new Error('Napaka med odstranjevanjem!');
  }
}

try {
  using resource = new FaultyResource();
  throw new Error('Napaka med operacijo!');
} catch (e) {
  console.log(`Ujeta napaka: ${e.message}`); // Napaka med operacijo!
  if (e.suppressed) {
    console.log(`Zadušena napaka: ${e.suppressed.message}`); // Napaka med odstranjevanjem!
    console.log(e instanceof SuppressedError); // false
    console.log(e.suppressed instanceof Error); // true
  }
}

To vedenje zagotavlja, da nikoli ne izgubite konteksta prvotne napake, kar vodi do veliko bolj robustnih in odpravljivih sistemov.

Praktični primeri uporabe po ekosistemu JavaScript

Uporaba eksplicitnega upravljanja z viri je obsežna in pomembna za razvijalce po vsem svetu, ne glede na to, ali delajo na zaledju, sprednjem delu ali pri testiranju.

Podpora brskalnikov in izvajalnih okolij

Kot sodobna funkcija je pomembno vedeti, kje lahko uporabite eksplicitno upravljanje z viri. Od konca leta 2023 / začetka leta 2024 je podpora razširjena v najnovejših različicah glavnih JavaScript okolij:

Za starejša okolja se boste morali zanašati na transpilatorje, kot je Babel z ustreznimi vtičniki, da pretvorite sintakso `using` in zapolnite potrebne simbole in razrede skladov.

Zaključek: Nova doba varnosti in jasnosti

Eksplicitno upravljanje z viri v JavaScriptu je več kot le sintaktični sladkor; je temeljita izboljšava jezika, ki spodbuja varnost, jasnost in vzdržljivost. Z avtomatizacijo dolgočasnega in nagnjenega k napakam procesa čiščenja virov razvijalcem omogoča, da se osredotočijo na svojo primarno poslovno logiko.

Ključni povzetki so:

Ko začnete nove projekte ali refaktorirate obstoječo kodo, razmislite o sprejetju tega zmogljivega novega vzorca. Zato bo vaš JavaScript čistejši, vaše aplikacije bolj zanesljive in vaše življenje kot razvijalec bo le malo lažje. Je resnično globalni standard za pisanje sodobnega, profesionalnega JavaScripta.