- PVSM.RU - https://www.pvsm.ru -
Привет, уважаемые читатели Хабра. Эта статья некое противопоставление недавно прочитанной мной статье «Односторонний binding данных с ECMAScript-2015 Proxy» [1]. Если вам интересно узнать, как же сделать двусторонний асинхронный биндинг без лишних структур в виде Proxy, то прошу под кат.
Тех, кому не интересно читать буквы, приглашаю сразу понажимать на эти самые буквы DEMO binding [2]
Итак, что же меня смутило в той статье и мотивировало на написание своей:
setTimeout(()=> listener(event), 0);
При минимальном таймауте все нормально, функции подписчики вызываются одна за другой через постоянный минимальный интервал (вроде как 4mc). Но что если нам необходимо увеличить его, например, до 500 мс. Тогда просто произойдет задержка на заданный интервал и потом все функции будут вызваны, но также с минимальным интервалом. А хотелось бы указать интервал, именно, между вызовами подписчиков.
Ну что ж, хватит умничать, самое время показать свое «творчество». Начнем с постановки задачи.
Дано:
Задание:
Решение:
Основные идеи реализации:
Реализация:
Static:
Instance:
constructor(obj){
let instance;
/*1*/
if (obj) {
instance = Binder.createProto(obj, this);
} else {
instance = this;
}
/*2*/
Object.defineProperty(instance, '_binder', {
configurable: true,
value: {}
});
/*3*/
Object.defineProperties(instance._binder, {
'properties': {
configurable: true,
value: {}
},
'bindings':{
configurable: true,
value: {}
},
'watchers':{
configurable: true,
value: {}
},
'emitter':{
configurable: true,
writable: true,
value: {}
}
});
return instance;
}
/*1*/ — Проверяем, если конструктор был вызван без аргументов, то создаем новый объект. Если был передан объект то модифицируем его. Статический метод `createProto` см. описание /*8*/
/*2*/ — /*3*/ — Указываем объекту поле-обертку `_bunder`, и записываем в него хранилище привязок, хранилище наблюдателей и значения свойств, которые подверглись трансформации. Поле «emitter» будет указывать на инициатора биндинга, но об этом чуть позже. Все свойства указываются через дескрипторы, таким образом защищаемся от прямой перезаписи (`writable: false`).
/* Очередь и таймауты */
/*4*/
static get timeout(){
return Binder._timeout || 0;
}
/*5*/
static set timeout(ms){
Object.defineProperty(this, '_timeout', {
configurable: true,
value: ms
});
}
/*6*/
static delay(ms = Binder.timeout){
return new Promise((res, rej) => {
if(ms > 0){
setTimeout(res, ms);
} else {
res();
}
});
}
/*7*/
static get queue(){
return Promise.resolve();
}
/*4*/-/*5*/— Геттер и сеттер для статического свойтсва `_timeout` задающего таймуат по умолчанию для асинхронной очереди. В принципе, геттер и сеттер тут ни к чему, но синтаксис ES6 не позволяет в описании класса указать статические свойства-значения.
/*6*/-/*7*/ метод queue задает начала асинхронных очередей, в которые будут добавляться задачи. Метод `delay` возвращает промис, который будет "зарезолвен" по истечении указанного таймаута или таймаута по умолчанию. При этом вся асинхронная очередь будет ждать.
/* Модицикация объектов */
/*8*/
static createProto(obj, instance){
let className = obj.constructor.name;
if(!this.prototypes){
Object.defineProperty(this, 'prototypes', {
configurable: true,
value: new Map()
});
}
if(!this.prototypes.has(className)){
let descriptor = {
'constructor': {
configurable: true,
value: obj.constructor
}
};
Object.getOwnPropertyNames(instance.__proto__).forEach(
( prop ) => {
if(prop !== 'constructor'){
descriptor[prop] = {
configurable: true,
value: instance[prop]
};
}
}
);
this.prototypes.set(
className,
Object.create(obj.__proto__, descriptor)
);
}
obj.__proto__ = this.prototypes.get(className);
return obj;
}
/*8*/— Используется в конструкторе класса. Встраивает в цепочку прототипов объект с необходимыми методами. Если необходимо создает новый объект прототипа, либо берет уже созданный из статического хранилища класса - `Binder.prototypes`
/* Модицикация объектов */
/*9*/
static transform(obj, prop){
let descriptor, nativeSet;
let newGet = function(){ return this._binder.properties[prop];};
let newSet = function(value){
/*10*/
let queues = [Binder.queue, Binder.queue];
/*11*/
if(this._binder.properties[prop] === value){ return; }
Object.defineProperty(this._binder.properties, prop, {
configurable: true,
value: value
});
if(this._binder.bindings[prop]){
this._binder.bindings[prop].forEach(( [prop, ms], boundObj ) => {
/*12*/
if(boundObj === this._binder.emitter) {
this._binder.emitter = null;
return;
}
if(boundObj[prop] === value) return;
/*13*/
queues[0] = queues[0]
.then(() => Binder.delay(ms) )
.then(() => {
boundObj._binder.emitter = obj;
boundObj[prop] = value;
});
});
queues[0] = queues[0].catch(err => console.log(err) );
}
/*14*/
if( this._binder.watchers[prop] ){
this._binder.watchers[prop].forEach( ( [cb, ms] ) => {
queues[1] = queues[1]
.then(() => Binder.delay(ms) )
.then(() => { cb(value); });
});
}
if( this._binder.watchers['*'] ){
this._binder.watchers['*'].forEach( ( [cb, ms] ) => {
queues[1] = queues[1]
.then(() => Binder.delay(ms) )
.then(() => { cb(value); });
});
}
queues[1] = queues[1].catch(err => console.log(err));
};
/*15*/
if(obj.constructor.name.indexOf('HTML') === -1){
descriptor = {
configurable: true,
enumerable: true,
get: newGet,
set: newSet
};
} else {
/*16*/
if('value' in obj) {
descriptor = Object.getOwnPropertyDescriptor(
obj.constructor.prototype,
'value'
);
obj.addEventListener('keydown', function(evob){
if(evob.key.length === 1){
newSet.call(this, this.value + evob.key);
} else {
Binder.queue.then(() => {
newSet.call(this, this.value);
});
}
});
} else {
descriptor = Object.getOwnPropertyDescriptor(
Node.prototype,
'textContent'
);
}
/*17*/
nativeSet = descriptor.set;
descriptor.set = function(value){
nativeSet.call(this, value);
newSet.call(this, value);
};
}
Object.defineProperty(obj._binder.properties, prop, {
configurable: true,
value: obj[prop]
});
Object.defineProperty(obj, prop, descriptor);
return obj;
}
/*9*/ - функция `transform` трансформируется свойства объекта. Если это JS объект, то значение свойтства записывается в `obj._binder.properties`, само свойство преобразуется в геттер/сеттер. Если же это DOM объект, то делает обертки над нативными геттером/сеттером.
/*10*/ - стартуем две асинхронные очереди для привязок и наблюдателей.
/*11*/ - проверяем если значение переданное в сеттер не отличает от текущего значения свойства то ничего не делаем.
/*12*/ - Защита от волны кросс привязок - проверка эмиттера и текущего значения свойства. Объект инициатор обновления привязки прописывает себя в свойство `obj._binder.emitter` привязанного объекта. Привязанный объект таким образом не будет обновлять значение привязки инициатора. Иначе был бы бесконечный цикл взаимных обновлений привязок.
/*13*/ - Добавление исполнения привязки в асинхронную очередь с заданными таймаутом.
/*14*/ - Добавление исполнения функций наблюдателей в асинхронную очередь с заданными таймаутом.
/*15*/ - Проверка на принадлежность объекта к DOM
/*16*/ - Проверка на тип DOM элемента. В данном случае подразумеваются "активные" элементы`input`, `textarea` со свойством `value` и остальные с `textContent`.
У "активных" элементов геттер/сеттер `value` находится в прототипе (см. `рис. 2`). Например, для `input` это будет `HTMLInputElementPrototype`. `textContent` это тоже геттер/сеттер который находится в `Node.prototype`(см. `рис. 3`). Чтобы получить нативные геттер/сеттер используем метод `Object.getOwnPropertyDescriptor`. Ну и в случае "активного" элемента без обработчика события не обойтись.
/*17*/ - Делаем обертку на нативным сеттером, что и позволяет реализовать механизм привязок.
/*Примечание*/ - Объявление `newSet` и `newGet`, конечно, следовало бы вынести во вне.
Рис.2 — наследование свойства `value`
Рис.3 — наследование свойства `textContent`, на примере элемента `div`
Для наглядности приведу еще одно изображение поясняющее трансформацию DOM элемента, на примере элемента «div» (рис. 4)
Рис.4 — схема трансформация DOM элемента.
Теперь про асинхронные очереди. В начале я предполагал сделать одну очередь исполнения для всех привязок конкретного свойства, но тут возник неприятный эффект см. рис. 5. Т.е. первая привязка будет ждать исполнения всей очереди, перед тем как вновь обновить значение. В случае раздельных очередей мы точно знаем, что первая привязка обновиться через заданный интервал, а все последующие через сумму интервалов предыдущих привязок.
Рис.5 сравнение общей очереди исполнения с раздельными.
/*18*/
_bind(ownProp, obj, objProp, ms){
if(!this._binder.bindings[ownProp]) {
this._binder.bindings[ownProp] = new Map();
Binder.transform(this, ownProp);
}
if(this._binder.bindings[ownProp].has(obj)){
return !!console.log('Binding for this object is already set');
}
this._binder.bindings[ownProp].set(obj, [objProp, ms]);
if( !obj._binder.bindings[objProp] ||
!obj._binder.bindings[objProp].has(this)) {
obj._bind(objProp, this, ownProp, ms);
}
return this;
}
/*19*/
_unbind(ownProp, obj, objProp){
try{
this._binder.bindings[ownProp].delete(obj);
obj._binder.bindings[objProp].delete(this);
return this;
} catch(e) {
return !!console.log(e);
}
};
/*20*/
_watch(prop = '*', cb, ms){
var cbHash = Binder.hash(cb.toString().replace(/s/g,''));
if(!this._binder.watchers[prop]) {
this._binder.watchers[prop] = new Map();
if(prop === '*'){
Object.keys(this).forEach( item => {
Binder.transform(this, item);
});
} else {
Binder.transform(this, prop);
}
}
if(this._binder.watchers[prop].has(cbHash)) {
return !!console.log('Watchers is already set');
}
this._binder.watchers[prop].set(cbHash, [cb, ms]);
return cbHash;
};
/*21*/
_unwatch(prop = '*', cbHash = 0){
try{
this._binder.watchers[prop].delete(cbHash);
return this;
} catch(e){
return !!console.log(e);
}
};
/*18*/ - /*19*/ - функции привязки/отвязки. Функции привязки получает в качестве аргументов имя собственного свойства объекта, ссылку на объект, к которому привязываемся, название свойства привязываемого объекта и таймаут привязки. После привязки вызывается аналогичный метод у привязываемого объекта для обратной (двусторонней) привязки. См. `рис. 6`
/*20*/ - /*21*/ - функции подписки/отписки. Функция подписки получает в качестве параметров имя собственного свойства объекта (по умолчанию все - "*"). Функцию наблюдателя и таймаут вызова этой функции при изменении свойства. В качестве возвращаемого значения используется вычисленный хэш функции.
Рис.6 Знакомтесь, кот Биндер
Итоги :
P.S. Это ни в коем случае не готовое для использования решение. Это просто реализация некоторых размышлений.
Спасибо всем за внимание. Комментарии и критика приветствуются.
Всё! Наконец-то конец :)
Автор: IPri
Источник [3]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/210494
Ссылки в тексте:
[1] «Односторонний binding данных с ECMAScript-2015 Proxy»: https://habrahabr.ru/company/rtl-service/blog/309978/
[2] DEMO binding: http://plnkr.co/edit/YXC6J0h7kvyWxSE5xwW8?p=preview
[3] Источник: https://habrahabr.ru/post/315410/?utm_source=habrahabr&utm_medium=rss&utm_campaign=sandbox
Нажмите здесь для печати.