Ключевое отличие AngularJS от Knockout

от автора

За последнее время я несколько раз успел поучаствовать в дискуссиях о том, чем AngularJS лучше или хуже Knockout и других JS-фреймворков. И очень часто я сталкивался с тем, что есть некоторое непонимание сути различий в подходах, заложенных в эти продукты. Иногда дело доходило даже до того, что в качестве преимущества Knockout приводились валидные по умолчанию префиксы «data-», что уже ну просто совсем смешно.

Хочу один раз зафиксировать в этой статье некоторые мысли, на которые потом можно было просто давать ссылку. По-моему мнению, действительно ключевых отличий AngularJS от некоторых других фреймворков существует три штуки:

  1. Модульная организация кода, тестируемость и жестокая война с любыми глобальными данными.
  2. Пропаганда декларативного подхода и «виджетирования» элементов интерфейса.
  3. Механизм проверки изменения данных в дата-биндинге без использования коллбэков.

И третий пункт мне здесь видится наиболее важным. Поговорим именно о нем.

Что такое дата-биндинг? Грубо говоря, это отображение данных в шаблоне, выполненное так, чтобы изменение данных изменяло их представление. Имея переменную:

var myVar = 1; 

… и шаблон:

<span class="myVar">...</span> 

… мы хотим, чтобы span.myVar при выполнении myVar++ обновлялся до актуального состояния с минимальным нашим участием. И наоборот, если это, например, поле ввода.

И для решения этой задачи есть два принципиально различных подхода.

Change listeners

Для механизма дата-биндинга в таких фреймворках как Knockout и Backbone была разработана система отслеживания изменений (change listeners). Вы работаете с данными не напрямую, а через специальный слой (например, observables в KO), призванный вовремя менять представление данных при их изменении. Любая переменная превращается в объект фреймворка, который следит за состоянием.

Умное и очевидное решение. Казалось бы, событийно-ориентированное программирование в javascript подходит как нельзя лучше для подобных задач, да и вообще работа с коллбэками — наш сильный конек.

Однако, тут возникают проблемы, которые приходится решать подобным фреймворкам.

Во-первых, что, если одна часть данных каким-то способом зависит от другой части? Изменив одну переменную, мы автоматически сообщаем об этом, но изменившаяся при этом вторая переменная останется незамеченной. Для разрешения подобных зависимостей в KO существует механизм dependency tracking, который работает хорошо, но само наличие решения говорит о существовании проблемы.

Во-вторых, срабатывание системы отслеживания изменений в основном происходит при каждом изменении, ведь в своей основе это просто коллбэки. Если мы изменяем данные непрерывно или большими порциями сразу, то это вызовет очень много ненужных срабатываний, ведь конечная цель — всего лишь изменить отображение, которые так или иначе будет приведено к итоговому виду, и промежуточные состояния тут не нужны.

Объединяя решения по предыдущим двум пунктам, мы получаем третью проблему: непрерывную микро-рассинхронизацию всего состояния данных. Каждый раз, изменяя что-либо, мы вызываем этим соответствующее срабатывание, которое в свое очередь может изменить другие данные и тоже вызвать срабатывание. Кроме того, что мы наращиваем таким образом стек выполнения, мы в каждый из моментов времени рискуем начать работать с данными, которые находятся в процессе редактирования (или подготовлены к нему) где-то в другом месте стека, и чтобы правильно отследить такие места в коде, нужно очень хорошо понимать всю внутреннюю кухню, скрытую за этими на первый взгляд простыми вещами; примерно так же, как при многопоточном программировании нужно очень щепетильно следить за использованием разделяемых данных.

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

Dirty checking

По этому принципу работает AngularJS. Dirty checking — это проверка на изменененность данных, простая как два рубля. Раньше переменная myVar была 1, теперь она 2 — значит данные изменились и надо их в шаблоне обновить. Для простых переменных это оператор !=, для сложных — соответствующие легковесные процедуры. Это простейший подход, который избавляет нас как от необходимости работать с данными через специальный «слушающий» слой, так и от всех проблем, связанных с зависимостями данных.

