Базовым модулем описания данных является basis.data
, в нем описываются основные базовые классы данных.
Выделяется несколько групп классов:
-
Значение (value) – данными является атомарное (неделимое) значение (даже если само значение имеет сложную структуру, например, объект, изменения внутри значения не отслеживаются). Базовым классом этой группы является
basis.data.Value
. (подробнее) -
Объект (object) – данные хранятся как ключ-значение. Наиболее часто используемый тип данных, является предком для многих других классов, в том числе для элементов интерфейса (
basis.ui.Node
). (подробнее) -
Набор (dataset) – неупорядоченное множество объектов данных (на данный момент экземпляров
basis.data.Object
и его потомков). Данными является информация о вхождении в некоторое множество. (подробнее) -
Карта (map) – ассоциация произвольного значения с другим (на данный момент значение должно быть экземпляром
basis.event.Emitter
) (подробнее)
Класс basis.data.AbstractData
(docs) является основой для классов данных. От него наследуются Value
, Object
, ReadOnlyDataset
и KeyObjectMap
.
Он не реализует механизмы для хранения непосредственно данных (поэтому в его названии есть слово Abstract
), но вводит несколько механизмов, общих для остальных классов данных:
-
состояние;
-
подписка;
-
абстрактная синхронизация.
Все объекты данных имеют состояние. Состояние хранится в свойстве state
. Значение состояния – это одно из константных значений, определенных в basis.data.STATE
:
-
UNDEFINED – говорит о том, что невозможно определить степень достоверности данных: данные еще не были инициализированы или синхронизированы. Это состояние по умолчанию.
-
PROCESSING – определяет, что происходит некоторый процесс, связанный с данным объектом, например, сохранение изменений или синхронизация. Пока объект данных находится в таком состоянии, другие процессы не должны инициироваться (пока он не перейдет в другое состояние).
-
ERROR – показывает, что последний процесс, связанный с этим объектом, закончился неудачей. В интерфейсе при этом может быть отображено сообщение об ошибке и предложены варианты ее исправления.
-
READY – нормальное состояние данных, показывает, что данные инициализированы или синхронизированы, в зависимости от логики.
-
DEPRECATED – данные устарели, возможно, нужна синхронизация.
Состояние может хранить произвольные данные в свойстве data
. Чаще всего это используется для состояния ERROR
, чтобы сохранить детали об ошибке.
Для изменения состояния используется метод setState
, который принимает аргументы:
- state - новое значение состояния;
- data - данные состояния (опционально).
Если значение является неправильным (такого кода нет в basis.data.STATE
), то будет выброшено исключение Wrong state value
.
var STATE = basis.require('basis.data').STATE;
var AbstractData = basis.require('basis.data').AbstractData;
var obj = new AbstractData({
state: STATE.READY // можно задавать состояние при создании
// экземпляра, по умолчанию будет UNDEFINED
});
obj.setState(STATE.ERROR, 'Some error text');
console.log(String(obj.state)); // 'error'
console.log(obj.state.data); // 'Some error text'
Можно заметить, что если явно не преобразовывать значение состояния в строковый тип, то оно выводится в "странном" виде.
Несмотря на то, что код состояния задается строкой, его значение хранится как объект (поэтому obj.state
оборачивается в String
, чтобы вывести значение как строку). Строковое значение оборачивается в Object
для того, чтобы можно было добавить свойство data
(данные состояния). При этом к такому значению хорошо применимы операторы сравнения ==
и !=
в контексте сравнения со строковым значениями (значение state
неявно приводится к строке), однако не работает с операторами эквивалентного сравнения ===
и !==
.
var STATE = basis.require('basis.data').STATE;
obj.state == STATE.ERROR; // true
obj.state != STATE.ERROR; // false
obj.state === STATE.ERROR; // false
obj.state !== STATE.ERROR; // true
String(obj.state) === STATE.ERROR; // true
Когда меняется состояние (его значение и/или данные), выбрасывается событие stateChanged
. Аргументом события является предыдущее значение состояния.
var AbstractData = basis.require('basis.data').AbstractData;
var STATE = basis.require('basis.data').STATE;
var obj = new AbstractData({
handler: {
stateChanged: function(sender, oldState){
console.log('Previous state:', String(oldState), oldState.data);
console.log('Current state:', String(this.state), this.state.data);
}
}
});
obj.setState(STATE.ERROR, 'Error message');
// В консоли будет выведено:
// Previous state: 'undefined' undefined
// Current state: 'error' 'Error message'
Для упрощения выставления состояния в DEPRECATED
используется метод deprecate
, который переводит объект в состояние DEPRECATED
только в том случае, если объект не находится в состоянии PROCESSING
.
Механизм подписки позволяет создавать объекты (источник данных) с определенной логикой синхронизации, но процесс синхронизации иницировать не сразу, а только тогда, когда эти данные станут нужны какому-либо другому объекту (подписчику). Другими словами, механизм подписки обеспечивает возможность одному объекту уведомить другой объект, что он заинтересован в его данных.
Процесс подписки и отписки автоматизирован и управляется свойствами active
и subscribeTo
.
var DataObject = basis.require('basis.data').Object;
var SUBSCRIPTION = basis.require('basis.data').SUBSCRIPTION;
var source = new DataObject({
handler: {
subscribersChanged: function(sender, delta){
if (delta > 0)
console.log('source: I\'m helpful, somebody uses me');
else
console.log('source: I\'m useless, nobody uses me');
}
}
});
var subscriber = new DataObject({
subscribeTo: SUBSCRIPTION.DELEGATE,
delegate: source
});
subscriber.setActive(true);
// console> source: I\'m helpful, somebody uses me
// console> true
subscriber.setActive(false);
// console> source: I\'m useless, nobody uses me
// console> true
Если объект-потребитель данных активен (свойство active
равно true
, в примере это subscriber
), то для объектов, которые хранятся в свойствах, перечисленных в subscribeTo
, счетчик заинтересованных объектов (свойство subscriberCount
) увеличивается на единицу. Когда объект перестает быть активным (active
-> false
), счетчики объектов, на которые он был подписан, уменьшаются на единицу. То же происходит, когда потребитель (subscriber
) перестает ссылаться на источник (в примере это source
).
var DataObject = basis.require('basis.data').Object;
var Data = DataObject.subclass({
emit_subscribersChanged: function(delta){
DataObject.prototype.emit_subscribersChanged.call(this, delta);
console.log(this.name, delta > 0 ? 'began to be used' : 'is no longer used');
}
})
var a = new Data({ name: 'source A' });
var b = new Data({ name: 'source B' });
var subscriber = new DataObject({
// по умочанию subscribeTo: basis.data.SUBSCRIPTION.DELEGATE + basis.data.SUBSCRIPTION.TARGET
active: true,
delegate: a // subscriber ссылается на `a` свойством `delegate`
});
// console> source A began to be used
subscriber.setDelegate(b);
// console> source A is no longer used
// console> source B began to be used
// console> true
subscriber.setDelegate();
// console> source B is no longer used
// console> true
При появлении у объекта данных (в примере это source
) первого подписчика или когда отписывается последний подписчик (свойство subscriberCount
становится равным 0), выбрасывается событие subscribersChanged
. Обработчики этого события получают в качестве единственного аргумента одно из двух значений:
-
1 - появился первый подписчик (
subscriberCount
== 1); -
-1 - отписался последний подписчик (
subscriberCount
== 0).
Обычно объекты синхронизируют свои данные с сервером, только когда имеют подписчиков, то есть активных потребителей данных, которые ссылаются на них одним из своих свойств, определенных в subscribeTo
.
Свойство active
определяет, является ли объект активным подписчиком (включает механизм). Свойство может быть задано при создании объекта или изменено позже методом setActive
. Когда это свойство меняется, выбрасывается событие activeChanged
. По умолчанию все объекты пассивны (active
равно false
).
То, на какие объекты влияет объект (вернее, в каких свойствах они хранятся), определяет свойство subscribeTo
. Свойство может быть задано при создании объекта или изменено позже методом setSubscription
. При изменении значения свойства событий не выбрасывается.
Пока неизвестны ситуации, когда было бы необходимо получать событие на изменение свойства
subscribeTo
. На практике это свойство не изменяется в течение жизненного цикла объекта, поэтому есть вероятность того, что методsetSubscription
будет убран, а задатьsubscribeTo
можно будет только при создании объекта.
Специальный объект basis.data.SUBSCRIPTION
регистрирует свойства объектов (название), которые доступны для подписки, и генерирует для них код. Такие поля должны ссылаться на экземпляры класса basis.data.AbstractData
. Код свойства – это числовое значение, которое определяется единственным битом на определенной позиции. Таким образом, чтобы определить подписку на несколько свойств, необходимо сложить соответствующие свойству коды (или использовать оператор бинарного "или" |
):
var example = basis.data.AbstractData.subclass({
subscribeTo: basis.data.SUBSCRIPTION.FOO + basis.data.SUBSCRIPTION.BAR,
/**
* @type {basis.data.AbstractData}
*/
foo: null,
/**
* @type {basis.data.Object}
*/
bar: null
});
Пример регистрации нового свойства, доступного для подписки:
// простой случай
basis.data.SUBSCRIPTION.addProperty('foo');
// после этого будет доступно константное значение
// для этого поля basis.data.SUBSCRIPTION.FOO
basis.data.SUBSCRIPTION.add(
// название кода
'BAR',
// хендлер, отслеживающий изменения свойства, ссылающегося
// на объект подписки (source)
{
barChanged: function(subscriber, oldBar){
if (oldBar)
// subscriber отписываемся от oldBar
basis.data.SUBSCRIPTION.unlink('bar', subscriber, oldBar);
if (subscriber.bar)
// subscriber подписывается на subscriber.bar
basis.data.SUBSCRIPTION.link('bar', subscriber, subscriber.bar);
}
},
// функция вызывается при активации/деактивации механизма подписки
// action – это либо basis.data.SUBSCRIPTION.link, либо basis.data.SUBSCRIPTION.unlink
function(action, subscriber){
if (subscriber.bar)
action('bar', subscriber, subscriber.bar);
}
);
В basisjs определены следующие свойства (в скобках указано событие, по которому проверяется значение).
-
basis.data:
- DELEGATE - свойство delegate (delegateChanged);
- TARGET - свойство target (targetChanged);
- DATASET - свойство dataset (datasetChanged).
-
basis.data.dataset:
- SOURCE - свойство source (sourceChanged) и элементы свойства sources (sourcesChanged);
- MINUEND - свойство minuend (minuendChanged);
- SUBTRAHEND - свойство subtrahend (subtrahendChanged).
-
basis.dom.wrapper:
- DATASOURCE - свойство dataSource (dataSourceChanged);
- OWNER - свойство owner (ownerChanged).
В basis.data.AbstractData
не описаны конкретные действия по синхронизации (поэтому абстрактная), а лишь реализован механизм для упрощения ее организации.
Сам механизм представлен свойствами:
-
isSyncRequired – метод, возвращающий булево значение, необходима ли синхронизация (по сути, правило). По умолчанию синхронизация требуется, если у объекта более одного подписчика и его состояние
UNDEFINED
илиDEPRECATED
. -
syncEvents – список событий, при наступлении которых делается проверка необходимости синхронизации (вызывается метод
isSyncRequired
). -
syncAction – метод, описывающий действия синхронизации. По умолчанию не задан (свойство равно
null
). Если свойство не задано, механизм синхронизации не будет активирован.
Чтобы активировать механизм синхронизации, необходимо задать свойство syncAction
. Оно может быть задано при создании экземпляра либо после создания, методом setSyncAction
. Метод setSyncAction
принимает единственный параметр – новую функцию синхронизации. Если методу передано значение, которое не является функцией, syncAction
приравнивается null
, а сам механизм синхронизации деактивируется.
var AbstractData = basis.require('basis.data').AbstractData;
var example = new AbstractData({
syncAction: function(){
console.log('sync requested');
}
});
Но определения syncAction
недостаточно, для того чтобы объект начал синхронизацию. Необходимо чтобы выполнялось условие, определенное в методе isSyncRequired
. Для AbstractData
этот метод определен так:
var AbstractData = basis.require('basis.data').AbstractData;
var STATE = basis.require('basis.data').STATE;
AbstractData.prototype.isSyncRequired = function(){
return this.subscriberCount > 0 &&
(this.state == STATE.UNDEFINED || this.state == STATE.DEPRECATED);
};
Для инициации синхронизации необходимо, чтобы объект находился в состоянии UNDEFINED
или DEPRECATED
, и у него был хотя бы один подписчик. Такое условие удовлетворяет большинству ситуаций, возникающих при создании интерфейсов. Для выполнения синхронизации из предыдущего примера не хватает подписчика. Обычно подписчиком является некоторое представление или другой объект, использующий данный объект в качестве источника данных - то есть, в любом случае, это некоторый стороний объект, заинтересованный в том, чтобы данный объект был сихронизирован.
Для того, чтобы инициировать синхронизацию сразу при создании экземпляра, можно переопределить метод isSyncRequired
:
var AbstractData = basis.require('basis.data').AbstractData;
var STATE = basis.require('basis.data').STATE;
// по умолчанию экземпляр создается в состоянии UNDEFINED
var example = new AbstractData({
isSyncRequired: function(){
return this.state == STATE.UNDEFINED;
},
syncAction: function(){
console.log('sync requested');
}
});
// console> sync requested
Обычно в syncAction
выполняется запрос к серверу для получения данных, а синхронизация инициируется при привязке объекта данных к активному представлению:
var net = basis.require('basis.net.ajax');
var Node = basis.require('basis.ui').Node;
var DataObject = basis.require('basis.data').Object;
var STATE = basis.require('basis.data').STATE;
var example = new DataObject({
syncAction: function(){
var self = this;
net.request('/api/whatever',
// success
function(data){
self.update(data);
self.setState(STATE.READY);
},
// failure
function(error){
self.setState(STATE.ERROR, error);
}
);
}
});
var view = new Node({
active: true,
delegate: example
});
Для упрощения работы с запросами и сменой состояний используется модуль basis.net.action
. Предыдущий пример может быть переписан следущим образом:
var action = basis.require('basis.net.action');
var Node = basis.require('basis.ui').Node;
var DataObject = basis.require('basis.data').Object;
var example = new DataObject({
syncAction: action.create({
url: '/api/whatever',
success: function(data){
this.update(data);
}
})
});
var view = new Node({
active: true,
delegate: example
});
Более того, сами представления, точнее экземпляры basis.dom.wrapper.Node
, могут получать данные без использования других объектов. Это подходит для простых случаев, когда данные не переиспользуются (но все же рекомендуется получать данные от сервера в объектах данных, а не в представлении) и предыдущий пример может выглядеть так:
var action = basis.require('basis.net.action');
var Node = basis.require('basis.ui').Node;
var view = new Node({
syncAction: action.create({
url: '/api/whatever',
success: function(data){
this.update(data);
}
})
});
Если syncAction
возвращает промис, то состояние объекта данных будет определяться автоматически, в зависимости от состояния промиса:
var example = new DataObject({
syncAction: function(){
return fetch('./data.json')
.then(function(res) {
return res.json();
})
.then(function(json) {
this.update(json);
}.bind(this));
}
});
Для централизованной работы с сервисом (некоторым серверным API) используются экземпляры `basis.net.service.Service`, которые так же позволяет создавать `action` (используя тот же модуль `basis.net.action`), но с общими настройками по умолчанию, поддержкой сессии и прочим.