WaBis

walter.bislins.ch

JavaScript: Image, onload, complete, Cross-Browser

Donnerstag, 7. Mai 2015 - 18:54 | Autor: wabis | Themen: Wissen, Programmierung, Demo | Kommentare(7)
Bilder auf Webseiten werden erst heruntergeladen, nachdem der HTML-Code heruntergeladen und darin enthaltene JavaScripts ausgeführt worden sind. Dadurch können die JavaScripts nicht bei ihrer ersten Ausführung auf Eigenschaften von Bildern zugreifen. Um dieses Problem zu lösen, gibt es sog. Events. Ein bei einem Bild installierter Event-Handler wird vom System aufgerufen, sobald das Bild geladen ist - manchmal!

Welche Probleme es beim Umgang mit Event-Handlern im Zusammenhang mit Bildern gibt und wie diese Probleme gelöst werden können, darum geht es in diesem Artikel.

Hier geht es direkt zum Code, Kompakt ohne Kommentar.

Asynchrone Programmierung

Mit JavaScript kann man Bilder einer Webseite dynamisch nachladen, ersetzen und animieren. Solche Scripts benötigen meist die Grösse des geladenen Bildes. Diese Grösse kann z.B. über die Properties offsetWidth, clientWidth oder scrollWidth und der entsprechenden Properties für die Höhe des Image-Objektes abgefragt werden. Der Unterschied dieser Properties wird in Understanding offsetWidth, clientWidth, scrollWidth and -Height, respectively auf stackoverflow.dom gut erklärt.

Das Problem dabei ist, dass jenachdem wann die Abfrage der Properties erfolgt, das enstprechende Bild zwar angefordert ist, aber noch nicht im Browser angekommen ist. Dann gibt die Abfrage einen falschen Wert zurück.

Die entsprechenden Aktionen müssen also zeitlich bis zu jenem Moment verschoben werden, wo ein betroffenes Bild oder alle Bilder geladen sind. Komplizierend kommt noch dazu, dass Bilder eventuell nicht geladen werden können oder das Laden durch den Benutzer abgebrochen wird.

Unter JavaScript kann das Problem gelöst werden, indem man beim Bild einen sog. Eventhandler installiert. Ein Eventhandler ist eine selbst zu programmierende Funktion, die beim Eintreten eines bestimmten Ereignisses (z.B. Bild ist geladen) vom System aufgerufen wird. Ein Eventhandler wird auch als Callback-Funktion bezeichnet, weil diese Funktion im System installiert und von diesem zurückgerufen (callback = Rückruf) wird.

Grundsätzlich muss ein JavaScript also so programmiert werden, dass es bei jedem Bild einen Image.onload-Eventhandler installiert. Alternativ kann ein globaler Eventhandler window.onload installiert werden, welcher gerufen wird, nachdem alle Bilder geladen sind. Durch die Eventhandler werden Teile der Programmausführung auf einen spätereren Zeitpunkt verschoben, an dem die benötigten Bilder geladen sind.

Die Reihenfolge des Auftretens der Image.onload-Events ist nicht definiert und kann bei jedem Aufruf der Webseite verschieden sein. Das heisst, man darf sich nicht auf eine bestimmte Reihenfolge verlassen. Ein JavaScript muss entsprechend programmiert sein. Man spricht von Asynchroner Programmierung, weil die Programmteile nicht der Reihe nach ausgeführt werden.

Event-Handler

Es gibt folgende Events, welche im Zusammenhang mit Bildern von Bedeutung sind:

  • Image.onload, Image.onerror, Image.onabort
  • window.onload, document.onDOMContentLoaded

Die einfache Variante, mit Bildern umzugehen, ist das Verwenden des window.onload Events. Es braucht nur eine Funktion, die dann alle Bilder behandelt. Der Nachteil ist jedoch, dass die Webseite so lange nicht auf dem endgültigen Stand ist, bis alle Bilder geladen sind. Das kann bei grösseren Seiten mit vielen Bilden nicht akzeptabel sein. Diese Variante funktioniert auch nicht, wenn Bilder dynamisch z.B. aus einer Datenbank nachgeladen werden sollen. Zudem kann es sein, dass eingebundene JavaScripts andere Programmierer den window.onload Handler bereits belegt haben.

Die elegantere Methode ist daher, für jedes Bild einen Eventhandler zu installieren.

Probleme mit Image.onload Events

Eventhandler für Image.onload-Events müssen bei einigen Browsern installiert werden, bevor das Bild angefordert wird, sonst wird nicht garantiert, dass die Handler in jedem Fall aufgerufen werden.

Wenn ein img-Tag in der Webseite steht, wird ein neues Bild in JavaScript wiefolgt angefordert:

HTML-Code

<body>
:
<img id="myImage" alt="...">
:
</body>

JavaScript

var img = document.getElementById('myImage');
img.src = url;

oder bei einem dynmisch erzeugten Bild:

var img = new Image();
img.src = url;
// img an gewünschter Stelle in das DOM hängen...

Vor der Zuweisung img.src = url müssen die Eventhandler installiert werden, damit sie in jedem Fall aufgerufen werden. Denn wenn ein Bild bereits im Cache des Browsers vorhanden ist, ist nicht mehr garantiert, dass später installierte Eventhandler auch aufgerufen werden.

var img = new Image();  // oder img = document.getElementById('myImage');
// event handler muss an dieser Stelle installiert werden!
img.addEventListener( 'load', onLoadHandler, false );
// erst danach das Bild anfordern
img.src = url;

Moderne Browser sind zwar unterdessen so programmiert, dass die Eventhandler in jedem Fall aufgerufen werden, aber das ist noch keine zuverlässige Methode.

Wie sieht es aus, wenn die Zuweisung img.src = url nicht per JavaScript erfolgt, sondern im HTML img-Tag selbst?

<img id="myImage" src="myImage.jpg" alt="...">

In diesem Fall kann man die Eventhandler ja nicht vor der Anforderung des Bildes installieren.

Für diesen Fall ist das Image.complete Flag vorgesehen:

Das Image.complete Flag

Beim Image gibt es ein Flag complete, welches true ist, wenn das Bild zum Zeitpunkt der Abfrage des Flags bereits geladen ist. Diese Information kann man nutzen, um festzustellen, ob das Bild bereits geladen ist oder ob ein onload-Eventhandler installiert werden muss.

Eine Funktion, welche diese Logik implementiert, sieht zum Beispiel folgendermassen aus:

function SetImageOnLoad( img, onLoadHandler ) {

  // abfragen, ob das Bild eventuell bereits geladen ist (im Cache des Browsers)
  if (img.complete) {

    // Bild ist bereits geladen, handler kann jetzt direkt gerufen werden
    onLoadHandler( img );
    return;
  }

  // Bild noch nicht geladen, event handler installieren
  img.addEventListener( 
    'load', 
    function(){ 
      // onLoadHandler wird vom System gerufen, wenn img geladen ist
      onLoadHandler(img); 
    }, 
    false 
  );
}

Hinweis: Über den "Trick", dass bei addEventListener nicht direkt die Funktion onLoadHandler übergeben wird, sondern die Inline-Funktion function(){ onLoadHandler(img); } kann erreicht werden, dass dem onLoadHandler das entsprechende img Objekt übergeben wird.

Anwendung

Der folgende Code zeigt, wie die Funktion SetImageOnLoad angewandt wird. Es wird angenommen, dass auf der Webseite ein Panoramabild eingebunden wird, welches automatisch hin und her geschwenkt wird, sobald es geladen ist. Das Bild werde mit einem img-Tag in der Webseite eingebunden:

<body>
:
<img id="myPanorama" src="Panorama.jpg" class="Panorama" alt="..">
<script>
PanPanorama( 'myPanorama' );
</script>
:
</body>

In diesem Beispiel wird die Funktion PanPanorama unmittelbar nach dem Erzeugen des HTML-Bildes gerufen. Sie könnte auch an anderer Stelle gerufen werden, das img-Tag muss aber in jedem Fall bereits existieren. Die Funktion PanPanorama kann nicht davon ausgehen, dass das Bild bereits geladen ist und muss daher über unsere Funktion SetImageOnLoad Eventhandler installieren, welche ihrerseits StartPan erst dann aufrufen, wenn das Bild geladen ist. Die Anwendung sieht wiefolgt aus:

function PanPanorama( imgId ) {

  // image objekt der HTML-Seite ermitteln
  var img = document.getElementById( imgId );

  // Das Bild ist hier eventuell noch nicht geladen, ausser es ist bereits im Cache des Browsers.
  // Die Funktion StartPan() kann daher nicht in jedem Fall an dieser Stelle aufgerufen werden.
  // Also wird hier StartPan() als Eventhandler installiert.

  // Wenn das img an dieser Stelle des Scripts bereits im Speicher ist (img.complete==true),
  // wird StartPan() bei dieser Implementierung von SetImageOnLoad() direkt ausgeführt.
  // Wenn das img noch nicht geladen ist, wird StartPan() vom System aufgerufen, 
  // sobald das Bild im Speicher ist.

  SetImageOnLoad( img, StartPan );
}

