Gunnar Bittersmann: asynchrones JavaScript

Folgende Situation: Zwei JSON-Dateien, die geladen werden müssen, und Code, der ausgeführt werden soll, wenn beide geladen sind. Verschachtelt sähe das so aus:

var aRequest = new Request('a.json');
var bRequest = new Request('b.json');

fetch(aRequest)
	.then(function (response) { return response.json(); })
	.then(function (aData)
	{
		fetch(bRequest)
			.then(function (response) { return response.json(); })
			.then(function (bData)
			{
				// do something with aData and bData
			});
	});

Ist aber nicht richtig asynchron. Die Dateien sollten unabhängig voneinander parallel geladen werden:

var aRequest = new Request('a.json');
var bRequest = new Request('b.json');

fetch(aRequest)
	.then(function (response) { return response.json(); })
	.then(function (aData)
	{
		// …
	});

fetch(bRequest)
	.then(function (response) { return response.json(); })
	.then(function (bData)
	{
		// …
	});

function doSomething(aData, bData)
{
	// do something with aData and bData
}

Aber was müsste dann anstelle von // … stehen, damit doSomething(aData, bData) aufgerufen wird, sobald beide JSON-Dateien geladen sind?

LLAP 🖖

--
“When UX doesn’t consider all users, shouldn’t it be known as ‘Some User Experience’ or... SUX? #a11y” —Billy Gregory
  1. Tach!

    Ist aber nicht richtig asynchron. Die Dateien sollten unabhängig voneinander parallel geladen werden:

    Es sieht mir nicht so aus, als ob das mit fetch() allein gelöst werden kann. Promise.all() wird wohl der Lösungsweg sein.

    dedlfix.

    1. @@dedlfix

      Es sieht mir nicht so aus, als ob das mit fetch() allein gelöst werden kann. Promise.all() wird wohl der Lösungsweg sein.

      Danke, funzt:

      var aRequest = new Request('a.json');
      var bRequest = new Request('b.json');
      
      var aPromise = fetch(aRequest).then(function (response) { return response.json(); })
      var bPromise = fetch(bRequest).then(function (response) { return response.json(); })
      
      Promise.all([aPromise, bPromise]).then(function (values)
      {
      	var aData = values[0];
      	var bData = values[1];
      
      	// do something with aData and bData
      });
      

      LLAP 🖖

      --
      “When UX doesn’t consider all users, shouldn’t it be known as ‘Some User Experience’ or... SUX? #a11y” —Billy Gregory
      1. Tach!

        var aRequest = new Request('a.json');
        var bRequest = new Request('b.json');
        
        var aPromise = fetch(aRequest).then(function (response) { return response.json(); })
        var bPromise = fetch(bRequest).then(function (response) { return response.json(); })
        
        Promise.all([aPromise, bPromise]).then(function (values)
        {
        	var aData = values[0];
        	var bData = values[1];
        
        	// do something with aData and bData
        });
        

        Du kannst auch noch die beiden ersten then()s einsparen. DRY statt WET. response.json() ist kein asynchroner Prozess. Das kann auch im then() vom Promise.all() ausgeführt werden.

        Promise.all([aPromise, bPromise]).then(function (responses) {
        	var aData = responses[0].json();
        	var bData = responses[1].json();
        
        	// do something with aData and bData
        });
        

        oder wenn es separat sein soll

        Promise.all([aPromise, bPromise]).then(function (responses) {
          var values = responses.map(function (response) { return response.json(); });
          // modern: const values = responses.map(response => response.json());
          // oder: const [aValue, bValue] = responses.map(response => response.json());
        
        	var aData = values[0];
        	var bData = values[1];
        
        	// do something with aData and bData
        });
        

        dedlfix.

        1. @@dedlfix

          Du kannst auch noch die beiden ersten then()s einsparen.

          Eher nach woanders verlagern.

          response.json() ist kein asynchroner Prozess.

          Hm, das heißt, das Parsen der JSON-Ressourcen kann nicht parallel erfolgen?

          Mein Gedankengang war: Angenommen, das Laden von a.json dauert 5 Sekunden, das Parsen 0.1 Sekunden. Das Laden von b.json dauert 0.1 Sekunden, das Parsen 5 Sekunden[1]. Dann wären beide Promises gleichzeitig erfüllt und der Nutzer bekommt schnellstmöglich das Ergebnis zu sehen.

          Das kann auch im then() vom Promise.all() ausgeführt werden.

          Dann würde das Parsen von b.json erst nach dem Laden von a.json beginnen; der Nutzer müsste 4.9 Sekunden länger warten. Wenn mein Gedankengang denn stimmt.

          LLAP 🖖

          --
          “When UX doesn’t consider all users, shouldn’t it be known as ‘Some User Experience’ or... SUX? #a11y” —Billy Gregory

          1. Das ist natürlich völlig aus der Luft gegriffen. Aber es soll ums Prinzip gehen. ↩︎

          1. Hello,

            Hm, das heißt, das Parsen der JSON-Ressourcen kann nicht parallel erfolgen?

            siehe hierzu auch Artikel über Webworker

            Liebe Grüße
            Tom S.

            --
            Es gibt nichts Gutes, außer man tut es
            Andersdenkende waren noch nie beliebt, aber meistens diejenigen, die die Freiheit vorangebracht haben.
          2. Tach!

            Du kannst auch noch die beiden ersten then()s einsparen.

            Eher nach woanders verlagern.

            response.json() ist kein asynchroner Prozess.

            Hm, das heißt, das Parsen der JSON-Ressourcen kann nicht parallel erfolgen?

            Parallel geht sowieso nichts im Single-Thread-Javascript.

            Mein Gedankengang war: Angenommen, das Laden von a.json dauert 5 Sekunden, das Parsen 0.1 Sekunden. Das Laden von b.json dauert 0.1 Sekunden, das Parsen 5 Sekunden[^1]. Dann wären beide Promises gleichzeitig erfüllt und der Nutzer bekommt schnellstmöglich das Ergebnis zu sehen.

            b wäre in dem Fall zuerst erfüllt, dessen Callback aufgerufen und das Parsen startet. Erst wenn das fertig ist, kann der Callback von a bearbeitet werden, dem dann sein Parsen folgt.

            Eigentlich solltest du das Parsen aber nicht bemerken. So groß werden die Daten nicht sein, dass das ins Gewicht fällt. Und dann wäre der Übertragungsprozess auch noch ein viel größerer Flaschenhals.

            Das kann auch im then() vom Promise.all() ausgeführt werden.

            Dann würde das Parsen von b.json erst nach dem Laden von a.json beginnen; der Nutzer müsste 4.9 Sekunden länger warten. Wenn mein Gedankengang denn stimmt.

            Ja. Die Idee ist gut, aber ich würde das zunächst in die Microoptimierungsschublade einsortieren, bis da was merkbares hochkommt.

            Du kannst jedoch die zwei fetch() mit then() in ein requests.map() stecken, dann sparst du dir den doppelten Code und hast den zeitlichen Vorteil.

            dedlfix.

            1. Hallo!

              response.json() ist kein asynchroner Prozess.

              Doch, schließlich gibt die Methode wieder einen Promise zurück.

              Hm, das heißt, das Parsen der JSON-Ressourcen kann nicht parallel erfolgen?

              Parallel geht sowieso nichts im Single-Thread-Javascript.

              Der Browser parst JSON auch nicht im JavaScript-Thread. Das kann in einem anderen Thread erfolgen. Währenddessen kann anderes JavaScript ausgeführt werden, anderes Parsing erfolgen usw. Natürlich muss Code, der auf den Promise wartet, auf das Parsingergebnis warten.

              Daher halte ich Gunnar's Lösung für angemessen.

              Du kannst jedoch die zwei fetch() mit then() in ein requests.map() stecken, dann sparst du dir den doppelten Code und hast den zeitlichen Vorteil.

              Das stimmt.

              Ludger

              1. Tach!

                response.json() ist kein asynchroner Prozess.

                Doch, schließlich gibt die Methode wieder einen Promise zurück.

                Ok, da lag ich falsch, JSON.parse() wäre synchron. Aber so richtig asynchron wird Response.json() nicht sein.

                Hm, das heißt, das Parsen der JSON-Ressourcen kann nicht parallel erfolgen?

                Parallel geht sowieso nichts im Single-Thread-Javascript.

                Der Browser parst JSON auch nicht im JavaScript-Thread. Das kann in einem anderen Thread erfolgen. Währenddessen kann anderes JavaScript ausgeführt werden, anderes Parsing erfolgen usw. Natürlich muss Code, der auf den Promise wartet, auf das Parsingergebnis warten.

                Das glaube ich jedoch nicht, dass extra für das Parsen ein Thread aufgemacht wird. Das wäre bei kleinen Mengen zu parsendem Code viel zu aufwendig. Und dann müsste dieses Handling eine andere Implementation haben als JSON.parse().

                Daher halte ich Gunnar's Lösung für angemessen.

                Du kannst jedoch die zwei fetch() mit then() in ein requests.map() stecken, dann sparst du dir den doppelten Code und hast den zeitlichen Vorteil.

                Das stimmt.

                An der Stelle meinte ich das übrigens so: Das Parsen findet vermutlicherweise direkt im Anschluss an den Callback statt und falls in der Zeit der zweite Request fertig wird, muss dessen Callback sich in der Event-Loop anstellen. Den zeitlichen Vorteil hat man nur zu dem Teil, der vor dem Eintreffen der Response fertig ist.

                Das ist meine Mutmaßung, und wahrscheinlich wird das sowieso nicht relevant werden, weil alles wegen zu geringer Datenmengen im Grundrauschen untergeht. Deshalb halte ich auch Aufwand für vertane Zeit, das genauer zu untersuchen zu wollen.

                dedlfix.

                1. Hello,

                  ich finde, dass wir das doch noch weiter verfolgen sollten. Wenn das tatsächlich alles ohne Zutun des Programmierers stattfindet, sollte manzumindest wissen, wo man Semaphore o. ä. vorsehen muss und wie man die atomrisiert anlegen kann.

                  Witzigerweise nennt sich das dann Self-Hosted

                  Liebe Grüße
                  Tom S.

                  --
                  Es gibt nichts Gutes, außer man tut es
                  Andersdenkende waren noch nie beliebt, aber meistens diejenigen, die die Freiheit vorangebracht haben.
                  1. Wenn das tatsächlich alles ohne Zutun des Programmierers stattfindet, sollte manzumindest wissen, wo man Semaphore o. ä. vorsehen muss und wie man die atomrisiert anlegen kann.

                    Das Rechenmodell von Parallel.js ist darauf ausgelegt, dass du mit unveränderlichen Zuständen und ohne Nebenwirkungen programmierst. Das heißt, du vermeidest Race-Conditions grundsätzlich und begibst dich erst gar nicht in die missliche Lage ein Problem wechselseitigen Ausschlusses lösen zu müssen. Das spart viel Code, der mit dem eigentlichen Programmierproblem nichts zu tun gehabt hätte, der also nur der Buchhaltung gedient hätte. Man vermeidet außerdem Folgeprobleme, wie Deadlocks oder Fairness-Bedingungen. Der beste Code ist der, den man nie schreiben muss.

                    Witzigerweise nennt sich das dann Self-Hosted

                    Self-hosted benutzt man ganz allgemein für Programmiersprachen-Interpreter, (JIT-)Compiler und Ausführungsumgebungen, die teilweise in der Programmiersprache selbst geschrieben sind. Der C++-Compiler wird zum Beispiel auch in C++ geschrieben, eine neue Compiler-Version wird also von einer älteren Compiler-Version übersetzt.

                    1. Hello,

                      Witzigerweise nennt sich das dann Self-Hosted

                      Self-hosted benutzt man ganz allgemein für Programmiersprachen-Interpreter, (JIT-)Compiler und Ausführungsumgebungen, die teilweise in der Programmiersprache selbst geschrieben sind. Der C++-Compiler wird zum Beispiel auch in C++ geschrieben, eine neue Compiler-Version wird also von einer älteren Compiler-Version übersetzt.

                      Das war mir schon irgendwie klar. Ich fand nur den Namen so vertraut :-)
                      Gewundert habe ich mich allerdings, dass das Verfahren bei JavaScript auch sinnvoll sein soll. Ich hatte immer angenommen, dass das dann zu langsam ist, genauso, wenn man neue PHP-Funktionen in PHP entwickelt...

                      Oder ist hier gemeint, dass doch Object-Code erzeugt wird, der dann noch bereinigt wird, bevor er durch den Linker (oder was sonst anschließend kommt) geschickt wird?

                      Liebe Grüße
                      Tom S.

                      --
                      Es gibt nichts Gutes, außer man tut es
                      Andersdenkende waren noch nie beliebt, aber meistens diejenigen, die die Freiheit vorangebracht haben.
                      1. Ich hatte immer angenommen, dass das dann zu langsam ist, genauso, wenn man neue PHP-Funktionen in PHP entwickelt...

                        JavaScript ist um Größenordnungen schneller als PHP, das liegt unter anderem am Sprachdesign (zum Beispiel asynchronous IO) und daran, dass Microsoft, Google, Apple und Firefox in einem ständigen Wettbewerb um die schnellste JavaScript-Engine ringen.

                        Das Interesse an PHP unter Wirtschaftsriesen ist mit der Ausnahme von Facebook dagegen verschwindend gering, und selbst Facebook investiert lieber in den hauseigenen Dialekt „Hack“. Wieviel von dem Know-How zurück in PHP fließt, kann ich nicht einschätzen, aber offenbar nicht zu viel.

                        Oder ist hier gemeint, dass doch Object-Code erzeugt wird, der dann noch bereinigt wird, bevor er durch den Linker (oder was sonst anschließend kommt) geschickt wird?

                        Die meisten heutigen JS-Engines sind Just-In-Time-Compiler, also richtig da wird Object-Code erzeugt.

                2. Doch, schließlich gibt die Methode wieder einen Promise zurück.

                  Ok, da lag ich falsch, JSON.parse() wäre synchron. Aber so richtig asynchron wird Response.json() nicht sein.

                  Die Erklärung stimmt auch nur halb. Promises können synchron oder asynchron resolved werden, zum Beispiel kann man auch das synchrone JSON.parse() in eine Promise-API wrappen:

                  function json (jsonString) {
                     return new Promise((resolve, reject) => {
                        try {
                           debugger;
                           const jsonObject = JSON.parse(jsonString);
                           resolve(jsonObject);
                        } catch(e) {
                           reject(e);
                        }
                     })
                  }
                  

                  Ein Aufruf json('42') würde den String '42' synchron/blockierend parsen und sofort ein Promise zurück geben, das den Status resolved hat. An dem Breakpoint liegt der json-Aufruf also auf dem Callstack. Erst wenn man das Ergebnis mit then weiter vearbeitet, wird ein neuer Task in die Event-Queue gepusht.

                  Es gibt also bei Promise-APIs zwei Stellen an denen Asynchronität eine Rolle spielt:

                  1. Der Übergang pending -> resolved (oder pending -> rejected) kann asynchron oder synchron sein.
                  2. Die Ergebnisverwertung mit then ist immer asynchron.

                  Das kann man sich vermutlich am einfachsten vor Augen führen, indem man sich mit den DevTools die Callstacks zu kritischen Zeitpunkten ansieht. Hab' da mal was gebastelt: https://jsfiddle.net/6mxbz9Lh/2/.

            2. @@dedlfix

              Du kannst jedoch die zwei fetch() mit then() in ein requests.map() stecken, dann sparst du dir den doppelten Code und hast den zeitlichen Vorteil.

              Wo finde ich was dazu?

              LLAP 🖖

              --
              “When UX doesn’t consider all users, shouldn’t it be known as ‘Some User Experience’ or... SUX? #a11y” —Billy Gregory
              1. Tach!

                Du kannst jedoch die zwei fetch() mit then() in ein requests.map() stecken, dann sparst du dir den doppelten Code und hast den zeitlichen Vorteil.

                Wo finde ich was dazu?

                Mehrere Vorschläge dazu fand ich bei stackoverflow, gesucht nach javascript fetch promise.all.

                Konkret meinte ich diesen Vorschlag von dort:

                Promise.all(urls.map(url =>
                    fetch(url).then(resp => resp.text())
                )).then(texts => {})
                

                Der macht das mit URLs statt Requests, aber das ist ja das gleiche Prinzip.

                dedlfix.

                1. @@dedlfix

                  Du kannst jedoch die zwei fetch() mit then() in ein requests.map() stecken, dann sparst du dir den doppelten Code und hast den zeitlichen Vorteil.

                  Wo finde ich was dazu?

                  Mehrere Vorschläge dazu fand ich bei stackoverflow, gesucht nach javascript fetch promise.all.

                  Konkret meinte ich diesen Vorschlag von dort:

                  Promise.all(urls.map(url =>
                      fetch(url).then(resp => resp.text())
                  )).then(texts => {})
                  

                  Der macht das mit URLs statt Requests, aber das ist ja das gleiche Prinzip.

                  Meintest du das so?

                  var aRequest = new Request('a.json');
                  var bRequest = new Request('b.json');
                  
                  Promise.all(
                  	[aRequest, bRequest].map(
                  		request => fetch(request).then(response => response.json())
                  	)
                  ).then(values => {
                  	var aData = values[0];
                  	var bData = values[1];
                  
                  	// do something with aData and bData
                  });
                  

                  Ja, das läuft. Irgendwie kann ich mich mit den Pfeilen => noch nicht anfreunden.

                  LLAP 🖖

                  --
                  “When UX doesn’t consider all users, shouldn’t it be known as ‘Some User Experience’ or... SUX? #a11y” —Billy Gregory
                  1. @@Gunnar Bittersmann

                    Der macht das mit URLs statt Requests, aber das ist ja das gleiche Prinzip.

                    Was ist überhaupt der Unterschied zwieschen fetch(url) und fetch(new Request(url))?

                    Mit Array der benötigten Ressourcen sieht’s so aus:

                    var urls = [
                      'a.json',
                      'b.json'
                    ];
                    
                    Promise.all(
                    	urls.map(
                    		url => fetch(url).then(response => response.json())
                    	)
                    ).then(values => {
                    	var aData = values[0];
                    	var bData = values[1];
                    
                    	// do something with aData and bData
                    });
                    

                    Unschön daran ist noch, dass man später beim Auslesen von values sich genau an die Reihenfolge halten muss wie man sie im Array urls festgelegt hatte – ohne dass dies im Code ersichtlich wäre.

                    Wie kommt man aus der Nummer raus? urls als Array von Objekten?

                    var urls = [
                      {
                        name: 'aData',
                        url: 'a.json'
                      },
                      {
                        name: 'bData',
                        url: 'b.json'
                      }
                    ];
                    

                    LLAP 🖖

                    --
                    “When UX doesn’t consider all users, shouldn’t it be known as ‘Some User Experience’ or... SUX? #a11y” —Billy Gregory
                    1. Tach!

                      Was ist überhaupt der Unterschied zwieschen fetch(url) und fetch(new Request(url))?

                      Sieht aus wie: nichts. Vermutlich kann man mit dem Request-Objekt noch eine Menge mehr Dinge anstellen. Andererseits kann man dem fetch() (genau wie dem Request-Konstruktor) auch einen zweiten Parameter mit Optionen übergeben.

                      Unschön daran ist noch, dass man später beim Auslesen von values sich genau an die Reihenfolge halten muss wie man sie im Array urls festgelegt hatte – ohne dass dies im Code ersichtlich wäre.

                      Zumindest kann man mit Array-Dekonstruktion den Zugriff über Nummern einsparen

                      	// var aData = values[0];
                      	// var bData = values[1];
                      
                        var [aData, bData] = values;
                      

                      Wie kommt man aus der Nummer raus?

                      Wohl eher nicht, weil das Promise.all() nur mit Iterables arbeitet und da nur eine reihenfolgenbasierte Zugriffgeschichte geht.

                      dedlfix.

  2. Lieber Gunnar,

    fetch(aRequest)
    	.then(function (response) { return response.json(); })
    	.then(function (aData)
    	{
    		// …
    	}); [...]
    

    Aber was müsste dann anstelle von // … stehen, damit doSomething(aData, bData) aufgerufen wird, sobald beide JSON-Dateien geladen sind?

    so wie ich das Deinem Code entnehme, müsste man jeweils prüfen, ob der andere Code auch schon da ist, um erst dann diese Code-Stelle (// …) auszuführen. Deiner Logik nach werden ja beide JSON-Daten unabhängig voneinander geladen, sodass eine ganz sicher zuletzt geladen wird, welche dann die eigentliche Abarbeitung anstoßen müsste. Und sollte ein Ladevorgang scheitern, dann ist das Ausführen ohnehin hinfällig.

    Vielleicht etwas in dieser Art?

    fetch(aRequest)
    	.then(function (response) { return response.json(); })
    	.then(function (aData)
    	{
    		this.jsonData = aData;
    
    		// check if other request has already loaded
    		if (bRequest.jsonData) {
    				processRequests();
    		}
    	});
    
    fetch(bRequest)
    	.then(function (response) { return response.json(); })
    	.then(function (bData)
    	{
    		this.jsonData = bData;
    
    		// check if other request has already loaded
    		if (aRequest.jsonData) {
    				processRequests();
    		}
    	});
    
    function processData () {
    		// aRequest.jsonData && bRequest.jsonData
    }
    

    Oder meintest Du etwas völlig anderes?

    Liebe Grüße,

    Felix Riesterer.

  3. Das Problem ist bereits hier: function doSomething(aData, bData)

    weil du nicht einfach auf zwei Variablen greifen kannst wo du doch noch gar nicht weißt ob die Response angekommen ist. Ich würde mir beide Dateien in einer Response schicken. MfG

    1. Tach!

      Das Problem ist bereits hier: function doSomething(aData, bData)

      weil du nicht einfach auf zwei Variablen greifen kannst wo du doch noch gar nicht weißt ob die Response angekommen ist.

      Das ist kein Problem, das ist die zu lösende Aufgabe. Der Aufruf soll ja erst erfolgen, nachdem die Daten in den beiden Variablen vorliegen, also wenn die Promises der beiden fetch()-Aufrufe erfüllt sind. Da greift niemand vorzeitig auf Variablen, ohne dass die Response da ist.

      Ich würde mir beide Dateien in einer Response schicken.

      Ohne zu wissen, warum das zwei Aufrufe und nicht bereits von vorn herein nur einer ist, würde ich nicht pauschal zu einem Zusammenlegen raten. Zudem gibt es mit Promise.all() eine einfache Lösung für das Problem, ohne dass serverseitig etwas umgebaut werden muss.

      dedlfix.

      1. @@dedlfix

        Ich würde mir beide Dateien in einer Response schicken.

        Ohne zu wissen, warum das zwei Aufrufe und nicht bereits von vorn herein nur einer ist, würde ich nicht pauschal zu einem Zusammenlegen raten.

        Das eine ist Bibliothek, in dem Fall Sprachennamen:

        {
          "art-x-navi": "Na’vi",
          "qya": "Quenya",
          "tlh": "tlhIngan Hol"
        }
        

        Das andere ist Konfiguration, in dem Fall die bei der Anwendung verfügbaren Sprachen:

        ["art-x-navi", "tlh"]
        

        (Sorry, Mittelerde, nicht deine Zeit.)

        Zudem gibt es mit Promise.all() eine einfache Lösung für das Problem, ohne dass serverseitig etwas umgebaut werden muss.

        Zumal die Ressourcen ja gar nicht vom selben Server kommen müssen. Das können verschiedene eigene Webservices sein; die Bibliothek könnte aber auch von einem Fremdanbieter kommen.


        Q. Und warum überhaupt JavaScript fürs Sprachmenü?
        A. Weil sich’s um eine Demo (kein Prototyp) handelt.

        Q. Und warum nicht die Lösung mit der Verschachtelung? Sollte doch für eine Demo gut genug sein?
        A. Weil ich wissen wollte, wie man’s richtig macht. Für später.

        LLAP 🖖

        --
        “When UX doesn’t consider all users, shouldn’t it be known as ‘Some User Experience’ or... SUX? #a11y” —Billy Gregory
        1. @@Gunnar Bittersmann

          Sieht jetzt so aus:

          lib/language-names.json:

          {
          	"de": "deutsch",
          	"en": "English",
          	"es": "español",
          	"tlh": "tlhIngan Hol"
          }
          

          lib/language-names.de.json:

          {
          	"de": "deutsch",
          	"en": "englisch",
          	"es": "spanisch",
          	"tlh": "klingonisch"
          }
          

          config/available-languages:

          ["de", "es", "en"]
          

          Markup (HTML):

          <ul id="lang-menu">
          	<template id="lang-menu-item">
          		<li><a></a></li>
          	</template>
          	<script src="lang-menu.js" async=""></script>
          </ul>
          

          lang-menu.js:

          var currentLanguage = document.documentElement.lang;
          
          var urls = [
          	'config/available-languages.json',
          	'lib/language-names.json',
          	'lib/language-names.' + currentLanguage + '.json'
          ];
          
          Promise.all(
          	urls.map(
          		url => fetch(url).then(response => response.json())
          	)
          ).then(values => {
          	
          	var [availableLanguages, languageNames, localLanguageNames] = values;
          	var template = document.querySelector('#lang-menu-item');
          
          	availableLanguages.forEach(lang => {
          		
          		if (lang != currentLanguage)
          		{
          			var clone = template.content.cloneNode(true);
          			var liElement = clone.querySelector('li');
          			var aElement = liElement.querySelector('a');
          			
          			aElement.href = '?lang=' + lang;
          			aElement.hreflang = lang;
          			aElement.lang = lang;
          			aElement.textContent = languageNames[lang] || lang;
          			liElement.title = localLanguageNames[lang] || '';
          			
          			template.parentNode.appendChild(clone);
          		}
          	});
          });
          

          Daraus generiertes Markup (DOM), wenn wir uns auf der deutschsprachigen Seite befinden (<html lang="de">:

          <ul id="lang-menu">
          	<template id="lang-menu-item"></template>
          	<script src="lang-menu.js" async=""></script>
          	<li title="englisch"><a href="?lang=en" hreflang="en" lang="en">English</a></li>
          	<li title="spanisch"><a href="?lang=es" hreflang="es" lang="es">español</a></li>
          </ul>
          

          LLAP 🖖

          --
          “When UX doesn’t consider all users, shouldn’t it be known as ‘Some User Experience’ or... SUX? #a11y” —Billy Gregory
          1. @@Gunnar Bittersmann

            Ich mach mal den Columbo: Eine Frage hätte ich dann doch noch.

            Im Script kann man jedes var durch let ersetzen. Aber was wäre wirklich stilvoll? Wo wäre var angebracht, wo let?

            var currentLanguage = document.documentElement.lang;
            
            var urls = [
            	'config/available-languages.json',
            	'lib/language-names.json',
            	'lib/language-names.' + currentLanguage + '.json'
            ];
            
            Promise.all(
            	urls.map(
            		url => fetch(url).then(response => response.json())
            	)
            ).then(values => {
            	
            	var [availableLanguages, languageNames, localLanguageNames] = values;
            	var template = document.querySelector('#lang-menu-item');
            
            	availableLanguages.forEach(lang => {
            		
            		if (lang != currentLanguage)
            		{
            			var clone = template.content.cloneNode(true);
            			var liElement = clone.querySelector('li');
            			var aElement = liElement.querySelector('a');
            			
            			aElement.href = '?lang=' + lang;
            			aElement.hreflang = lang;
            			aElement.lang = lang;
            			aElement.textContent = languageNames[lang] || lang;
            			liElement.title = localLanguageNames[lang] || '';
            			
            			template.parentNode.appendChild(clone);
            		}
            	});
            });
            

            LLAP 🖖

            --
            “When UX doesn’t consider all users, shouldn’t it be known as ‘Some User Experience’ or... SUX? #a11y” —Billy Gregory
            1. Hello,

              Ich mach mal den Columbo: Eine Frage hätte ich dann doch noch.

              Im Script kann man jedes var durch let ersetzen. Aber was wäre wirklich stilvoll? Wo wäre var angebracht, wo let?

              Dumm zurückgefragt:
              Beschreiben die beiden nicht unterschiedliche Scopes?

              Liebe Grüße
              Tom S.

              --
              Es gibt nichts Gutes, außer man tut es
              Andersdenkende waren noch nie beliebt, aber meistens diejenigen, die die Freiheit vorangebracht haben.
              1. Hallo TS,

                Beschreiben die beiden nicht unterschiedliche Scopes?

                Ja.

                Matthias Kleine, MDN

                Bis demnächst
                Matthias

                --
                Rosen sind rot.
            2. Hallo Gunnar Bittersmann,

              Im Script kann man jedes var durch let ersetzen. Aber was wäre wirklich stilvoll? Wo wäre var angebracht, wo let?

              Vorsicht rot. Tendenz steigend?

              Bis demnächst
              Matthias

              --
              Rosen sind rot.
              1. @@Matthias Apsel

                Vorsicht rot.

                ?? Grün, soweit das Auge reicht.

                Und Browser, die let nicht verstehen, sind hier sowieso aus dem Rennen, da sie auch fetch() nicht verstehen.

                LLAP 🖖

                --
                “When UX doesn’t consider all users, shouldn’t it be known as ‘Some User Experience’ or... SUX? #a11y” —Billy Gregory
                1. Hallo Gunnar Bittersmann,

                  Vorsicht rot.

                  ?? Grün, soweit das Auge reicht.

                  Man muss "show all" wählen, dann bekommt man den auch UC-Browser angezeigt, der in Europa zugegebenermaßen keine Rolle spielt.

                  Bis demnächst
                  Matthias

                  --
                  Rosen sind rot.
              2. Vorsicht rot. Tendenz steigend?

                Das sollte niemanden mehr aufhalten, es gibt Pre-Compiler die sorgen schon dafür, dass moderner JavaScript-Code auch in älteren Browsergenerationen funktioniert. Babel und TypeScript, nur um mal die zwei Größten zu nennen.

            3. Hallo Gunnar

              Im Script kann man jedes var durch let ersetzen. Aber was wäre wirklich stilvoll? Wo wäre var angebracht, wo let?

              Vielleicht hilft dir die Lektüre dieses und dieses Beitrags von mir bei der Entscheidungsfindung.

              Edit: Und dieser Beitrag im selben Thread befasst sich ebenfalls mit dem Thema.

              Gruß,

              Orlok

              1. @@Orlok

                Vielleicht hilft dir die Lektüre dieses und dieses Beitrags von mir bei der Entscheidungsfindung.

                Weil „Variablen prinzipiell so lokal wie möglich angelegt werden sollten“ also nirgends var. Und da sämtliche Variablen ihren einmal zugewiesenen Wert behalten, überall const. So also?

                
                const currentLanguage = document.documentElement.lang;
                
                const urls = [
                	'config/available-languages.json',
                	'lib/language-names.json',
                	'lib/language-names.' + currentLanguage + '.json'
                ];
                
                Promise.all(
                	urls.map(
                		url => fetch(url).then(response => response.json())
                	)
                ).then(values => {
                	
                	const [availableLanguages, languageNames, localLanguageNames] = values;
                	const template = document.querySelector('#lang-menu-item');
                
                	availableLanguages.forEach(lang => {
                		
                		if (lang != currentLanguage)
                		{
                			const clone = template.content.cloneNode(true);
                			const liElement = clone.querySelector('li');
                			const aElement = liElement.querySelector('a');
                			
                			aElement.href = '?lang=' + lang;
                			aElement.hreflang = lang;
                			aElement.lang = lang;
                			aElement.textContent = languageNames[lang] || lang;
                			liElement.title = localLanguageNames[lang] || '';
                			
                			template.parentNode.appendChild(clone);
                		}
                	});
                });
                

                LLAP 🖖

                --
                “When UX doesn’t consider all users, shouldn’t it be known as ‘Some User Experience’ or... SUX? #a11y” —Billy Gregory
                1. Hallo Gunnar

                  Vielleicht hilft dir die Lektüre dieses und dieses Beitrags von mir bei der Entscheidungsfindung.

                  Weil „Variablen prinzipiell so lokal wie möglich angelegt werden sollten“ also nirgends var. Und da sämtliche Variablen ihren einmal zugewiesenen Wert behalten, überall const. So also?

                  Im Prinzip ja.

                  .then(values => {
                  	
                  	const [availableLanguages, languageNames, localLanguageNames] = values;
                  

                  Diese Zeile ist aber eigentlich überflüssig.

                  .then(([availableLanguages, languageNames, localLanguageNames]) => {
                  

                  Du kannst das Array mit dem der Parameter values initialisiert wird auch direkt in der Parameterliste destrukturieren. Dabei müssen allerdings wie in dem Beispiel oben Klammern gesetzt werden, sonst gibt das einen Syntaxfehler.

                  			const clone = template.content.cloneNode(true);
                  			const liElement = clone.querySelector('li');
                  			const aElement = liElement.querySelector('a');
                  

                  Hier könnte man darüber nachdenken, ob es wirklich alle drei const braucht.

                  			aElement.href = '?lang=' + lang;
                  			aElement.hreflang = lang;
                  			aElement.lang = lang;
                  			aElement.textContent = languageNames[lang] || lang;
                  

                  Wenn mehreren Eigenschaften eines Objektes Werte zugewiesen werden sollen, dann mache ich das ganz gern mit Object.assign. Da ein Zuweisungsausdruck grundsätzlich zu dem zugewiesenen Wert aufgelöst wird, lässt es sich in solchen Fällen leider nicht vermeiden, das Objekt, welches das Ziel der Zuweisung ist, jedes mal aufs neue zu referenzieren. Das kann man sich mit assign sparen.

                  Object.assign(liElement.querySelector('a'), {
                    href: '?lang=' + lang,
                    hreflang: lang,
                    lang: lang,
                    textContent: languageNames[lang] || lang
                  });
                  

                  Zurückgegeben wird von assign das Zielobjekt, welches als ersts Argument übergeben wird, aber da die Referenz hier nicht benötigt wird, kann man auf die Deklaration einer Konstante für das Element verzichten. Ich finde, dass schaut ein wenig eleganter aus. Aber das ist nur eine Idee. ;-)

                  {
                    lang: lang
                  }
                  

                  Das könnte man dann wenn man wollte auch noch abkürzen und einfach lang, schreiben. Dann wird der Parameter referenziert, sein Bezeichner als Eigenschaftsname verwendet und sein Wert zugewiesen.

                  Edit:

                  Ich habe gerade beim Überfliegen des Threads gesehen, dass dein Markup so aussieht:

                  <ul id="lang-menu">
                  	<template id="lang-menu-item">
                  		<li><a></a></li>
                  	</template>
                  	<script src="lang-menu.js" async=""></script>
                  </ul>
                  

                  Das sieht nicht so aus, als würde sich da regelmäßig was ändern, also würde ich an dieser Stelle vermutlich auf die Methode querySelector() verzichten und die beiden Elemente statt dessen über die Eigenschaft firstElementChild referenzieren.

                  Viele Grüße,

                  Orlok

                  1. @@Orlok

                    Vielen Dank für deine Erläuterungen. Einiges davon würde ich aber wegen „kürzerer Code ist nicht unbedingt besser lesbar“ verwerfen. Aber gut zu wissen, was alles möglich ist.

                    Wenn mehreren Eigenschaften eines Objektes Werte zugewiesen werden sollen, dann mache ich das ganz gern mit Object.assign.

                    Object.assign ist das neue with? 😉

                    	<template id="lang-menu-item">
                    

                    Das sieht nicht so aus, als würde sich da regelmäßig was ändern

                    Da bin ich mir nicht so sicher. Ich bin mir nicht sicher, ob li das geeignete Element für das title-Attribut ist. Vielleicht gehört das ja ans a-Element; dann bräuchte dieses noch ein inneres span:

                    <li>
                    	<a href="?lang=es" hreflang="es" title="spanisch">
                    		<span lang="es">español</span>
                    	</a>
                    </li>
                    

                    also würde ich an dieser Stelle vermutlich auf die Methode querySelector() verzichten und die beiden Elemente statt dessen über die Eigenschaft firstElementChild referenzieren.

                    Ich würde das JavaScript so robust halten, dass es auch bei kleineren Markup-Änderungen noch funktioniert.

                    LLAP 🖖

                    --
                    “When UX doesn’t consider all users, shouldn’t it be known as ‘Some User Experience’ or... SUX? #a11y” —Billy Gregory
            4. Im Script kann man jedes var durch let ersetzen. Aber was wäre wirklich stilvoll? Wo wäre var angebracht, wo let?

              Du kannst sogar alle Vorkommen von var durch const ersetzen und das würde ich auch tun. Es signalisiert den Leser(innen), dass die Variablen während ihrer gesamten Lebensdauer nicht mehr überschrieben werden und erspart ihnen dadurch viel Gehirnschmalz.

  4. Hallo Gunnar,

    kannst Du das bitte genauer spezifizieren?
    `Ist aber nicht richtig asynchron. Die Dateien sollten unabhängig voneinander parallel geladen werden:´

    Sind das zwei eigensständige Threads oder ist die Abarbeitung serialisiert?

    Grüße Tote Locke

  5. Hallo Gunnar,

    Aber was müsste dann anstelle von // … stehen, damit doSomething(aData, bData) aufgerufen wird, sobald beide JSON-Dateien geladen sind?

    in einem ähnlichen Fall mache ich es so, dass jeder Request ein Flag setzt und dann prüft, ob die anderen auch schon fertig sind.

    In einem anderen Fall setzt auch jeder Request ein Flag. Die Auswertefunktion fragt dann über ein sinnvoll getaktetes setInterval die Flags ab.

    Gruß
    Jürgen

    1. Hallo JürgenB,

      da fehlt JS einfach die Möglichkeit, einen Eventlistener für Variablen anzulegen. Dann wäre die Aufgabe elegant lösbar.

      Nur Mut, Ex & wait...

      1. Tach!

        da fehlt JS einfach die Möglichkeit, einen Eventlistener für Variablen anzulegen. Dann wäre die Aufgabe elegant lösbar.

        Ist mittlerweile eingebaut, nennt sich Promise.

        dedlfix.

        1. Hallo,

          Ist mittlerweile eingebaut, nennt sich Promise.

          leider bleibt der IE da draußen vor, bzw. braucht etwas Nachhilfe.

          Gruß
          Jürgen

          1. Hallo JürgenB,

            bei fetch auch. Polyfills to the rescue :)

            Rolf

            --
            Dosen sind silbern