JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

Бәріңе сәлем. Сергей Омельницкий байланыста. Жақында мен реактивті бағдарламалау ағынын өткіздім, онда мен JavaScript-тегі асинхрония туралы айттым. Бүгін мен осы материалды жазып алғым келеді.

JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

Бірақ негізгі материалды бастамас бұрын, біз кіріспе жазба жасауымыз керек. Сонымен анықтамалардан бастайық: стек және кезек дегеніміз не?

Стек элементтері соңғы кірген, бірінші шығатын LIFO негізінде алынған жинақ болып табылады

Кезек элементтері бірінші кірген, бірінші шығатын FIFO негізінде алынатын жинақ

Жарайды, жалғастырайық.

JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

JavaScript – бір ағынды бағдарламалау тілі. Бұл тек бір орындалу ағыны және функциялар орындау үшін кезекке қойылған бір стек бар екенін білдіреді. Сондықтан JavaScript бір уақытта тек бір операцияны орындай алады, ал басқа операциялар шақырылғанша стекке кезек күтеді.

Қоңыраулар стек қарапайым тілмен айтқанда, біз орналасқан бағдарламадағы орын туралы ақпаратты жазатын деректер құрылымы. Функцияға өтсек, оның жазбасын стектің жоғарғы жағына итереміз. Функциядан оралған кезде біз стектен ең жоғарғы элементті шығарып, функцияны шақырған жерге қайта ораламыз. Бұл стек жасай алатын барлық нәрсе. Ал енді өте қызықты сұрақ. JavasScript-те асинхрония қалай жұмыс істейді?

JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

Шындығында, стектен басқа, браузерлерде WebAPI деп аталатын жұмыс үшін арнайы кезек бар. Бұл кезектегі функциялар стек толығымен тазартылғаннан кейін ғана ретімен орындалады. Осыдан кейін ғана олар орындалу үшін кезектен стекке шығарылады. Егер стекте кем дегенде бір элемент болса, оларды стекке қосу мүмкін емес. Дәл осыған байланысты функцияларды күту уақыты бойынша шақыру көбінесе уақыт бойынша дәл болмайды, өйткені функция толған кезде кезектен стекке жете алмайды.

Келесі мысалды қарастырып, оның қадамдық орындалуын бастайық. Сондай-ақ жүйеде не болатынын көрейік.

console.log('Hi');
setTimeout(function cb1() {
    console.log('cb1');
}, 5000);
console.log('Bye');

JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

1) Әлі ештеңе болып жатқан жоқ. Браузер консолі анық, қоңыраулар стегі бос.

JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

2) Содан кейін console.log('Hi') пәрмені қоңыраулар стекіне қосылады.

JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

3) Және ол орындалды

JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

4) Содан кейін console.log('Hi') қоңыраулар стекінен жойылады.

JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

5) Енді setTimeout (cb1() функциясы {… }) пәрменіне өтіңіз. Ол қоңыраулар стекіне қосылады.

JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

6) setTimeout(cb1() {… }) пәрмені орындалады. Браузер Web API бөлігі болып табылатын таймерді жасайды. Ол кері санақ жасайды.

JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

7) setTimeout(функция cb1() {... }) пәрмені өз жұмысын аяқтады және шақыру стекінен жойылды.

JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

8) console.log('Bye') пәрмені қоңыраулар стекіне қосылады.

JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

9) console.log('Bye') пәрмені орындалады.

JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

10) console.log('Bye') пәрмені қоңыраулар стекінен жойылады.

JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

11) Кем дегенде 5000 мс өткеннен кейін таймер жұмысын тоқтатады және cb1 кері қоңырауды кері шақыру кезегіне қояды.

JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

12) Оқиғалар циклі кері шақыру кезегінен cb1 функциясын алып, оны шақыру стекіне орналастырады.

JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

13) cb1 функциясы орындалады және console.log('cb1') шақыру стекіне қосады.

JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

14) console.log('cb1') пәрмені орындалды.

JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

15) console.log('cb1') пәрмені қоңыраулар стекінен жойылады.

JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

