geektimes

Изучаем Javascript перебирая косточки Backbone.js

  • пятница, 24 октября 2014 г. в 03:11:03
http://habrahabr.ru/post/240969/

В этом посте любитель javascript тряхнет костями, доставая что-нибудь полезно-интересное из исходника Backbone.

Тут не будет рассматриваться вопрос применения библиотеки, это на Хабре уже давно сделали, а будет простой конспект-шпаргалка по js с примерами, в роли примеров — сам Backbone.


Лирическое отступление


Идея сделать что-то подобное возникает после того, как прочитаешь кучу литературы по основам, паттернам, алгоритмам, хорошим практикам. Возникает ощущение, что знаний много, но их нельзя толком применить в своих целях, более того, материал из разных книг иногда даже, едва уловимо, противоречит друг другу. Начинают терзать смутные сомненья.
Потом узнал, что хорошие программисты, оказывается, не пользуются всем вышеперечисленным в полном объеме, а используют какое-то подмножество и превосходно себя чувствуют (все остальное можно доставать из справочников или подключать либки по мере надобности). Дальше надо было вычислить это самое «оптимальное подмножество».

Профессионал точно знает ответ на этот вопрос: «Самое лучшее и наиправильнейшее подмножество — это то, которым пользуюсь Я». А вот любителям без наставников придется пораскинуть мозгами.
Хотя, можете не тратить время, а сразу поверить, что в исходниках связки Underscore+Backbone как раз и находится тот необходимый оптимально подогнанный минимум «основ javascript», «алгоритмов», «паттернов» и «лучших практик».

Большинство не согласится, особенно Дуглас Крокфорд, к тому же там забыли добавить алгоритм Дейкстры по обходу графов и, какой ужас, там нету промисов!

Для тех, кто все таки мне поверил, решил создать конспект, который захватывал бы побольше уникальных приемов из Backbone, позволял повторить javascript тем, кто его знает и понять куда копать — тем, кто его не знает. Тем, кто не знает, стоит сразу открыть исходники backbone-underscore и походу чтения выполнять задание типа: «Найдите десять случаев, где встречается данный прием, попытайтесь понять, что конкретно происходит в каждом случае, выполните в Firebug, а потом, напишите свой конспект с моментами, которые пропустил автор поста».

Создание модуля
(function(root, factory) {

  // это для загрузчика модуля АМД
  if (typeof define === 'function' && define.amd) { // то есть, если можем дотянуться до define с amd,
    define(['underscore', 'jquery', 'exports'], function(_, $, exports) { // то им модуль и создадим
      // а вдруг кто-то ждет бэкбон еще и в глобальном объекте
      root.Backbone = factory(root, exports, _, $);
    });

  // это для Node, где jQuery не нужен
  } else if (typeof exports !== 'undefined') {
    var _ = require('underscore');
    factory(root, exports, _);

  // это для браузера
  } else {
    root.Backbone = factory(root, {}, root._, (root.jQuery || root.Zepto || root.ender || root.$));
  }

}(this, function(root, Backbone, _, $) { }));



