Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, RxJs)

Hei kaikki. Ota yhteyttä Omelnitsky Sergey. Ei niin kauan sitten isännöin reaktiivista ohjelmointia käsittelevää streamia, jossa puhuin JavaScriptin asynkronisuudesta. Tänään haluan tehdä yhteenvedon tästä materiaalista.

Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, RxJs)

Mutta ennen kuin aloitamme päämateriaalin, meidän on tehtävä johdanto. Joten aloitetaan määritelmistä: mitä ovat pino ja jono?

Pino on kokoelma, jonka elementit haetaan "viimeinen sisään, ensimmäinen ulos" LIFO-periaatteella

käännä on kokoelma, jonka elementit saadaan periaatteen mukaisesti ("first in, first out" FIFO

Okei, jatketaan.

Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, RxJs)

JavaScript on yksisäikeinen ohjelmointikieli. Tämä tarkoittaa, että sillä on vain yksi suoritussäie ja yksi pino, jossa funktiot ovat jonossa suoritettavaksi. Siksi JavaScript voi suorittaa vain yhden toiminnon kerrallaan, kun taas muut toiminnot odottavat vuoroaan pinossa, kunnes niitä kutsutaan.

kutsupino on tietorakenne, joka yksinkertaistetusti tallentaa tietoja ohjelman paikasta, jossa olemme. Jos hyppäämme funktioon, työnnämme sen merkinnän pinon yläosaan. Kun palaamme funktiosta, ponnaamme pinon ylimmän elementin ja päädymme siihen, mistä kutsuimme tämän funktion. Siinä on kaikki, mitä pino voi tehdä. Ja nyt erittäin mielenkiintoinen kysymys. Miten asynkronisuus sitten toimii JavaScriptissä?

Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, RxJs)

Itse asiassa, pinon lisäksi selaimilla on erityinen jono niin sanotun WebAPI:n kanssa työskentelemiseen. Tämän jonon toiminnot suoritetaan järjestyksessä vasta, kun pino on tyhjennetty kokonaan. Vasta sen jälkeen ne sijoitetaan jonosta pinoon suoritettaviksi. Jos pinossa on tällä hetkellä ainakin yksi elementti, ne eivät pääse pinoon. Juuri tästä syystä funktioiden kutsuminen aikakatkaisulla on usein ajallisesti epätarkkoja, koska funktio ei pääse jonosta pinoon sen ollessa täynnä.

Katsotaanpa seuraavaa esimerkkiä ja käydään se läpi vaihe vaiheelta. Katsotaan myös mitä järjestelmässä tapahtuu.

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

Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, RxJs)

1) Toistaiseksi mitään ei ole tapahtunut. Selainkonsoli on puhdas, puhelupino tyhjä.

Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, RxJs)

2) Sitten komento console.log('Hi') lisätään kutsupinoon.

Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, RxJs)

3) Ja se täyttyy

Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, RxJs)

4) Sitten console.log('Hi') poistetaan puhelupinosta.

Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, RxJs)

5) Siirrytään nyt komentoon setTimeout(function cb1() {… }). Se lisätään puhelupinoon.

Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, RxJs)

6) Komento setTimeout(funktio cb1() {… }) suoritetaan. Selain luo ajastimen, joka on osa Web API:ta. Se suorittaa lähtölaskennan.

Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, RxJs)

7) Komento setTimeout(function cb1() {… }) on suorittanut työnsä ja se poistetaan kutsupinosta.

Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, RxJs)

8) Console.log('Bye') -komento lisätään kutsupinoon.

Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, RxJs)

9) Console.log('Bye') -komento suoritetaan.

Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, RxJs)

10) Komento console.log('Bye') poistetaan kutsupinosta.

Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, RxJs)

11) Kun vähintään 5000 ms on kulunut, ajastin päättyy ja asettaa cb1-soiton takaisinsoittojonoon.

Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, RxJs)

12) Tapahtumasilmukka ottaa funktion cb1 takaisinsoittojonosta ja työntää sen puhelupinoon.

Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, RxJs)

13) cb1-toiminto suoritetaan ja se lisää console.log('cb1') puhelupinoon.

Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, RxJs)

14) Console.log('cb1') -komento suoritetaan.

Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, RxJs)

15) Komento console.log('cb1') poistetaan kutsupinosta.

Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, RxJs)

16) Funktio cb1 poistetaan puhelupinosta.

Katsotaanpa esimerkkiä dynamiikasta:

Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, RxJs)

No, tarkastelimme, kuinka asynkronia toteutetaan JavaScriptissä. Puhutaanpa nyt lyhyesti asynkronisen koodin kehityksestä.