16) cb1 функциясы шақыру стекінен жойылады.

Динамикадағы мысалды қарастырайық:

JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

Біз 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);
                    })
                })
            })
        })
    })
});

Асинхронды бағдарламалауды 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 арқылы параллель орындау мүмкіндігі қосылды
  • Біз кірістірілген асинхронияны асинхронды/күту арқылы шеше аламыз

Бірақ уәделердің өз шектеулері бар. Мысалы, домбырамен билеусіз уәденің күшін жою мүмкін емес, ең бастысы, ол бір мәнмен жұмыс істейді.

Біз реактивті бағдарламалауға оңай жақындадық. Шаршадыңыз ба? Бақытымызға орай, сіз шай қайнатып, ойланып, көбірек оқу үшін қайта аласыз. Ал мен жалғастырамын.

JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

Реактивті бағдарламалау деректер ағындары мен өзгерістердің таралуына бағытталған бағдарламалау парадигмасы болып табылады. Деректер ағынының не екенін егжей-тегжейлі қарастырайық.

// Получаем ссылку на элемент
const input = ducument.querySelector('input');

const eventsArray = [];

// Пушим каждое событие в массив eventsArray
input.addEventListener('keyup',
    event => eventsArray.push(event)
);

Бізде енгізу өрісі бар деп елестетейік. Біз массив жасап жатырмыз және кіріс оқиғасының әрбір пернелері үшін оқиғаны массивте сақтаймыз. Сонымен қатар, біздің массив уақыт бойынша сұрыпталғанын атап өткім келеді, яғни. кейінгі оқиғалардың көрсеткіші алдыңғылардың көрсеткішінен үлкен. Мұндай массив деректер ағынының жеңілдетілген үлгісі болып табылады, бірақ ол әлі ағын емес. Бұл массив қауіпсіз түрде ағын деп аталу үшін, ол жазылушыларға оған жаңа деректер келгені туралы қандай да бір түрде хабарлауы керек. Осылайша біз ағынның анықтамасына келеміз.

Деректер ағыны

const { interval } = Rx;
const { take } = RxOperators;

interval(1000).pipe(
    take(4)
)

JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

Ағын деректердің өзгергенін көрсете алатын уақыт бойынша сұрыпталған деректер жиымы. Енді бір әрекет кодтың әртүрлі бөліктеріндегі бірнеше оқиғаны шақыруды қажет ететін кодты жазу қаншалықты ыңғайлы болатынын елестетіп көріңіз. Біз жай ғана ағынға жазыламыз және ол өзгерістер болған кезде бізге хабарлайды. Ал RxJs кітапханасы мұны істей алады.

JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

RxJS бақыланатын тізбектерді пайдалана отырып, асинхронды және оқиғаға негізделген бағдарламалармен жұмыс істеуге арналған кітапхана болып табылады. Кітапхана негізгі түрін ұсынады Байқауға болады, бірнеше көмекші түрлері (Бақылаушы, жоспарлаушылар, субъектілер) және коллекциялар сияқты оқиғалармен жұмыс істеу операторлары (карта, сүзгі, азайту, әрбір және JavaScript массивіндегі ұқсастары).

Осы кітапхананың негізгі ұғымдарын түсінейік.

Бақыланатын, бақылаушы, өндіруші

Бақыланатын - біз қарастыратын бірінші негізгі түрі. Бұл класс RxJs іске асырудың негізгі бөлігін қамтиды. Ол жазылу әдісі арқылы жазылуға болатын бақыланатын ағынмен байланысты.

Observable жаңартулар деп аталатын көмекші механизмді жүзеге асырады Observer. Observer үшін мәндер көзі деп аталады өндіруші. Бұл массив, итератор, веб-розетка, қандай да бір оқиға түрі және т.б. болуы мүмкін. Сонымен, біз бақыланатын продюсер мен бақылаушы арасындағы өткізгіш деп айта аламыз.

