Асінхроннае праграмаванне ў JavaScript (Callback, Promise, RxJs )

Ўсім прывітанне. На сувязі Амельніцкі Сяргей. Не так даўно я вёў стрым па рэактыўным праграмаванні, дзе распавядаў пра асінхроннасць у JavaScript. Сёння я хацеў бы заканспектаваць гэты матэрыял.

Асінхроннае праграмаванне ў JavaScript (Callback, Promise, RxJs )

Але перад тым як пачаць асноўны матэрыял нам трэба зрабіць уступную. Такім чынам, давайце пачнем з азначэнняў: што такое стэк і чарга?

стэк - гэта калекцыя, элементы якой атрымліваюць па прынцыпе "апошні ўвайшоў, першы выйшаў" LIFO

чарга - гэта калекцыя, элементы якой атрымліваюць па прынцыпе («першы ўвайшоў, першы выйшаў» FIFO

Окей, працягнем.

Асінхроннае праграмаванне ў JavaScript (Callback, Promise, RxJs )

JavaScript - гэта аднаструменная мова праграмавання. Гэта значыць, што ён маецца толькі адзін струмень выканання і адзін стэк, у які змяшчаюцца функцыі ў чаргу на выкананне. Такім чынам у адзін момант часу JavaScript можа выканаць толькі адну аперацыю, іншыя аперацыі пры гэтым будуць чакаць сваёй чаргі ў стэку, пакуль іх не выклічуць.

Стэк выклікаў - Гэта структура дадзеных, якая, спрошчана кажучы, запісвае звесткі аб месцы ў праграме, дзе мы знаходзімся. Калі мы пераходзім у функцыю, мы змяшчаем запіс пра яе ў верхнюю частку стэка. Калі мы з функцыі вяртаемся, мы выцягваем са стэка самы верхні элемент і апыняемся тамака, адкуль выклікалі гэтую функцыю. Гэта - усё, што ўмее стэк. А зараз вельмі цікавае пытанне. Як тады працуе асінхроннасць у JavasScript?

Асінхроннае праграмаванне ў JavaScript (Callback, Promise, RxJs )

Насамрэч апроч стэка ў браўзэрах прысутнічае адмысловая чарга для працы з так званым WebAPI. Функцыі з гэтай чаргі выконваюцца па парадку толькі пасля таго, як стэк будзе поўнасцю ачышчаны. Толькі пасля гэтага яны змяшчаюцца з чаргі ў стэк на выкананне. Калі ў стэку ў дадзены момант знаходзіцца хаця б адзін элемент, то яны ў стэк патрапіць не могуць. Якраз менавіта з-за гэтага выклік функцый па таймаўту часта бывае не дакладным па часе, бо функцыя не можа патрапіць з чаргі ў стэк, пакуль ён запоўнены.

Разгледзім наступны прыклад і зоймемся яго пакрокавым "выкананнем". Таксама паглядзім, што пры гэтым адбываецца ў сыстэме.

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

Асінхроннае праграмаванне ў JavaScript (Callback, Promise, RxJs )

1) Пакуль нічога не адбываецца. Кансоль браўзэра чыстая, стэк выклікаў пусты.

Асінхроннае праграмаванне ў JavaScript (Callback, Promise, RxJs )

2) Потым каманда console.log('Hi') дадаецца ў стэк выклікаў.

Асінхроннае праграмаванне ў JavaScript (Callback, Promise, RxJs )

3) І яна выконваецца

Асінхроннае праграмаванне ў JavaScript (Callback, Promise, RxJs )

4) Затым console.log('Hi') выдаляецца са стэка выклікаў.

Асінхроннае праграмаванне ў JavaScript (Callback, Promise, RxJs )

5) Цяпер пераходзім да каманды setTimeout(function cb1() {… }). Яна дадаецца ў стэк выклікаў.

Асінхроннае праграмаванне ў JavaScript (Callback, Promise, RxJs )

