Асинхронные задачи Task.js в JavaScript 1.8


В последней версии JavaScript есть несколько интересных вещей, которым уже найдено новое применение. Task.js использует новые возможности генератора для создания асинхронной задачи.

Если вы хоть немного знакомы с JavaScript, то вы также знаете, что асинхронное программирование является ключом к тому, чтобы он работал правильно. Вы не можете просто использовать единственный поток выполнения. Если вы это сделаете, то в результате система и пользовательский интерфейс остановятся. Тем не менее, для языка, который зависит от асихронного программирования, JavaScript мало что может предложить для помощи в том, что может быть очень запутанной идиомой программирования.

Асинхронные задачи Task.js в JavaScript

Лучшее, что можно предложить в настоящее время, это Promise или объект Deferred в jQuery. Это позволяет асихронным функциям немедленно возвращать объект Promise, а клиенту в коде указывать функцию, которая будет запущена, когда Promise разрешится.

Это улучшение по сравнению с базовым механизмом обратного вызова, но оно все еще немного нарушает естественное расположение кода и может привести к вложенному беспорядку, если вам нужно иметь несколько Promise.

На данный момент, возможно, лучшим решением проблемы асинхронности являются команды async и await в C#, и нам остается надеяться, что когда-нибудь в JavaScript появится нечто подобное. Тем временем Mozilla проводит хороший эксперимент, в котором используется новая функция генератора в ES6. Большая проблема заключается в том, что на данный момент только Firefox поддерживает генераторы.

Сначала давайте рассмотрим идею генератора — yield и send.

Генератор

Если вы еще не сталкивались с идеей генератора, то она довольно проста.

Если вы напишете

yield expression

в функции, то она становится генератором.

Если вы вызываете функцию, она возвращает объект-итератор, который имеет метод next.

Каждый раз, когда вы вызываете метод next, итератор выполняет код исходной функции, пока не встретит выражение yield, тогда он останавливается и возвращает значение. При повторном вызове метода next выполнение начинается с инструкции, следующей за yield. Механизм yield является простой формой продолжения.

Например:

function myGen()
{
   yield 1;
   yield 2;
   yield 3;
}

тогда

var gen = myGen();
gen.next()

вернет 1

gen.next()

вернет 2

и так далее.

Вызывающая программа может также отправить результат возврата в yield, используя метод send генераторов. Если вы используете

gen.send(0);

вместо next(), то yield вернет ноль:

var answer = yield 1;

сохраняет ноль в ответе. Обратите внимание, что если вызывающая программа использует next для возобновления работы после выхода, то answer устанавливается в неопределенное значение, что не совсем ясно из документации.

При обычном использовании метод send используется для сброса или изменения последовательности, которую производит генератор, но Task.js использует его для совершенно другой цели.

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

Во-первых, чтобы все нижеперечисленное работало, вы должны использовать Firefox и загрузить скрипт с помощью:

<script type="application/javascript;version=1.8">

После этого вы можете использовать yield:

function myFib() {
    var v1 = 0;
    var v2 = 1;
    while (true) {
        yield v1;
        v2 = v1 + v2;
        v1 = v2 - v1;
    }
}

После определения вы можете использовать её для перебора чисел Fibbonaci:

var fib = myFib();
console.log(fib.next()); 
console.log(fib.next()); 
console.log(fib.next()); 
console.log(fib.next()); 
console.log(fib.next());

которая напечатает 0,1,1,2,3 и так далее.

Если вы хотите позволить вызывающей стороне сбросить генератор, вы можете сделать это следующим образом:

function myFib() {
    var v1 = 0;
    var v2 = 1;
    while (true) {
        var restart = yield v1;
        v2 = v1 + v2;
        v1 = v2 - v1;
        if (restart) { 
            v1 = 0;
            v2 = 1; 
        }
    }
}

и вызвать так

console.log(fib.send(true));

чтобы перезапустить его. Обратите внимание, что в этом случае нам не нужно проверять на undefined, потому что, согласно правилам истинности JavaScript, undefined рассматривается как false.

Task.js

До сих пор мы видели стандартное использование оператора yield. В Task.js оператор yield используется для реализации не итератора, а кооперативного прерывания задачи.

Идея заключается в том, что вы пишете функцию, которая выходит каждый раз, когда хочет или должна позволить чему-то другому произойти. Умная часть заключается в том, что она выдает объект Promise, который планировщик использует для перезапуска вашей функции, выполняющей выход, когда Promise разрешится.

Таким образом, Task.js использует следующую схему:

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

В этот момент выполняется операция, которая возвращает Promise, разрешаемый по завершении операции.

Планировщик получает объект Promise и запускает другую задачу или освобождает поток пользовательского интерфейса, чтобы можно было обрабатывать события.

Когда объект Promise разрешается, задача, заблокированная на нем, перезапускается с помощью метода send(value). Результат выполнения или значение обещания возвращается в качестве значения yield.