function StartPan( img ) {

  // Jetzt ist das Bild im Speicher und seine Grösse kann abgefragt werden
  var width = img.offsetWidth;
  var height = img.offsetHeight;
  :
}

Bei dieser Methode ist speziell, dass in den Fällen, wo complete = true ist, der Eventhandler StartPan sofort in der Funktion PanPanorama beim Aufruf von SetImageOnLoad gerufen wird. Im anderen Fall jedoch erst, nachdem diese Funktion (und alle aufrufenden Funktionen) beendet ist. Dies muss bei der Programmierung des Eventhandlers StartPan berücksichtigt werden. Er darf nicht von der Reigenfolge des Aufrufes abhängig sein.

Zudem stellt sich die Frage, ob der Eventhandler nach dem Aufruf deinstalliert werden soll oder nicht.

Mit der folgenden Testseite kannst du in verschiedenen Browsern das unterschiedliche Verhalten im Zusammenspiel mit den Events und dem Flag Image.complete beobachten.

Image on load Testseite

Eine universellere Lösung

Eine Lösung für alle aufgeführten Probleme kann mit einem Helper-Image erreicht werden. Die Idee ist folgende:

Man erzeugt ein temporäres Helper-Image und weist diesem die Eventhandler zu. Danach wird das HelperImage.src Attribut auf das entsprechende Attribut des zu beobachtenden Images gesetzt. Dadurch ist garantiert, dass die Handler installiert sind, bevor das Bild an das HelperImage übergeben wird. Die Handler werden also in jedem Fall gerufen. Das Flag complete braucht nicht beachtet zu werden. Die Funktion reagiert in jedem Fall identisch.

Die revidierte Funktion SetImageOnLoad sieht dann so aus:

function SetImageOnLoad( img, onLoadHandler ) {

  // 1) HelperImage erzeugen und für später dem img zuweisen
  var helperImage = new Image();
  img.helperImage = helperImage;

  // 2) load-Eventhandler onLoadHandler installieren
  helperImage.addEventListener( 
    'load', 
    function(){
      // 4) Diese Inline-Funktion wird vom System aufgerufen, 
      // nachdem SetImageOnLoad beendet ist und nachdem das Bild geladen ist

      // Das HelperImage wird nicht mehr benötigt und kann samt seinen 
      // Eventhandlern gelöscht werden:
      img.helperImg = null;

      // Jetzt erst erfolgt der Aufruf des onLoadHandler's
      onLoadHandler( img );
    }, 
    false 
  );

  // 3) Durch die folgende Zuweisung wird das Bild img an das HelperImage übergeben.
  // Das helperImg kümmert sich danach um den Aufruf der Event-Handler 
  // in der obigen Inline-Funktion.
  helperImg.src = img.src;
}

Diese Funktion hat folgende Eigenschaften:

  • Der Eventhandler onLoadHandler wird in jedem Fall gerufen, nachdem die Funktion SetImageOnLoad beendet ist.
  • Das Flag Image.complete spielt keine Rolle und muss nicht beachtet werden, da im HelperImage das Bild garantiert noch nicht geladen ist, zum Zeitpunkt wo der Eventhandler dort installiert wird.
  • Weil das HelperImage in der Inline-Funktion gelöscht wird, werden auch alle installierten Eventhandler gelöscht. Damit ist garantiert, dass der Event-Handler nur einmal pro Bild gerufen wird und die nicht mehr benötigten Resourcen werden freigegeben.
  • Die Funktion SetImageOnLoad kann sowohl für Bilder verwendet werden, welche im HTML-Code definiert und geladen werden, als auch für dynamisch mit JavaScript geladene Bilder. Ihre Anwendung ist genau gleich wie im Beispiel weiter oben.

Komplette Funktion SetImageOnLoad()

Nachfolgend eine komplettere Version der Funktion SetImageOnLoad, welche auch Fehler behandeln kann:

function SetImageOnLoad( img, loadHandler, errorHandler ) {

  // HelperImage erzeugen und im Original img speichern für später
  var helperImg = new Image();
  img.helperImg = helperImg;

  // loadHandler im helperImg installieren, falls definiert
  if (loadHandler) {
    helperImg.addEventListener( 
      'load', 
      function(){
        // Diese Inline-Funktion wird vom System aufgerufen, 
        // nachdem SetImageOnLoad beendet ist und nachdem das Bild geladen ist

        // Das HelperImage wird nicht mehr benötigt und kann samt seinen 
        // Eventhandlern gelöscht werden:
        img.helperImg = null;

        // Jetzt erst erfolgt der Aufruf des loadHandler's
        loadHandler( img );
      }, 
      false
    );
  }

  // errorHandler im helperImg installieren, falls definiert
  if (errorHandler) {
    helperImg.addEventListener( 
      'error', 
      function(){
        // Diese Inline-Funktion wird vom System aufgerufen, 
        // nachdem SetImageOnLoad beendet ist, 
        // wenn beim Laden ein Fehler aufgetreten ist.

        // Das HelperImage wird nicht mehr benötigt und kann samt seinen 
        // Eventhandlern gelöscht werden:
        img.helperImg = null;

        // Jetzt erst erfolgt der Aufruf des errorHandler's
        errorHandler( img, false );
      }, 
      false
    );
    helperImg.addEventListener( 
      'abort', 
      function(){
        // Diese Inline-Funktion wird vom System aufgerufen, 
        // nachdem SetImageOnLoad beendet ist, 
        // wenn das Laden abgebrochen wurde.

        // Das HelperImage wird nicht mehr benötigt und kann samt seinen 
        // Eventhandlern gelöscht werden:
        img.helperImg = null;

        // Jetzt erst erfolgt der Aufruf des errorHandler's
        errorHandler( img, true );
      }, false
    );
  }

  // Bild vom img übernehmen und dem helperImg übergeben
  // Das helperImg kümmert sich danach um den Aufruf der Event-Handler 
  // in den Inline-Funktionen.
  helperImg.src = img.src;
}

Kompakt ohne Kommentar

function SetImageOnLoad( img, loadHandler, errorHandler ) {
  var helperImg = new Image();
  img.helperImg = helperImg;
  if (loadHandler) {
    helperImg.addEventListener( 'load', 
      function(){ img.helperImg = null; loadHandler( img ); }, false );
  }
  if (errorHandler) {
    helperImg.addEventListener( 'error', 
      function(){ img.helperImg = null; errorHandler( img, false ); }, false );
    helperImg.addEventListener( 'abort', 
      function(){ img.helperImg = null; errorHandler( img, true ); }, false );
  }
  helperImg.src = img.src;
}

Für einfache Anwendungen spielt es keine Rolle, ob die Event-Handler und das Helper-Image freigegeben werden oder nicht. Dadurch kann die Funktion weiter vereinfacht werden:

function SetImageOnLoad( img, loadHandler, errorHandler ) {
  var helperImg = new Image();
  if (loadHandler) {
    helperImg.addEventListener( 'load', function(){ loadHandler(img); }, false );
  }
  if (errorHandler) {
    helperImg.addEventListener( 'error', function(){ errorHandler(img,false); }, false );
    helperImg.addEventListener( 'abort', function(){ errorHandler(img,true); }, false );
  }
  helperImg.src = img.src;
}

Anwendung

function PanPanorama( imgId ) {
  var img = document.getElementById( imgId );
  SetImageOnLoad( img, StartPan, PanError );
}

function StartPan( img ) {
  // Jetzt ist das Bild im Speicher und seine Grösse kann abgefragt werden
  var width = img.offsetWidth;
  var height = img.offsetHeight;
  :
}

function PanError( img, abort ) {
  if (abort) {
    // das Laden des Panoramas wurde abgebrochen
  } else {
    // Fehler beim Laden des Panoramas (z.B. Bild nicht gefunden)
  }
}

Beachte, dass für das Behandeln der Events error und abort nur ein Eventhandler übergeben werden muss. Wenn dieser gerufen wird, wird ihm als zweites Argument ein Flag übergeben, welches angibt, ob ein Fehler beim Laden auftrat oder ob das Laden vom Benutzer abgebrochen worden ist.

Kommentare

1Christian 04.06.2015 | 23:06

hey, du hast da einen kleinen fehler in zeile 5 gemacht.

du wolltest bestimmt
helperImg.addEventListener

anstatt von
helperImg = addEventListener

schreiben oder?

LG,
Christian

2wabiswalter@bislins.ch (Walter Bislin, Autor dieser Seite) 05.06.2015 | 03:11

Yep! Danke für Deine Aufmerksamkeit und Meldung. Walter

3Thomas 08.06.2015 | 10:01

Ich habe das mal vereinfacht und einen String anstatt eines Bildobjektes übergeben und außerdem das Helperimage zurückgegeben, da sind ja alle wichtigen Parameter drin.

function loadImage( url, loadHandler)
{	loadImageFrom( url, loadHandler, null);
}