6) Каманда setTimeout(function cb1() {… }) выконваецца. Браўзэр стварае таймер, які з'яўляецца часткай Web API. Ён будзе выконваць адваротны адлік часу.

Асінхроннае праграмаванне ў JavaScript (Callback, Promise, RxJs )

7) Каманда setTimeout(function cb1() {… }) завяршыла працу і выдаляецца са стэка выклікаў.

Асінхроннае праграмаванне ў JavaScript (Callback, Promise, RxJs )

8) Каманда console.log('Bye') дадаецца ў стэк выклікаў.

Асінхроннае праграмаванне ў JavaScript (Callback, Promise, RxJs )

9) Каманда console.log('Bye') выконваецца.

Асінхроннае праграмаванне ў JavaScript (Callback, Promise, RxJs )

10) Каманда console.log('Bye') выдаляецца са стэка выклікаў.

Асінхроннае праграмаванне ў JavaScript (Callback, Promise, RxJs )

11) Пасля таго, як пройдуць, як мінімум, 5000 мс., таймер завяршае працу і змяшчае коллбэк cb1 у чаргу коллбэкаў.

Асінхроннае праграмаванне ў JavaScript (Callback, Promise, RxJs )

12) Цыкл падзей бярэ c функцыю cb1 з чаргі коллбэкаў і змяшчае яе ў стэк выклікаў.

Асінхроннае праграмаванне ў JavaScript (Callback, Promise, RxJs )

13) Функцыя cb1 выконваецца і дадае console.log('cb1') у стэк выклікаў.

Асінхроннае праграмаванне ў JavaScript (Callback, Promise, RxJs )

14) Каманда console.log('cb1') выконваецца.

Асінхроннае праграмаванне ў JavaScript (Callback, Promise, RxJs )

15) Каманда console.log('cb1') выдаляецца са стэка выклікаў.

Асінхроннае праграмаванне ў JavaScript (Callback, Promise, RxJs )

16) Функцыя cb1 выдаляецца са стэка выклікаў.

Зірнем на прыклад у дынаміцы:

Асінхроннае праграмаванне ў JavaScript (Callback, Promise, 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, можа быць рэалізавана толькі функцыямі. Яны могуць быць перададзены як любая іншая зменная іншым функцый. Так нарадзіліся коллбэкі. І гэта прыкольна, весела і забіяцка, пакуль не ператвараецца ў сум, нуду і смутак. Чаму? Ды ўсё проста:

  • З ростам складанасці кода, праект хутка ператвараецца ў малазразумелыя шматкроць укладзеныя блокі – "callback hell".
  • Апрацоўку памылак можна лёгка ўпусціць.
  • Нельга вяртаць выразы з return.

З з'яўленнем 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
  • Укладзеную асінхроннасць мы можам вырашыць з дапамогай async/await

Але ў промісу ёсць свае абмежаванні. Напрыклад проміс, без танцаў з бубнам, нельга адмяніць, а што самае галоўнае - працуе з адным значэннем.

Ну вось мы і плаўна падышлі да рэактыўнага праграмавання. Стаміліся? Ну балазе справа можна пайсці заварыць чаек, абмазгаваць і вярнуцца чытаць далей. А я працягну.

Асінхроннае праграмаванне ў JavaScript (Callback, Promise, RxJs )

Рэактыўнае праграмаванне - Парадыгма праграмавання, арыентаваная на струмені дадзеных і распаўсюджванне змен. Давайце больш дэталёва разбяром што такое струмень дадзеных.

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

const eventsArray = [];

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

Уявім, што ў нас ёсць поле ўводу. Мы ствараем масіў, і на кожны keyup падзеі input мы будзем захоўваць падзею ў нашым масіве. Пры гэтым хацелася б адзначыць, што наш масіў адсартаваны па часе, г.зн. індэкс пазнейшых падзей больш, чым індэкс больш ранніх. Такі масіў уяўляе сабою спрошчаную мадэль струменя дадзеных, але гэта яшчэ не струмень. Для таго каб гэты масіў можна было смела назваць плынню ён павінен умець нейкім чынам паведамляць падпісчыкам, што ў яго паступілі новыя дадзеныя. Такім чынам мы падышлі да вызначэння плыні.

Струмень дадзеных

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

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

Асінхроннае праграмаванне ў JavaScript (Callback, Promise, RxJs )

паток - Гэта масіў дадзеных, адсартаваных па часе, які можа паведамляць аб тым, што дадзеныя змяніліся. А зараз прадстаўце як зручна становіцца пісаць код, у якім на адно дзеянне запатрабуецца выклікаць некалькі падзей у розных участках кода. Мы проста робім падпіску на паток і ён нам сам паведаміць калі адбудуцца змены. І гэта ўмее рабіць бібліятэка RxJs.

Асінхроннае праграмаванне ў JavaScript (Callback, Promise, RxJs )

RxJS - гэта бібліятэка для працы з асінхроннымі і заснаванымі на падзеях праграмамі з выкарыстаннем назіраных паслядоўнасцяў. Бібліятэка дае асноўны тып Назіраемая, некалькі дапаможных тыпаў (Observer, Schedulers, Subjects) і аператары працы з падзеямі як з калекцыямі (map, filter, reduce, every і падобныя з JavaScript (Array).

Давайце разбяромся з асноўнымі паняццямі гэтай бібліятэкі.

Observable, Observer, Producer

Observable - першы базавы тып, які мы разгледзім. Гэты клас утрымоўвае ў сабе асноўную частку рэалізацыі RxJs. Ён злучаны з назіраным струменем, на які можна як падпісацца з дапамогай метаду subscribe.

У Observable рэалізуецца дапаможны механізм для стварэння абнаўленняў, так званы Назіральнік. Крыніцай значэнняў для Observer называецца Вытворца. Гэта можа быць масіў, ітэратар, web socket, нейкая падзея і да т.п. Так што можна сказаць, што observable з'яўляецца правадніком паміж Producer і Observer.

Observable апрацоўвае тры віды падзей Observer:

  • next - новыя дадзеныя
  • error - памылку, калі паслядоўнасць завяршылася з прычыны выключнай сітуацыі. гэта падзея гэтак жа мяркуе завяршэнне паслядоўнасці.
  • complete - сігнал аб завяршэнні паслядоўнасці. Гэта азначае, што новых дадзеных больш не будзе

Паглядзім дэма:

Асінхроннае праграмаванне ў JavaScript (Callback, Promise, RxJs )

У пачатку мы апрацуем значэння 1, 2, 3, а праз 1 сек. мы атрымаем 4 і завершым наш паток.

Думкі ўслых

І тут я зразумеў, што расказваць было цікавей чым пісаць пра гэта. 😀

падпіска

Калі мы робім падпіску на паток, мы ствараем новы клас падпіска, які дае нам магчымасць адмяніць падпіску з дапамогай метаду адмовіцца ад падпіскі. Таксама мы можам згрупаваць падпіскі з дапамогай метаду. дадаваць. Ну і лагічна, што мы можам разгрупаваць патокі з дапамогай выдаленне. Метады add і remove на ўваход прымаюць іншую падпіску. Хацелася б адзначыць, што калі мы робім адпіску, то мы адпісваемся ад усіх даччыных падпісак нібыта і ў іх выклікалі метад unsubscribe. Ідзем далей.

Віды патокаў

ГАРАЧАЯ
Халодны

Producer ствараецца звонку observable
Producer ствараецца ўнутры observable

Дадзеныя перадаюцца ў момант стварэння observable
Даныя паведамляюцца ў момант падпіскі

Патрэбна дадатковая логіка для адпіскі
Струмень завяршаецца самастойна

Выкарыстоўвае сувязь адзін-да-многім
Выкарыстоўвае сувязь выгляду адзін да аднаго

Усе падпіскі маюць адзінае значэнне
Падпіскі незалежныя

Дадзеныя можна страціць, калі няма падпіскі
Перавыдае ўсе значэнні патоку для новай падпіскі

Калі прыводзіць аналогію, то я бы прадставіў гарачую плынь як фільм у кінатэатры. У які момант ты прыйшоў, з таго моманту і пачаў прагляд. Халодны паток я б параўнаў са званком у тых. падтрымку. Любы які патэлефанаваў слухае запіс аўтаадказчыка ад пачатку да канца, але ты можаш кінуць трубку з дапамогай unsubscribe.

Хацелася б адзначыць, што існуюць яшчэ так званыя warm патокі (такое вызначэнне я сустракаў вельмі рэдка і толькі на замежных супольнасцях) - гэта паток, які трансфармуецца з халоднага патоку ў гарачы. Узнікае пытанне - дзе выкарыстоўваць)) Прывяду прыклад з практыкі.

