අපි උදාහරණ භාවිතා කරමින් JavaScript හි Async/Await බලමු

ලිපියේ කතුවරයා JavaScript හි Async/Await හි උදාහරණ පරීක්ෂා කරයි. සමස්තයක් ලෙස, Async/Await යනු අසමමුහුර්ත කේතය ලිවීමට පහසු ක්‍රමයකි. මෙම විශේෂාංගය දර්ශනය වීමට පෙර, එවැනි කේතයක් නැවත ඇමතුම් සහ පොරොන්දු භාවිතයෙන් ලියා ඇත. මුල් ලිපියේ කතුවරයා විවිධ උදාහරණ විශ්ලේෂණය කිරීමෙන් Async/Await හි වාසි හෙළි කරයි.

අපි ඔබට මතක් කරමු: "Habr" හි සියලුම පාඨකයින් සඳහා - "Habr" ප්‍රවර්ධන කේතය භාවිතයෙන් ඕනෑම Skillbox පාඨමාලාවකට ලියාපදිංචි වන විට රූබල් 10 ක වට්ටමක්.

Skillbox නිර්දේශ කරයි: අධ්‍යාපනික මාර්ගගත පාඨමාලාව "ජාවා සංවර්ධකයා".

callback

Callback යනු ඇමතුම දින නියමයක් නොමැතිව ප්‍රමාද වන ශ්‍රිතයකි. මින් පෙර, ප්‍රතිඵලය ක්ෂණිකව ලබා ගත නොහැකි වූ එම කේත ක්ෂේත්‍රවල නැවත ඇමතුම් භාවිතා කරන ලදී.

Node.js හි ගොනුවක් අසමමුහුර්තව කියවීමේ උදාහරණයක් මෙන්න:

fs.readFile(__filename, 'utf-8', (err, data) => {
  if (err) {
    throw err;
  }
  console.log(data);
});

ඔබට එකවර අසමමුහුර්ත මෙහෙයුම් කිහිපයක් සිදු කිරීමට අවශ්‍ය වූ විට ගැටළු මතු වේ. අපි මෙම අවස්ථාව සිතමු: Arfat පරිශීලක දත්ත ගබඩාවට ඉල්ලීමක් කරනු ලැබේ, ඔබ එහි profile_img_url ක්ෂේත්‍රය කියවා someserver.com සේවාදායකයෙන් රූපයක් බාගත කළ යුතුය.
බාගත කිරීමෙන් පසු, අපි රූපය වෙනත් ආකෘතියකට පරිවර්තනය කරමු, උදාහරණයක් ලෙස PNG සිට JPEG දක්වා. පරිවර්තනය සාර්ථක වූයේ නම්, පරිශීලකයාගේ විද්‍යුත් තැපෑලට ලිපියක් යවනු ලැබේ. ඊළඟට, දිනය සඳහන් කරමින්, සිදුවීම පිළිබඳ තොරතුරු transformations.log ගොනුවට ඇතුළත් කර ඇත.

කේතයේ අවසාන කොටසෙහි ඇමතුම් අතිච්ඡාදනය වීම සහ }) විශාල සංඛ්‍යාව කෙරෙහි අවධානය යොමු කිරීම වටී. එය හඳුන්වන්නේ Callback Hell හෝ Pyramid of Doom යනුවෙනි.

මෙම ක්රමයේ අවාසි පැහැදිලිය:

  • මෙම කේතය කියවීමට අපහසුය.
  • බොහෝ විට දුර්වල කේතයේ ගුණාත්මක භාවයට හේතු වන දෝෂ හැසිරවීම ද අපහසු වේ.

මෙම ගැටළුව විසඳීම සඳහා, JavaScript වෙත පොරොන්දු එකතු කරන ලදී. ඔවුන් ඔබට ඇමතුම් වල ගැඹුරු කැදැල්ල වෙනුවට .then යන වචනයෙන් ප්‍රතිස්ථාපනය කිරීමට ඉඩ සලසයි.