Asynkronisen koodin kehitys.

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

Asynkroninen ohjelmointi sellaisena kuin sen tunnemme JavaScriptissä voidaan tehdä vain funktioilla. Ne voidaan välittää muille funktioille kuten muutkin muuttujat. Näin takaisinsoitot syntyivät. Ja se on siistiä, hauskaa ja kiihkeää, kunnes se muuttuu suruksi, melankoliaksi ja suruksi. Miksi? Kyllä, se on yksinkertainen:

  • Kun koodin monimutkaisuus kasvaa, projekti muuttuu nopeasti epäselviksi useiksi sisäkkäisiksi lohkoiksi - "takaisinkutsuhelvetiksi".
  • Virheiden käsittely voidaan helposti jättää huomiotta.
  • Et voi palauttaa lausekkeita returnilla.

Promisen myötä tilanne on parantunut hieman.

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

  • Lupausketjuja ilmestyi, mikä paransi koodin luettavuutta
  • Virheiden sieppaamiseen oli erillinen menetelmä
  • Rinnakkainen suoritus lisätyn Promise.all:n kanssa
  • Voimme ratkaista sisäkkäisen asynkronian async/awaitilla

Mutta lupauksella on rajansa. Esimerkiksi lupausta ei voi peruuttaa ilman tamburiinilla tanssimista, ja mikä tärkeintä, se toimii yhdellä arvolla.

No, tässä lähestytään tasaisesti reaktiivista ohjelmointia. Väsynyt? No, hyvä asia on, että voit mennä hauduttamaan lokkeja, aivoriihiä ja palata lukemaan lisää. Ja jatkan.

Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, RxJs)

Reaktiivinen ohjelmointi - tietovirtoihin ja muutosten etenemiseen keskittyvä ohjelmointiparadigma. Katsotaanpa tarkemmin, mitä datavirta on.

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

const eventsArray = [];

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

Kuvitellaan, että meillä on syöttökenttä. Luomme taulukon ja tallennamme tapahtuman jokaiselle syötetapahtuman avaimelle taulukkoomme. Samalla haluan huomauttaa, että taulukkomme on lajiteltu ajan mukaan, ts. myöhempien tapahtumien indeksi on suurempi kuin aikaisempien tapahtumien indeksi. Tällainen taulukko on yksinkertaistettu tietovirtamalli, mutta se ei ole vielä vuo. Jotta tätä ryhmää voitaisiin turvallisesti kutsua virraksi, sen on kyettävä jollakin tavalla ilmoittamaan tilaajille, että siihen on saapunut uutta dataa. Siten tulemme virtauksen määritelmään.

Tietovirta

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

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

Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, RxJs)

virta on ajan mukaan lajiteltu joukko tietoja, jotka voivat osoittaa, että tiedot ovat muuttuneet. Kuvittele nyt, kuinka kätevää on kirjoittaa koodia, jossa sinun täytyy käynnistää useita tapahtumia koodin eri osissa yhtä toimintoa varten. Tilaamme streamin ja se ilmoittaa meille, kun muutoksia tapahtuu. Ja RxJs-kirjasto voi tehdä tämän.

Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, RxJs)

RxJS on kirjasto asynkronisten ja tapahtumapohjaisten ohjelmien kanssa työskentelyyn havainnoitavien sekvenssien avulla. Kirjasto tarjoaa päätyypin havaittava, useita aputyyppejä (Tarkkailijat, ajoittajat, aiheet) ja operaattorit tapahtumien ja kokoelmien kanssa työskentelyyn (kartta, suodattaa, pienentää, jokainen ja vastaavat JavaScript Arraysta).

Ymmärretään tämän kirjaston peruskäsitteet.

Havaittava, tarkkailija, tuottaja

Observable on ensimmäinen perustyyppi, jota tarkastelemme. Tämä luokka sisältää pääosan RxJs-toteutuksesta. Se liittyy havaittavaan streamiin, joka voidaan tilata tilausmenetelmällä.

Observable toteuttaa päivitysten luomiseen tarkoitetun apumekanismin, ns Tarkkailija. Tarkkailijan arvojen lähdettä kutsutaan Tuottaja. Se voi olla taulukko, iteraattori, verkkoliitäntä, jonkinlainen tapahtuma jne. Voimme siis sanoa, että havaittava on johdin tuottajan ja tarkkailijan välillä.