Структура фабрики
function(root, Backbone, _, $) {

  // сохраняем то, что раньше в вашем скрипте называлось Backbone
  var previousBackbone = root.Backbone;

  // делаем массив и сохраняем с него методы для удобства
  var array = [];
  var push = array.push;
  var slice = array.slice;
  var splice = array.splice;

  // версия 
  Backbone.VERSION = '1.1.2';

  // копируем доллар в другое пространство имен
  Backbone.$ = $;

  // возвращает назад, то, что раньше было бэкбоном
  Backbone.noConflict = function() {
    root.Backbone = previousBackbone;
    return this;
  };

  // настройки для старинных серверов
  Backbone.emulateHTTP = false;
  Backbone.emulateJSON = false;

  // объект событий, чтобы куски нашей программы удобно общались
  // танцует вокруг объекта, где свойства массивы, в которых лежат функции-колбэки
  var Events = Backbone.Events = {

    // подключает функции, которые запустятся, если сделать так obj.trigger('имя события'); 
    on: function(name/*имя события*/, callback/*функция*/, context/* то, что будет this внутри функции*/) { },
    
	// аналогично, только с самоуничтожением
    once: function(name, callback, context) { },

	// удаление событийных функций
    off: function(name, callback, context) { },

	// запуск событийных функций
    trigger: function(name) { },

    // чтобы по каким-то признакам отключить срабатывание колбэка
    stopListening: function(obj, name, callback) {};

  // чтобы слушать один объект, а методы-колбэки и this брать с другого объекта
  var listenMethods = {listenTo: 'on', listenToOnce: 'once'};

  // чтобы методы можно было вызывать, как в старых версиях
  Events.bind = Events.on;
  Events.unbind = Events.off;

  // чтобы Backbone использовать как глобальный передатчик
  _.extend(Backbone, Events);

  
  // Конструктор Модели. 
  //Модели танцуют вокруг своего внутреннего объекта со свойствами-данными
  var Model = Backbone.Model = function(attributes, options) {};

  // добавка в объекты, которые созданы конструктором
  _.extend(Model.prototype, Events/* возможность запускать и ловить события */, {/*кучи модельной функциональности*/});

  // Конструктор Коллекции
  // Танцует вокруг массива с моделями, увеличивая его возможности
  var Collection = Backbone.Collection = function(models, options) {
  };
  
  // собственно, увеличение возможностей коллекции, аналогично модели
  _.extend(Collection.prototype, Events, {});
  
  // Конструктор Вида
  // Вид - это такая конструкция, которая отвечает за рисование и реагирование на действия пользователя с помощью jQuery
  var View = Backbone.View = function(options) {};

  // аналогично
  _.extend(View.prototype, Events, {});

  // Эта штука передает/получает данные с сервера
  // по-умолчанию танцует вокруг jQuery.ajax
  Backbone.sync = function(method, model, options) {};

  // Этот товарищ реагирует на изменения в адресной строке
  // С его помощью удобно запускать подпрограммки, когда пользователь клацает по ссылкам
  // танцует в паре с history
  var Router = Backbone.Router = function(options) {};

  _.extend(Router.prototype, Events, {});

  // История сразу создается и запускается, мониторит историю
  // позволяет работать роутерам
  var History = Backbone.History = function() {};

  _.extend(History.prototype, Events, {});

  // Cоздание истории
  Backbone.history = new History;

  // Дальше вспомогательный набор

  return Backbone;

}



Цепочка прототипов
var extend = function(protoProps/*пойдут в создаваемые объекты*/, staticProps/*навесятся на сам конструктор*/) {
    var parent = this; // предыдущий конструктор, который мы расширяем
    var child; // новый конструктор, который получится

    if (protoProps && _.has(protoProps, 'constructor')) { // если передали свою функцию как конструктор
      child = protoProps.constructor; // то из нее конструктор и сделаем
    } else { // иначе перекинем всю работу на предыдущий конструктор
      child = function(){ return parent.apply(this, arguments); }; // только this будет из прототипа child
    }

    // перевешиваем на функцию child все, что висело на предыдущей функции
    _.extend(child, parent, staticProps); // и то, что мы передали в статике
	// работает так: child.staticProp();

    // делаем child собственный прототип, который знает
    var Surrogate = function(){ this.constructor = child; }; // про свой конструктор
    Surrogate.prototype = parent.prototype; // про свойства из прототипа parent
    child.prototype = new Surrogate; // находится в цепочке прототипов parent

	// крепим свойства на сам прототип, работает так: var x = new child; x.protoProp();
    if (protoProps) _.extend(child.prototype, protoProps);

    // добавляем, на всякий случай, ссылку, чтобы добраться до прототипа предыдущего конструктора
    child.__super__ = parent.prototype;

    return child;
  };

  // навешиваем функцию для расширения на конструкторы для всех типов объектов
  Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend;



Работа с this, arguments, prototype, constructor
  // использование существующего или создание нового массива внутреннего использования для событий
  var events = this._events[name] || (this._events[name] = []); // в роли this - объект Events 
  // или объект созданный конструктором, на котором было сделано так _.extend(Model.prototype, Events);
  
  // функция создается, принимает глобальный объект и только что созданную анонимную функцию
  (function(root, factory) {}(this, function(root, Backbone, _, $) {})) // и сразу выполняется
  
  // функция, которая 
  once: function(name, callback, context) {
    var self = this; // сохраняет объект, на котором вызвана
    var once = _.once(function() {
      self.off(name, once); // чтобы знать, с какого объекта себя автоматически удалить при первом вызове
      callback.apply(this, arguments);
    });
    once._callback = callback; // ссылка на колбэк навешивается еще и на функцию обертку, чтобы можно было найти и выключить событие зная колбэк
    return this.on(name, once, context); // функция-обертка назначается колбэком на событие
  },
  
  
    trigger: function(name) {
      var args = slice.call(arguments, 1); // сохраняем аргументы без имени
      if (events) triggerEvents(events, args); // колбэкам, которые закреплены за своим именем, имя не передается
      if (allEvents) triggerEvents(allEvents, arguments); // колбэкам, которые вызываются при любом имени, передается комплект аргументов вместе с именем
      return this;
    }
	
	// а тут пришлось малость пожертвовать красотой кода ради скорости
	var triggerEvents = function(events, args) {
    var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2];
    switch (args.length) {
      case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return; // внутри колбэка this будет ev.ctx
      case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return;
      case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return;
      case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return;
	   // самый медленный вариант, аргсы высыпятся из массива так: ev.ctx.callback(args[0], args[1], args[2] и т.д.) 
      default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return;
    }
  };
  
  // функция, которая вызовется на новеньком объекте ,созданном конструктором из прототипа, вместе с аргументами конструктора
  this.initialize.apply(this, arguments);
  
  has: function(attr) { // одна функция объекта
    return this.get(attr) != null; // вызывает другую функцию этого же объекта
  }
  
  // проверка, выполнение своей функции объектом, выбор и возврат значения 
  if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
  
 
  fetch: function(options) {
    options = options ? _.clone(options) : {}; // свои новая копия опций в этом пространстве имен
    if (options.parse === void 0) options.parse = true; // если нет парса - добавить и включить
    var model = this; // сохраняем объект в переменную, чтобы ссылка не менялась
    var success = options.success; // сохраняем старую функцию
    options.success = function(resp) { // подменяем новой, которая уедет в далекие дали для асинхронного общения, но будет видеть то, что посохраняли
      if (!model.set(model.parse(resp, options), options)) return false;
      if (success) success(model, resp, options); // вызов старой, если та была
      model.trigger('sync', model, resp, options);
    };
    wrapError(this, options); // error из опций будет знать, с какой моделью работает
    return this.sync('read', this, options); // отправка функции в далекие дали для общения с по аяксу
  },
  
  // клонирование модели
  return new this.constructor(this.attributes);
  
  // массовое добавление методов 
  var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit'];
  _.each(modelMethods, function(method) {
    Model.prototype[method] = function() { // в прототип конструктора модели
      var args = slice.call(arguments); // которые передадут свои аргументы 
      args.unshift(this.attributes); // c внутренним объектом модели воглаве
      return _[method].apply(_, args); // для обработки соответствующим методам Underscore
    };
  });
  
  attrs instanceof Model // сконструирован ли объект данным конструктором
  
  // выбор и сразу вызов необходимой функции
  this[first ? 'find' : 'filter'](function(model) {
        for (var key in attrs) {
          if (attrs[key] !== model.get(key)) return false;
        }
        return true;
      });
	  
  
  this.models.sort(_.bind(this.comparator, this)/*вернет функцию обертку, внутри которой всегда будет делаться типа так comparator.call(this)*/);
 