Это все, и это очень просто.

Настоящая красота этого подхода в том, что когда вы просто смотрите на код, который вам нужно написать, он выглядит очень простым, даже если за кулисами происходит много всего.

Чтобы Task.js заработал, необходимо загрузить его, поэтому включите:

<script type="application/javascript" src="/task.js">
</script>

Теперь вы можете начать работать с Task.js, но не забывайте начинать свои скрипты с:

<script type="application/javascript;version=1.8">

Первый пример

Давайте рассмотрим очень простой пример асихронной задачи с использованием jQuery и его метода ajax для загрузки файла с данными.

task.spawn(function() {
    var data = yield $.ajax("text.html");
    $('#result').html(data);
});

Все методы Task.js находятся в пространстве имен задачи.

Функция spawn добавляет задачу в планировщик и начинает ее выполнение. Первая инструкция:

var data = yield $.ajax(url);

Использует метод jQuery ajax для загрузки HTML-файла. Метод ajax возвращает обещание, а значением обещания является содержимое файла.

Метод yield возвращает управление планировщику, который сохраняет обещание и проверяет, есть ли другая задача, готовая к запуску. Если есть, то он запускает ее, если нет, то освобождает поток UI. В какой-то момент Promise разрешается, и вызывается функция onResolve. Это снова активирует планировщик, который распаковывает значение Promise и отправляет его в поток yield с помощью send(value).

Это может показаться сложным, и это лишь грубый набросок того, что происходит, но чистый эффект заключается в том, что вызов ajax выполняется асинхронно без блокировки пользовательского интерфейса или любой другой задачи, а содержимое файла сохраняется в переменной data.

Для программиста это выглядит так же, как если бы ajax был синхронным блокирующим методом.

Обратите внимание, что пользователю не нужно беспокоиться о Promises, call backs или чем-то еще. Вы также можете вывести сообщение о том, что данные загружены:

var status = $('#status').hide().html('Download complete.');
yield status.fadeIn().promise();

Причина, по которой это интересно, заключается в том, что многие методы jQuery, которые вы уже знаете, оснащены возможностью возвращать объект promise, если вы попросите об этом. Метод fadeIn может возвращать объект Promise с версии jQuery 1.6, хотя это не часто используется, потому что кажется сложным. Используемый с Task.js, он позволяет легко дождаться завершения fadeIn без блокировки потока пользовательского интерфейса. Обратите внимание, что в данном случае Promise не возвращает никакого значения, поэтому мы не используем его.

Цикл блокировки потока

Возможно, самое интересное в Task.js то, что он позволяет писать «узкие» циклы, которые в стандартном JavaScript привели бы к остановке системы.

Например, если вы напишете:

var i = 0 while (true){
    console.log(i);
    i = i + 1;
}

В результате пользовательский интерфейс замирает, и вы не видите никаких значений, отображаемых в журнале.

Это распространенная ошибка новичков, но с помощью Task.js этого замкнутого цикла больше не нужно избегать. Все, что вам нужно сделать, — это время от времени отступать, чтобы другие задачи имели возможность выполнить какую-то работу:

task.spawn(function() {
   var i = 0;
   while (true) {
      console.log(i);
      i = i + 1 
      yield task.sleep(1000);
   }
});

Теперь все работает, как ожидалось, и вы увидите, что значения появляются в консоли, а пользовательский интерфейс остается активным, как и все другие задачи, которые вы могли запустить. Метод sleep просто переводит задачу в спящий режим на 1000 миллисекунд, а планировщик запускает другие задачи в течение этого периода.

Вы также можете использовать метод sleep для демонстрации работы двух задач:

task.spawn(function() {
    var i = 0 while (true) {
        console.log("A");
        i = i + 1; 
        yield task.sleep(100);
    }
});
task.spawn(function() {
    var i=0 while(true){
        console.log("B");
        i=i+1;
        yield task.sleep(1000);
    }
}); 

Первая задача печатает A каждую десятую долю секунды, а вторая задача печатает B каждую секунду. Если вы запустите программу, то увидите в журнале чередование A и B, причем A в десять раз больше, чем B.

Перспективы

Конечно, вы не можете использовать такой подход повсеместно, пока доходность не будет лучше поддерживаться. Существуют способы достижения тех же результатов, но ни один из них не является настолько элегантным, как использование Task.js — это умное использование средства, созданного для одной цели, для другой.

Если вы изучите документацию по Task.js, то обнаружите, что существует множество дополнительных методов и средств для управления задачами — большинство из них вам никогда не понадобится использовать.

Единственное требование, которое позволит сделать Task.js полезным в будущем, — это использование объекта Promise асихронными методами. Это еще одна веская причина, если она вам нужна, для использования Promises в ваших собственных асихронных процедурах.


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