Начиная с версии 1.7 в яваскрипте появились так называемые "генераторы" - особые функции, которые могут останавливаться и продолжать своё исполнения с точки останова. Основное их предназанчение - генерировать разнообразные последовательности. Но их замечательные свойства можно использовать и в других целях. Например, для реализации нитей на основе волокон...
Давайте нарисуем какой-нибудь несложный пример работы с внешними источниками. Напримерм, пусть это будет функция, которая скачивает данные с определённого ресурса в сети и кладёт его в не менее определённый файл на диске. Вот, что у нас примерно может получиться:
Чем плох данный код? А тем, что он синхронный. Пока мы ждём ответа от сервера и записи в файл у нас останавливается работа всего приложения. Визуально это выглядит как зависший браузер. Чтобы исправить эту ситауцию программисты всего мира используют асинхронные вызовы. То есть, выполнение функции не останавливается, а она просто завершается, то на событие завершения асинхронной операции вешается специальный обработчик:
Мы сделали приятно пользователю, но наш код превратился в спагетти, в котором сложно проследить за логикой выполняемых действий. Чтобы хоть как-то сгладить углы , часто используют специальные "выравниватели". Они хоть и не избавляют от нагромождения функций, но хотябы выстраивают их в одну конвеерную линию:
Уже лучше, не правда ли? Но косяк такого способа заключается в том, что мы теряем возможность использовать стандартные операторы управления потоком (for, while, if, switch и тд) - их приходится эмулировать специальными методами, что выглядит слишком брутально.
Из рассмотренных выше примеров можно сделать два простых вывода: асинхронный код - зло, но асинхронные запросы - добро. Как совместить эти две противоречащие друг другу концепции? Об этом чуть позже, а пока давайте разберёмся с однм простым понятием, которое я назвал "волокно"..
Волокно - это функция, удовлетворяющая следующим требованиям:
Проиллюстрируем сей интерфейс на примере реализации функции, которая запрашивает у пользователя подтверждение:
Для защиты от исключений, весь код завёрнут в try-catch, который является достаточно типовым, и поэтому просто напрашивается на вынос его в отдельый хэлпер:
Ну хорошо, вот сделали мы волокно, и оно даже может возвращать значения аж в двух разных направлениях, но как же передать ему какие-либо параметры? Интерфейсом это не предусмотрено. Чтобы запустить волокно с какими-то параметрами, эти параметры должны быть переданы ему при создании, а не при вызове:
Важно отметить, что Confirm теперь - это не волокно, а фабрика волокон, которая создаёт волокна спрашивающие пользователя о разных вещах. Но постойте, код получился уж слишком сложным. Аналогичная безволоконная функция выглядит сильно проще:
Так давайте просто напишем хэлпер, который бы преобразовывал безволоконную функцию в фабрику волокон:
Однако, FiberThread может принимать не только функцию, но и фабрику генераторов. Функция становится фабрикой генераторов, если в ней используется оператор yield.
Логика работы с волоконизированными генераторами простая: справа оператору yield передётся волокно (в примере, оно сконструировано фабрикой Confirm), а слева мы получим результат полученный от этого волокна после исполнения. Если же в волокне возникнет ошибка, то yield запустит исключение, которое мы при желании сможем перехватить с помощью try-catch внутри генератора:
Важно отметить, что код выглядит как синхронный, хотя волокно переданное в yield может быть и асинхронным (на самом деле только в этом случае и есть смысл использовать волокна). Не меняя кода AskUser мы можем реализовать Confirm, например, так:
Заметили? Опять вылез этот try-catch, от которого мы избавлялись с помощью хэлпера Fiber, только тут он не применим, так как нужно оборачивать не волокно, а обычную колбэк функцию. Опять эта передача параметров через замыкание, от которого мы избавлялись с помощью FiberThread, но он годится только для оборачивания синхронного кода, а у нас асинхронный. Для соединения волоконных нитей с классическим асинхронным апи имеет смысл ввести понятие триггера:
Триггер - это волокно, которое имеет 2 отчуждаемых метода: done и fail. Эти методы можно передать куда угодно в качестве колбэка, и когда какой-нибудь из них будет вызван, триггер активируется либо вернёт значение, либо бросит исключение. Не правда ли, с триггером код получился более простым и понятным?
Вроде всё красиво, но есть один костыль, который оставляет некоторое чувство неполноценности. если вы используете yield, то интерпретатор не позволяет использовать return. Поэтому FiberThread реализован так, что возвращает вызвавшему его коду результат последнего дочернего волокна. То есть, чтобы вернуть какое-то значение приходится создавать специальное волокно с помощью FiberValue, которое ничего полезного не делает, а только возвращает заданное значение.
Ну что ж, теперь, когда мы разобрались с волокнами, попробуем применить их для реализации скачивателя файлов из начала статьи:
Полученный код совмещает в себе все преимущества синхронного и асинхронного подходов. Но что если нам нужно скачать несколько файлов? Не делать же это последовательно! Самый простой вариант - запустить несколько волокон и забыть про них:
Но зачастую хочется не просто запустить скачивание, но и выполнить какой-то код после завершения всех закачек. Для этого нужно просто создать специальное волокно, которое возьмёт на себя всю заботу о синхронизации нескольких задач:
В общем, тут ещё наверняка можно много чего придумать, но моей фантазии пока хватило лишь на это. Упомянутые в статье рализации доступны в составе фреймворка fenix для мозиллы. Больше вроде никто из яваскриптовых интерпретаторов генераторы не поддерживает.