Примеры с регулярными выражениями
  // создается и присваивается переменной (кэшируется)
  var eventSplitter = /\s+/; // чуть раньше места использования
  // потом разбивает строку с именами событий в массив
  var names = name.split(eventSplitter) // на каждом имени будет вызвано действие, которое изначально вызывалось над строкой
  
  // добавляется черточка в конце, если ее там небыло и id c заменой некоторых символов
  base.replace(/([^\/])$/, '$1/') + encodeURIComponent(this.id)
  
  // заменяет путь на соответствующее регулярное выражение
      _routeToRegExp: function(route) {
      route = route.replace(escapeRegExp, '\\$&')
                   .replace(optionalParam, '(?:$1)?')
                   .replace(namedParam, function(match, optional) {
                     return optional ? match : '([^/?]+)';
                   })
                   .replace(splatParam, '([^?]*?)');
      return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$');
    }
	
  // превращение адресного фрагмента в параметры
  var params = route.exec(fragment).slice(1);
  
  // проверка на соответствие с получением соответствия
  var match = (window || this).location.href.match(/#(.*)$/);



Использование $
  // работает внутри видового элемента
  $: function(selector) {
      return this.$el.find(selector);
    },
	
  this.el = this.$el[0]; // достает элемент из обертки
  
  method = _.bind(method, this); // заменяет метод, так, чтобы он всегда вызывался на данном контексте
  eventName += '.delegateEvents' + this.cid; // добавка для удобного снятия колбэков
    if (selector === '') {
      this.$el.on(eventName, method); // делегирование эвентов
    } else { // т.е. jQuery сделает так, чтобы, когда пользователь что-то клацнет вызвался наш метод
       this.$el.on(eventName, selector, method);
    }
	
  this.$el.off('.delegateEvents' + this.cid); // открепление пачки событий-колбэков
  
  // создание элемента по тагу и атрибутам
  var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs);
  
  // запрос на сервер
  var xhr = options.xhr = Backbone.ajax(_.extend(params, options));
  Backbone.ajax = function() {
    return Backbone.$.ajax.apply(Backbone.$, arguments);
  };