පොරොන්දුවල ධනාත්මක අංගය නම්, ඔවුන් කේතය වමේ සිට දකුණට වඩා ඉහළ සිට පහළට වඩා හොඳින් කියවිය හැකි වීමයි. කෙසේ වෙතත්, පොරොන්දුවලට ද ඔවුන්ගේ ගැටළු තිබේ:

  • ගොඩක් .එකතු කරන්න ඕන එහෙනම්.
  • උත්සාහ/අල්ලා ගැනීම වෙනුවට, .catch සියලු දෝෂ හැසිරවීමට භාවිතා කරයි.
  • එක් ලූපයක් තුළ බහු පොරොන්දු සමඟ වැඩ කිරීම සැමවිටම පහසු නොවේ; සමහර අවස්ථාවලදී, ඔවුන් කේතය සංකීර්ණ කරයි.

අවසාන කරුණෙහි තේරුම පෙන්වන ගැටළුවක් මෙන්න.

අප සතුව 0 සිට 10 දක්වා සංඛ්‍යා අනුපිළිවෙලක් අහඹු කාල පරාසයන් (තත්පර 0–n) මුද්‍රණය කරන for loop එකක් ඇතැයි සිතමු. පොරොන්දම් භාවිතා කරමින්, ඔබ මෙම ලූපය වෙනස් කළ යුතු අතර එමඟින් ඉලක්කම් 0 සිට 10 දක්වා අනුපිළිවෙලින් මුද්‍රණය වේ. එබැවින්, බිංදුවක් මුද්‍රණය කිරීමට තත්පර 6 ක් සහ එකක් මුද්‍රණය කිරීමට තත්පර 2 ක් ගතවේ නම්, පළමුව බිංදුව මුද්‍රණය කළ යුතුය, පසුව එක මුද්‍රණය කිරීම සඳහා ගණන් කිරීම ආරම්භ වේ.

ඇත්ත වශයෙන්ම, අපි මෙම ගැටලුව විසඳීමට Async/Await හෝ .sort භාවිතා නොකරමු. උදාහරණයක් විසඳුම අවසානයේ ඇත.

Async කාර්යයන්

ES2017 (ES8) හි අසමමුහුර්ත කාර්යයන් එකතු කිරීම පොරොන්දු සමඟ වැඩ කිරීමේ කාර්යය සරල කළේය. Async කාර්යයන් පොරොන්දු "ඉහළින්" ක්‍රියා කරන බව මම සටහන් කරමි. මෙම කාර්යයන් ගුණාත්මකව වෙනස් සංකල්ප නියෝජනය නොකරයි. Async ශ්‍රිතයන් බලාපොරොත්තු වන්නේ පොරොන්දු භාවිතා කරන කේතයට විකල්පයක් ලෙසය.

Async/Await මඟින් සමමුහුර්ත ශෛලියකින් අසමමුහුර්ත කේතය සමඟ වැඩ සංවිධානය කිරීමට හැකි වේ.

මේ අනුව, පොරොන්දු දැනගැනීම Async/Await හි මූලධර්ම තේරුම් ගැනීම පහසු කරයි.

කාරක රීති

සාමාන්‍යයෙන් එය මූල පද දෙකකින් සමන්විත වේ: async සහ wait. පළමු වචනය ශ්‍රිතය අසමමුහුර්ත බවට පත් කරයි. එවැනි කාර්යයන් බලා සිටීම භාවිතා කිරීමට ඉඩ සලසයි. වෙනත් ඕනෑම අවස්ථාවක, මෙම කාර්යය භාවිතා කිරීම දෝෂයක් ජනනය කරයි.

// With function declaration
 
async function myFn() {
  // await ...
}
 
// With arrow function
 
const myFn = async () => {
  // await ...
}
 
function myFn() {
  // await fn(); (Syntax Error since no async)
}
 

Async ශ්‍රිත ප්‍රකාශනයේ ආරම්භයේදීම සහ ඊතල ශ්‍රිතයක නම්, “=” ලකුණ සහ වරහන් අතර ඇතුල් කරනු ලැබේ.

මෙම ශ්‍රිතයන් වස්තුවක ක්‍රම ලෙස හෝ පන්ති ප්‍රකාශනයක භාවිතා කළ හැක.

// As an object's method
 
const obj = {
  async getName() {
    return fetch('https://www.example.com');
  }
}
 
// In a class
 
class Obj {
  async getResource() {
    return fetch('https://www.example.com');
  }
}

සැ.යු! class constructors සහ getters/setters අසමමිතික විය නොහැකි බව මතක තබා ගැනීම වටී.

අර්ථ ශාස්ත්‍රය සහ ක්‍රියාත්මක කිරීමේ නීති

Async ශ්‍රිත මූලික වශයෙන් සම්මත JS ශ්‍රිතවලට සමාන වේ, නමුත් ව්‍යතිරේක පවතී.

මේ අනුව, අසමමුහුර්ත කාර්යයන් සෑම විටම පොරොන්දු ලබා දෙයි:

async function fn() {
  return 'hello';
}
fn().then(console.log)
// hello

විශේෂයෙන්ම, fn හලෝ තන්තුව ආපසු ලබා දෙයි. හොඳයි, මෙය අසමමුහුර්ත ශ්‍රිතයක් බැවින්, තන්තු අගය කන්ස්ට්‍රක්ටරයක් ​​භාවිතයෙන් පොරොන්දුවකින් ඔතා ඇත.

Async නොමැතිව විකල්ප නිර්මාණයක් මෙන්න:

function fn() {
  return Promise.resolve('hello');
}
 
fn().then(console.log);
// hello

මෙම අවස්ථාවේදී, පොරොන්දුව "අතින්" ආපසු ලබා දෙනු ලැබේ. අසමමුහුර්ත ශ්‍රිතයක් සෑම විටම නව පොරොන්දුවකින් ඔතා ඇත.

ප්‍රතිලාභ අගය ප්‍රාථමික එකක් නම්, async ශ්‍රිතය එය පොරොන්දුවකින් ඔතා අගය ලබා දෙයි. ආපසු ලබා දෙන අගය පොරොන්දු වස්තුවක් නම්, එහි විභේදනය නව පොරොන්දුවකින් ආපසු ලබා දෙනු ලැබේ.

const p = Promise.resolve('hello')
p instanceof Promise;
// true
 
Promise.resolve(p) === p;
// true
 

නමුත් අසමමුහුර්ත ශ්‍රිතයක් තුළ දෝෂයක් ඇත්නම් කුමක් සිදුවේද?

async function foo() {
  throw Error('bar');
}
 
foo().catch(console.log);

එය සකසනු නොලැබේ නම්, foo() ප්‍රතික්ෂේප කිරීම සමඟ පොරොන්දුවක් ලබා දෙනු ඇත. මෙම තත්වය තුළ, Promise.resolve වෙනුවට දෝෂයක් අඩංගු Promise.reject ආපසු ලබා දෙනු ඇත.

Async ශ්‍රිත සෑම විටම පොරොන්දුවක් ලබා දෙයි, ආපසු ලබා දෙන දේ නොසලකා.

අසමමුහුර්ත ශ්‍රිත සෑම පොරොත්තුවකදීම විරාම ගන්වයි.

බලා සිටීම ප්‍රකාශනවලට බලපායි. එබැවින්, ප්‍රකාශනය පොරොන්දුවක් නම්, පොරොන්දුව ඉටු වන තෙක් අසමමිතික ශ්‍රිතය අත්හිටුවනු ලැබේ. ප්‍රකාශනය පොරොන්දුවක් නොවේ නම්, එය Promise.resolve හරහා පොරොන්දුවක් බවට පරිවර්තනය කර සම්පූර්ණ කරනු ලැබේ.

// utility function to cause delay
// and get random value
 
const delayAndGetRandom = (ms) => {
  return new Promise(resolve => setTimeout(
    () => {
      const val = Math.trunc(Math.random() * 100);
      resolve(val);
    }, ms
  ));
};
 
async function fn() {
  const a = await 9;
  const b = await delayAndGetRandom(1000);
  const c = await 5;
  await delayAndGetRandom(1000);
 
  return a + b * c;
}
 
// Execute fn
fn().then(console.log);

