Բարեւ բոլորին. Օմելնիցկի Սերգեյի հետ կապ. Ոչ վաղ անցյալում ես ռեակտիվ ծրագրավորման հոսք եմ կազմակերպել, որտեղ ես խոսում էի JavaScript-ում ասինխրոնության մասին: Այսօր ես ուզում եմ ամփոփել այս նյութը:
Բայց նախքան հիմնական նյութը սկսելը, մենք պետք է ներածություն անենք։ Այսպիսով, եկեք սկսենք սահմանումներից. ի՞նչ են ստեկը և հերթը:
Դարձ հավաքածու է, որի տարրերը վերցված են «վերջին մուտք, առաջինը դուրս» LIFO հիմունքներով
Հերթ հավաքածու է, որի տարրերը ստացվում են սկզբունքի համաձայն («առաջինը ներս, առաջինը դուրս» FIFO
Լավ, շարունակենք։
JavaScript-ը մեկ շղթայով ծրագրավորման լեզու է: Սա նշանակում է, որ այն ունի կատարման միայն մեկ շարան և մեկ ստեկ, որտեղ գործառույթները հերթագրված են կատարման համար: Հետևաբար, JavaScript-ը կարող է միաժամանակ կատարել միայն մեկ գործողություն, մինչդեռ մյուս գործողությունները կսպասեն իրենց հերթին փաթեթի վրա մինչև կանչվեն:
զանգերի բուրգ Տվյալների կառուցվածք է, որը պարզ տերմիններով գրանցում է տեղեկատվություն ծրագրի այն վայրի մասին, որտեղ մենք գտնվում ենք: Եթե մենք ցատկենք ֆունկցիայի մեջ, մենք դրա մուտքը հրում ենք փաթեթի վերևում: Երբ մենք վերադառնում ենք ֆունկցիայից, մենք դուրս ենք հանում ամենավերին տարրը փաթեթից և հայտնվում այնտեղ, որտեղից կանչել ենք այս ֆունկցիան: Դա այն ամենն է, ինչ կարող է անել բուրգը: Իսկ հիմա մի շատ հետաքրքիր հարց. Ինչպե՞ս է այդ դեպքում ասինխրոնիան աշխատում JavasScript-ում:
Իրականում, բացի stack-ից, բրաուզերներն ունեն հատուկ հերթ՝ այսպես կոչված WebAPI-ի հետ աշխատելու համար։ Այս հերթից գործառույթները կկատարվեն հերթականությամբ միայն այն բանից հետո, երբ կույտը ամբողջությամբ մաքրվի: Միայն դրանից հետո դրանք տեղադրվում են հերթից դեպի բուրգ՝ կատարման համար։ Եթե կա առնվազն մեկ տարր այս պահին կույտի վրա, ապա նրանք չեն կարող հայտնվել կույտի վրա: Հենց դրա պատճառով ֆունկցիաները ժամանակի վերջնաժամկետով կանչելը հաճախ ժամանակի ընթացքում սխալ է, քանի որ ֆունկցիան չի կարող հերթից հասնել կույտ, քանի դեռ այն լի է:
Եկեք նայենք հետևյալ օրինակին և անցնենք քայլ առ քայլ: Տեսնենք նաև, թե ինչ է կատարվում համակարգում։
1) Առայժմ ոչինչ տեղի չի ունենում: Զննարկիչի վահանակը մաքուր է, զանգերի կույտը դատարկ է:
2) Այնուհետև հրամանը console.log('Hi') ավելացվում է զանգերի փաթեթին:
3) Եվ դա կատարվում է
4) Այնուհետև console.log('Hi') հեռացվում է զանգերի կույտից:
5) Այժմ անցնենք setTimeout(function cb1() {… }) հրամանին: Այն ավելացվում է զանգերի փաթեթին:
6) setTimeout(function cb1() {… }) հրամանը կատարվում է: Զննարկիչը ստեղծում է ժամանակաչափ, որը հանդիսանում է Web API-ի մի մասը: Այն կկատարի հետհաշվարկ:
7) setTimeout(function cb1() {… }) հրամանն ավարտել է իր աշխատանքը և հեռացվել է զանգերի փաթեթից:
8) Consol.log('Bye') հրամանը ավելացվում է զանգերի փաթեթին:
9) Կատարված է console.log('Bye') հրամանը:
10) հրամանը console.log ('Bye') հեռացվում է զանգերի կույտից:
11) Առնվազն 5000 մս անցնելուց հետո ժմչփն ավարտվում է և cb1 հետ կանչը դնում է հետ կանչի հերթում:
12) Իրադարձությունների հանգույցը վերցնում է cb1 ֆունկցիան հետադարձ զանգի հերթից և այն հրում զանգերի կույտի վրա:
13) Cb1 ֆունկցիան կատարվում է և ավելացնում է console.log('cb1') զանգերի կույտին:
14) Կատարված է console.log('cb1') հրամանը:
15) հրամանը console.log('cb1') հեռացվում է զանգերի կույտից:
16) cb1 ֆունկցիան հանվում է զանգերի կույտից:
Դինամիկայի օրինակով նայենք.
Դե, մենք նայեցինք, թե ինչպես է ասինխրոնությունն իրականացվում JavaScript-ում: Հիմա հակիրճ խոսենք ասինխրոն կոդի էվոլյուցիայի մասին։
Ասինխրոն կոդի էվոլյուցիան.
a(function (resultsFromA) {
b(resultsFromA, function (resultsFromB) {
c(resultsFromB, function (resultsFromC) {
d(resultsFromC, function (resultsFromD) {
e(resultsFromD, function (resultsFromE) {
f(resultsFromE, function (resultsFromF) {
console.log(resultsFromF);
})
})
})
})
})
});
Asynchronous ծրագրավորումը, ինչպես մենք գիտենք JavaScript-ում, կարող է իրականացվել միայն գործառույթներով: Նրանք կարող են փոխանցվել, ինչպես ցանկացած այլ փոփոխական, այլ գործառույթների: Ահա թե ինչպես են ծնվել հետզանգերը. Եվ դա զով է, զվարճալի և ջերմեռանդ, մինչև այն վերածվի տխրության, մելամաղձության և տխրության: Ինչո՞ւ։ Այո, պարզ է.
Քանի որ կոդի բարդությունը մեծանում է, նախագիծն արագորեն վերածվում է անհասկանալի բազմաթիվ ներկառուցված բլոկների՝ «հետ կանչի դժոխք»:
Սխալների հետ կապված խնդիրները կարելի է հեշտությամբ անտեսել:
Դուք չեք կարող վերադարձնել արտահայտությունները վերադարձով:
Promise-ի գալուստով իրավիճակը մի փոքր լավացել է։
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 2000);
}).then((result) => {
alert(result);
return result + 2;
}).then((result) => {
throw new Error('FAILED HERE');
alert(result);
return result + 2;
}).then((result) => {
alert(result);
return result + 2;
}).catch((e) => {
console.log('error: ', e);
});
Հայտնվեցին խոստման շղթաներ, որոնք բարելավեցին կոդի ընթեռնելիությունը
Սխալների գաղտնալսման առանձին մեթոդ կար
Զուգահեռ կատարումը Promise.all-ի հետ ավելացված է
Մենք կարող ենք լուծել nested asynchrony-ը async/wait-ով
Բայց խոստումն ունի իր սահմանափակումները. Օրինակ, խոստումը, առանց դափի հետ պարելու, չի կարելի չեղարկել, և որ ամենակարեւորն է, այն գործում է մեկ արժեքով.
Դե, այստեղ մենք սահուն մոտենում ենք ռեակտիվ ծրագրավորմանը։ Հոգնե՞լ եք: Լավ, լավն այն է, որ կարող ես գնալ ճայեր եփելու, ուղեղի փոթորիկ անել և վերադառնալ ավելին կարդալու: Եվ ես կշարունակեմ.
Ռեակտիվ ծրագրավորում - ծրագրավորման պարադիգմ, որը կենտրոնացած է տվյալների հոսքերի և փոփոխությունների տարածման վրա: Եկեք ավելի սերտ նայենք, թե ինչ է տվյալների հոսքը:
// Получаем ссылку на элемент
const input = ducument.querySelector('input');
const eventsArray = [];
// Пушим каждое событие в массив eventsArray
input.addEventListener('keyup',
event => eventsArray.push(event)
);
Պատկերացնենք, որ ունենք մուտքագրման դաշտ։ Մենք ստեղծում ենք զանգված, և մուտքային իրադարձության յուրաքանչյուր ստեղնաշարի համար մենք կպահենք իրադարձությունը մեր զանգվածում: Միևնույն ժամանակ, ես կցանկանայի նշել, որ մեր զանգվածը դասավորված է ըստ ժամանակի, այսինքն. ավելի ուշ իրադարձությունների ցուցանիշը ավելի մեծ է, քան ավելի վաղ տեղի ունեցած իրադարձությունների ցուցանիշը: Նման զանգվածը տվյալների հոսքի պարզեցված մոդել է, բայց դա դեռ հոսք չէ: Որպեսզի այս զանգվածը ապահով կոչվի հոսք, այն պետք է կարողանա ինչ-որ կերպ տեղեկացնել բաժանորդներին, որ նոր տվյալներ են հայտնվել դրանում։ Այսպիսով, մենք գալիս ենք հոսքի սահմանմանը:
Հոսք ըստ ժամանակի դասավորված տվյալների զանգված է, որը կարող է ցույց տալ, որ տվյալները փոխվել են: Հիմա պատկերացրեք, թե որքան հարմար է դառնում կոդ գրելը, որում մեկ գործողության համար անհրաժեշտ է մի քանի իրադարձություն գործարկել կոդի տարբեր մասերում: Մենք պարզապես բաժանորդագրվում ենք հոսքին, և այն մեզ կտեղեկացնի, թե երբ կլինեն փոփոխություններ: Եվ RxJs գրադարանը կարող է դա անել:
RxJS գրադարան է ասինխրոն և իրադարձությունների վրա հիմնված ծրագրերի հետ աշխատելու համար՝ օգտագործելով դիտվող հաջորդականությունը։ Գրադարանը տրամադրում է հիմնական տեսակը Դիտարկելի է, օգնականների մի քանի տեսակներ (Դիտորդներ, ժամանակացույցներ, առարկաներ) և օպերատորներ՝ իրադարձությունների, ինչպես նաև հավաքածուների հետ աշխատելու համար (քարտեզ, զտել, կրճատել, ամեն և նմանատիպերը JavaScript Array-ից):
Եկեք հասկանանք այս գրադարանի հիմնական հասկացությունները:
Դիտելի, Դիտորդ, Արտադրող
Դիտարկվողը առաջին բազային տեսակն է, որը մենք կանդրադառնանք: Այս դասը պարունակում է RxJs իրականացման հիմնական մասը։ Այն կապված է դիտելի հոսքի հետ, որին կարելի է բաժանորդագրվել՝ օգտագործելով բաժանորդագրման մեթոդը:
Observable-ն իրականացնում է թարմացումների ստեղծման օժանդակ մեխանիզմ, այսպես կոչված Դիտորդ. Դիտորդի համար արժեքների աղբյուրը կոչվում է Արտադրող. Դա կարող է լինել զանգված, կրկնող, վեբ վարդակ, ինչ-որ իրադարձություն և այլն: Այսպիսով, մենք կարող ենք ասել, որ դիտարկվողը դիրիժոր է Պրոդյուսերի և Դիտորդի միջև:
Observable-ն իրականացնում է Observer-ի երեք տեսակի իրադարձություններ.
հաջորդը՝ նոր տվյալներ
սխալ - սխալ, եթե հաջորդականությունը դադարեցվել է բացառության պատճառով: այս իրադարձությունը ենթադրում է նաև հաջորդականության ավարտ։
ամբողջական - ազդանշան հաջորդականության ավարտի մասին: Սա նշանակում է, որ այլևս նոր տվյալներ չեն լինի
Եկեք տեսնենք ցուցադրություն.
Սկզբում մենք կմշակենք 1, 2, 3 արժեքները և 1 վայրկյան հետո: մենք ստանում ենք 4 և վերջացնում մեր շարանը:
Բարձրաձայն մտածելը
Եվ հետո հասկացա, որ ավելի հետաքրքիր է պատմել, քան գրել այդ մասին։ 😀
Բաժանորդագրություն
Երբ մենք բաժանորդագրվում ենք հոսքի, մենք ստեղծում ենք նոր դաս բաժանորդագրություն, որը մեզ հնարավորություն է տալիս չեղարկել բաժանորդագրությունը մեթոդով unsubscribe. Մենք կարող ենք նաև խմբավորել բաժանորդագրությունները՝ օգտագործելով մեթոդը ավելացնել. Դե, տրամաբանական է, որ մենք կարող ենք ապախմբավորել թելերը՝ օգտագործելով հեռացնել. Ավելացնել և հեռացնել մեթոդներն ընդունում են այլ բաժանորդագրություն որպես մուտքագրում: Ուզում եմ նշել, որ երբ մենք դուրս ենք գալիս բաժանորդագրությունից, ապա բաժանորդագրվում ենք բոլոր մանկական բաժանորդագրություններից, կարծես նրանք նաև անվանել են ապաբաժանորդագրման մեթոդ: Շարունակիր.
Հոսքերի տեսակները
HOT
ՑՈՒՐՏ
Արտադրողը ստեղծվում է դիտարկելիից դուրս
Արտադրողը ստեղծվում է դիտարկելի ներսում
Տվյալները փոխանցվում են դիտարկելիի ստեղծման պահին
Տվյալները տրամադրվում են բաժանորդագրության պահին:
Բաժանորդագրությունից դուրս գալու համար ավելի շատ տրամաբանություն է պետք
Թեման ավարտվում է ինքնուրույն
Օգտագործում է մեկից շատ հարաբերություններ
Օգտագործում է մեկ առ մեկ հարաբերություն
Բոլոր բաժանորդագրություններն ունեն նույն արժեքը
Բաժանորդագրությունները անկախ են
Տվյալները կարող են կորցնել, եթե բաժանորդագրություն չկա
Նոր բաժանորդագրության համար վերաթողարկում է հոսքի բոլոր արժեքները
Համեմատության համար ես կպատկերացնեմ թեժ հոսք, ինչպես կինոնկարը: Ժամանակի որ պահին եկար, այդ պահից սկսեցիր դիտել։ Ես կհամեմատեի սառը հոսքը դրանցում զանգի հետ: աջակցություն. Ցանկացած զանգահարող լսում է ինքնապատասխանիչի ձայնագրությունը սկզբից մինչև վերջ, բայց դուք կարող եք անջատել հեռախոսը բաժանորդագրությունից դուրս գալու դեպքում:
Նշեմ, որ կան նաև այսպես կոչված տաք հոսքեր (նման սահմանման ես հանդիպել եմ չափազանց հազվադեպ և միայն օտար համայնքներում) - սա հոսք է, որը սառը հոսքից վերածվում է տաքի: Հարց է առաջանում՝ որտեղ օգտագործել)) պրակտիկայից օրինակ բերեմ։
Ես աշխատում եմ Angular-ի հետ։ Նա ակտիվորեն օգտագործում է rxjs: Սերվերին տվյալներ ստանալու համար ես ակնկալում եմ սառը հոսք և ես օգտագործում եմ այս հոսքը կաղապարում՝ օգտագործելով asyncPipe: Եթե ես այս խողովակն օգտագործեմ մի քանի անգամ, ապա վերադառնալով սառը հոսքի սահմանմանը, յուրաքանչյուր խողովակ սերվերից տվյալներ կպահանջի, ինչը մեղմ ասած տարօրինակ է: Իսկ եթե սառը հոսքը վերածեմ տաքի, ապա խնդրանքը մեկ անգամ կլինի։
Ընդհանուր առմամբ, սկսնակների համար հոսքերի տեսակը հասկանալը բավականին դժվար է, բայց կարևոր:
Օպերատորներ
return this.http.get(`${environment.apiUrl}/${this.apiUrl}/trade_companies`)
.pipe(
tap(({ data }: TradeCompanyList) => this.companies$$.next(cloneDeep(data))),
map(({ data }: TradeCompanyList) => data)
);
Օպերատորները մեզ հնարավորություն են տալիս աշխատել հոսքերի հետ։ Նրանք օգնում են վերահսկել իրադարձությունները, որոնք հոսում են Observable-ում: Մենք կքննարկենք ամենահայտնիներից մի քանիսը, իսկ օպերատորների մասին լրացուցիչ տեղեկություններ կարելի է գտնել օգտակար տեղեկատվության հղումներում:
Օպերատորներ-ի
Սկսենք օպերատորի օգնականից: Այն ստեղծում է դիտարկելի՝ հիմնվելով պարզ արժեքի վրա:
Օպերատորներ-ֆիլտր
Ֆիլտրի օպերատորը, ինչպես հուշում է անունը, զտում է հոսքի ազդանշանը: Եթե օպերատորը վերադարձնում է true, ապա այն բաց է թողնում հետագա:
Օպերատորներ - վերցնել
take - Վերցնում է արտանետումների քանակի արժեքը, որից հետո հոսքն ավարտվում է:
Operators-debounceTime
debounceTime - մերժում է թողարկված արժեքները, որոնք ընկնում են ելքային տվյալների միջև նշված ժամանակային միջակայքում - ժամանակի ընդմիջումից հետո թողարկում է վերջին արժեքը:
const { Observable } = Rx;
const { debounceTime, take } = RxOperators;
Observable.create((observer) => {
let i = 1;
observer.next(i++);
// Испускаем значение раз в 1000мс
setInterval(() => {
observer.next(i++)
}, 1000);
// Испускаем значение раз в 1500мс
setInterval(() => {
observer.next(i++)
}, 1500);
}).pipe(
debounceTime(700), // Ожидаем 700мс значения прежде чем обработать
take(3)
);
Operators-takeWhile
Արտադրում է արժեքներ, մինչև takeWhile-ը վերադարձնի false-ը, այնուհետև չեղարկվի հոսքի բաժանորդագրությունը:
const { Observable } = Rx;
const { debounceTime, takeWhile } = RxOperators;
Observable.create((observer) => {
let i = 1;
observer.next(i++);
// Испускаем значение раз в 1000мс
setInterval(() => {
observer.next(i++)
}, 1000);
}).pipe(
takeWhile( producer => producer < 5 )
);
Operators-combine Latest
CombinedLatest համակցված օպերատորը որոշ չափով նման է soz.all-ին: Այն միավորում է բազմաթիվ հոսքեր մեկի մեջ: Այն բանից հետո, երբ յուրաքանչյուր շարանը կատարել է առնվազն մեկ արտանետում, մենք ստանում ենք վերջին արժեքները յուրաքանչյուրից որպես զանգված: Ավելին, համակցված հոսքերից ցանկացած արտանետումից հետո այն նոր արժեքներ կտա:
const { combineLatest, Observable } = Rx;
const { take } = RxOperators;
const observer_1 = Observable.create((observer) => {
let i = 1;
// Испускаем значение раз в 1000мс
setInterval(() => {
observer.next('a: ' + i++);
}, 1000);
});
const observer_2 = Observable.create((observer) => {
let i = 1;
// Испускаем значение раз в 750мс
setInterval(() => {
observer.next('b: ' + i++);
}, 750);
});
combineLatest(observer_1, observer_2).pipe(take(5));
Օպերատորներ-zip
Zip - սպասում է արժեքի յուրաքանչյուր հոսքից և այս արժեքների հիման վրա ձևավորում է զանգված: Եթե արժեքը որևէ թելից չի գալիս, ապա խումբը չի ձևավորվի։
const { zip, Observable } = Rx;
const { take } = RxOperators;
const observer_1 = Observable.create((observer) => {
let i = 1;
// Испускаем значение раз в 1000мс
setInterval(() => {
observer.next('a: ' + i++);
}, 1000);
});
const observer_2 = Observable.create((observer) => {
let i = 1;
// Испускаем значение раз в 750
setInterval(() => {
observer.next('b: ' + i++);
}, 750);
});
const observer_3 = Observable.create((observer) => {
let i = 1;
// Испускаем значение раз в 500
setInterval(() => {
observer.next('c: ' + i++);
}, 500);
});
zip(observer_1, observer_2, observer_3).pipe(take(5));
Օպերատորներ - forkJoin
forkJoin-ը նաև միանում է շղթաներին, բայց այն թողարկում է արժեք միայն այն ժամանակ, երբ բոլոր շղթաներն ավարտված են:
const { forkJoin, Observable } = Rx;
const { take } = RxOperators;
const observer_1 = Observable.create((observer) => {
let i = 1;
// Испускаем значение раз в 1000мс
setInterval(() => {
observer.next('a: ' + i++);
}, 1000);
}).pipe(take(3));
const observer_2 = Observable.create((observer) => {
let i = 1;
// Испускаем значение раз в 750
setInterval(() => {
observer.next('b: ' + i++);
}, 750);
}).pipe(take(5));
const observer_3 = Observable.create((observer) => {
let i = 1;
// Испускаем значение раз в 500
setInterval(() => {
observer.next('c: ' + i++);
}, 500);
}).pipe(take(4));
forkJoin(observer_1, observer_2, observer_3);
Օպերատորներ-քարտեզ
Քարտեզի փոխակերպման օպերատորը փոխակերպում է թողարկման արժեքը նորի:
const { Observable } = Rx;
const { take, map } = RxOperators;
Observable.create((observer) => {
let i = 1;
// Испускаем значение раз в 1000мс
setInterval(() => {
observer.next(i++);
}, 1000);
}).pipe(
map(x => x * 10),
take(3)
);
Օպերատորներ - կիսվեք, հպեք
Ծորակի օպերատորը թույլ է տալիս անել կողմնակի ազդեցությունները, այսինքն՝ ցանկացած գործողություն, որը չի ազդում հաջորդականության վրա։
Բաժնետոմսերի կոմունալ ծառայությունների օպերատորը կարող է սառը հոսքը վերածել տաքի:
Օպերատորներն ավարտված են: Անցնենք թեմային։
Բարձրաձայն մտածելը
Իսկ հետո գնացի թեյ խմելու։ Ես հոգնել եմ այս օրինակներից 😀
Առարկայական ընտանիք
Առարկայական ընտանիքը տաք թելերի վառ օրինակ է: Այս դասերը մի տեսակ հիբրիդ են, որոնք միաժամանակ գործում են որպես դիտելի և դիտորդ: Քանի որ թեման թեժ հոսք է, այն պետք է չեղարկվի: Եթե խոսենք հիմնական մեթոդների մասին, ապա դրանք են.
հաջորդը` հոսքին նոր տվյալներ փոխանցելը
error - սխալ և թելի ավարտ
ամբողջական - թելի վերջ
բաժանորդագրվել - բաժանորդագրվել հոսքին
ապաբաժանորդագրվել - դուրս գալ հոսքից
asObservable - վերածվել դիտորդի
toPromise - վերածվում է խոստման
Հատկացնել 4 5 տեսակի առարկաներ.
Բարձրաձայն մտածելը
Սթրիմում ասացի 4, բայց պարզվեց, որ ավելացրել են եւս մեկը։ Ինչպես ասում են՝ ապրիր և սովորիր։
Պարզ թեմա new Subject()- առարկաների ամենապարզ տեսակը: Ստեղծված է առանց պարամետրերի: Անցնում է այն արժեքները, որոնք եկել են միայն բաժանորդագրությունից հետո:
ՎարքագիծԹեմա new BehaviorSubject( defaultData<T> ) - Իմ կարծիքով առարկաների ամենատարածված տեսակը: Մուտքը վերցնում է լռելյայն արժեքը: Միշտ պահպանում է վերջին թողարկման տվյալները, որոնք փոխանցվում են բաժանորդագրվելիս: Այս դասը ունի նաև օգտակար արժեքի մեթոդ, որը վերադարձնում է հոսքի ընթացիկ արժեքը:
ReplaySubject new ReplaySubject(bufferSize?: number, windowTime?: number) - Ընտրովի, որպես առաջին արգումենտ կարող է վերցնել արժեքների բուֆերի չափը, որը կպահի իր մեջ, և երկրորդ անգամ, որի ընթացքում մեզ փոփոխություններ են պետք:
ասին թեմա new AsyncSubject() - Բաժանորդագրվելիս ոչինչ տեղի չի ունենում, և արժեքը կվերադարձվի միայն ավարտվելուց հետո: Միայն հոսքի վերջին արժեքը կվերադարձվի:
WebSocketSubject new WebSocketSubject(urlConfigOrSource: string | WebSocketSubjectConfig<T> | Observable<T>, destination?: Observer<T>) - Փաստաթղթերը լռում են այդ մասին, և ես ինքս առաջին անգամ եմ դա տեսնում։ Ով գիտի ինչ է անում, գրի, կավելացնենք։
Ֆու Դե, մենք քննարկել ենք այն ամենը, ինչ ես ուզում էի պատմել այսօր։ Հուսով եմ, որ այս տեղեկատվությունը օգտակար էր: Գրականության ցանկը կարող եք ինքնուրույն կարդալ Օգտակար տեղեկություններ ներդիրում: