Webprogramozás

Aszinkron programozás, AJAX, this, hibakezelés

Horváth Győző
Egyetemi docens
1117 Budapest, Pázmány Péter sétány 1/c., 2.408-as szoba
Tel: (1) 372-2500/8469
horvath.gyozo@inf.elte.hu

Ismétlés

Ismétlés

  • JavaScript nyelvi elemei
  • DOM programozás
  • Eseménykezelés részletei
  • JavaScript beépített objektumai
  • Kódszervezés
  • Űrlapok, képek, táblázatok
  • Progresszív fejlesztés
  • window, canvas, HTML5 API-k

JSON

JSON

  • JavaScript Object Notation
  • A JavaScript literálformáira épülő adatleírási formátum
  • Központ eleme
    • objektum: {}
    • tömb:
  • Elterjedt és népszerű
  • JSON.stringify()
  • JSON.parse()
{
  "Title": "The Hobbit: An Unexpected Journey",
  "Year": "2012",
  "Rated": "PG-13",
  "Released": "14 Dec 2012",
  "Runtime": "169 min",
  "Genre": "Adventure, Fantasy",
  "Director": "Peter Jackson",
  "Writer": "Fran Walsh (screenplay), Philippa Boyens (screenplay), Peter Jackson (screenplay), Guillermo del Toro (screenplay), J.R.R. Tolkien (novel)",
  "Actors": "Ian McKellen, Martin Freeman, Richard Armitage, Ken Stott",
  "Plot": "A reluctant Hobbit, Bilbo Baggins, sets out to the Lonely Mountain with a spirited group of dwarves to reclaim their mountain home, and the gold within it from the dragon Smaug.",
  "Language": "English",
  "Country": "USA, New Zealand",
  "Awards": "Nominated for 3 Oscars. Another 10 wins & 72 nominations.",
  "Poster": "https://m.media-amazon.com/images/M/MV5BMTcwNTE4MTUxMl5BMl5BanBnXkFtZTcwMDIyODM4OA@@._V1_SX300.jpg",
  "Ratings": [
    {
      "Source": "Internet Movie Database",
      "Value": "7.8/10"
    },
    {
      "Source": "Rotten Tomatoes",
      "Value": "64%"
    },
    {
      "Source": "Metacritic",
      "Value": "58/100"
    }
  ],
  "Metascore": "58",
  "imdbRating": "7.8",
  "imdbVotes": "725,598",
  "imdbID": "tt0903624",
  "Type": "movie",
  "DVD": "19 Mar 2013",
  "BoxOffice": "$303,001,229",
  "Production": "Warner Bros.",
  "Website": "N/A",
  "Response": "True"
}

JSON sorosítás oda-vissza

const obj = {
  alma: 'piros',
  korte: [1, 2, 3]
}

//Sorosítás
const sz = JSON.stringify(obj);
console.log(sz);
// '{"alma":"piros","korte":[1,2,3]}'

//Visszaalakítás
console.log( JSON.parse(sz) );
// Object { alma: "piros", korte: Array[3] }

Szinkron vs aszinkron programozás

Szinkron műveletek

  • Szinkron ~ szinkronizált ~ összekapcsolt ~ függő
  • Szinkron művelet: meg kell várni a végét, mielőtt a következőre ugranánk
  • Az egyik művelet eleje függ a másik végétől
  • Szekvencia
|--------A--------|
                  |--------B--------|
1 thread ->   |<---A---->||<----B---------->||<------C----->|
thread A -> |<---A---->|   
                        \  
thread B ------------>   ->|<----B---------->|   
                                              \   
thread C ---------------------------------->   ->|<------C----->|

Szinkron példa

console.log('first')
alert('second')
console.log('third')

Szinkron végrehajtás

Szinkron hátránya

  • Hosszú műveletek megvárása
    • időzítők
    • hálózat
    • lemezkezelés

Aszinkron

  • Másik feladat elindítható az egyik vége előtt
  • Nem függnek egymástól
|--------A--------|
    |--------B--------|

thread A ->     |<---A---->|
thread B ----->     |<----B---------->|
thread C --------->     |<------C--------->|

Szinkron vs aszinkron

Szinkron vs aszinkron

  • Szinkron
    • Mozijegyért állsz sorba. Addig nem tudod megvenni, amíg az előtted lévők nem vették meg, és ugyanez igaz a mögötted állókra is.
  • Aszinkron
    • Étteremben vagy sok más emberrel együtt. Megrendeled az ételedet. Mások is rendelhetnek ételt, nem kell megvárniuk, míg a tiédet elkészítik és felszolgálják. A konyhában a dolgozók folyamatosan főznek, szolgálnak fel és rendeléseket fogadnak. Az emberek akkor fogják az ételüket megkapni, amikor az elkészült.

Szinkron vs aszinkron

  • Szinkron
    • A főnököm egy elfoglalt ember. Azt mondja, írjam meg a kódot. Mondom neki: Rendben. Nekilátok, és úgy áll a hátam mögött, mint egy keselyű. Kérdezem: “Haver, miért nem mész és csinálsz valamit, amíg befejezem?” Mire ő: “Nem, megvárom, amíg befejezed.”
  • Aszinkron
    • A főnök azt mondja nekem, hogy oldjam meg a feladatot, és ahelyett, hogy ott várná a munkámat, kimegy és más feladatokat hajt végre. Amikor befejezem a munkámat, egyszerűen beszámolok a főnökömnek, és azt mondom: “KÉSZ!”

Példák

// Timer
console.log('first')
setTimeout(function () {
  console.log('second')
}, 1000)
console.log('third')
// Event handler
console.log('first')
button.addEventListener('click', function () {
  console.log('second')
})
console.log('third')

Event loop

Konkurrencia egy szálon

Callback függvény

  • Paraméterként átadott függvény meghívása
  • Ő maga nem szinkron/aszinkron
  • Az API szinkron/aszinkron
// Szinkron
function a(b) {
    b();  // callback
}
console.log('first')
a(function () {
  console.log('second')
});
console.log('third')

// vagy

[1, 3, 5].map(e => e * 2)
// Aszinkron
console.log('first')
setTimeout(function () {
  console.log('second')
}, 1000)
console.log('third')

Callback hell

setTimeout(() => {
  console.log('first')
  setTimeout(() => {
    console.log('second')
    setTimeout(() => {
      console.log('third')
      setTimeout(() => {
        console.log('fourth')
      },1000)
    }, 1000)
  }, 1000)
}, 1000)

Promise

  • Egy aszinkron művelet jövőbeli értékét reprezentáló objektum
  • Állapota: pending, fulfilled, rejected
function delay(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(`${ms} timeout`)
      resolve(ms)
    }, ms)
  })
}

// USING

delay(1000).then(ms => console.log('Result', ms))

Promise lánc

delay(1000)
  .then(ms => { return delay(500)      })
  .then(ms => { return delay(2000)     })
  .then(ms => { return 800             })
  .then(ms => { console.log('finally') })
  .catch() {
    console.log('There are some errors')
  }

Async-await

Promise-okkal dolgozik

async function lotOfDelays() {
  try {
    await delay(1000)
    await delay(500)
    await delay(2000)
    await delay(800)
  }
  catch() {
    console.log('There are some errors')
  }
}

lotOfDelays();

Web workers

  • Valódi többszálúság
  • Kommunikáció: üzenetek/események
//főszál
const worker = new Worker('masik.js');
worker.onmessage = function(e) {
  console.log(e.data);
};
worker.postMessage('valami');

//masik.js
self.onmessage = function(e) {
  self.postMessage("Kapott adat: " + e.data);
};

AJAX

Hagyományos oldalak

AJAX – oldalkiszolgálás

  • A kapcsolatfelvétel a szerverrel szükséges
  • Asynchronous JavaScript and XML
  • Csak a szükséges adatok továbbítása a háttérben, a teljes oldal újratöltése nélkül

AJAX-os oldal tulajdonságai

  • A felhasználói felület folyamatosan használható
  • Nincs szaggatottság, villogás, ugrálás
  • A szerverrel való kommunikáció a háttérben történik
  • Aszinkron módon, azaz párhuzamosan a többi eseménnyel
  • Csak a szükséges adatok közlekednek a szerver és kliens között

AJAX hívás eszközei

Példa

http://www.omdbapi.com/?t=the+shack&apikey=<key>

Szinkron megoldás

const input = document.querySelector('input')
const button = document.querySelector('button')
const img = document.querySelector('img')

button.addEventListener('click', getPoster)
function getPoster() {
  const title = input.value
  const xhr = new XMLHttpRequest();
  xhr.open('GET', `http://www.omdbapi.com/?t=${title}&apikey=<key>`, false);
  xhr.send(null);
  const response = JSON.parse(xhr.responseText)
  img.src = response.Poster;
}

Aszinkron megoldás

load, loadend, abort, error, timeout események

function getPoster() {
  const title = input.value
  const xhr = new XMLHttpRequest();
  xhr.addEventListener('load', responseHandler);
  xhr.open('GET', `http://www.omdbapi.com/?t=${title}&apikey=<key>`);
  xhr.send(null);
}
function responseHandler() {
  const response = JSON.parse(this.responseText)
  img.src = response.Poster;
}

Választípus

responseType, response

function getPoster() {
  const title = input.value
  const xhr = new XMLHttpRequest()
  xhr.addEventListener('load', responseHandler)
  xhr.open('GET', `http://www.omdbapi.com/?t=${title}&apikey=<key>`)
  xhr.responseType = 'json'
  xhr.send(null)
}
function responseHandler() {
  img.src = this.response.Poster
}

Hibakezelés

function getPoster() {
  const title = input.value
  const xhr = new XMLHttpRequest()
  xhr.addEventListener('load', responseHandler)
  xhr.addEventListener('error', errorHandler)
  xhr.open('GET', `http://www.omdbapi.com/?t=${title}&apikey=<key>`)
  xhr.responseType = 'json'
  xhr.send(null)
}
function errorHandler() {
  console.error('Error')
}
function responseHandler() {
  img.src = this.response.Poster
}

Folyamat

progress esemény

const progress = document.querySelector('progress')
function getPoster() {
  const title = document.querySelector('input').value
  const xhr = new XMLHttpRequest()
  xhr.addEventListener('load', responseHandler)
  xhr.addEventListener('progress', progressHandler)
  xhr.open('GET', `http://www.omdbapi.com/?t=${title}&apikey=2dd0dbee`)
  xhr.responseType = 'json'
  xhr.send(null)
}
function progressHandler(e) {
  if(e.lengthComputable) {
    progress.max = e.total
    progress.value = e.loaded
  }
}
function responseHandler(e) {
  document.querySelector('img').src = this.response.Poster
  progress.value = e.loaded
}

fetch API

Promise-ok

function getPoster() {
  const title = document.querySelector('input').value
  fetch(`http://www.omdbapi.com/?t=${title}&apikey=2dd0dbee`)
    .then(response => response.json())
    .then(response => {
      document.querySelector('img').src = response.Poster
    })
}

// or

async function getPoster() {
  const title = document.querySelector('input').value
  const response = await fetch(`http://www.omdbapi.com/?t=${title}&apikey=2dd0dbee`)
  const json = await response.json()
  document.querySelector('img').src = json.Poster
}

this-lexia

Azaz a this kontextusa

A this kontextusa

//Global
let name = 'Peter';  // window.name
function hello() {
    return this.name  // this === global (window)
}
hello()  // window.hello()

//Method call
let peter = {
    name: 'Peter',
    describe: function () {
        return this.name  // this === peter
    }
}
peter.describe()

//Constructor call
let Person = function(name) {
    this.name = name;  // this === instance object
}
let peter = new Person('Peter')

//Setting the context of this with call and apply
let peter = {
    name: 'Peter',
    hello: function () {
        return this.name
    }
};
let julia = {
    name: 'Julia',
    hello: function () {
        return this.name
    }
};
peter.hello.call(julia) // "Julia"

A kontextus elveszítése

let peter = {
    name: 'Peter',
    age: 42,
    describe: function () {
        function getAge() {
            return this.age  // this === global (window)
        }
        return this.name + ':' + getAge() // global call, ~ window.getAge()
    }
}
peter.describe()  // "Peter:undefined"

A this kontextusának helyreállítása belső függvényeknél

//With call and apply
let peter = {
    name: 'Peter',
    age: 42,
    describe: function () {
        function getAge() {
            return this.age  // this depends on the call
        }
        return this.name + ':' + getAge.call(this)
    }
}
peter.describe()  // "Peter:42"

//ES5 bind()
let peter = {
    name: 'Peter',
    age: 42,
    describe: function () {
        let getAge = (function () {
            return this.age  // inner this is always outer this
        }).bind(this)
        return this.name + ':' + getAge()
    }
}
peter.describe()  // "Peter:42"

//ES6 fat arrow syntax
let peter = {
    name: 'Peter',
    age: 42,
    describe: function () {
        let getAge = () => this.age
        return this.name + ':' + getAge()
    }
}
peter.describe()  // "Peter:42"

Példa: document.querySelector

const $ = document.querySelector
$('p') // Illegal invocation

// Magyarázat
const document = {
  somethingInside: ...,
  querySelector: function (sel) {
    this.somethingInside
  }
}
document.querySelector() // --> this === document
window.$() // --> this === window, window.somethingInside nincs

// Megoldás: a kontextus rögzítése
const $ = document.querySelector.bind(this)
const $ = sel => document.querySelector(sel)

Példa: osztályon belüli eseménykezelők

// Alapeset
class AppView {
  constructor(appState) {
    this.elem = document.querySelector('something')
    this.elem.addEventListener('click', this.onClick)
  }
  onClick(e) {
    // this === document.querySelector('something')
  }
}

// Bind
class AppView {
  constructor(appState) {
    this.elem = document.querySelector('something')
    this.elem.addEventListener('click', this.onClick.bind(this))
  }
  onClick(e) {
    // this === appView
  }
}

// Fat arrow
class AppView {
  constructor(appState) {
    this.elem = document.querySelector('something')
    this.elem.addEventListener('click', e => this.onClick(e))
  }
  onClick(e) {
    // this === appView
  }
}

// Class property initializer
class AppView {
  constructor(appState) {
    this.elem = document.querySelector('something')
    this.elem.addEventListener('click', this.onClick)
  }
  onClick = (e) => {
    // this === appView
  }
}

Kivételkezelés

Hibák

  • Error
    • EvalError
    • RangeError
    • ReferenceError
    • SyntaxError
    • TypeError
    • URIError
  • Tulajdonságok
    • name
    • message

Hibakezelés

  • try-catch-finally
    • try: védendő kód
    • catch: hibakezelő kód
    • finally: a végén lefutó kód (nem kötelező)
try {
  alma.kukacos = true;
}
catch (e) {
  console.log(e.name);    //ReferenceError
  console.log(e.message); //alma is not defined
}
finally {  //Elhagyható
  console.log('Végem van...');
}

Hiba dobása

Beépített hiba dobása

if (typeof a !== 'number') {
  throw new Error('Nem szam a parameter!');
}

Hiba dobása

Saját hibaobjektum dobása

if (oszto == 0) {
  throw {
    name: 'DivisionByZeroError',
    message: 'Az oszto nulla!'
  };
}

vagy

function DivisionByZeroError(message) {
  this.name = "DivisionByZeroError";
  this.message = message;
}
//...
if (oszto == 0) {
  throw new DivisionByZeroError('Az oszto nulla!');
}

Időzítős példák

  • kirajzolás
  • hosszú folyamat késleltetése
  • hosszú folyamat felbontása

Újrarajzolás megvárása

<p>Before</p>
<p id="par">Paragraph to fade out/in</p>
<p>After</p>
const p = document.querySelector('#par');
p.style.transition = 'opacity 1s';

function fadeOut() {
  p.style.opacity = 0;
  p.addEventListener('transitionend', vege, {once: true});
}
function fadeIn() {
  p.removeEventListener('transitionend', vege)
  p.style.display = '';
  requestAnimationFrame(function () {   // waiting for style recalculation
    requestAnimationFrame(function () { // before repaint --> next tick
      p.style.opacity = 1;
    })
  });
}
function vege() {
  p.style.display = 'none';
}

Hosszú folyamat késleltetése

Ciklustól az időzítőig

function vegrehajtas() {
  for (let i = 0; i < 5; i++) {
    console.log("feldolgoz", i)
  }
}

// A számlálós ciklust átírhatjuk elöltesztelősre
function vegrehajtas(i = 0) {
  while (i < 5) {
    console.log("feldolgoz", i)
    i = i + 1
  }
}

// Ebből már könnyű az ismétlődést rekurzióval megoldani, 
// tulajdonképpen a `while` helyett `if`-et kell írni, 
// és a végén egy rekurzív hívást elhelyezni:
function vegrehajtas(i = 0) {
  if (i < 5) {
    console.log("feldolgoz", i)
    vegrehajtas(i + 1)
  }
}

// Az időzítés hasonló a rekurzióhoz, csak nem szinkron közvetlen hívás van, hanem egy aszinkron közvetett.
// Egy állapotváltozóba ki kell szervezni, hogy kell-e újra időzítőt hívni. A mi esetünkben ez az `i` változó volt.
function vegrehajtas(i = 0) {
  if (i < 5) {
    console.log("feldolgoz", i)
    setTimeout(() => vegrehajtas(i + 1), 500)
  }
}

// Korábban bevezetett delay függvénnyel
async function vegrehajtas() {
  for (let i = 0; i < 5; i++) {
    console.log("feldolgoz", i)
    await delay(500)
  }
}

Hosszú folyamat felbontása

  • Ugyanaz, csak azonnal végrehajtva
  • Következő iterációba időzíti
  • Alternatíva: web worker
// Normal, intensive, operation
const table = document.getElementsByTagName("tbody");
for (let i = 0; i < 2000; i++ ) {
  const tr = document.createElement("tr");
  for (let t = 0; i < 6; i++ )
    const td = document.createElement("td");
    td.appendChild( document.createTextNode("" + t);
    tr.appendChild( td );
  }
  table.appendChild( tr );
}

// Using a timer to break up a long-running task
const table = document.getElementsByTagName("tbody");
let i = 0;
const max = 1999;
setTimeout(function(){
  for (let step = i + 500; i < step; i++ ) {
    const tr = document.createElement("tr");
    for (let t = 0; i < 6; i++ )
      const td = document.createElement("td");
      td.appendChild( document.createTextNode("" + t);
      tr.appendChild( td );
    }
    table.appendChild( tr );
  }

  if ( i < max ) {
    setTimeout( arguments.callee, 0 );
  }
}, 0);