Я працую з ангулярам. Ён актыўна выкарыстоўвае rxjs. Для атрымання дадзеных на сервер я чакаю халодны струмень і гэты струмень выкарыстоўваю ў шаблоне з дапамогай asyncPipe. Калі я выкарыстоўваю гэты пайп некалькі разоў, то, вяртаючыся да вызначэння халоднага патоку, кожны pipe будзе запытваць дадзеныя з сервера, што мякка кажучы дзіўна. А калі я пераўтвору халодны паток у цёплы, то запыт адбудзецца аднойчы.

Наогул разуменне віду патокаў дастаткова складана для пачаткоўцаў, але важная.

Аператары

return this.http.get(`${environment.apiUrl}/${this.apiUrl}/trade_companies`)
    .pipe(
        tap(({ data }: TradeCompanyList) => this.companies$$.next(cloneDeep(data))),
        map(({ data }: TradeCompanyList) => data)
    );

Пашырыць магчымасць працы са струменямі нам падаюць аператары. Яны дапамагаюць кантраляваць падзеі, якія праходзяць у Observable. Мы разгледзім парачку найболей папулярных, а падрабязней з аператарамі можна азнаёміцца ​​па спасылках у карыснай інфармацыі.

Operators - of

Пачнём з дапаможнага аператара of. Ён стварае Observable на аснове простага значэння.

Асінхроннае праграмаванне ў JavaScript (Callback, Promise, RxJs )

Operators - filter

Асінхроннае праграмаванне ў JavaScript (Callback, Promise, RxJs )

Аператар фільтрацыі filter, як можна зразумець па назове, фільтруе сігнал струменя. Калі аператар вяртае ісціну, то прапускае далей.

Operators - take

Асінхроннае праграмаванне ў JavaScript (Callback, Promise, RxJs )

take - Прымае значэнне кол-у эмітаў, пасля якога завяршае паток.

Operators - debounceTime

