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:
- Obsežnost: Jedro logike (odpiranje in pisanje) je obdano z znatno količino standardnih nastavitev za čiščenje in obravnavo napak.
- Ločevanje interesov: Pridobitev vira (
fs.open
) je daleč od ustreznega čiščenja (fileHandle.close
), zaradi česar je kodo težje brati in razumevati. - Nagnjeno k napakam: Preprosto je pozabiti na preverjanje
if (fileHandle)
, kar bi povzročilo zrušitev, če bi prvi klicfs.open
spodletel. Poleg tega se napaka med klicemfileHandle.close()
sama po sebi ne obravnava in bi lahko prikrila prvotno napako iz blokatry
.
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:
- Protokol Disposable: Standardni način, da predmeti določijo svojo lastno logiko čiščenja z uporabo posebnih simbolov:
Symbol.dispose
za sinhrono čiščenje inSymbol.asyncDispose
za asinhrono čiščenje. - 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:
.use(resource)
: Doda objekt, ki ima metodo `[Symbol.dispose]` na sklad. Vrne vir, tako da ga lahko povežete..defer(callback)
: Doda poljubno funkcijo čiščenja na sklad. To je neverjetno uporabno za ad-hoc čiščenje..adopt(value, callback)
: Doda vrednost in funkcijo čiščenja za to vrednost. To je popolno za ovijanje virov iz knjižnic, ki ne podpirajo protokola za odstranjevanje..move()
: Prenese lastništvo virov na nov sklad in počisti trenutnega.
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.
- Zaledje (Node.js, Deno, Bun): Najbolj očitni primeri uporabe živijo tukaj. Upravljanje s povezavami z bazami podatkov, ročaji datotek, omrežnimi vtičnicami in odjemalci čakalne vrste sporočil postane trivialno in varno.
- Sprednji del (spletni brskalniki): ERM je koristen tudi v brskalniku. Lahko upravljate povezave `WebSocket`, sprostite ključavnice iz API-ja Web Locks ali očistite zapletene povezave WebRTC.
- Testni okviri (Jest, Mocha itd.): Uporabite `DisposableStack` v `beforeEach` ali znotraj testov, da samodejno odstranite posmehe, vohune, testne strežnike ali stanja baz podatkov, kar zagotavlja čisto izolacijo testa.
- UI okviri (React, Svelte, Vue): Čeprav imajo ti okviri svoje metode življenjskega cikla, lahko uporabite `DisposableStack` v komponenti za upravljanje neokvirnih virov, kot so poslušalci dogodkov ali naročnine knjižnic tretjih oseb, s čimer zagotovite, da so vsi očiščeni ob izklopu.
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:
- Node.js: Različica 20+ (za zastavico v prejšnjih različicah)
- Deno: Različica 1.32+
- Bun: Različica 1.0+
- Brskalniki: Chrome 119+, Firefox 121+, Safari 17.2+
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:
- Avtomatizirajte čiščenje: Uporabite
using
inawait using
, da odpravite ročno nastavitevtry...finally
. - Izboljšajte berljivost: Ohranjajte pridobivanje virov in njegov življenjski cikel tesno povezana in vidna.
- Preprečite uhajanje: Zagotovite, da se izvede logika čiščenja, s čimer preprečite drage uhajanje virov v vaših aplikacijah.
- Robustno obravnavajte napake: Izkoristite nov mehanizem
SuppressedError
, da nikoli ne izgubite kritičnega konteksta napake.
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.