- PVSM.RU - https://www.pvsm.ru -
Я фронтенд-разработчик в компании Wrike, пишу на JavaScript и на Dart, который компилируется в JavaScript. Сегодня я хочу рассказать о библиотеке Zone.js, лежащей в основе Angular 2.
Изначально Zone.js была придумана разработчиками Google для языка программирования Dart и утилиты Dart2JS. С помощью этой библиотеки «гугловцы» решили проблему с дайджест-циклом, которая была характерна для первого Angular’а.
Чтобы понять, где эта библиотека используется и для чего нужна, прошу под кат.
Если вы пишите на JavaScript или на языках, которые компилируются в JavaScript, то наверняка сталкивались с такой ситуацией:
работающий пример [1]:
var feedback = {
message: 'Привет!',
send: function () {
alert(this.message)
}
}
setTimeout(feedback.send)
Проблема известна давно — потерялся контекст. Поэтому во всплывающем сообщении мы увидим «undefined». Я знаю 4 способа выйти из положения:
На этом можно было бы и закончить, если бы не…
Рассмотренный нами пример — это верхушка айсберга, на самом деле проблема потери контекста более глобальна. Дело в том, что контекст в JavaScript теряется в месте вызова асинхронных функций, и чтобы его сохранить, необходимо всю цепочку вызовов постоянно держать в голове и контролировать. Это особенно сложно в больших проектах
Есть обратный эффект: из-за потери контекста теряется связь с местом, откуда вызывается асинхронная функция. Например, непонятно, в каком месте мы навесили обработчик клика, который вызвал ошибку. В консоли видна только ссылка на сам обработчик клика, но не показано, где он подписан.
Сколько способов вызовов функций асинхронно вы знаете? Мне на ум приходит setTimeout, addEventListener, асинхронные запросы к серверу и т.д. и т. п. Не так уж и много — количество этих мест конечно. Что это значит? Если предотвратить потерю контекста на каждый асинхронный вызов, проблема решится. Для начала давайте попробуем предотвратить потерю контекста в setTimeout:
Напишем класс с конструктором и тремя методами
class Context {
constructor(parentContext) {
let context;
if (parentContext) {
// Создаем копию
context = Object.create(parentContext)
context.parent = parentContext;
} else {
// Возвращаем текущий контекст
context = this;
}
return context;
}
fork() {
// Возвращаем копию
return new Context(this);
}
bind(fn) {
// Получаем текущий контекст
const context = this.fork();
// Возвращаем функцию в которой уже замкнут контекст
return () => {
return context.run(() => fn.apply(this, arguments), this, arguments);
}
}
run(fn) {
// Заменяем текущий контекст на наш
let oldContext = context;
context = this;
const result = fn.call() // Выполняем функцию в контексте
context = oldContext; // Возвращаем как было
return result; // Результат выполнения
}
}
После этого подменим исходный setTimeout:
context = new Context();
var nativeSetTimeout = window.setTimeout; // Подменяем setTimeout
context.setTimeout = (callback, time) => {
callback = context.bind(callback);
return nativeSetTimeout.call(window, callback.bind(context), time);
};
window.setTimeout = function (){
return context.setTimeout.apply(this, arguments);
};
Теперь клиентский код:
context.fork({} /* пустой объект, чтобы склонировалось*/).run(() => {
context.message = 'Привет!';
setTimeout(() => {
console.log(`%cСообщение в контексте: «${context.message}»`, 'font-size: x-large');
}, 0);
});
console.log(`%cСообщение вне контекста: «${context.message}»`,'font-size: x-large');
Теперь связь сохраняется. Вот пример рабочего кода [6]. Используя ту же технику, можно заменить асинхронные вызовы для большинства случаев и управлять контекстом в ручном режиме предсказуемо.
А вот ниже наглядная иллюстрация работы. Каждая зона раскрашена в собственный цвет:
По сути, мы реализовали часть библиотеки Zone.js. Думаю, что на этом можно остановиться и не писать свой велосипед, а продолжить изучение зон, используя оригинальную библиотеку.
После подключения библиотеки в глобальной области видимости появляется объект Zone. Zone.current содержит ссылку на текущую зону. Метод fork объекта Zone возвращает новую зону на основе родительской. О том, какие параметры здесь возможны, лучше посмотреть в документации на github [7]. Метод run принимает функцию, тело которой выполнится в пределах этой зоны. Вот пример [8].
const childZone = Zone.current.fork({
name: 'Дочерняя зона'
});
const handler = () => {
alert(`Код запустился в зоне с именем «${Zone.current.name}»`);
}
childZone.run(handler);
handler();
Разработчики библиотеки выделяют три вида асинхронных задач:
Zone.js перехватывает попытку планирования асинхронных задач, выполнение обратных вызов, ошибки и прочее. Задачи планируются как явно, при помощи вызова специальных методов у объекта Zone.current, так и неявно, с помощью вызова асинхронной функции (setTimeout), как мы это делали в первой части статьи.
Зоны легко комбинировать: например, одна зона отлавливает ошибки в своих границах и отправляет нотификации на сервер, а дочерняя зона (потомок) выполняет функцию трекера и отправляет на сервер статистику работы пользователя в графическом интерфейсе. При этом, если в пределах дочерней зоны случится ошибка, то родительская зона перехватит и пошлет информацию на сервер. Вот пример комбинирования зон [9].
const errorHandlerZone = Zone.current.fork({
name: 'ErrorZone',
onHandleError: (parentZoneDelegate, currentZone, targetZone, error) => {
sendError(error);
return false;
}
});
const trackingZone = errorHandlerZone.fork({
name: 'TrackingZone'
});
class Widget {
render = () => {
throw 'render error';
}
}
trackingZone.runGuarded(function(){
document.addEventListener('click', (event) => {
trackEvent(event);
}, true);
const widget = new Widget();
widget.render();
return this;
});
function sendError(error){
alert(`Ошибка: ${error} Название зоны: ${Zone.current.name}`);
}
function trackEvent(event){
alert(`Трекинг: ${event} Название зоны: ${Zone.current.name}`);
}
Первый и довольно типичный пример, о котором я писал выше, — ошибка в консоли. Упал обработчик клика по кнопке и все бы ничего, вот только непонятно, где он навешен. При помощи Zone.js мы можем это определить. Для этого используем специальную зону из репозитория Zone.js. Вот пример [10].
При помощи зон замеряем время работы кода, в котором содержатся асинхронные вызовы, исключая задержку не связанную с кодом: таймауты, ожидания ответа сервера и паузы между событиями. Пример [11].
Зоны используются во втором Angular’е. Фреймворк понимает, что нужно запустить механизм поиска изменений, когда происходят асинхронные событие. О наступлении этого асинхронного события он узнает как раз от Zone.js.
Если представить все выше изложенное как код, то получится нечто подобное:
// Новая версия addEventListener
function addEventListener(eventName, callback) {
// Вызов настоящего addEventListener
callRealAddEventListener(eventName, function () {
// Оригинальный обратный вызов
callback(...);
// Заускаем поиск изменений
var changed = angular2.runChangeDetection();
if (changed) {
angular2.reRenderUIPart(); // Отображаем изменения
}
});
}
Благодаря зонам мы знаем, в каком элементе произошло асинхронное событие. Остается понять, нужно ли рендерить изменения для дочерних элементов, и здесь есть отличительная особенность Angular 2. В первом Angular’е приходилось запускать дайджест-цикл, который много раз обходил дочерние и родительские элементы, чтобы проверить: изменилась модель или нет. Второй Angular проверяет на изменения однонаправленно.
Zone.js меняет стандартное поведение браузерного API (переопределять setTimeout [12] нехорошо). Это минус. При том, что манкипатчинг [13] выполнен аккуратно, мы использовали антипаттерн [14]. Появились дополнительные издержки при вызове базовых функций. Эти издержки малы, но они есть.
Манкипатчинг может привести к дополнительным багам в стандартных ситуациях. Хотя сам я ни разу не натыкался на эти баги, потенциально они возможны.
Еще у меня не получилось заставить работать зоны в приложении на React и Angular первой версии так, как я хотел.
На самом деле ничего не мешает использовать Zone.js как таковой, но вот сделать так, чтобы каждый компонент вызывался в отдельной зоне — проблематично. Для полноценной работы зон нужно, чтобы каждый кусок асинхронного кода (привязка событий, асинхронные запросы к серверу) вызывался в пределах зоны. Мне не удалось управлять этими процессами. Реакт использует виртуальный дом и умный рендеринг с кэшированием, а базовые события в Angular навешиваются в базовых директивах типа ngClick, переписывать которые утомительно. Есть шанс, что у вас это получится. Делитесь успехами в комментариях.
Zone.js — тот случай, когда манкипатчинг уместен. Те преимущества, которые дает библиотека, перекрывают недостатки подхода, а нарушение неписаных правил иногда приводит к победе.
Автор: Wrike
Источник [15]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/190096
Ссылки в тексте:
[1] работающий пример: https://jsfiddle.net/zolotyh/1cmpf0ov/
[2] замыкания: https://jsfiddle.net/zolotyh/jwjuLk4u/
[3] bind: https://jsfiddle.net/zolotyh/ypegy5v6/
[4] ES6 классы и стрелочные функции: https://jsfiddle.net/zolotyh/u4v3ja8f/
[5] двойное двоеточие: https://jsfiddle.net/zolotyh/wpath6dz/
[6] рабочего кода: https://jsfiddle.net/zolotyh/mm0tsh1n/
[7] документации на github: https://github.com/angular/zone.js/blob/master/dist/zone.js.d.ts
[8] пример: https://jsfiddle.net/zolotyh/4st0wtv7/
[9] пример комбинирования зон: https://jsfiddle.net/zolotyh/w7trLaL3/
[10] пример: https://jsfiddle.net/zolotyh/zkdzky2r/
[11] Пример: https://jsfiddle.net/zolotyh/7f404twp/
[12] переопределять setTimeout: #setTimeout
[13] манкипатчинг: https://ru.wikipedia.org/wiki/Monkey_patch
[14] антипаттерн: https://ru.wikipedia.org/wiki/%D0%90%D0%BD%D1%82%D0%B8%D0%BF%D0%B0%D1%82%D1%82%D0%B5%D1%80%D0%BD
[15] Источник: https://habrahabr.ru/post/310422/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.