තවද මෙහි fn ශ්‍රිතය ක්‍රියා කරන ආකාරය පිළිබඳ විස්තරයක් ඇත.

  • එය ඇමතීමෙන් පසු, පළමු පේළිය const a = Wait 9 වෙතින් පරිවර්තනය වේ; const a = Wait Promise.resolve(9);.
  • Await භාවිතා කිරීමෙන් පසු, a එහි අගය ලබා ගන්නා තෙක් ශ්‍රිත ක්‍රියාත්මක කිරීම අත්හිටුවනු ලැබේ (වර්තමාන තත්වයේ එය 9 වේ).
  • delayAndGetRandom(1000) fn ශ්‍රිතය සම්පූර්ණ වන තෙක් (තත්පර 1කට පසු) ක්‍රියාත්මක කිරීම විරාම කරයි. මෙය තත්පර 1 ක් සඳහා fn කාර්යය ඵලදායී ලෙස නතර කරයි.
  • delayAndGetRandom(1000) විසදුම හරහා අහඹු අගයක් ලබා දෙයි, එය b විචල්‍යයට පවරනු ලැබේ.
  • හොඳයි, විචල්‍ය c සමඟ ඇති නඩුව a විචල්‍යයේ නඩුවට සමාන වේ. ඊට පසු, සියල්ල තත්පරයකට නතර වේ, නමුත් දැන් delayAndGetRandom(1000) එය අවශ්‍ය නොවන නිසා කිසිවක් ලබා නොදේ.
  • ප්රතිඵලයක් වශයෙන්, අගයන් ගණනය කරනු ලබන්නේ a + b * c සූත්රය භාවිතා කරමිනි. ප්‍රතිඵලය Promise.resolve භාවිතයෙන් පොරොන්දුවකින් ඔතා ශ්‍රිතය මඟින් ආපසු ලබා දේ.

මෙම විරාමයන් ES6 හි ජනක යන්ත්‍ර සිහිගන්වයි, නමුත් එයට යමක් තිබේ ඔබේ හේතු.

ගැටලුව විසඳීම

හොඳයි, දැන් අපි ඉහත සඳහන් කළ ගැටලුවට විසඳුම බලමු.

FinishMyTask ශ්‍රිතය queryDatabase, sendEmail, logTaskInFile, සහ වෙනත් මෙහෙයුම් වල ප්‍රතිඵල එනතෙක් බලා සිටීමට Await භාවිතා කරයි. ඔබ මෙම විසඳුම පොරොන්දු භාවිතා කළ විසඳුම සමඟ සංසන්දනය කළහොත්, සමානකම් පැහැදිලි වනු ඇත. කෙසේ වෙතත්, Async/Await අනුවාදය මගින් සියලුම වාක්‍ය සංකීර්ණතා බෙහෙවින් සරල කරයි. මෙම අවස්ථාවේදී, .then/.catch වැනි විශාල ඇමතුම් සහ දාමයන් නොමැත.

මෙන්න ඉලක්කම් ප්‍රතිදානය සමඟ විසඳුමක්, විකල්ප දෙකක් තිබේ.

const wait = (i, ms) => new Promise(resolve => setTimeout(() => resolve(i), ms));
 
// Implementation One (Using for-loop)
const printNumbers = () => new Promise((resolve) => {
  let pr = Promise.resolve(0);
  for (let i = 1; i <= 10; i += 1) {
    pr = pr.then((val) => {
      console.log(val);
      return wait(i, Math.random() * 1000);
    });
  }
  resolve(pr);
});
 
// Implementation Two (Using Recursion)
 
const printNumbersRecursive = () => {
  return Promise.resolve(0).then(function processNextPromise(i) {
 
    if (i === 10) {
      return undefined;
    }
 
    return wait(i, Math.random() * 1000).then((val) => {
      console.log(val);
      return processNextPromise(i + 1);
    });
  });
};

මෙන්න async functions භාවිතා කරන විසඳුමක්.

async function printNumbersUsingAsync() {
  for (let i = 0; i < 10; i++) {
    await wait(i, Math.random() * 1000);
    console.log(i);
  }
}

සැකසීමේ දෝෂයකි

හසුරුවා නොගත් වැරදි ප්‍රතික්ෂේප කළ පොරොන්දුවකින් ඔතා ඇත. කෙසේ වෙතත්, සමමුහුර්තව දෝෂ හැසිරවීමට අසයින්ක් ශ්‍රිතයන්ට try/catch භාවිතා කළ හැක.

async function canRejectOrReturn() {
  // wait one second
  await new Promise(res => setTimeout(res, 1000));
 
// Reject with ~50% probability
  if (Math.random() > 0.5) {
    throw new Error('Sorry, number too big.')
  }
 
return 'perfect number';
}

canRejectOrReturn() යනු අසමමුහුර්ත ශ්‍රිතයක් වන අතර එය එක්කෝ සාර්ථක වේ (“පරිපූර්ණ අංකය”) හෝ දෝෂයක් සමඟ අසාර්ථක වේ (“සමාවෙන්න, අංකය විශාල වැඩියි”).