Весь вопрос в том, когда производить эту проверку? Непрерывно, по таймеру? Учитывая, что модель данных может быть довольно сложной, то непрерывно производящиеся проверки могут сильно ухудшить UX. В Angular этот вопрос решается путем вызова функции $digest после каждого участка кода, предположительно могущего изменить данные. Это ключевой момент — проверка выполняется тогда и только тогда, когда данные могли быть изменены (например, при действии пользователя), и никогда не выполняется в других случаях. Если вы ожидаете изменения данных в какой-то другой момент времени (например, при поступлении события от сервера или завершении какого-либо процесса), вы должна явно указать angular, что стоит выполнить проверку, вызвав функцию $apply.

При этом данные каждый проверяются все сразу, целиком, а не лишь какая-то часть. До и после окончания выполнения $digest мы имеем стабильную версию всего состояния без рассинхронизаций. Если из-за зависимостей данных какая-то одна их часть в процессе проверки изменилась, то сразу после окончания текущей проверки будет запланировано выполнение следующей. И следующая проверка снова выполнится целиком, обновляя состояние модели полностью, ликвидируя возможные проблемы с неучтенными зависимостями.

Очевидный минус этого подхода — производительность. Хотя и здесь есть небольшое исключение: например, при пакетном обновлении большого количества данных сразу проверка выполняется всего один раз в конце, а не при каждом изменении каждого из отслеживаемых объектов, как это происходит в первом случае. Но в целом, это тем не менее минус, так как при изменении всего одной переменной выполняется dirty check всех данных.

Нужно лишь понять, насколько сильны потери производительности.

Тут стоит отметить, что Angular во время выполнении dirty check никогда не работает с DOM. Все данные — это нативные объекты js, с которыми все современные движки браузеров молниеносно выполняют большинство основных операций. Хотя вы и можете сами вставлять процедуры проверки в процесс dirty check, документация Angular настоятельно рекомендует не работать с DOM внутри них, так как это может сильно замедлить весь процесс.

Учитывая это, можно сказать, что потеря производительности в сегодняшних условиях работы веб-приложений на практике не ощущается. Раньше я немалое время занимался разработкой игр под мобильные платформы, и там (особенно на старых вроде Palm OS) на счету обычно был каждый лишний такт процессора. Но даже при такой нехватке ресурсов основным принципом работы «дата-биндинга» был именно простейший dirty check. Что такое дата-биндинг в случае игры? Это отображение картинок на игровом экране в зависимости от того, что происходит в состоянии данных игры. Иногда, действительно, использовался подход, близкий к подходу слушающих коллбэков — например, обновление экрана только лишь в тех местах, где картинка поменялась. Но в основном экран просто перерисовался каждый раз заново целиком, замещая текущий кадр новым актуальным состоянием графики. И единственным критерием правомерности такого подхода был и остается показатель FPS — как часто мы можем менять таким образом кадры и насколько плавным будет соответствующий UX. До тех пор, пока FPS находится в районе максимально возможного для восприятия человеком (в районе 30 кадров в секунду), о потерях быстродействия просто нет смысла думать, так как они не приводят ни к чему, что можно назвать ухудшением UX.

Факт заключается в том, что тотальный dirty checking, применяемый в AngularJS, позволяет работать с данными практически любой сложности, и при этом выполнять проверку и менять отображение менее, чем за 50мс, а это хоть и больше, чем если бы мы проверяли лишь часть данных, но тем не менее мгновенно для пользователя. И при этом такой подход избавляет от множества различных головных болей, вызываемых сложным механизмом change listeners, да и просто упрощает работу, ведь мы продолжаем обращаться с данными как с обычными переменными и POJO-объектами, не задействуя сложный синтаксис «слушающего» слоя.

ссылка на оригинал статьи http://habrahabr.ru/post/165275/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *