Lietuvių

Į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ų:

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:

  1. Vienkartinis protokolas: Standartinis būdas objektams apibrėžti savo valymo logiką naudojant specialius simbolius: Symbol.dispose sinchroniniam valymui ir Symbol.asyncDispose asinchroniniam valymui.
  2. using ir await 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ų:

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.

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:

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:

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.