function loadImageFrom( url, loadHandler, errorHandler )
{	var img = document.createElement("img");

	img.src = url;

	var helperImg = new Image();
	
	if (loadHandler)
	{	helperImg.addEventListener( 'load', function()
		{	img._helperImg = null;
			loadHandler( helperImg );
		}, false);
	}
  
	if (errorHandler)
	{
		helperImg.addEventListener( 'error', function()
		{	img._helperImg = null;
			errorHandler( helperImg, false );
		}, false);
		
		helperImg.addEventListener( 'abort', function()
		{	img._helperImg = null;
			errorHandler( helperImg, true );
		}, false);
	}
  
	helperImg.src = img.src;
}

4wabiswalter@bislins.ch (Walter Bislin, Autor dieser Seite) 08.06.2015 | 22:56

Wozu brauchst Du das Objekt img in deiner Funktion? Du kannst ja die url direkt dem HelperImage zuweisen: statt helperImg.src = img.src kannst du schreiben: helperImg.src = url. Dann braucht es img nicht. Es wird auch nicht benötigt, um das HelperImage darin zu speichern. Die Eventhandler haben auch so Zugriff auf das HelperImage, ohne dass img benötigt wird. Auf img hat man ausserhalb der Funktion keinerlei Zugriff, so wie das jetzt programmiert ist! Ich hoffe, dass meine überarbeitete Beschreibung mit Anwendungsbeispiel klarer macht, wie das Konzept funktioniert.

Vorsicht: Obwohl document.createElement('img') suggeriert, dass das HTML-Objekt im Dokument eingefügt wird, ist dies nicht der Fall. Das img müsste explizit noch in das DOM eingefügt werden, falls dies Deine Absicht mit img gewesen wäre. Ansonsten braucht es wie gesagt dieses img Objekt nicht.

Ich habe diesen Blog überarbeitet, ein paar Schnitzer korrigiert, ein Anwendungsbeispiel integriert und den Code ausführlich kommentiert. Wer weitere Fehler findet oder Anregungen hat, bitte melden.

Noch ein Tipp: Um Code in die Kommentare einzufügen, setze man diesen in die Tags:

<code>
Dein Code hier...
</code>

5Thomas 09.06.2015 | 09:26

Vielleicht war Dein Quelltext mit all den Artikelinformation zu verwirrend für mich und ich habe nicht alles gleich verstanden - ich hasse JavaScript und programmiere damit nur äußerst selten und ungern.

Meine Idee ist die folgende: ich habe eine allgemeine Funktion, die ein Bild laden soll und die aufrufende Funktion informiert, wenn es fertig ist. Was die aufrufende Funktion dann macht, ist ja der allgemeinen Funktion egal, daher wird nur eine URL bzw. ein relativer Pfad übergeben und ein neu erstelltes Imageobjekt zurückgegeben (und damit ist ein Zugriff auf Bilddaten wie Breite/Höhe möglich).

Unabhängig davon, daß es bei mir noch einige Ungereimtheiten gibt: DANKE FÜR DEN ARTIKEL, das macht einen unabhängig von all den JavaScript-Bibliotheken mit ihren vielen Dateien und Parametern. Das Layout kann mit solch einer einfachen Ladefunktion bei jeder Anwendung anders aussehen..

6wabiswalter@bislins.ch (Walter Bislin, Autor dieser Seite) 12.11.2015 | 23:20

Ergänzung: Ich habe unter Kompakt ohne Kommentar die Funktion in zwei Varianten aufgelistet.

7Sascha M. 17.08.2016 | 12:11

Hallo Walter,

besten Dank für den informativen Artikel, das hat mir noch einmal die Augen für die Spezialitäten von Javascript vor Augen geführt. Die Lösung mit "img.complete" hat perfekt bei meinem Problem geholfen, Danke dafür!

Besten Gruß, Sascha

Dein Kommentar zu diesem Artikel
Name
Email optional; wird nicht angezeigt
Kommentar
  • Name wird bei deinem Kommentar angezeigt.
  • Email ist nur für den Administrator, sie wird nicht angezeigt.
  • Du kannst deine Kommentare eine Zeit lang editieren oder löschen.
  • Du kannst Formatierungen im Kommentar verwenden, z.B: Code, Formeln, usw.
  • Externen Links und Bilder werden nicht angezeigt, bis sie der Admin freischaltet.
Weitere Infos zur Seite
Erzeugt Donnerstag, 7. Mai 2015
von wabis
Zum Seitenanfang
Geändert Mittwoch, 17. August 2016
von wabis