zone.js от А до Я

Artur Androsovych
Sep 8, 2018 · 6 min read

В этой статье я постараюсь развеять ваши страхи по поводу zone.js, покажу как в действительности зоны работают под капотом и как их использует Angular.

Введение

Начнем с теоретического вступления, куда же без него :) Что такое зоны? В самом начале, еще 3 года назад, зоны были tc39 proposal, который предложили Доминик Деникола и Мишко Хевери (тимлид Angular). Но что-то пошло не так и этот пропозал до сих пор находится в stage-0. Погуглив я наткнулся на то, что все туториалы дают одинаковое определение зонам: зона — это контекст выполнения (execution context). Это определение слишком абстрактное и к тому же не все поймут, что значит контекст выполнения. Давайте немного перефразируем, зона — это всего навсего механизм, просто имеет такое название, зоны позволяют нам отслеживать вызовы асинхронных функций. Зоны имеют множество вариантов использования, это просто полифил в разных вариантах, его можно использовать в браузере (zone.js/lib/browser), на бекенде вместе с node.js (zone.js/lib/node), вместе с rxjs (zone.js/lib/rxjs).

Как работает API zone.js?

Zone — это просто глобальный объект у которого есть определенные свойства, давайте взглянем на определение типа:

В данном объекте нас интересуют несколько свойств:

parent — у каждой зоны есть свой родитель, который создал эту дочернюю зону. У самой верхней зоны нет родителя, если мы выведем в консоль Zone.current.parent, то получим null.

name — название зоны, вы можете задать любую строку и это обязательный параметр.

fork — создает дочернюю зону, на самом деле копирует родительскую, скопированная зона становится потомком.

run и runGuarded— то, что позволяет нам запустить любую асинхронную задачу(setTimeout || setInterval || Promise….) в нужной нам зоне.

Слишком много непонятной теории, хочу больше практики! Хорошо, давайте начнем писать код и создадим нашу первую зону:

Как видите родителем нашей зоны является самая верхняя зона, то есть Zone.current. Теперь нам нужно опять вернуться к теории и посмотреть на определение объекта, который принимает параметром fork, этот объект называется ZoneSpec:

В консоли выведется мы где-то вызвали метод run и привет из колбека. Посмотрим на параметры метода onInvoke:

parentZoneDelegate — это объект, тип которого ZoneDelegate, ZoneDelegate — это обертка над родительской зоной, дочерняя зона не может просто так вызвать метод родительской зоны, поэтому команда Angular прибегнула к такой реализации.

targetZone — зона, в которой вызван метод run.

delegate — колбек, переданный в метод run.

C applyThis и applyArgs я думаю все понятно :)

А теперь десерт — самые интересные 2 метода — onScheduleTask и onInvokeTask:

И так, в какой последовательности мы увидим эти логи в консоли? Вызывается метод run, наша зона узнает об этом, как она это делает? Да очень просто, давайте посмотрим исходный код:

Метод run вызывает invoke в нашей зоне, смотрим исходный код invoke:

А вот и место где вызывается наш метод onInvoke, что такое this._invokeZS? Это boolean свойство, которое сеттится в конструкторе, когда мы передаем в fork параметром объект ZoneSpec:

Идем дальше, после onInvokeTask сразу вызовется метод onScheduleTask, это эдакой планировщик задачи и в консоли мы увидим Где-то мы вызвали асинхронный таск и его колбек будет чуть позже вызван в нашей зоне…, и только через 2 секунды мы увидим Где-то вызвался колбек асинхронного таска… и привет через 2 секунды, потому что вызовется метод onInvokeTask и сам invokeTask вызовет этот колбек, который мы передали в setTimeout. Как глобальный объект Zone вообще узнает об асинхронных задачах и колбеках в них? Очень просто, это называется monkey-patching, зона подменяет оригинальный setTimeout на свой, но только в том случае если вы используете zone.run. Давайте посмотрим на то, как зона подменяет setTimeout:

Такс, метод __load_patch всего лишь вызывает колбек вторым аргументом, нас интересует функция patchTimer:

Внутри функции patchTimer очень много оптимизированного кода, но все же вкратце, что он делает? При использовании Zone.prototype.run — зона каждый раз заменяет все асинхронное API на свои патчи, но не сразу, а на определенных этапах жизненного цикла. Функция patchTimer внутри себя создает метод patchMetod:

Также стоит заметить что при вызове функции patchTimer передаются параметры (window, ‘set’, ‘clear’, ‘Timeout’), внутри setName конкатенируется с nameSuffix и получается ‘setTimeout’ :) Внутри функции patchMetod вызывается функция scheduleMacroTaskWithCurrentZone, куда параметрами передаются:

setName‘setTimeout’.

args[0] — это колбек, который мы передаем первым параметром в setTimeout.

options — это объект типа TaskData, который описывает задачу:

scheduleTask — это функция, внутри patchTimer:

clearTask — делает clearTimeout или clearInterval, зависит от того, что вы используете. Что делает функция scheduleMacroTaskWithCurrentZone?

Что делает scheduleMacroTask?

scheduleTask — подготавливает задачу к запуску через промежуток (delay), добавляет в очередь и запускает дальнейший жизненный цикл зоны.

Возвращаемся к API zone.js — еще есть довольно интересный метод onHasTask:

Он вызывается каждый раз, когда изменяется длина очереди задач в зоне. hasTaskState — это объект типа HasTaskState, взглянем на его определение:

То есть когда setTimeout добавлен в очередь на обработку после onScheduleTask сразу вызывается onHasTask, так как изменилась длина очереди, значение объекта будет:

change — это тип задачи, всего существует 3 типа. Макрозадача — задача, которая может быть отменена (setTimeout || setInterval || setImmediate), имеет низкий приоритет. Микрозадача — задача, которая не может быть отменена и всегда выполнится хотя бы 1 раз (process.nextTick || Promise.resolve().then), имеет высокий приоритет. Событийная задача (он же eventTask) — любое DOM событие (addEventListener), не имеет приоритета. Задача— это абстракция, команда Google характеризует задачу как unit of work (единица работы).

Как Angular использует zone.js?

Обнаружение изменений в Angular работает только благодаря полифилу zone.js, как мы в примере создавали дочернюю зону, точно также это делает Angular, в @angular/core есть метод forkInnerZoneWithAngularBehavior:

Вызывается эта функция в конструкторе класса NgZone, NgZone — это синглтон, но на самом деле обертка, которая хранит в себе ссылки на родительскую зону и зону Angular. Когда вы заинжектили класс NgZone — у вас есть доступ к методам run и runOutsideAngular, что они делают:

Метод NgZone.prototype.run вызывает run в дочерней зоне, которую Angular зафоркал в методе forkInnerZoneWithAngularBehavior, а runOutsideAngular всего лишь вызывает run в родительской зоне, this._outer.run аналогично вызову Zone.current.run, где Zone.current.name => root. Используя runOutsideAngular вы можете увеличить производительность приложения в разы, представьте ситуацию, что у вас есть кастомный прогресс бар, ширина которого изменяется в setInterval:

50 * 100 = 5000 миллисекунд займет данный интервал, к тому же каждые 50 миллисекунд зона Angular будет перехватывать эту асинхронную задачу и тем самым запускать tick (обнаружение изменений по всему дереву). За 5000 мс Angular запустит tick 100 раз, обнаружение изменений — это тяжелый синхронный процесс, просто команда Angular максимально оптимизировала его, в данном случае нам вообще не нужен механизм обнаружения изменений, так как объект style изменяется и тем самым заставляет браузер запустить процесс reflow + repaint. Используя метод runOutsideAngular мы можем вызвать setInterval в родительской зоне, это не значит, что Angular не узнает об этой задаче, но и не запустит обнаружение изменений по всему дереву:

А как же Angular запускает tick на любую асинхронную задачу, которая выполняется в ее зоне? Довольно просто, когда Angular форкает дочернюю зону, она передает в fork параметром ZoneSpec с некоторыми методами, один из них это onHasTask, я думаю вы помните, что onHasTask вызывается каждый раз, когда меняется длина очереди, внутри жизненного хука onHasTask Angular вызывает функцию checkStable(zone), куда параметром передает инстанс NgZone, NgZone имеет публичное свойство onMicroTaskEmpty, это генератор событий (EventEmitter), то есть, все, что делает функция checkStable — это вызывает метод emit у генератора событий инстанса NgZone.

Что происходит при генерации события? В любом приложении Angular есть класс ApplicationRef, который является ссылкой на все ваше приложение, хранит в себе очень много полезной информации, но нам нужно только то, что в класс ApplicationRef инжектится синглтон NgZone и в конструкторе делается подписка на этот генератор событий:

Каждый раз, когда выполняется колбек асинхронной задачи — Angular вызывает метод tick, что делает tick:

Ничего сверхъестественного, просто в цикле проходится по активным вьюхам на текущем роуте и вызывает detectChanges.


Спасибо за чтение! Я надеюсь, что вы теперь лучше понимаете как в действительности работают зоны, что это всего лишь механизм и также, как Angular узнает, когда нужно запускать механизм обнаружения изменений.

Artur Androsovych

Written by

Angular mentalist 😈 One of the core NGXS framework developers

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade