Статьи

Отложенный объект jQuery, часть 2: как создать свой собственный


В моем последнем посте
jQuery Deferred Object — ваш новый лучший друг
Я дал краткий обзор того, что такое отложенный объект и как его использовать. На этот раз давайте немного углубимся в изучение создания собственного отложенного объекта для гораздо более практического использования — асинхронной загрузки и обработки аудиофайлов.

Один из моих любимых веб-сайтов — HTML5Rocks. У них есть вся последняя информация о разработке современных веб-приложений HTML5. Продолжая создавать свою HTML5-игру, я обратился к ним за дополнительной информацией по нескольким темам, включая API Web Audio. В одной из своих статей, «
 Начало работы с Web Audio API» , они создают класс BufferLoader.

function BufferLoader(context, urlList, callback) {
  this.context = context;
  this.urlList = urlList;
  this.onload = callback;
  this.bufferList = new Array();
  this.loadCount = 0;
}

BufferLoader.prototype.loadBuffer = function(url, index) {
  // Load buffer asynchronously
  var request = new XMLHttpRequest();
  request.open("GET", url, true);
  request.responseType = "arraybuffer";

  var loader = this;

  request.onload = function() {
    // Asynchronously decode the audio file data in request.response
    loader.context.decodeAudioData(
      request.response,
      function(buffer) {
        if (!buffer) {
          alert('error decoding file data: ' + url);
          return;
        }
        loader.bufferList[index] = buffer;
        if (++loader.loadCount == loader.urlList.length)
          loader.onload(loader.bufferList);
      },
      function(error) {
        console.error('decodeAudioData error', error);
      }
    );
  }

  request.onerror = function() {
    alert('BufferLoader: XHR error');
  }

  request.send();
}

BufferLoader.prototype.load = function() {
  for (var i = 0; i < this.urlList.length; ++i)
    this.loadBuffer(this.urlList[i], i);
}

Я подумал, что было бы интересно преобразовать этот класс в использование отложенного объекта и еще более упростить его, избавившись от конструктора объекта, и вместо этого использовал замыкание. Итак, начнем.

Избавление от конструктора

BufferLoader использует функцию в качестве создателя объекта. В этом нет ничего плохого, это просто шаблон, который я редко использую в наши дни. Вместо этого я обычно предпочитаю использовать закрытие. Подумайте, почему они создают объект. Поскольку загрузка и преобразование файлов являются асинхронными операциями, они хотят поддерживать несколько вызовов в одном и том же коде. Использование конструктора гарантирует, что каждый экземпляр имеет доступ к своему набору переменных и не будет мешать любому другому вызову. Закрытие делает то же самое, но более кратко. Поэтому мы удалим функцию конструктора. Мы также обернем весь наш код в функцию, чтобы изолировать его от любого другого кода в браузере. Мы используем один глобальный объект, RocknCoder, для хранения всех наших глобальных вещей.Мы делаем метод loadBuffer () внутренним методом нового метода RocknCoder.loadAudioFiles (). Это также делает его доступным во всем мире.

Создание Закрытия

Большинство переменных экземпляра перемещаются или передаются в метод loadAudioFiles (). Переменная this или ее альтернативное имя, loader, больше не нужны, поэтому мы удаляем ее в. Все его применения становятся именами без «loader». Например, «loader.context» становится просто «context». Я не знаю, как вы, но слишком частое использование «this» в JavaScript может сбить вас с толку. Чтобы создать замыкание, мы вызываем метод loadBuffer. Когда этот метод вызывается, он по сути создает снимок среды, все переменные и их значения замораживаются, поэтому при выполнении асинхронного обратного вызова состояние переменных восстанавливается. Обратите внимание на тонкое изменение параметров, переданных в loadBuffer (). Ранее было передано один URL и индекс, который был в массиве bufferList [].Теперь передаются все URL-адреса в виде массива и значение индекса, указывающее, какой URL-адрес загружается в данный момент. Как только URL загружен и обработан, индекс увеличивается, и если мы еще не загрузили все URL, loadBuffer () вызывается рекурсивно. Это также избавляет от необходимости использовать функцию load (), поэтому мы тоже ее удаляем.

Использование отложенного объекта

С самого начала в методе loadAudioFile () мы создаем наш отложенный объект, вызывая метод jQuery $ .Deferred (). Последняя строка метода возвращает отложенный объект myDeferred вызывающей стороне. Имейте в виду, что этот метод будет продолжать выполняться даже после того, как он вернется к вызывающей стороне, так как здесь происходит несколько асинхронных обратных вызовов. В этом методе мы с любопытством используем объект XMLHttpRequest вместо того, чтобы использовать его версию в jQuery. Это происходит главным образом потому, что объект XMLHttpRequest поддерживает тип «arraybuffer», который позволяет нам загружать двоичные данные, что очень важно для аудио.

Если по пути мы сталкиваемся с ошибкой, мы можем вызвать метод reject () и передать ему всю информацию об ошибке.
Это приведет к сбою нашего отложенного объекта, и информация, переданная ему, будет доступна для метода fail ().

