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(function 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'те функциялар аркылуу гана ишке ашырылышы мүмкүн. Алар башка өзгөрмөлөр сыяктуу башка функцияларга берилиши мүмкүн. Кайра чалуулар ушинтип жаралган. Ал кайгыга, меланхолияга жана кайгыга айланганга чейин муздак, кызыктуу жана ойноок. Неге? Бул жөнөкөй:

  • Коддун татаалдыгы жогорулаган сайын, долбоор бат эле бүдөмүк, кайра-кайра уя салынган блокторго - "кайра чалуу тозогуна" айланат.
  • Ката менен иштөө оңой болушу мүмкүн.
  • Сиз кайтаруу менен туюнтмаларды кайтара албайсыз.

Убада пайда болгондон кийин абал бир аз жакшырды.

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

Бирок убадалардын да чеги болот. Мисалы, дап менен бийлебей туруп убаданы жокко чыгарууга болбойт, эң негизгиси ал бир баалуулук менен иштейт.

Ооба, биз реактивдүү программалоого акырындык менен жакындап калдык. Чарчадыңызбы? Бактыга жараша, сиз барып чай кайнатып, ойлонуп көрүп, дагы окуу үчүн кайтып келсеңиз болот. А мен улантам.

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)

Flow маалыматтар өзгөргөнүн көрсөтө ала турган убакыт боюнча сорттолгон маалыматтардын массиви. Эми элестетип көргүлөчү, код жазуу канчалык ыңгайлуу болуп калат, анда бир аракет коддун ар кайсы бөлүмдөрүндө бир нече окуяны чакырууну талап кылат. Биз жөн гана агымга жазылабыз жана ал өзгөрүүлөр болгондо бизге кабарлайт. Жана RxJs китепканасы муну кыла алат.

JavaScript'те асинхрондук программалоо (кайра чалуу, убада, RxJs)

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

Келгиле, бул китепкананын негизги түшүнүктөрүн түшүнүп көрөлү.

Байкоочу, байкоочу, продюсер

Байкалуучу - бул биз карай турган биринчи негизги түрү. Бул класс RxJs ишке ашыруунун негизги бөлүгүн камтыйт. Бул жазылуу ыкмасын колдонуу менен жазылууга мүмкүн болгон байкоочу агым менен байланышкан.

Observable жаңыртууларды түзүү үчүн жардамчы механизмди ишке ашырат, деп аталган Байкоочу. Байкоочу үчүн баалуулуктардын булагы деп аталат чыгаруучу. Бул массив, итератор, веб розетка, кандайдыр бир окуя ж.б. болушу мүмкүн. Ошентип, биз байкоочу Продюсер менен Байкоочунун ортосундагы өткөрүүчү деп айта алабыз.

Observable Байкоочу окуяларынын үч түрүн иштетет:

  • кийинки - жаңы маалыматтар
  • ката – эгерде ырааттуулугу өзгөчө кырдаалдан улам аяктаган болсо, ката. бул окуя да ырааттуулуктун аякташын билдирет.
  • толук — ырааттуулуктун аяктагандыгы жөнүндө сигнал. Бул мындан ары жаңы маалыматтар болбойт дегенди билдирет.

Келгиле демонстрацияны карап көрөлү:

JavaScript'те асинхрондук программалоо (кайра чалуу, убада, RxJs)

Башында биз 1, 2, 3 жана 1 секунддан кийин баалуулуктарды иштетебиз. биз 4 алып, агымыбызды бүтүрөбүз.

Үн чыгарып ойлонуп

Анан ал жөнүндө жазганга караганда айтып берүү кызыктуураак экенин түшүндүм. 😀

жазылуу

Агымга жазылганда биз жаңы класс түзөбүз жазылууыкманы колдонуу менен жазылууну токтотууга мүмкүнчүлүк берет жазылууну токтотуу. Биз ошондой эле ыкманы колдонуу менен жазылууларды топтой алабыз кошуу. Ооба, биз жиптерди колдонуу менен ача алабыз деген логикалык жок кылуу. Кошуу жана алып салуу ыкмалары башка жазылууну киргизүү катары кабыл алат. Биз жазылууну токтоткондо, алар жазылууну токтотуу ыкмасын чакыргандай, бардык балдардын жазылууларынан баш тарта турганыбызды белгилегим келет. Уланта бер.

Агымдардын түрлөрү

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

Операторлор бизге агымдар менен иштөө жөндөмүбүздү кеңейтүү мүмкүнчүлүгүн беришет. Алар Көзөмөлдө болуп жаткан окуяларды көзөмөлдөөгө жардам берет. Биз эң популярдуу болгондордун бир нечесин карап чыгабыз жана операторлор жөнүндө көбүрөөк маалыматты пайдалуу маалыматтагы шилтемелер аркылуу тапса болот.

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

нын жардамчы операторунан баштайлы. Жөнөкөй мааниге негизделген байкоочу түзөт.

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>) — Документтер ал жөнүндө унчукпайт жана мен аны биринчи жолу көрүп жатам. Ким эмне менен алектенет билсе жазыңыз, кошобуз.

Фу. Ооба, биз бүгүн сага айткым келгендин баарын камтыдык. Бул маалымат пайдалуу болду деп үмүттөнөм. Пайдалуу маалымат өтмөгүнөн шилтемелердин тизмесин өзүңүз окуй аласыз.

Пайдалуу маалымат

Source: www.habr.com

Комментарий кошуу