Асінхроннае праграмаванне ў JavaScript (Callback, Promise, 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 (Callback, Promise, RxJs )

Operators – takeWhile

Асінхроннае праграмаванне ў JavaScript (Callback, Promise, 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 (Callback, Promise, RxJs )

Operators - combineLatest

Камбінаваны аператар combineLatest нечым падобны на promise.all. Ён аб'ядноўвае некалькі плыняў у адзін. Пасля таго як кожны струмень зробіць хаця б адзін эміт, мы атрымліваем апошнія значэнні ад кожнага ў выглядзе масіва. Далей, пасьля любога эміту з аб'яднаных плыняў ён будзе аддаваць новыя значэньні.

Асінхроннае праграмаванне ў JavaScript (Callback, Promise, 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 (Callback, Promise, RxJs )

Operators - zip

Zip - чакае значэнне з кожнага патоку і фармуе масіў на аснове гэтых значэнняў. Калі значэнне не прыйдзе з якога-небудзь патоку, то група не будзе сфарміравана.

Асінхроннае праграмаванне ў JavaScript (Callback, Promise, 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 (Callback, Promise, RxJs )

Operators - forkJoin

forkJoin таксама аб'ядноўвае патокі, але ён эмітніт значэнне толькі калі ўсе патокі будуць завершаны (complete).

Асінхроннае праграмаванне ў JavaScript (Callback, Promise, 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 (Callback, Promise, RxJs )

Operators - map

Аператар трансфармацыі map пераўтворыць значэнне эміта ў новае.

Асінхроннае праграмаванне ў JavaScript (Callback, Promise, 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 (Callback, Promise, RxJs )

Operators - share, tap

Аператар tap - дазваляе рабіць side эфекты, гэта значыць якія-небудзь дзеянні, якія не ўплываюць на паслядоўнасць.

Утылітны аператар share здольны з халоднай плыні зрабіць гарачым.

Асінхроннае праграмаванне ў JavaScript (Callback, Promise, RxJs )

З аператарамі скончылі. Пяройдзем да Subject.

Думкі ўслых

І тут я пайшоў гарбату піць. Змарылі мяне гэтыя прыклады 😀

Сямейства subject-ов

Сямейства subject-ов з'яўляюцца яркім прыкладам гарачых струменяў. Гэтыя класы з'яўляюцца нейкім гібрыдам, якія выступаюць адначасова ў ролі observable і observer. Бо subject з'яўляецца гарачым струменем, то ад яго неабходна адпісвацца. Калі казаць па асноўных метадах, то гэта:

  • next - перадача новых дадзеных у паток
  • error - памылка і завяршэнне патоку
  • complete - завяршэнне патоку
  • subscribe - падпісацца на паток
  • unsubscribe - адпісацца ад патоку
  • asObservable - трансфармуем у назіральніка
  • toPromise - трансфармуе ў проміс

Вылучаюць 4 5 тыпаў subject-ов.

Думкі ўслых

На стрыме казаў 4, а аказалася яны яшчэ адзін дадалі. Як той казаў век жыві век вучыся.

Просты Subject new Subject()- Самы просты выгляд subject-ов. Ствараецца без параметраў. Перадае значэння, якія прыйшлі толькі пасля падпіскі.

BehaviorSubject new BehaviorSubject( defaultData<T> ) – на мой погляд самы распаўсюджаны від subject-аў. На ўваход прымае значэнне па змаўчанні. Заўсёды захоўвае даныя апошняга эміта, якія перадае пры падпісцы. Дадзены клас мае гэтак жа карысны метад value, які вяртае бягучае значэнне патоку.

ReplaySubject new ReplaySubject(bufferSize?: number, windowTime?: number) - На ўваход апцыянальна можа прыняць першым аргументам памер буфера значэнняў, якія ён будзе ў сабе захоўваць, а другім час на працягу якога нам патрэбныя змены.

AsyncSubject new AsyncSubject() - Пры падпісцы нічога не адбываецца, і значэнне будзе вернута толькі пры complete. Будзе вернута толькі апошняе значэнне патоку.

WebSocketSubject new WebSocketSubject(urlConfigOrSource: string | WebSocketSubjectConfig<T> | Observable<T>, destination?: Observer<T>) - Пра яго дакументацыя маўчыць і я сам яго ўпершыню бачу. Хто ведае што ён робіць пішыце, дапоўнім.

Фуф. Ну вось мы і разгледзелі ўсё, што я хацеў сёння расказаць. Спадзяюся, дадзеная інфармацыя была карыснай. Самастойна азнаёміцца ​​са спісам літаратуры можна ва ўкладцы карысная інфармацыя.

Карысная інфармацыя

Крыніца: habr.com

Дадаць каментар