Как только все наши аудиофайлы загружены и обработаны, мы просто разрешаем () наш отложенный объект. В этом случае мы передаем наш буферный список, который содержит все обработанные нами аудиофайлы, нашему отложенному объекту. Это сделает их доступными для нашего метода done (). Также обратите внимание, как мы устранили необходимость отслеживать количество индексов в bufferList, используя метод push () вместо индекса.

Мы могли бы также использовать метод progress () отложенного объекта каждый раз, когда мы успешно заканчивали загрузку или обработку файла. Это позволило бы вызывающей стороне обновить элемент пользовательского интерфейса, чтобы держать пользователя в курсе нашего прогресса. Ну ладно, может в следующий раз.

/**
 * User: Troy
 * Date: 11/4/13
 * Time: 4:24 AM
 */


var RocknCoder = RocknCoder || {};

(function () {
  "use strict";

  /*
   The loadAudioFiles method uses jQuery deferred object,
   but not its ajax loader since it doesn't support binary data (arraybuffer)
   once it has loaded all of the audio files, it will resolve() itself and
   pass the audio data to the done method
   */
  RocknCoder.loadAudioFiles = function(context, urlList) {
    var myDeferred = $.Deferred(),
      len = urlList.length,
      bufferList = [],
      loadBuffer = function (urls, index) {
        // Load buffer asynchronously
        var request = new XMLHttpRequest();

        request.open("GET", urls[index], true);
        request.responseType = "arraybuffer";
        request.onload = function () {
          // Asynchronously decode the audio file data in request.response
          context.decodeAudioData(
            request.response,
            function (buffer) {
              if (!buffer) {
                myDeferred.reject('error decoding file data: ' + urls[index]);
              } else {
                bufferList.push(buffer);
                if (++index === len) {
                  myDeferred.resolve(bufferList);
                } else {
                  loadBuffer(urls, index);
                }
              }
            },
            function (error) {
              myDeferred.reject('decodeAudioData error', error);
            }
          );
        }
        // if there is some kind of loading error come here
        request.onerror = function () {
          myDeferred.reject('unknown error occurred');
        }

        // begin the download
        request.send();
      };

    loadBuffer(urlList, 0);
    return myDeferred;
  };
}());

Резюме

Мы начали с примерно 46 строк кода в трех методах и преобразовали примерно в 42 строки одним методом. Не кажется слишком большой победой, но код читается чище, и большая часть выигрыша предназначена для звонящего. Вместо того, чтобы иметь дело с единственным обратным вызовом, где они должны отсортировать результаты. Теперь они получают отдельные методы done () и fail (). Кроме того, отложенный объект, переданный здесь, может быть объединен с другими в операторе when (). Примером этого является игра, которую я строю. Мы ждем все аудиофайлы, карту спрайтов и минимум три секунды, прежде чем покинуть заставку. Пример чего ниже.

Если вы хотите увидеть больше игры, обязательно посмотрите мой репозиторий на GitHub по адресу: 
https://github.com/Rockncoder/planegame, Обязательно ознакомьтесь с первым постом этой серии:
jQuery Deferred Object — ваш новый лучший друг
,

  RocknCoder.Pages.splash = (function () {
    return {
      pageshow: function () {
        RocknCoder.Game.dims = RocknCoder.Dimensions.get();

        var context, loaderReady,
          timerReady = $.Deferred(),
          imageReady = $.Deferred(),
          sounds = [
            "sounds/83560__nbs-dark__ship-fire.wav",
            "sounds/95078__sandyrb__the-crash.wav",
            "sounds/143611__d-w__weapons-synth-blast-01.wav",
            "sounds/DST-Afternoon.mp3"
          ];

        try {
          // Fix up for prefixing
          window.AudioContext = window.AudioContext || window.webkitAudioContext;
          context = new AudioContext();
          console.log('Web Audio API is supported in this browser');
          loaderReady = RocknCoder.loadAudioFiles(context, sounds);
        }
        catch (e) {
          console.log('Web Audio API is NOT supported in this browser');
        }

        RocknCoder.Game.spriteSheet = new Image();
        RocknCoder.Game.spriteSheet.src = "images/1945.png";
        RocknCoder.Game.spriteSheet.onload = function () {
          imageReady.resolve();
        };

        // our timer simply waits until it times out, then sets timerReady to resolve
        setTimeout(function () {
          timerReady.resolve();
        }, 3000);

        // put our load screen up
        $.mobile.loading( 'show', {
          text: "Loading resources...",
          textVisible: true,
          theme: "a"
        });

        $.when(loaderReady, timerReady, imageReady)
          .done(function (loaderResponse) {
            // let's put the data in our global
            RocknCoder.Resources = RocknCoder.Resources || {};
            RocknCoder.Resources.audios = loaderResponse;
          })
          // here you would check to find out what failed
          .fail(function () {
            console.log("An ERROR Occurred")
          })
          // the always method runs whether or not there were errors
          .always(function () {
            $.mobile.loading("hide");
            $.mobile.changePage("#attract");
          });
      },
      pagehide: function () {
      }
    };
  }());