Apgūstiet JavaScript jauno eksplicīto resursu pārvaldību ar `using` un `await using`. Iemācieties automatizēt tīrīšanu, novērst resursu noplūdes un rakstīt tīrāku, robustāku kodu.
JavaScript jaunā superspēja: padziļināts ieskats eksplicītajā resursu pārvaldībā
Dinamiskajā programmatūras izstrādes pasaulē efektīva resursu pārvaldība ir stabila, uzticama un veiktspējīga lietojumprogrammu stūrakmens. Gadu desmitiem JavaScript izstrādātāji ir paļāvušies uz manuāliem modeļiem, piemēram, try...catch...finally
, lai nodrošinātu, ka kritiski resursi — piemēram, failu apstrādātāji, tīkla savienojumi vai datubāzes sesijas — tiek pareizi atbrīvoti. Lai gan šī pieeja ir funkcionāla, tā bieži ir pārāk izvērsta, pakļauta kļūdām un var ātri kļūt grūti pārvaldāma — modelis, ko sarežģītos scenārijos dažkārt dēvē par "nolādētības piramīdu".
Ienāk valodas paradigmas maiņa: eksplicītā resursu pārvaldība (ERM). Šī jaudīgā funkcija, kas tika pabeigta ECMAScript 2024 (ES2024) standartā un iedvesmota no līdzīgām konstrukcijām tādās valodās kā C#, Python un Java, ievieš deklaratīvu un automatizētu veidu, kā pārvaldīt resursu tīrīšanu. Izmantojot jaunos atslēgvārdus using
un await using
, JavaScript tagad nodrošina daudz elegantāku un drošāku risinājumu mūžsenam programmēšanas izaicinājumam.
Šis visaptverošais ceļvedis aizvedīs jūs ceļojumā cauri JavaScript eksplicītajai resursu pārvaldībai. Mēs izpētīsim problēmas, ko tā risina, analizēsim tās pamatjēdzienus, apskatīsim praktiskus piemērus un atklāsim progresīvus modeļus, kas ļaus jums rakstīt tīrāku, noturīgāku kodu, neatkarīgi no tā, kurā pasaules malā jūs izstrādājat.
Vecā gvarde: manuālās resursu tīrīšanas izaicinājumi
Pirms mēs varam novērtēt jaunās sistēmas eleganci, mums vispirms ir jāsaprot vecās sistēmas sāpīgie punkti. Klasiskais resurss pārvaldības modelis JavaScript ir try...finally
bloks.
Loģika ir vienkārša: jūs iegūstat resursu try
blokā un atbrīvojat to finally
blokā. finally
bloks garantē izpildi neatkarīgi no tā, vai kods try
blokā ir veiksmīgs, neizdodas vai tiek priekšlaicīgi pārtraukts.
Apskatīsim bieži sastopamu servera puses scenāriju: faila atvēršana, datu ierakstīšana tajā un pēc tam nodrošināšana, ka fails tiek aizvērts.
Piemērs: vienkārša faila operācija ar try...finally
const fs = require('fs/promises');
async function processFile(filePath, data) {
let fileHandle;
try {
console.log('Atver failu...');
fileHandle = await fs.open(filePath, 'w');
console.log('Raksta failā...');
await fileHandle.write(data);
console.log('Dati veiksmīgi ierakstīti.');
} catch (error) {
console.error('Faila apstrādes laikā radās kļūda:', error);
} finally {
if (fileHandle) {
console.log('Aizver failu...');
await fileHandle.close();
}
}
}
Šis kods darbojas, bet tas atklāj vairākas vājās vietas:
- Izvērstība: Pamatloģiku (atvēršana un rakstīšana) ieskauj ievērojams daudzums šablona koda tīrīšanai un kļūdu apstrādei.
- Atbildību nošķiršana: Resursa iegūšana (
fs.open
) ir tālu no tam atbilstošās tīrīšanas (fileHandle.close
), padarot kodu grūtāk lasāmu un saprotamu. - Pakļauts kļūdām: Ir viegli aizmirst
if (fileHandle)
pārbaudi, kas izraisītu avāriju, ja sākotnējaisfs.open
izsaukums neizdotos. Turklāt kļūda pašafileHandle.close()
izsaukuma laikā netiek apstrādāta un varētu maskēt sākotnējo kļūdu notry
bloka.
Tagad iedomājieties pārvaldīt vairākus resursus, piemēram, datubāzes savienojumu un failu apstrādātāju. Kods ātri kļūst par ligzdotu jucekli:
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();
}
}
}
Šī ligzdošana ir grūti uzturama un mērogojama. Tas ir skaidrs signāls, ka ir nepieciešama labāka abstrakcija. Tieši šo problēmu tika izstrādāta risināt eksplicītā resursu pārvaldība.
Paradigmas maiņa: eksplicītās resursu pārvaldības principi
Eksplicītā resursu pārvaldība (ERM) ievieš līgumu starp resursa objektu un JavaScript izpildlaiku. Pamatideja ir vienkārša: objekts var deklarēt, kā tas būtu jātīra, un valoda nodrošina sintaksi, lai automātiski veiktu šo tīrīšanu, kad objekts iziet no tvēruma (scope).
Tas tiek panākts, izmantojot divas galvenās sastāvdaļas:
- Atbrīvojamais protokols (Disposable Protocol): Standarta veids, kā objekti var definēt savu tīrīšanas loģiku, izmantojot īpašus simbolus:
Symbol.dispose
sinhronai tīrīšanai unSymbol.asyncDispose
asinhronai tīrīšanai. using
unawait using
deklarācijas: Jauni atslēgvārdi, kas piesaista resursu bloka tvērumam. Izejot no bloka, automātiski tiek izsaukta resursa tīrīšanas metode.
Pamatjēdzieni: `Symbol.dispose` un `Symbol.asyncDispose`
ERM pamatā ir divi jauni labi zināmi simboli. Objekts, kuram ir metode ar vienu no šiem simboliem kā atslēgu, tiek uzskatīts par "atbrīvojamu resursu".
Sinhronā atbrīvošana ar `Symbol.dispose`
Simbols Symbol.dispose
norāda uz sinhronu tīrīšanas metodi. Tas ir piemērots resursiem, kuru tīrīšanai nav nepieciešamas asinhronas operācijas, piemēram, faila apstrādātāja sinhrona aizvēršana vai atmiņā esošas slēdzenes atbrīvošana.
Izveidosim pagaidu faila apvalku, kas pats sevi iztīra.
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(`Izveidots pagaidu fails: ${this.path}`);
}
// Šī ir sinhronā atbrīvošanas metode
[Symbol.dispose]() {
console.log(`Atbrīvo pagaidu failu: ${this.path}`);
try {
fs.unlinkSync(this.path);
console.log('Fails veiksmīgi izdzēsts.');
} catch (error) {
console.error(`Neizdevās izdzēst failu: ${this.path}`, error);
// Ir svarīgi apstrādāt kļūdas arī dispose metodē!
}
}
}
Jebkurš `TempFile` instances objekts tagad ir atbrīvojams resurss. Tam ir metode ar atslēgu `Symbol.dispose`, kas satur loģiku faila dzēšanai no diska.
Asinhronā atbrīvošana ar `Symbol.asyncDispose`
Daudzas mūsdienu tīrīšanas operācijas ir asinhronas. Datubāzes savienojuma aizvēršana var ietvert `QUIT` komandas nosūtīšanu pa tīklu, vai ziņojumu rindas klientam var būt nepieciešams iztukšot savu izejošo buferi. Šiem scenārijiem mēs izmantojam `Symbol.asyncDispose`.
Metodei, kas saistīta ar `Symbol.asyncDispose`, ir jāatgriež `Promise` (vai jābūt `async` funkcijai).
Modelēsim fiktīvu datubāzes savienojumu, kas asinhroni jāatgriež atpakaļ savienojumu krātuvē (pool).
// Fiktīva datubāzes savienojumu krātuve
const mockDbPool = {
getConnection: () => {
console.log('DB savienojums iegūts.');
return new MockDbConnection();
}
};
class MockDbConnection {
query(sql) {
console.log(`Izpilda vaicājumu: ${sql}`);
return Promise.resolve({ success: true, rows: [] });
}
// Šī ir asinhronā atbrīvošanas metode
async [Symbol.asyncDispose]() {
console.log('Atbrīvo DB savienojumu atpakaļ krātuvē...');
// Simulē tīkla aizkavi savienojuma atbrīvošanai
await new Promise(resolve => setTimeout(resolve, 50));
console.log('DB savienojums atbrīvots.');
}
}
Tagad jebkura `MockDbConnection` instance ir asinhroni atbrīvojams resurss. Tas zina, kā sevi asinhroni atbrīvot, kad tas vairs nav nepieciešams.
Jaunā sintakse: `using` un `await using` darbībā
Ar mūsu definētajām atbrīvojamām klasēm mēs tagad varam izmantot jaunos atslēgvārdus, lai tos automātiski pārvaldītu. Šie atslēgvārdi izveido bloka tvēruma deklarācijas, tāpat kā `let` un `const`.
Sinhronā tīrīšana ar `using`
Atslēgvārds `using` tiek izmantots resursiem, kas implementē `Symbol.dispose`. Kad koda izpilde atstāj bloku, kurā tika veikta `using` deklarācija, automātiski tiek izsaukta `[Symbol.dispose]()` metode.
Izmantosim mūsu `TempFile` klasi:
function processDataWithTempFile() {
console.log('Ieiet blokā...');
using tempFile = new TempFile('Šie ir daži svarīgi dati.');
// Šeit varat strādāt ar tempFile
const content = fs.readFileSync(tempFile.path, 'utf8');
console.log(`Nolasīts no pagaidu faila: "${content}"`);
// Šeit nav nepieciešams tīrīšanas kods!
console.log('...veic vairāk darba...');
} // <-- tempFile.[Symbol.dispose]() tiek automātiski izsaukts tieši šeit!
processDataWithTempFile();
console.log('Bloks ir atstāts.');
Izvade būtu šāda:
Ieiet blokā... Izveidots pagaidu fails: /path/to/temp_1678886400000.txt Nolasīts no pagaidu faila: "Šie ir daži svarīgi dati." ...veic vairāk darba... Atbrīvo pagaidu failu: /path/to/temp_1678886400000.txt Fails veiksmīgi izdzēsts. Bloks ir atstāts.
Paskatieties, cik tas ir tīrs! Resursa viss dzīves cikls ir ietverts blokā. Mēs to deklarējam, izmantojam un aizmirstam par to. Valoda nodrošina tīrīšanu. Tas ir milzīgs uzlabojums lasāmībā un drošībā.
Vairāku resursu pārvaldība
Vienā blokā var būt vairākas `using` deklarācijas. Tie tiks atbrīvoti apgrieztā secībā to izveidei (LIFO jeb "steka" uzvedība).
{
using resourceA = new MyDisposable('A'); // Izveidots pirmais
using resourceB = new MyDisposable('B'); // Izveidots otrais
console.log('Bloka iekšienē, izmanto resursus...');
} // resourceB tiek atbrīvots pirmais, pēc tam resourceA
Asinhronā tīrīšana ar `await using`
Atslēgvārds `await using` ir `using` asinhronais līdzinieks. To izmanto resursiem, kas implementē `Symbol.asyncDispose`. Tā kā tīrīšana ir asinhrona, šo atslēgvārdu var izmantot tikai `async` funkcijā vai moduļa augstākajā līmenī (ja tiek atbalstīts augstākā līmeņa await).
Izmantosim mūsu `MockDbConnection` klasi:
async function performDatabaseOperation() {
console.log('Ieiet asinhronā funkcijā...');
await using db = mockDbPool.getConnection();
await db.query('SELECT * FROM users');
console.log('Datubāzes operācija pabeigta.');
} // <-- await db.[Symbol.asyncDispose]() tiek automātiski izsaukts šeit!
(async () => {
await performDatabaseOperation();
console.log('Asinhronā funkcija ir pabeigta.');
})();
Izvade demonstrē asinhrono tīrīšanu:
Ieiet asinhronā funkcijā... DB savienojums iegūts. Izpilda vaicājumu: SELECT * FROM users Datubāzes operācija pabeigta. Atbrīvo DB savienojumu atpakaļ krātuvē... (gaida 50ms) DB savienojums atbrīvots. Asinhronā funkcija ir pabeigta.
Tāpat kā ar `using`, `await using` sintakse pārvalda visu dzīves ciklu, bet tā pareizi `awaits` asinhrono tīrīšanas procesu. Tā pat var apstrādāt resursus, kas ir tikai sinhroni atbrīvojami — tā vienkārši tos negaidīs.
Progresīvi modeļi: `DisposableStack` un `AsyncDisposableStack`
Dažreiz vienkāršā `using` bloka tvēruma definēšana nav pietiekami elastīga. Ko darīt, ja jums ir jāpārvalda resursu grupa ar dzīves ciklu, kas nav piesaistīts vienam leksiskam blokam? Vai ko darīt, ja jūs integrējaties ar vecāku bibliotēku, kas neražo objektus ar `Symbol.dispose`?
Šiem scenārijiem JavaScript nodrošina divas palīgklases: `DisposableStack` un `AsyncDisposableStack`.
`DisposableStack`: elastīgais tīrīšanas pārvaldnieks
`DisposableStack` ir objekts, kas pārvalda tīrīšanas operāciju kolekciju. Tas pats par sevi ir atbrīvojams resurss, tāpēc jūs varat pārvaldīt visu tā dzīves ciklu ar `using` bloku.
Tam ir vairākas noderīgas metodes:
.use(resource)
: Pievieno stekam objektu, kuram ir `[Symbol.dispose]` metode. Atgriež resursu, lai jūs varētu to saķēdēt..defer(callback)
: Pievieno stekam patvaļīgu tīrīšanas funkciju. Tas ir neticami noderīgi ad-hoc tīrīšanai..adopt(value, callback)
: Pievieno vērtību un tīrīšanas funkciju šai vērtībai. Tas ir ideāli piemērots resursu ietīšanai no bibliotēkām, kas neatbalsta atbrīvojamo protokolu..move()
: Pārnes resursu īpašumtiesības uz jaunu steku, notīrot pašreizējo.
Piemērs: nosacījumu resursu pārvaldība
Iedomājieties funkciju, kas atver žurnālfailu tikai tad, ja ir izpildīts noteikts nosacījums, bet jūs vēlaties, lai visa tīrīšana notiktu vienuviet beigās.
function processWithConditionalLogging(shouldLog) {
using stack = new DisposableStack();
const db = stack.use(getDbConnection()); // Vienmēr izmanto DB
if (shouldLog) {
const logFileStream = fs.createWriteStream('app.log');
// Atliek straumes tīrīšanu
stack.defer(() => {
console.log('Aizver žurnālfaila straumi...');
logFileStream.end();
});
db.logTo(logFileStream);
}
db.doWork();
} // <-- Steks tiek atbrīvots, izsaucot visas reģistrētās tīrīšanas funkcijas LIFO secībā.
`AsyncDisposableStack`: asinhronajai pasaulei
Kā jūs varat nojaust, `AsyncDisposableStack` ir asinhronā versija. Tā var pārvaldīt gan sinhronus, gan asinhronus atbrīvojamos resursus. Tās galvenā tīrīšanas metode ir `.disposeAsync()`, kas atgriež `Promise`, kas atrisinās, kad visas asinhronās tīrīšanas operācijas ir pabeigtas.
Piemērs: resursu sajaukuma pārvaldība
Izveidosim tīmekļa servera pieprasījumu apstrādātāju, kuram nepieciešams datubāzes savienojums (asinhrona tīrīšana) un pagaidu fails (sinhrona tīrīšana).
async function handleRequest() {
await using stack = new AsyncDisposableStack();
// Pārvalda asinhronu atbrīvojamu resursu
const dbConnection = await stack.use(getAsyncDbConnection());
// Pārvalda sinhronu atbrīvojamu resursu
const tempFile = stack.use(new TempFile('request data'));
// Adoptē resursu no veca API
const legacyResource = getLegacyResource();
stack.adopt(legacyResource, () => legacyResource.shutdown());
console.log('Apstrādā pieprasījumu...');
await doWork(dbConnection, tempFile.path);
} // <-- Tiek izsaukts stack.disposeAsync(). Tas pareizi gaidīs asinhrono tīrīšanu.
`AsyncDisposableStack` ir spēcīgs rīks sarežģītu iestatīšanas un nojaukšanas loģiku organizēšanai tīrā, paredzamā veidā.
Robusta kļūdu apstrāde ar `SuppressedError`
Viens no smalkākajiem, bet nozīmīgākajiem ERM uzlabojumiem ir tas, kā tā apstrādā kļūdas. Kas notiek, ja `using` blokā tiek izmesta kļūda, un *cita* kļūda tiek izmesta sekojošās automātiskās atbrīvošanas laikā?
Vecajā `try...finally` pasaulē kļūda no `finally` bloka parasti pārrakstītu vai "nomāktu" sākotnējo, svarīgāko kļūdu no `try` bloka. Tas bieži vien padarīja atkļūdošanu neticami sarežģītu.
ERM to atrisina ar jaunu globālu kļūdas tipu: `SuppressedError`. Ja atbrīvošanas laikā rodas kļūda, kamēr cita kļūda jau izplatās, atbrīvošanas kļūda tiek "nomākta". Sākotnējā kļūda tiek izmesta, bet tai tagad ir `suppressed` īpašība, kas satur atbrīvošanas kļūdu.
class FaultyResource {
[Symbol.dispose]() {
throw new Error('Kļūda atbrīvošanas laikā!');
}
}
try {
using resource = new FaultyResource();
throw new Error('Kļūda operācijas laikā!');
} catch (e) {
console.log(`Noķerta kļūda: ${e.message}`); // Kļūda operācijas laikā!
if (e.suppressed) {
console.log(`Nomāktā kļūda: ${e.suppressed.message}`); // Kļūda atbrīvošanas laikā!
console.log(e instanceof SuppressedError); // false
console.log(e.suppressed instanceof Error); // true
}
}
Šī uzvedība nodrošina, ka jūs nekad nezaudējat sākotnējās kļūmes kontekstu, kas noved pie daudz robustākām un atkļūdojamākām sistēmām.
Praktiski pielietojuma gadījumi visā JavaScript ekosistēmā
Eksplicītās resursu pārvaldības pielietojumi ir plaši un aktuāli izstrādātājiem visā pasaulē, neatkarīgi no tā, vai viņi strādā ar back-end, front-end vai testēšanā.
- Back-End (Node.js, Deno, Bun): Visacīmredzamākie lietošanas gadījumi ir šeit. Datubāzes savienojumu, failu apstrādātāju, tīkla ligzdu un ziņojumu rindu klientu pārvaldība kļūst triviāla un droša.
- Front-End (tīmekļa pārlūkprogrammas): ERM ir vērtīgs arī pārlūkprogrammā. Jūs varat pārvaldīt `WebSocket` savienojumus, atbrīvot slēdzenes no Web Locks API vai tīrīt sarežģītus WebRTC savienojumus.
- Testēšanas ietvari (Jest, Mocha, u.c.): Izmantojiet `DisposableStack` `beforeEach` vai testu ietvaros, lai automātiski nojauktu maketus (mocks), spiegus (spies), testa serverus vai datubāzes stāvokļus, nodrošinot tīru testu izolāciju.
- UI ietvari (React, Svelte, Vue): Lai gan šiem ietvariem ir savas dzīves cikla metodes, jūs varat izmantot `DisposableStack` komponenta ietvaros, lai pārvaldītu resursus, kas nav saistīti ar ietvaru, piemēram, notikumu klausītājus vai trešo pušu bibliotēku abonementus, nodrošinot, ka tie visi tiek iztīrīti atvienošanas (unmount) brīdī.
Pārlūkprogrammu un izpildlaika atbalsts
Tā kā šī ir moderna funkcija, ir svarīgi zināt, kur jūs varat izmantot eksplicīto resursu pārvaldību. Sākot ar 2023. gada beigām / 2024. gada sākumu, atbalsts ir plaši izplatīts jaunākajās galveno JavaScript vidi versijās:
- Node.js: Versija 20+ (aiz karoga agrākās versijās)
- Deno: Versija 1.32+
- Bun: Versija 1.0+
- Pārlūkprogrammas: Chrome 119+, Firefox 121+, Safari 17.2+
Vecākām vidēm jums būs jāpaļaujas uz transpaileriem, piemēram, Babel, ar atbilstošiem spraudņiem, lai pārveidotu `using` sintaksi un polifilētu nepieciešamos simbolus un steka klases.
Secinājums: jauna drošības un skaidrības ēra
JavaScript eksplicītā resursu pārvaldība ir vairāk nekā tikai sintaktiskais cukurs; tas ir fundamentāls uzlabojums valodai, kas veicina drošību, skaidrību un uzturamību. Automatizējot nogurdinošo un kļūdām pakļauto resursu tīrīšanas procesu, tas atbrīvo izstrādātājus, lai viņi varētu koncentrēties uz savu galveno biznesa loģiku.
Galvenie secinājumi ir:
- Automatizējiet tīrīšanu: Izmantojiet
using
unawait using
, lai novērstu manuālotry...finally
šablona kodu. - Uzlabojiet lasāmību: Turiet resursu iegūšanu un tā dzīves cikla tvērumu cieši saistītus un redzamus.
- Novērsiet noplūdes: Garantējiet, ka tīrīšanas loģika tiek izpildīta, novēršot dārgas resursu noplūdes jūsu lietojumprogrammās.
- Robustu kļūdu apstrāde: Gūstiet labumu no jaunā
SuppressedError
mehānisma, lai nekad nezaudētu kritisku kļūdu kontekstu.
Sākot jaunus projektus vai refaktorējot esošo kodu, apsveriet iespēju pieņemt šo jaudīgo jauno modeli. Tas padarīs jūsu JavaScript tīrāku, jūsu lietojumprogrammas uzticamākas un jūsu kā izstrādātāja dzīvi nedaudz vieglāku. Tas ir patiesi globāls standarts modernas, profesionālas JavaScript rakstīšanai.