Всім привіт.
Іноді на клієнтській стороні необхідно виконувати фонові завдання. Головна вимога щоб вони не переривали роботу всього веб-додатку а, спокійно у фоновому режимі спілкувалися між собою, завершувалися і додавалися. Мета запропонованого планувальника зняти з розробника головний біль про такі завдання і звести до загального інтерфейсу, за допомогою якого можна поступово розширювати спектр вирішуваних завдань.
Можна запитати, а що за завдання і нафіга це потрібно? Завдань насправді на клієнті багато: це і періодична зміна ідентифікатора сесії (наприклад поштовий клієнт оновлює у фоновому режимі ідентифікатор), і автоматична прокрутка (наприклад, скролінг карти), переміщення об'єкта в графічному інтерфейсі, це і обробка безлічі запитів з клієнта на сервер (Наприклад, запитується інформація з сервера через XHR, багато таких запитів, отже багато об'єктів XHR, а це вже сильне навантаження на браузер), це і вибірка даних з iframe у разі реалізації JSRS на його базі. Основа запропонованого планувальника це функції setInterval і clearInterval. Саме навколо них все і буде крутитися. Але просто самих функцій недостатньо, необхідно абстрагувати саме поняття завдання і запропонувати інтерфейс. І зробити планувальник, який буде керувати завданнями.
Для реалізації класу завдань нам необхідно ввести поняття прапорів, в них ми будемо зберігати стану завдання.
HClass.Define(«HFlag», { extend: HCore.Object, static: {
MAX_FLAG_VALUE: 65535
}, props: {
dwFlag: null,
construct: function(dwFlag) { this. Add(dwFlag); return this; },
Set: function(dwFlag) { this.dwFlag = dwFlag; },
Get: function() { return this.dwFlag; },
Add: function(dwFlag) { this.dwFlag |= dwFlag; },
Zero: function(dwFlag) { this.dwFlag &= (HFlag.MAX_FLAG_VALUE — dwFlag); },
Check: function(dwFlag) { return !!(this.dwFlag & dwFlag); },
Swap: function(dwNew, dwOld) { this. Zero(dwOld); this. Add(dwNew); },
Clear: function() { this. Set(0); }
}});* This source code was highlighted with Source Code Highlighter.
У цьому класі стандартний набір методів для роботи з прапорами.
Далі підуть витяги з класу HSheduler, а саме методи додавання завдання і видалення RunTask - запуск завдання через setInterval, RemoveTask - видалення через clearInterval:
RunTask: function(pTask) {
pTask.nTaskId = this.nTotalTasks;
this.aTaskHeap[this.nTotalTasks++] = pTask;
pTask.nTimerId = setInterval(«HSheduler.aTaskHeap[» + pTask.nTaskId + ""].Cycle()", pTask.nCycleTimeout);
},
RemoveTask: function(pTask) {
clearInterval(pTask.nTimerId);
delete this.aTaskHeap[pTask.nTaskId];
}* This source code was highlighted with Source Code Highlighter.
Основне тут саме робота з функціями setInterval і clearInterval.
У функції RunTask є рядок setInterval («HSheduler.aTaskHeap [» + pTask.nTaskId + «»] .Cycle () «», pTask.nCycleTimeout); де ми встановлюємо що буде викликатися завдання (точніше метод Cycle, поточного завдання) зі своїм ідентифікатором з купи завдань, з інтервалом вказаним у завданні. Це базові методи планувальника. Перейдемо до завдання і подивимося що має робити завдання взагалі:
1. Вона повинна мати якісь вхідні дані.
2. Завдання мають алгоритми вирішення.
3. Завдання видають результат (також можна реалізувати можливість отримання проміжних результатів).
Крім того, необхідно додати стан завдання. Завдання можна:
1. Ініціалізувати (попередній розрахунок похідних параметрів, створення об'єктів якщо потрібно) - стан SF_Ready.
2. Виконувати (розрахунок результату за заданими вхідними та похідними параметрами) - стан SF_Process.
3. Завершити (за умовою отримання або неможливості отримання результату) - стан SF_Remove.
4. Призупинити - SF_Wait (можна, якщо не потрібні проміжні результати і є подія з поза, яка скине цей стан, наприклад, очікування відповіді від сервера).
5. Пропустити один цикл - SF_SkipCycle (аналог SF_Wait, але стан скидається автоматично).
Використовуючи цей набір станів можна реалізувати безліч завдань, а які не можна, то ніхто не заважає додати додатково нові стани. Нижче підуть витяги з класу HBaseTask. Головний метод з цього класу Cycle, він і викликається через setInterval:
Cycle: function() {
if(!this.pStaticAddress) return;
if(this.oStateFlags.Check(HBaseTask.SF_Remove)) {
this. Remove();
return;
}
if(!this.oStateFlags.Check(HBaseTask.SF_Wait)) {
if(!this.oStateFlags.Check(HBaseTask.SF_SkipCycle)) {
this.pStaticAddress.apply(this, this.oTaskParams);
this.nCyclesCount++;
} else this.oStateFlags.Zero(HBaseTask.SF_SkipCycle);
}
this.oStateFlags.Zero(HBaseTask.SF_SetParams);
}* This source code was highlighted with Source Code Highlighter.
Змінна this.pStaticAddress містить алгоритм завдання, банально функція, вона і викликається кожен новий цикл. Найважливіше тут це те що алгоритм потрібно виконувати в контексті об'єкта завдання щоб з нього був доступ до методів класу HBaseTask:
this.pStaticAddress.apply(this, this.oTaskParams);
Тепер з алгоритму завдання можна отримати доступ до станів завдання (запис, читання та перевірка станів). Крім того через apply в алгоритм передаються параметри завдання. Параметри встановлюються через метод Params. Нижче повна реалізація планувальника HSheduler і базового класу завдання HBaseTask:
//
// Sheduler.
//
HClass.Define(«HSheduler», { static: {
nTotalTasks: 0,
aTaskHeap: [],
RunTask: function(pTask) {
pTask.nTaskId = this.nTotalTasks;
this.aTaskHeap[this.nTotalTasks++] = pTask;
pTask.nTimerId = setInterval(«HSheduler.aTaskHeap[» + pTask.nTaskId + ""].Cycle()", pTask.nCycleTimeout);
},
RemoveTask: function(pTask) {
clearInterval(pTask.nTimerId);
delete this.aTaskHeap[pTask.nTaskId];
},
//
// Base Task.
//
HBaseTask: HClass.Define(«HBaseTask», { extend: HProcess, static: {
TF_Nothing : 0,
TF_FireRun : 1,
SF_Null : 0,
SF_Ready : 1,
SF_Process : 2,
SF_SetParams : 4,
SF_SkipCycle : 8,
SF_Remove : 16,
SF_Wait : 32
}, props: {
oTaskFlags: null,
oStateFlags: null,
oTaskParams: null,
nTaskId: null,
nTimerId: null,
nCycleTimeout: 1000,
nCyclesCount: 0,
nCyclesLimit: 0,
construct: function(fCode) {
this.oTaskFlags = new HFlag(HBaseTask.TF_Nothing);
this.oStateFlags = new HFlag(HBaseTask.SF_Ready);
this.nCycleTimeout = 1000; // ms.
this.nCyclesCount = 0;
this.nCyclesLimit = 0;
if(fCode) this. Create(fCode);
return this;
},
Run: function() { HSheduler.RunTask(this); },
Remove: function() { HSheduler.RemoveTask(this); },
Params: function() {
this.oTaskParams = arguments;
this.oStateFlags.Add(HBaseTask.SF_SetParams);
},
CycleTimeout: function(nCycleTimeout) { this.nCycleTimeout = nCycleTimeout; },
Cycle: function() {
if(!this.pStaticAddress) return;
if(this.oStateFlags.Check(HBaseTask.SF_Remove)) {
this. Remove();
return;
}
if(!this.oStateFlags.Check(HBaseTask.SF_Wait)) {
if(!this.oStateFlags.Check(HBaseTask.SF_SkipCycle)) {
this.pStaticAddress.apply(this, this.oTaskParams);
this.nCyclesCount++;
} else this.oStateFlags.Zero(HBaseTask.SF_SkipCycle);
}
this.oStateFlags.Zero(HBaseTask.SF_SetParams);
},
AddState: function(dwFlag) { this.oStateFlags.Add(dwFlag); },
SwapState: function(dwNew, dwOld) { this.oStateFlags.Swap(dwNew, dwOld); },
GetState: function() { return this.oStateFlags; }
}})
}});* This source code was highlighted with Source Code Highlighter.
За допомогою цього класу можна дуже просто створювати завдання працюючі у фоновому режимі і щось роблять. Ось кілька простих прикладів:
//Приклад простого завдання буде працювати в циклі.
var pTask = HBaseTask(function() { alert(«Hello world!»); });
pTask.Run();
//Приклад завдання отримує параметри.
var pTask = HBaseTask(function(a, b) { alert(a + "" " + b); });
pTask.Params(32, 128);
pTask.Run();
//Приклад завдання керуючої станами.
var pTask = HBaseTask(function() {
var oState = this. GetState () ;//Беремо поточний стан.
if(oState.Check(HBaseTask.SF_Ready)) {
//Тут готуємо похідні змінні або створюємо потрібні об'єкти, наприклад XHR.
this. SwapState(HBaseTask.SF_Process, HBaseTask.SF_Ready);
} else if(oState.Check(HBaseTask.SF_Process)) {
//Тут щось рахуємо і якщо отримуємо результат переходимо зі стан завершення
this. SwapState(HBaseTask.SF_Remove, HBaseTask.SF_Process);
}
});
pTask.Run();* This source code was highlighted with Source Code Highlighter.
Але найголовніше в тому що з'явилося абстрактне поняття завдання, і можна робити похідні, вузькоспеціалізовані завдання. Нижче наведу приклад реалізації обчислення лінійної інтерполяції (можна використовувати для переміщення вздовж лінії будь-якого об'єкта, наприклад скролювати карту з точки А в точку Б). Формула обчислення F (t) = t1 + t * (t2 - t1). Для скролінгу необхідно отримання проміжного результату, тому потрібно буде додати callback функцію. Ось сам клас, він успадковується від HBaseTask:
HClass.Define(«HLerpTask», { extend: HBaseTask, props: {
fStart: 0, fEnd: 0,
fStep: 0, fT: 0,
fRange: 0,
fCallback: null,
EvalLerp: function() { return this.fStart + this.fT * this.fRange; },
construct: function(fStart, fEnd, fStep, fCallback) {
this.fStart = fStart;
this.fEnd = fEnd;
this.fStep = fStep;
this.fCallback = fCallback;
this. CycleTimeout(10);
this. Create(function() {
var oState = this. GetState();
if(oState.Check(HBaseTask.SF_Ready)) {
this.fT = 0;
this.fRange = this.fEnd — this.fStart;
this. SwapState(HBaseTask.SF_Process, HBaseTask.SF_Ready);
} else if(oState.Check(HBaseTask.SF_Process)) {
this.fT += this.fStep;
if(this.fT >= 1.0) {
this.fT = 1.0;
this. SwapState(HBaseTask.SF_Remove, HBaseTask.SF_Process);
}
if(this.fCallback) this.fCallback(this.EvalLerp());
}
});
}
}});* This source code was highlighted with Source Code Highlighter.
А ось приклад використання, створюється два обчислювачі і проміжні результати пишуться в діви, при цьому сам додаток доступний тобто користувач може взаємодіяти з іншим функціоналом:
function GE(sId) { return document.getElementById(sId); }
var pTask = new HLerpTask(0, 100, 0.001, function(fRet) { GE(«counter1»).innerHTML = fRet; });
var pTask2 = new HLerpTask(0, 1000, 0.0005, function(fRet) { GE(«counter2»).innerHTML = fRet; });
pTask.Run();
pTask2.Run();* This source code was highlighted with Source Code Highlighter.
Також можна робити інші типи завдань, реалізуючи в похідних класах унікальну логіку необхідну для роботи спеціалізованого завдання, але в той же час не йдучи від інтерфейсу базового завдання, що дуже важливо коли інший розробник починає працювати з твоїм модулем, йому вже по суті все відомо, тільки необхідно дізнатися які вхідні та вихідні параметри.
Нестоїть нехтувати начебто непримітними на перший погляд функціями типу setInterval і clearInterval, адже на їх базі можна робити дійсно цікаві рішення.
P.S. Рішення запропоновані в даній статті і в попередньому стосується динамічного завантаження скриптів реалізовані в проекті www.okarta.ru, це по суті експериментальний проект, вся логіка винесена на клієнта, сервер нічого не генерує з інтерфейсу абсолютно, є тільки запити даних з клієнта на сервер (SOAP), до речі для запитів відмінно юзається планувальник завдань. Для завантаження карт і блогів юзається збирач додатків.
Якщо десь непрацює, то це не в запропонованих реалізаціях проблема, моє завдання запропонувати сутності, вони на іншому рівні це не рівень браузера, якщо де помилки і виникають, то тільки там де викликаються нативні функції для роботи з XML або елементами документа, стилями тощо. Треба всетаки розуміти що є різні рівні абстракції чим нижче рівень (тобто ближче до нативних функцій), тим вища ймовірність помилки з причини що стандарти ніхто не любить, і навпаки чим вище рівень абстракції тим ймовірність помилок пов'язаних з нативними функціями нижче або взагалі виключається.