async function foo() {
  try {
    await canRejectOrReturn();
  } catch (e) {
    return 'error caught';
  }
}

ඉහත උදාහරණය canRejectOrReturn ක්‍රියාත්මක කිරීමට අපේක්ෂා කරන බැවින්, එහිම අසාර්ථකත්වය Catch block ක්‍රියාත්මක වීමට හේතු වේ. එහි ප්‍රතිඵලයක් වශයෙන්, foo ශ්‍රිතය නිර්වචනය නොකළ (උත්සාහක කොටසෙහි කිසිවක් ආපසු නොදෙන විට) හෝ හසු වූ දෝෂයකින් අවසන් වේ. එහි ප්‍රතිඵලයක් වශයෙන්, උත්සාහය/අල්ලා ගැනීම foo ශ්‍රිතයම හසුරුවන බැවින් මෙම ශ්‍රිතය අසාර්ථක නොවනු ඇත.

මෙන්න තවත් උදාහරණයක්:

async function foo() {
  try {
    return canRejectOrReturn();
  } catch (e) {
    return 'error caught';
  }
}

උදාහරණයේ, canRejectOrReturn foo වෙතින් ආපසු ලබා දීම කෙරෙහි අවධානය යොමු කිරීම වටී. Foo මෙම අවස්ථාවෙහිදී පරිපූර්ණ අංකයකින් අවසන් වේ හෝ දෝෂයක් ලබා දෙයි ("සමාවෙන්න, අංකය විශාල වැඩියි"). අල්ලා ගැනීමේ වාරණ කිසි විටෙකත් ක්‍රියාත්මක නොවේ.

ගැටලුව වන්නේ canRejectOrReturn වෙතින් දුන් පොරොන්දුව foo නැවත ලබා දීමයි. එබැවින් foo සඳහා විසඳුම canRejectOrReturn සඳහා විසඳුම බවට පත්වේ. මෙම අවස්ථාවේදී, කේතය සමන්විත වන්නේ පේළි දෙකකින් පමණි:

try {
    const promise = canRejectOrReturn();
    return promise;
}

ඔබ Wait සහ return එකට භාවිතා කළහොත් සිදුවන දේ මෙන්න:

async function foo() {
  try {
    return await canRejectOrReturn();
  } catch (e) {
    return 'error caught';
  }
}

ඉහත කේතයෙහි, foo පරිපූර්ණ අංකයක් සහ දෝෂයක් හසුව සාර්ථකව පිටව යනු ඇත. මෙහි ප්රතික්ෂේප කිරීම් සිදු නොවනු ඇත. නමුත් foo නැවත පැමිණෙන්නේ canRejectOrReturn සමඟ මිස නිර්වචනය නොකළ සමඟ නොවේ. ආපසු එන Wait canRejectOrReturn() රේඛාව ඉවත් කිරීමෙන් මෙය තහවුරු කර ගනිමු:

try {
    const value = await canRejectOrReturn();
    return value;
}
// …

පොදු වැරදි සහ අන්තරායන්

සමහර අවස්ථාවලදී, Async/Await භාවිතා කිරීම දෝෂ වලට තුඩු දිය හැක.

අමතක වෙලා බලාගෙන ඉන්නවා

මෙය බොහෝ විට සිදු වේ - පොරොන්දු වීමට පෙර බලා සිටීමේ මූල පදය අමතක වේ:

async function foo() {
  try {
    canRejectOrReturn();
  } catch (e) {
    return 'caught';
  }
}

ඔබට පෙනෙන පරිදි, කේතයේ රැඳී සිටීමක් හෝ ආපසු පැමිණීමක් නොමැත. එබැවින් foo සෑම විටම තත්පර 1ක ප්‍රමාදයකින් තොරව නිර්වචනය කර පිටවෙයි. නමුත් පොරොන්දුව ඉටු වනු ඇත. එය දෝෂයක් හෝ ප්‍රතික්ෂේප කිරීමක් කරන්නේ නම්, UnhandledPromiseRejectionWarning කැඳවනු ලැබේ.

Callbacks හි Async Functions