Обработка переходов в браузере
  // делаем ссылки на историю и текущую локацию
    if (typeof window !== 'undefined') {
      this.location = window.location;
      this.history = window.history;
    }
  
  // получение фрагмента
  fragment = decodeURI(this.location.pathname + this.location.search)
  
  Backbone.$(window).on('popstate', this.checkUrl); // событие при изменении истории
  Backbone.$(window).on('hashchange', this.checkUrl);
  
  this.location.replace(this.root + '#' + this.fragment); // смена адреса
  
  // когда надо остановить обработку истории
  Backbone.$(window).off('popstate', this.checkUrl).off('hashchange', this.checkUrl);



Разные мелочи
  // в глобальном объекте делаем пространство имен Backbone и тут же заполняем его значениями
  root.Backbone = factory(root, {}/*вновь созданный объект*/, root._, (root.jQuery || root.Zepto || root.ender || root.$))
  
  // функция, которая вызывает дополнительную, функцию
  on: function(name, callback, context) { // которая при необходимости разобьет входящие значения на более мелкие
    if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this; // и на каждом вызовет изначальную
  }
  
  off: function(name, callback, context) {
    var retain, ev, events, names, i, l, j, k; // куча переменных создается вначале функции, чтобы потом где-то использовать их для кучи циклов
    return this;
  },
  
  // переменной присваивается вновь созданный объект и тут же 
  if (obj) (listeningTo = {})[obj._listenId] = obj; // на него цепляется свойство
  
  delete this._listeningTo[id] // удаление ссылки из внутреннего служебного массива
  
  var remove = !name && !callback; // нет имени и нет колбэка
  
  // так среагирует eventsApi, если, например вызвать x.on({объект с кучей имен и колбэков})
  for (var key in name) {
        obj[action].apply(obj, [key, name[key]].concat(rest)); // разделит имена и для каждого вызовет x.on('имя');
      }

  // генерация похожих методов (сама идея)	  
  var listenMethods = {listenTo: 'on', listenToOnce: 'once'}; 
  _.each(listenMethods, function(implementation, method) {
    Events[method] = function(obj, name, callback) { // полный код искать в исходнике
      obj[implementation](name, callback, this);
      return this;
    };
  });
  
  // создание новых объектов, если нету старых
  var attrs = attributes || {};
    options || (options = {});
	
	
  unset: function(attr, options) {
    return this.set(attr, void 0/*типа undefined*/, _.extend({}, options, {unset: true})/*вновь созданный и сразу расширенный объект*/);
  }
  
  // конструкция для обработки разных способов подачи аргументов функции
  if (key == null || typeof key === 'object') {
    attrs = key;
    options = val;
  } else {
    (attrs = {})[key] = val;
  }
  
  // выбор метода для сохранения данных на сервере.
  method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');
  
  // функция выдающая признак, для других функций, которые по нему будут сортировать, группировать, считать 
  var iterator = _.isFunction(value) ? value : function(model) {
        return model.get(value);
      };
	  
  // штука для получения одного названия зная другое
  var methodMap = {
    'create': 'POST',
    'update': 'PUT',
    'patch':  'PATCH',
    'delete': 'DELETE',
    'read':   'GET'
  };
  
  // вылет с ошибкой
  throw new Error('A "url" property or function must be specified');
  
  // создание нового обработчика ошибки, знающего про конкретную модель
  var wrapError = function(model, options) {
    var error = options.error;
    options.error = function(resp) {
      if (error) error(model, resp, options);
      model.trigger('error', model, resp, options);
    };
  };