По стопах Meteor, або велосипедируем реактивність


Добрий час доби, хабраюзер! Сьогодні ми спробуємо трохи розібрати реактивність, яка лежить в основі одного із найбільш хипстерских фреймворків — Meteor.

Для початку наведемо трохи сухої теорії з Вікіпедії:
Реактивне програмування — парадигма програмування, орієнтована на потоки даних та розповсюдження змін. Це означає, що повинна існувати можливість легко висловлювати статичні і динамічні потоки даних, а також те, що виконується модель повинна автоматично поширювати зміни крізь потік даних.

Наприклад, імперативний програмуванні присвоювання a = b + c буде означати, що змінної a буде присвоєно результат виконання операції b + c, використовуючи поточні (на момент обчислення) значення змінних. Пізніше значення змінних b і c можуть бути змінені без будь-якого впливу на значення змінної a.

Відразу обмовлюся, що даний пост більше націлений на садистів новачків, ніж на досвідчених JS-гуру.

* велосипедируем — пишемо свій велосипед, підглядаючи на вже готову реалізацію.

Для простоти викладу будемо писати код для для клієнтської частини, однак, з невеликими змінами, його можна буде використовувати і на сервері.

Для початку нам потрібно якийсь глобальний об'єкт, в якому ми зможемо зберігати всі наші залежності.
var Deps = {
funcs: [], // масив функцій, в яких твориться реактивність
vars: [] // масив масивів з реактивними змінними
};

Кожен раз, коли ми будемо створювати нове реактивне оточення, в обидва масиву будуть вставлятися потрібні нам дані. Зокрема, в масив funcs у нас буде потрапляти функція, усередині якої розташовуються реактивні змінні, за долею яких треба стежити. В масив vars ж під тим же індексом буде потрапляти масив реактивних змінних, за долею яких і стежить наше реактивне оточення.

Наступним кроком нам потрібно реалізувати саме реактивне оточення, також відоме як Tracker. Воно представляє із себе функцію, яка приймає в якості аргументу іншу функцію і відразу ж викликає її. Однак, за лаштунками відбувається магія: як тільки трекер викликає нашу функцію, у ньому активується спеціальний прапор, який вказує, що йде збір інформації про всіх реактивних змінних в нашій функції.
var Tracker = function(fn) {
Tracker.active = true; // встановлюємо прапор
var count = Deps.funcs.push(fn); // вставляємо функцію в список трекерів
Deps.vars[count - 1] = []; // ініціалізуємо масив для поміщення туди реактивних змінних

fn();

Tracker.active = false;
};

На даний момент все це марно, тому що у нас немає головної суті — реактивної змінної. Під нею ми розуміємо об'єкт, що має аксессоры до його єдиного властивості. Аксессоры — це геттер і сетер, функції, що замінюють собою отримання і завдання потрібного нам властивості, розбавляючи це дія своєю логікою.
Наша реактивна змінна повинна приймати аргумент при ініціалізації — стандартне значення. При виклику геттера (зазвичай, це метод .get без параметрів) ми повинні перевірити, чи викликається він всередині реактивного оточення, а потім записати нашу змінну в список досліджуваних змінних. При виклику ж сетера (зазвичай це метод .set з параметром val — нове значення змінної) ми повинні записати це значення в властивість об'єкта і оновити всі залежності.
var ReactiveVar = function(val) {
this.value = val; // ініціалізуємо значення змінної за замовчуванням
};
ReactiveVar.prototype.get = function() {
if(Tracker.active) { // ми всередині реактивного оточення
Deps.vars[Deps.vars.length - 1].push(this); // встромляємо змінну в стек для зв'язку її з трекером
}

return this.value; // повертаємо значення властивості
};
ReactiveVar.prototype.set = function(val) {
this.value = val; // оновлюємо значення властивості

for(var i = 0; i < Deps.vars.length; i++) {
if(Deps.vars[i].indexOf(this) > -1) { // залежність знайдена
Deps.funcs[i](); // викликаємо функцію оточення заново
}
}

return this.value; // повертаємо значення властивості
};

Зберемо всі разом і отримаємо майже готову бібліотеку:
Заголовок спойлера
(function() {
var Deps = {
funcs: [],
vars: []
};

var Tracker = function(fn) {
Deps.funcs.push(fn);
Deps.vars.push([]);
Deps.tracker = true;

fn();

Deps.tracker = false;
};

var ReactiveVar = function(init) {
this.value = init;
};

ReactiveVar.prototype.get = function() {
if(Deps.tracker) {
Deps.vars[Deps.vars.length - 1].push(this);
}

return this.value;
};

ReactiveVar.prototype.set = function(val) {
var i = 0;
var self = this;
this.value = val;
Deps.vars.forEach(function(arr) {
if(arr.indexOf(self) > -1) {
Deps.funcs[i]();
}
i += 1;
});
};

// прибираємо window. для NodeJS додатки
window.Tracker = Tracker;
window.ReactiveVar = ReactiveVar;
})();


Тепер давайте спробуємо використовувати реактивність у наших цілях! Для цього реалізуємо найпростіший секундомір. Нам знадобиться 1 реактивна мінлива і інтервал, цокаючий кожну секунду:
var time = new ReactiveVar(1); // ініціалізуємо значення

Tracker(function() {
var curTime = time.get(); // починаємо відстеження змін змінної
document.querySelector('#time').innerHTML = curTime; // змінюємо DOM у зв'язку зі зміною нашої змінної
});

setInterval(function() {
time.set(time.get() + 1); // так як ми знаходимося поза реактивного оточення, то не підписуємося на події оновлення змінної time, однак, оновлюємо всі залежно цієї змінної
}, 1000);

От і все. Як ми змогли побачити, реактивність в JS — це не щось з розряду фантастики, а дуже навіть проста парадигма програмування.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.