Бақыланатын бақылаушы оқиғаларының үш түрін өңдейді:

  • келесі – жаңа деректер
  • қате – егер реттілік ерекше жағдайға байланысты аяқталса, қате. бұл оқиға сонымен қатар дәйектіліктің аяқталуын білдіреді.
  • толық — тізбектің аяқталғаны туралы сигнал. Бұл енді жаңа деректер болмайды дегенді білдіреді.

Демонстрацияны көрейік:

JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

Басында біз 1, 2, 3 мәндерін өңдейміз және 1 секундтан кейін. біз 4 аламыз және ағынымызды аяқтаймыз.

Дауыстап ойлау

Сосын мен бұл туралы жазғаннан гөрі айту қызық екенін түсіндім. 😀

жазылу

Біз ағынға жазылған кезде жаңа класс жасаймыз жазылубұл әдіс арқылы жазылудан бас тартуға мүмкіндік береді жазылымнан бас тарту. Әдісті пайдаланып жазылымдарды да топтастыруға болады қосу. Біз ағындарды пайдаланып топтарды ажырата алатынымыз қисынды кетіру. Қосу және жою әдістері басқа жазылымды енгізу ретінде қабылдайды. Жазылымнан бас тартқан кезде біз барлық балалар жазылымдарынан бас тарту әдісін шақырғандай бас тартатынымызды атап өткім келеді. Ілгері жүру.

Ағындардың түрлері

ЖЕДЕЛ
АҚЫЛ

Өндіруші бақыланбайтыннан тыс құрылады
Продюсер бақыланатын жерде жасалады

Деректер бақыланатын нәрсе жасалған уақытта тасымалданады
Деректер жазылу кезінде беріледі

Жазылымнан бас тарту үшін қосымша логика қажет
Жіп өздігінен аяқталады

Бірден көпке қатынасты қолданады
Бірден-бірге қатынасты қолданады

Барлық жазылулардың мағынасы бірдей
Жазылымдар тәуелсіз

Жазылымыңыз болмаса, деректер жоғалуы мүмкін
Жаңа жазылым үшін барлық ағындық мәндерді қайта шығарады

Аналогияны келтіретін болсам, мен ыстық ағынды театрдағы кино деп санар едім. Сіз қай уақытта келдіңіз, сол сәттен бастап қарай бастадыңыз. Мен суық ағынды технологиядағы қоңыраумен салыстырар едім. қолдау көрсету. Кез келген қоңырау шалушы дауыстық пошта жазбасын басынан аяғына дейін тыңдайды, бірақ жазылудан бас тарту арқылы телефон тұтқасын қоюға болады.

Жылы ағындар деп аталатындар да бар екенін атап өткім келеді (мен бұл анықтаманы өте сирек және тек шетелдік қауымдастықтарда кездестірдім) - бұл суық ағыннан ыстыққа ауысатын ағын. Сұрақ туындайды - қайда қолдануға болады)) Мен тәжірибеден мысал келтіремін.

Мен 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)
    );

Операторлар бізге ағындармен жұмыс істеу мүмкіндігін кеңейту мүмкіндігін береді. Олар бақылауда болып жатқан оқиғаларды басқаруға көмектеседі. Біз ең танымалдардың бірнешеуін қарастырамыз және операторлар туралы толығырақ ақпаратты пайдалы ақпараттағы сілтемелер арқылы табуға болады.

Операторлар -

Көмекші операторынан бастайық. Ол қарапайым мәнге негізделген Бақыланатын мәнді жасайды.

JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

Операторлар – сүзгі

JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

Сүзгі операторы, аты айтып тұрғандай, ағындық сигналды сүзеді. Оператор true мәнін қайтарса, ол әрі қарай өткізіп жібереді.

Операторлар - алыңыз

JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

take — Эмитенттердің санының мәнін қабылдайды, содан кейін жіп аяқталады.

Операторлар - debounceTime

JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

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)
);  

JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

Операторлар - takeWhile

JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

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 )
);  

JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

Операторлар - combineLatest

combineLatest операторы сөздік.all операторына біршама ұқсас. Ол бірнеше ағындарды біріктіреді. Әрбір ағын кем дегенде бір эмиссия жасағаннан кейін біз әрқайсысынан массив түрінде ең соңғы мәндерді аламыз. Әрі қарай, біріктірілген ағындардан кез келген эмиссиядан кейін ол жаңа мәндер береді.

JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

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));

JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

Операторлар - zip

Zip - әрбір ағыннан мәнді күтеді және осы мәндер негізінде массив құрайды. Егер мән ешбір ағыннан келмесе, онда топ құрылмайды.

JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

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));

JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

Операторлар - forkJoin

forkJoin сонымен қатар ағындарды қосады, бірақ ол барлық ағындар аяқталған кезде ғана мән шығарады.

JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

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);

JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

Операторлар - карта

Картаны түрлендіру операторы эмитент мәнін жаңасына түрлендіреді.

JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

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)
);

JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

Операторлар – бөлісу, түртіңіз

Кран операторы жанама әсерлерді, яғни реттілікке әсер етпейтін кез келген әрекеттерді жасауға мүмкіндік береді.

Ортақ коммуналдық оператор суық ағынды ыстыққа айналдыра алады.

JavaScript-те асинхронды бағдарламалау (кері қоңырау, уәде, RxJs)

Операторлармен жұмысымыз аяқталды. Тақырыпқа көшейік.

Дауыстап ойлау

Сосын мен шай ішуге кеттім. Мына мысалдардан шаршадым😀

Субъектілер отбасы

Пәндік отбасы ыстық ағындардың тамаша мысалы болып табылады. Бұл кластар бір мезгілде бақыланатын және бақылаушы ретінде әрекет ететін гибридтердің бір түрі болып табылады. Тақырып қызу тақырып болғандықтан, оған жазылудан бас тарту керек. Егер негізгі әдістер туралы айтатын болсақ, онда олар:

  • келесі – жаңа деректерді ағынға тасымалдау
  • қате – қате және ағынды тоқтату
  • толық – жіпті аяқтау
  • жазылу – ағынға жазылу
  • жазылудан бас тарту – ағыннан бас тарту
  • asObservable – бақылаушыға айналдыру
  • toPromise – уәдеге айналады

Пәндердің 4 түрі бар.

Дауыстап ойлау

Ағында 4 адам сөйлесті, бірақ олар тағы біреуін қосқан. Олар айтқандай, өмір сүріп, үйреніңіз.

Қарапайым тақырып new Subject()– пәндердің ең қарапайым түрі. Параметрлерсіз жасалған. Жазылымнан кейін ғана алынған мәндерді жібереді.

BehaviorSubject new BehaviorSubject( defaultData<T> ) – менің ойымша, пәннің ең көп тараған түрі. Кіріс әдепкі мәнді қабылдайды. Әрқашан жазылу кезінде берілетін соңғы шығарылымның деректерін сақтайды. Бұл сыныпта ағынның ағымдағы мәнін қайтаратын пайдалы мән әдісі де бар.

ReplaySubject new ReplaySubject(bufferSize?: number, windowTime?: number) — Енгізу міндетті түрде бірінші аргумент ретінде өзі сақтайтын мәндер буферінің өлшемін, ал екіншісі ретінде бізге өзгерістер қажет болатын уақытты қабылдай алады.

AsyncSubject new AsyncSubject() — жазылу кезінде ештеңе болмайды және мән аяқталған кезде ғана қайтарылады. Ағынның соңғы мәні ғана қайтарылады.

WebSocketSubject new WebSocketSubject(urlConfigOrSource: string | WebSocketSubjectConfig<T> | Observable<T>, destination?: Observer<T>) — Құжаттама ол туралы үнсіз, мен оны бірінші рет көріп тұрмын. Кімде-кім оның не істейтінін білсе, жазыңыз, біз оны қосамыз.

Фу. Міне, мен бүгін айтқым келгеннің барлығын қамтыдық. Бұл ақпарат пайдалы болды деп үміттенемін. Пайдалы ақпарат қойындысында сілтемелер тізімін өзіңіз оқи аласыз.

Пайдалы ақпарат

Ақпарат көзі: www.habr.com

пікір қалдыру