Õ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:
- Sõnaohtrus: Põhiloogika (avamine ja kirjutamine) on ümbritsetud märkimisväärse hulga šabloonkoodiga puhastamiseks ja veatöötluseks.
- Huvide lahusus (Separation of Concerns): Ressursi omandamine (
fs.open
) on kaugel selle vastavast puhastamisest (fileHandle.close
), mis muudab koodi raskemini loetavaks ja mõistetavaks. - Vigaderohke: Lihtne on unustada
if (fileHandle)
kontroll, mis põhjustaks vea, kui esialgnefs.open
kutse ebaõnnestuks. Lisaks ei käsitleta vigafileHandle.close()
kutse enda ajal ja see võib varjata algse veatry
plokist.
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:
- Vabastamisprotokoll (Disposable Protocol): Standardne viis, kuidas objektid saavad defineerida oma puhastusloogika, kasutades spetsiaalseid sümboleid:
Symbol.dispose
sünkroonseks puhastamiseks jaSymbol.asyncDispose
asünkroonseks puhastamiseks. using
jaawait 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:
.use(resource)
: Lisab stack'i objekti, millel on[Symbol.dispose]
meetod. Tagastab ressursi, nii et saate seda aheldada..defer(callback)
: Lisab stack'i suvalise puhastusfunktsiooni. See on uskumatult kasulik ad-hoc puhastuseks..adopt(value, callback)
: Lisab väärtuse ja selle väärtuse jaoks puhastusfunktsiooni. See sobib ideaalselt ressursside ümbritsemiseks teekidest, mis ei toeta vabastamisprotokolli..move()
: Annab ressursside omandiõiguse üle uuele stack'ile, tühjendades praeguse.
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.
- Back-End (Node.js, Deno, Bun): Kõige ilmselgemad kasutusjuhud on siin. Andmebaasiühenduste, failikäepidemete, võrgusoklite ja sõnumijärjekorra klientide haldamine muutub triviaalseks ja turvaliseks.
- Front-End (Veebibrauserid): ERM on väärtuslik ka brauseris. Saate hallata
WebSocket
ühendusi, vabastada lukke Web Locks API-st või puhastada keerulisi WebRTC ühendusi. - Testimisraamistikud (Jest, Mocha jne): Kasutage
DisposableStack
'ibeforeEach
'is või testide sees, et automaatselt maha võtta mock'e, spioone, testiservereid või andmebaasi olekuid, tagades testide puhta isolatsiooni. - UI raamistikud (React, Svelte, Vue): Kuigi nendel raamistikel on oma elutsükli meetodid, saate komponendi sees kasutada
DisposableStack
'i, et hallata raamistikuväliseid ressursse nagu sündmuste kuulajaid või kolmandate osapoolte teekide tellimusi, tagades, et need kõik puhastatakse komponendi eemaldamisel (unmount).
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:
- Node.js: Versioon 20+ (varasemates versioonides lipu taga)
- Deno: Versioon 1.32+
- Bun: Versioon 1.0+
- Brauserid: Chrome 119+, Firefox 121+, Safari 17.2+
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:
- Automatiseerige puhastus: Kasutage
using
jaawait using
, et vabaneda käsitsi kirjutatavasttry...finally
šabloonkoodist. - Parandage loetavust: Hoidke ressursi omandamine ja selle elutsükli skoop tihedalt seotud ja nähtaval.
- Ennetage lekkeid: Garanteerige, et puhastusloogika käivitatakse, ennetades kulukaid ressursilekkeid teie rakendustes.
- Käsitlege vigu robustselt: Kasutage uut
SuppressedError
mehhanismi, et mitte kunagi kaotada kriitilist veakonteksti.
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.