Async ශ්‍රිත බොහෝ විට .map හෝ .filter හි ඇමතුම් ලෙස භාවිතා වේ. උදාහරණයක් ලෙස GitHub හි විවෘත ගබඩා සංඛ්‍යාව ලබා දෙන fetchPublicReposCount(පරිශීලක නාමය) ශ්‍රිතය වේ. අපි හිතමු අපිට අවශ්‍ය ප්‍රමිතික භාවිතා කරන්නන් තුන් දෙනෙක් ඉන්නවා කියලා. මෙම කාර්යය සඳහා කේතය මෙන්න:

const url = 'https://api.github.com/users';
 
// Utility fn to fetch repo counts
const fetchPublicReposCount = async (username) => {
  const response = await fetch(`${url}/${username}`);
  const json = await response.json();
  return json['public_repos'];
}

අපිට ArfatSalman, octocat, norvig ගිණුම් අවශ්‍යයි. මෙම අවස්ථාවේදී අපි කරන්නේ:

const users = [
  'ArfatSalman',
  'octocat',
  'norvig'
];
 
const counts = users.map(async username => {
  const count = await fetchPublicReposCount(username);
  return count;
});

.map නැවත ඇමතුමේ Await වෙත අවධානය යොමු කිරීම වටී. මෙහි ගණන් යනු පොරොන්දු මාලාවක් වන අතර, .map යනු එක් එක් නිශ්චිත පරිශීලකයා සඳහා නිර්නාමික ඇමතුමකි.

බලා සිටීමේ ඕනෑවට වඩා ස්ථාවර භාවිතය

අපි මෙම කේතය උදාහරණයක් ලෙස ගනිමු:

async function fetchAllCounts(users) {
  const counts = [];
  for (let i = 0; i < users.length; i++) {
    const username = users[i];
    const count = await fetchPublicReposCount(username);
    counts.push(count);
  }
  return counts;
}

මෙහිදී repo අංකය ගණන් කිරීමේ විචල්‍යයේ තබා ඇත, එවිට මෙම අංකය ගණන් අරාවට එකතු වේ. කේතය සමඟ ඇති ගැටළුව නම්, පළමු පරිශීලකයාගේ දත්ත සේවාදායකයෙන් ලැබෙන තෙක්, සියලු පසුකාලීන පරිශීලකයින් පොරොත්තු මාදිලියේ සිටීමයි. මේ අනුව, වරකට සකසනු ලබන්නේ එක් පරිශීලකයෙකු පමණි.

උදාහරණයක් ලෙස, එක් පරිශීලකයෙකු සැකසීමට ms 300 ක් පමණ ගත වේ නම්, සියලුම පරිශීලකයින් සඳහා එය දැනටමත් තත්පරයකි; රේඛීයව ගත කරන කාලය පරිශීලකයින් ගණන මත රඳා පවතී. නමුත් repo සංඛ්යාව ලබා ගැනීම එකිනෙකා මත රඳා නොපවතින බැවින්, ක්රියාවලීන් සමාන්තරගත කළ හැකිය. මෙයට .map සහ Promise.all සමඟ වැඩ කිරීම අවශ්‍ය වේ:

async function fetchAllCounts(users) {
  const promises = users.map(async username => {
    const count = await fetchPublicReposCount(username);
    return count;
  });
  return Promise.all(promises);
}

Promise.all හට ආදානය ලෙස පොරොන්දු මාලාවක් ලැබෙන අතර පොරොන්දුවක් ලබා දෙයි. දෙවැන්න, අරාවේ ඇති සියලුම පොරොන්දු සම්පූර්ණ කිරීමෙන් පසුව හෝ පළමු ප්‍රතික්ෂේප කිරීමේදී සම්පූර්ණ වේ. ඒවා සියල්ලම එකවර ආරම්භ නොකිරීම සිදුවිය හැකිය - එකවර ආරම්භය සහතික කිරීම සඳහා, ඔබට p-map භාවිතා කළ හැකිය.

නිගමනය

Async කාර්යයන් සංවර්ධනය සඳහා වඩ වඩාත් වැදගත් වෙමින් පවතී. හොඳයි, async ශ්රිතයන් අනුවර්තී භාවිතය සඳහා, ඔබ භාවිතා කළ යුතුය Async පුනරාවර්තක. JavaScript සංවර්ධකයෙකු මේ ගැන හොඳින් දැන සිටිය යුතුය.

Skillbox නිර්දේශ කරයි:

මූලාශ්රය: www.habr.com

අදහස් එක් කරන්න