Планувальник завдань на JavaScript

Всім привіт.

Іноді на клієнтській стороні необхідно виконувати фонові завдання. Головна вимога щоб вони не переривали роботу всього веб-додатку а, спокійно у фоновому режимі спілкувалися між собою, завершувалися і додавалися. Мета запропонованого планувальника зняти з розробника головний біль про такі завдання і звести до загального інтерфейсу, за допомогою якого можна поступово розширювати спектр вирішуваних завдань.

Можна запитати, а що за завдання і нафіга це потрібно? Завдань насправді на клієнті багато: це і періодична зміна ідентифікатора сесії (наприклад поштовий клієнт оновлює у фоновому режимі ідентифікатор), і автоматична прокрутка (наприклад, скролінг карти), переміщення об'єкта в графічному інтерфейсі, це і обробка безлічі запитів з клієнта на сервер (Наприклад, запитується інформація з сервера через 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 або елементами документа, стилями тощо. Треба всетаки розуміти що є різні рівні абстракції чим нижче рівень (тобто ближче до нативних функцій), тим вища ймовірність помилки з причини що стандарти ніхто не любить, і навпаки чим вище рівень абстракції тим ймовірність помилок пов'язаних з нативними функціями нижче або взагалі виключається.