Observable käsittelee kolmenlaisia ​​Observer-tapahtumia:

  • seuraava - uudet tiedot
  • virhe - virhe, jos sekvenssi katkesi poikkeuksen vuoksi. tämä tapahtuma merkitsee myös sekvenssin päättymistä.
  • valmis - signaali sekvenssin päättymisestä. Tämä tarkoittaa, että uusia tietoja ei enää tule

Katsotaanpa demo:

Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, RxJs)

Aluksi käsittelemme arvot 1, 2, 3 ja 1 sekunnin kuluttua. saamme 4 ja lopetamme ketjumme.

Ajatella ääneen

Ja sitten tajusin, että oli mielenkiintoisempaa kertoa kuin kirjoittaa siitä. 😀

tilaus

Kun tilaamme streamin, luomme uuden luokan tilaus, joka antaa meille mahdollisuuden peruuttaa tilaus menetelmällä peruuttaa. Voimme myös ryhmitellä tilauksia menetelmällä lisätä. No, on loogista, että voimme purkaa säikeitä käyttämällä poistaa. Lisää ja poista -menetelmät hyväksyvät syötteeksi toisen tilauksen. Haluaisin huomauttaa, että kun peruutamme tilauksen, peruutamme kaikki lapsitilaukset ikään kuin he kutsuisivat myös tilauksen peruutusmenetelmää. Mene eteenpäin.

Virtojen tyypit

KUUMA
KYLMÄ

Tuottaja luodaan havaittavan ulkopuolelle
Tuottaja luodaan havaittavan sisällä

Tiedot välitetään, kun havaittava luodaan
Tiedot toimitetaan tilauksen yhteydessä.

Tilauksen peruuttamiseen tarvitaan lisää logiikkaa
Lanka päättyy itsestään

Käyttää yksi-moneen-suhdetta
Käyttää kahdenkeskistä suhdetta

Kaikilla tilauksilla on sama arvo
Tilaukset ovat riippumattomia

Tiedot voivat kadota, jos tilausta ei ole
Julkaisee uudelleen kaikki streamin arvot uutta tilausta varten

Vertailun antamiseksi kuvittelisin kuuman virran kuin elokuvan elokuvateatterissa. Mihin aikaan tulit, siitä hetkestä lähtien aloit katsomaan. Vertaisin kylmää virtaa kutsuun niissä. tuki. Kuka tahansa soittaja kuuntelee puhelinvastaajan nauhoituksen alusta loppuun, mutta voit lopettaa puhelun tilauksen peruuttamistoiminnolla.

Haluaisin huomauttaa, että on myös niin kutsuttuja lämpimiä virtoja (olen tavannut tällaisen määritelmän erittäin harvoin ja vain ulkomaisissa yhteisöissä) - tämä on virta, joka muuttuu kylmästä virrasta kuumaksi. Herää kysymys - missä käyttää)) Annan esimerkin käytännöstä.

Työskentelen Angularin kanssa. Hän käyttää aktiivisesti rxjs:ää. Saadakseni tietoja palvelimelle odotan kylmää streamia ja käytän tätä tietovirtaa mallissa asyncPipe-sovelluksella. Jos käytän tätä putkea useita kertoja, palatakseni kylmävirran määritelmään, jokainen putki pyytää tietoja palvelimelta, mikä on vähintäänkin outoa. Ja jos muutan kylmän virran lämpimäksi, pyyntö tapahtuu kerran.

Yleensä virtaustyypin ymmärtäminen on melko vaikeaa aloittelijoille, mutta tärkeää.

Operaattorit

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

Operaattorit tarjoavat meille mahdollisuuden työskennellä streamien kanssa. Ne auttavat hallitsemaan havainnoitavissa olevia tapahtumia. Käsittelemme pari suosituinta, ja lisää tietoa operaattoreista löytyy hyödyllisten tietojen linkeistä.

Operaattorit

Aloitetaan apuoperaattorista. Se luo havaittavan yksinkertaisen arvon perusteella.

Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, RxJs)

Operaattorit-suodatin

Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, RxJs)

Suodatinoperaattori, kuten nimestä voi päätellä, suodattaa stream-signaalin. Jos operaattori palauttaa tosi, se hyppää pidemmälle.

Operaattorit - ota

Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, RxJs)

take - Ottaa arvon emittioiden lukumäärästä, jonka jälkeen stream päättyy.

Operaattorit-debounceTime

Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, RxJs)

debounceTime - hylkää lähetetyt arvot, jotka kuuluvat määritettyyn aikaväliin lähtötietojen välillä - kun aikaväli on kulunut, lähettää viimeisen arvon.

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

Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, RxJs)

Operaattorit-takeWhile

Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, RxJs)

Lähettää arvoja, kunnes takeWhile palauttaa false, lopettaa sitten viestiketjun tilauksen.

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

Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, RxJs)

Operaattorit-yhdistelmä Viimeisin

Yhdistetty operaattori combinedLatest on jossain määrin samanlainen kuin lupaus.all. Se yhdistää useita virtoja yhdeksi. Kun jokainen säie on tehnyt vähintään yhden emit, saamme kustakin viimeisimmät arvot matriisina. Lisäksi yhdistetyistä virroista saatavan säteilyn jälkeen se antaa uusia arvoja.

Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, 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));

Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, RxJs)

Operaattorit-zip

Zip - odottaa arvoa jokaisesta virrasta ja muodostaa taulukon näiden arvojen perusteella. Jos arvo ei tule mistään säikeestä, ryhmää ei muodosteta.

Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, 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));

Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, RxJs)

Operaattorit - forkJoin

forkJoin myös yhdistää säikeitä, mutta se lähettää arvon vasta, kun kaikki säikeet ovat valmiit.

Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, 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);

Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, RxJs)

Operaattorit-kartta

Karttamuunnosoperaattori muuttaa emit-arvon uudeksi.

Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, 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)
);

Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, RxJs)

Operaattorit - jaa, napauta

Napautusoperaattorin avulla voit tehdä sivuvaikutuksia, eli mitä tahansa toimintoja, jotka eivät vaikuta sekvenssiin.

Jakamisoperaattori voi muuttaa kylmän virran kuumaksi virraksi.

Asynkroninen ohjelmointi JavaScriptissä (takaisinsoitto, lupaus, RxJs)

Operaattorit ovat valmiit. Siirrytään aiheeseen.

Ajatella ääneen

Ja sitten menin juomaan teetä. Olen kyllästynyt näihin esimerkkeihin 😀

Aiheperhe

Aiheperhe on loistava esimerkki kuumista langoista. Nämä luokat ovat eräänlainen hybridi, joka toimii havainnoivana ja tarkkailijana samanaikaisesti. Koska aihe on kuuma stream, sen tilaus on peruutettava. Jos puhumme tärkeimmistä menetelmistä, nämä ovat:

  • seuraava - uusien tietojen välittäminen streamiin
  • virhe - virhe ja säikeen päättäminen
  • täydellinen - langan loppu
  • tilaa - tilaa stream
  • unsubscribe - peruuta ketjun tilaus
  • asObservable - muuttuu tarkkailijaksi
  • toPromise - muuttuu lupaukseksi

Varaa 4 5 aihetyyppiä.

Ajatella ääneen

Sanoin 4 streamissa, mutta kävi ilmi, että he lisäsivät yhden. Kuten sanonta kuuluu, elä ja opi.

Yksinkertainen aihe new Subject()- yksinkertaisimmat aiheet. Luotu ilman parametreja. Välittää arvot, jotka tulivat vasta tilauksen jälkeen.

BehaviorSubject new BehaviorSubject( defaultData<T> ) - mielestäni yleisin aihetyyppi. Syöte ottaa oletusarvon. Tallentaa aina viimeisimmän numeron tiedot, jotka välitetään tilauksen yhteydessä. Tällä luokalla on myös hyödyllinen arvomenetelmä, joka palauttaa virran nykyisen arvon.

ReplaySubject new ReplaySubject(bufferSize?: number, windowTime?: number) - Vaihtoehtoisesti se voi ottaa ensimmäisenä argumenttina arvopuskurin koon, jonka se tallentaa itseensä, ja toisen kerran, jolloin tarvitsemme muutoksia.

asynkkisubjekti new AsyncSubject() - Tilauksen aikana ei tapahdu mitään, ja arvo palautetaan vasta, kun se on valmis. Vain virran viimeinen arvo palautetaan.

WebSocketSubject new WebSocketSubject(urlConfigOrSource: string | WebSocketSubjectConfig<T> | Observable<T>, destination?: Observer<T>) – Asiasta dokumentaatio on hiljaa ja itse näen sen ensimmäistä kertaa. Kuka tietää mitä tekee, kirjoita, me lisäämme.

Huh huh. No, olemme pohtineet kaikkea, mitä halusin kertoa tänään. Toivottavasti näistä tiedoista oli apua. Voit lukea kirjallisuusluettelon itse Hyödyllisiä tietoja -välilehdeltä.

hyödyllistä tietoa

Lähde: will.com

Lisää kommentti