Įvaldykite naująjį JavaScript aiškų išteklių valdymą su `using` ir `await using`. Išmokite automatizuoti valymą, užkirsti kelią išteklių nutekėjimams ir rašyti švaresnį, patikimesnį kodą.
JavaScript naujoji supergalia: gilus panardinimas į aiškų išteklių valdymą
Dinamiškame programinės įrangos kūrimo pasaulyje efektyvus išteklių valdymas yra tvirtų, patikimų ir našumą užtikrinančių programų kūrimo pagrindas. Dešimtmečius JavaScript kūrėjai rėmėsi rankiniais modeliais, tokiais kaip try...catch...finally
, siekdami užtikrinti, kad svarbūs ištekliai, tokie kaip failų tvarkyklės, tinklo jungtys ar duomenų bazių sesijos, būtų tinkamai paleisti. Nors šis metodas veikia, jis dažnai yra daugiažodis, linkęs į klaidas ir gali greitai tapti nevaldomas, o sudėtinguose scenarijuose šis modelis kartais vadinamas "pražūties piramide".
Įveskite kalbos paradigmos poslinkį: Aiškus išteklių valdymas (ERM). Ši galinga funkcija, baigta ECMAScript 2024 (ES2024) standarte, įkvėpta panašių konstrukcijų tokiose kalbose kaip C#, Python ir Java, įveda deklaratyvų ir automatizuotą būdą tvarkyti išteklių valymą. Naudodama naujus raktinius žodžius using
ir await using
, JavaScript dabar siūlo daug elegantiškesnį ir saugesnį sprendimą amžinam programavimo iššūkiui.
Šis išsamus vadovas nuves jus į kelionę per JavaScript aiškų išteklių valdymą. Išnagrinėsime problemas, kurias jis sprendžia, išanalizuosime pagrindines jo sąvokas, pateiksime praktinių pavyzdžių ir atskleisime pažangius modelius, kurie leis jums rašyti švaresnį, atsparesnį kodą, nesvarbu, kurioje pasaulio vietoje kuriate.
Sena gvardija: rankinio išteklių valymo iššūkiai
Prieš įvertindami naujosios sistemos eleganciją, pirmiausia turime suprasti senosios sistemos trūkumus. Klasikinis išteklių valdymo modelis JavaScript yra try...finally
blokas.
Logika paprasta: išteklių įgyjate try
bloke, o atleidžiate jį finally
bloke. finally
blokas garantuoja vykdymą, nesvarbu, ar try
bloke esantis kodas pavyksta, ar nepavyksta, ar grįžta per anksti.
Apsvarstykime įprastą serverio pusės scenarijų: failo atidarymas, tam tikrų duomenų įrašymas į jį ir užtikrinimas, kad failas būtų uždarytas.
Pavyzdys: paprasta failo operacija su try...finally
const fs = require('fs/promises');
async function processFile(filePath, data) {
let fileHandle;
try {
console.log('Opening file...');
fileHandle = await fs.open(filePath, 'w');
console.log('Writing to file...');
await fileHandle.write(data);
console.log('Data written successfully.');
} catch (error) {
console.error('An error occurred during file processing:', error);
} finally {
if (fileHandle) {
console.log('Closing file...');
await fileHandle.close();
}
}
}
Šis kodas veikia, tačiau atskleidžia keletą trūkumų:
- Daugiažodiškumas: Pagrindinė logika (atidarymas ir rašymas) yra apsupta didelio kiekio šabloninio kodo valymui ir klaidų tvarkymui.
- Atskirų atsakomybių: Išteklių įsigijimas (
fs.open
) yra toli nuo atitinkamo valymo (fileHandle.close
), todėl kodą sunkiau skaityti ir suprasti. - Linkęs į klaidas: Lengva pamiršti
if (fileHandle)
patikrinimą, kuris sukeltų gedimą, jei pradinisfs.open
iškvietimas nepavyktų. Be to, klaidafileHandle.close()
iškvietimo metu nėra apdorojama ir gali užmaskuoti pradinę klaidą ištry
bloko.
Dabar įsivaizduokite, kad valdote kelis išteklius, pvz., duomenų bazės jungtį ir failo tvarkyklę. Kodas greitai tampa įdėtų netvarka:
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();
}
}
}
Šį įdėtį sunku prižiūrėti ir keisti. Tai aiškus signalas, kad reikia geresnės abstrakcijos. Būtent šią problemą ir buvo sukurtas išspręsti aiškus išteklių valdymas.
Paradigmos poslinkis: aiškaus išteklių valdymo principai
Aiškus išteklių valdymas (ERM) įveda sutartį tarp išteklių objekto ir JavaScript vykdymo aplinkos. Pagrindinė idėja paprasta: objektas gali deklaruoti, kaip jis turėtų būti išvalytas, o kalba pateikia sintaksę, kad automatiškai atliktų tą valymą, kai objektas išeina iš galiojimo srities.
Tai pasiekiama naudojant du pagrindinius komponentus:
- Vienkartinis protokolas: Standartinis būdas objektams apibrėžti savo valymo logiką naudojant specialius simbolius:
Symbol.dispose
sinchroniniam valymui irSymbol.asyncDispose
asinchroniniam valymui. using
irawait using
deklaracijos: Nauji raktiniai žodžiai, kurie susieja išteklius su bloko aprėptimi. Kai išeinama iš bloko, automatiškai iškviečiamas ištekliaus valymo metodas.
Pagrindinės sąvokos: Symbol.dispose
ir Symbol.asyncDispose
ERM pagrindą sudaro du nauji gerai žinomi simboliai. Objektas, turintis metodą su vienu iš šių simbolių kaip raktu, laikomas "vienkartiniu ištekliumi".
Sinchroninis sunaikinimas su Symbol.dispose
Simbolis Symbol.dispose
nurodo sinchroninį valymo metodą. Tai tinka ištekliams, kurių valymui nereikia jokių asinchroninių operacijų, pvz., sinchroninis failo tvarkyklės uždarymas arba atminties užrakto atlaisvinimas.
Sukurkime laikinojo failo, kuris pats išsivalo, apvalkalą.
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(`Created temp file: ${this.path}`);
}
// This is the synchronous disposable method
[Symbol.dispose]() {
console.log(`Disposing temp file: ${this.path}`);
try {
fs.unlinkSync(this.path);
console.log('File deleted successfully.');
} catch (error) {
console.error(`Failed to delete file: ${this.path}`, error);
// It's important to handle errors within dispose, too!
}
}
}
Bet kuris `TempFile` egzempliorius dabar yra vienkartinis išteklius. Jis turi metodą, pažymėtą raktu `Symbol.dispose`, kuriame yra logika, skirta ištrinti failą iš disko.
Asinchroninis sunaikinimas su Symbol.asyncDispose
Daugelis šiuolaikinių valymo operacijų yra asinchroninės. Duomenų bazės jungties uždarymas gali apimti `QUIT` komandos siuntimą per tinklą, arba pranešimų eilės klientui gali prireikti išvalyti savo išeinantį buferį. Šiems scenarijams naudojame Symbol.asyncDispose
.
Su `Symbol.asyncDispose` susietas metodas turi grąžinti `Promise` (arba būti `async` funkcija).
Sumodeliuokime netikrą duomenų bazės jungtį, kurią reikia asinchroniškai grąžinti į telkinį.
// A mock database pool
const mockDbPool = {
getConnection: () => {
console.log('DB connection acquired.');
return new MockDbConnection();
}
};
class MockDbConnection {
query(sql) {
console.log(`Executing query: ${sql}`);
return Promise.resolve({ success: true, rows: [] });
}
// This is the asynchronous disposable method
async [Symbol.asyncDispose]() {
console.log('Releasing DB connection back to the pool...');
// Simulate a network delay for releasing the connection
await new Promise(resolve => setTimeout(resolve, 50));
console.log('DB connection released.');
}
}
Dabar bet kuris `MockDbConnection` egzempliorius yra asinchroninis vienkartinis išteklius. Jis žino, kaip asinchroniškai atsileisti, kai jo nebereikia.
Nauja sintaksė: using
ir await using
veiksme
Apibrėžę savo vienkartines klases, dabar galime naudoti naujus raktinius žodžius, kad juos automatiškai valdytume. Šie raktiniai žodžiai sukuria bloko aprėpties deklaracijas, kaip ir let
bei const
.
Sinchroninis valymas su using
Raktinis žodis using
naudojamas ištekliams, kurie įgyvendina Symbol.dispose
. Kai kodo vykdymas palieka bloką, kuriame buvo pateikta using
deklaracija, automatiškai iškviečiamas metodas [Symbol.dispose]()
.
Naudokime savo `TempFile` klasę:
function processDataWithTempFile() {
console.log('Entering block...');
using tempFile = new TempFile('This is some important data.');
// You can work with tempFile here
const content = fs.readFileSync(tempFile.path, 'utf8');
console.log(`Read from temp file: "${content}"`);
// No cleanup code needed here!
console.log('...doing more work...');
} // <-- tempFile.[Symbol.dispose]() is called automatically right here!
processDataWithTempFile();
console.log('Block has been exited.');
Išvestis būtų:
Entering block... Created temp file: /path/to/temp_1678886400000.txt Read from temp file: "This is some important data." ...doing more work... Disposing temp file: /path/to/temp_1678886400000.txt File deleted successfully. Block has been exited.
Pažvelkite, kaip švaru! Visas ištekliaus gyvavimo ciklas yra bloke. Mes jį deklaruojame, naudojame ir pamirštame. Kalba tvarko valymą. Tai didžiulis skaitymo ir saugos patobulinimas.
Kelių išteklių valdymas
Tame pačiame bloke galite turėti kelias using
deklaracijas. Jie bus sunaikinti atvirkštine jų sukūrimo tvarka (LIFO arba "į rietuvę panašus" elgesys).
{
using resourceA = new MyDisposable('A'); // Created first
using resourceB = new MyDisposable('B'); // Created second
console.log('Inside block, using resources...');
} // resourceB is disposed of first, then resourceA
Asinchroninis valymas su await using
Raktinis žodis await using
yra asinchroninis using
atitikmuo. Jis naudojamas ištekliams, kurie įgyvendina Symbol.asyncDispose
. Kadangi valymas yra asinchroninis, šis raktinis žodis gali būti naudojamas tik `async` funkcijoje arba modulio viršutiniame lygyje (jei palaikomas aukščiausio lygio await).
Naudokime savo `MockDbConnection` klasę:
async function performDatabaseOperation() {
console.log('Entering async function...');
await using db = mockDbPool.getConnection();
await db.query('SELECT * FROM users');
console.log('Database operation complete.');
} // <-- await db.[Symbol.asyncDispose]() is called automatically here!
(async () => {
await performDatabaseOperation();
console.log('Async function has completed.');
})();
Išvestis parodo asinchroninį valymą:
Entering async function... DB connection acquired. Executing query: SELECT * FROM users Database operation complete. Releasing DB connection back to the pool... (waits 50ms) DB connection released. Async function has completed.
Kaip ir naudojant using
, sintaksė await using
tvarko visą gyvavimo ciklą, tačiau teisingai `laukiama` asinchroninio valymo proceso. Jis netgi gali tvarkyti išteklius, kurie yra tik sinchroniškai vienkartiniai – jis tiesiog nelauks jų.
Išplėstiniai modeliai: DisposableStack
ir AsyncDisposableStack
Kartais paprasto bloko aprėpties using
nepakanka. Ką daryti, jei jums reikia valdyti išteklių grupę, kurios gyvavimo laikas nėra susietas su vienu leksiniu bloku? Arba ką daryti, jei integruojatės su senesne biblioteka, kuri negamina objektų su Symbol.dispose
?
Šiems scenarijams JavaScript pateikia dvi pagalbininkų klases: DisposableStack
ir AsyncDisposableStack
.
DisposableStack
: lankstus valymo tvarkytuvas
DisposableStack
yra objektas, kuris valdo valymo operacijų rinkinį. Jis pats yra vienkartinis išteklius, todėl galite valdyti visą jo gyvavimo laiką naudodami using
bloką.
Jis turi keletą naudingų metodų:
.use(resource)
: į rietuvę įtraukia objektą, kuris turi metodą[Symbol.dispose]
. Grąžina išteklių, todėl galite jį sujungti..defer(callback)
: į rietuvę įtraukia savavališką valymo funkciją. Tai neįtikėtinai naudinga valant ad-hoc..adopt(value, callback)
: įtraukia vertę ir tos vertės valymo funkciją. Tai puikiai tinka apvyniojant išteklius iš bibliotekų, kurios nepalaiko vienkartinio protokolo..move()
: perkelia išteklių nuosavybę į naują rietuvę, išvalydamas esamą.
Pavyzdys: sąlyginis išteklių valdymas
Įsivaizduokite funkciją, kuri atidaro žurnalo failą tik tuo atveju, jei įvykdoma tam tikra sąlyga, bet norite, kad visas valymas įvyktų vienoje vietoje pabaigoje.
function processWithConditionalLogging(shouldLog) {
using stack = new DisposableStack();
const db = stack.use(getDbConnection()); // Always use the DB
if (shouldLog) {
const logFileStream = fs.createWriteStream('app.log');
// Defer the cleanup for the stream
stack.defer(() => {
console.log('Closing log file stream...');
logFileStream.end();
});
db.logTo(logFileStream);
}
db.doWork();
} // <-- The stack is disposed, calling all registered cleanup functions in LIFO order.
AsyncDisposableStack
: asinchroniniam pasauliui
Kaip galite atspėti, `AsyncDisposableStack` yra asinchroninė versija. Jis gali valdyti tiek sinchroninius, tiek asinchroninius vienkartinius elementus. Pagrindinis jo valymo metodas yra `.disposeAsync()`, kuris grąžina `Promise`, kuris išsprendžia, kai visos asinchroninės valymo operacijos yra baigtos.
Pavyzdys: išteklių mišinio valdymas
Sukurkime žiniatinklio serverio užklausos tvarkyklę, kuriai reikia duomenų bazės jungties (asinchroninis valymas) ir laikinojo failo (sinchroninis valymas).
async function handleRequest() {
await using stack = new AsyncDisposableStack();
// Manage an async disposable resource
const dbConnection = await stack.use(getAsyncDbConnection());
// Manage a sync disposable resource
const tempFile = stack.use(new TempFile('request data'));
// Adopt a resource from an old API
const legacyResource = getLegacyResource();
stack.adopt(legacyResource, () => legacyResource.shutdown());
console.log('Processing request...');
await doWork(dbConnection, tempFile.path);
} // <-- stack.disposeAsync() is called. It will correctly await async cleanup.
AsyncDisposableStack
yra galingas įrankis, skirtas organizuoti sudėtingą sąrankos ir išardymo logiką švariu, nuspėjamu būdu.
Patikimas klaidų tvarkymas naudojant SuppressedError
Vienas iš subtiliausių, bet reikšmingiausių ERM patobulinimų yra tai, kaip jis tvarko klaidas. Kas atsitiks, jei klaida išmetama using
bloke, o *kita* klaida išmetama per automatinį sunaikinimą?
Sename try...finally
pasaulyje klaida iš finally
bloko paprastai perrašytų arba "nuslopintų" pradinę, svarbesnę klaidą iš try
bloko. Dėl to dažnai buvo neįtikėtinai sunku derinti.
ERM tai išsprendžia naudodamas naują visuotinį klaidos tipą: SuppressedError
. Jei sunaikinimo metu įvyksta klaida, kai kita klaida jau sklinda, sunaikinimo klaida "nuslopinama". Išmetama pradinė klaida, tačiau dabar ji turi savybę suppressed
, kurioje yra sunaikinimo klaida.
class FaultyResource {
[Symbol.dispose]() {
throw new Error('Error during disposal!');
}
}
try {
using resource = new FaultyResource();
throw new Error('Error during operation!');
} catch (e) {
console.log(`Caught error: ${e.message}`); // Error during operation!
if (e.suppressed) {
console.log(`Suppressed error: ${e.suppressed.message}`); // Error during disposal!
console.log(e instanceof SuppressedError); // false
console.log(e.suppressed instanceof Error); // true
}
}
Šis elgesys užtikrina, kad niekada neprarastumėte pradinės nesėkmės konteksto, todėl sistemos bus daug patikimesnės ir lengviau derinti.
Praktiniai naudojimo atvejai visoje JavaScript ekosistemoje
Aiškus išteklių valdymas yra labai įvairus ir aktualus kūrėjams visame pasaulyje, nesvarbu, ar jie dirba su galine dalimi, priekine dalimi ar testavimu.
- Galinė dalis (Node.js, Deno, Bun): Akivaizdžiausi naudojimo atvejai yra čia. Duomenų bazės jungčių, failų tvarkyklių, tinklo lizdų ir pranešimų eilių klientų valdymas tampa trivialus ir saugus.
- Priekinė dalis (žiniatinklio naršyklės): ERM taip pat yra vertinga naršyklėje. Galite valdyti `WebSocket` jungtis, atlaisvinti užraktus iš žiniatinklio užraktų API arba išvalyti sudėtingas WebRTC jungtis.
- Testavimo sistemos (Jest, Mocha ir kt.): Naudokite
DisposableStack
beforeEach
arba testuose, kad automatiškai išardytumėte maketus, šnipus, testavimo serverius arba duomenų bazės būsenas, užtikrindami švarią testo izoliaciją. - UI sistemos (React, Svelte, Vue): Nors šios sistemos turi savo gyvavimo ciklo metodus, galite naudoti
DisposableStack
komponente, kad valdytumėte ne sistemos išteklius, pvz., įvykių klausytojus arba trečiųjų šalių bibliotekų prenumeratas, užtikrindami, kad jie visi būtų išvalyti atjungiant.
Naršyklės ir vykdymo aplinkos palaikymas
Kaip šiuolaikinė funkcija, svarbu žinoti, kur galite naudoti aiškų išteklių valdymą. 2023 m. pabaigoje / 2024 m. pradžioje palaikymas yra plačiai paplitęs naujausiose pagrindinių JavaScript aplinkų versijose:
- Node.js: 20+ versija (už vėliavos ankstesnėse versijose)
- Deno: 1.32+ versija
- Bun: 1.0+ versija
- Naršyklės: Chrome 119+, Firefox 121+, Safari 17.2+
Senesnėms aplinkoms turėsite pasikliauti transpiliatoriais, tokiais kaip Babel su atitinkamais priedais, kad transformuotumėte sintaksę using
ir užpildytumėte reikiamus simbolius ir rietuvės klases.
Išvada: nauja saugos ir aiškumo era
JavaScript aiškus išteklių valdymas yra daugiau nei tik sintaksinis cukrus; tai esminis kalbos patobulinimas, skatinantis saugą, aiškumą ir prižiūrimumą. Automatizuodama varginantį ir klaidų linkusį išteklių valymo procesą, ji išlaisvina kūrėjus sutelkti dėmesį į pagrindinę savo verslo logiką.
Pagrindiniai punktai yra:
- Automatizuokite valymą: Naudokite
using
irawait using
, kad pašalintumėte rankinįtry...finally
šabloninį kodą. - Pagerinkite skaitomumą: Išteklių įsigijimą ir jo gyvavimo ciklo aprėptį laikykite glaudžiai susietus ir matomus.
- Užkirskite kelią nutekėjimams: Garantuokite, kad bus vykdoma valymo logika, užkertant kelią brangiems išteklių nutekėjimams jūsų programose.
- Patikimai tvarkykite klaidas: Pasinaudokite naujuoju
SuppressedError
mechanizmu, kad niekada neprarastumėte kritinio klaidos konteksto.
Pradėdami naujus projektus arba pertvarkydami esamą kodą, apsvarstykite galimybę pritaikyti šį galingą naują modelį. Tai padarys jūsų JavaScript švaresnį, jūsų programas patikimesnes, o jūsų, kaip kūrėjo, gyvenimą šiek tiek lengvesnį. Tai tikrai visuotinis modernaus, profesionalaus JavaScript rašymo standartas.