Starcode Twitter

_chain.js – цепочки функций в JavaScript. Часть I.

дата: 19.10.09
категория: JavaScript, Интсрументарий
тэги: , ,
комментариев: (0)
  • Digg
  • Facebook
  • Twitter


Сегодня мы расскажем об одном приёме программирования, позволяющем вызывать функции по цепочке, и предложим свою реализацию для него в JavaScript. Этот приём называется Continuation Passing Style – CPS (стиль передачи продолжений). Его суть заключается в том, что функция, работающая в CPS, кроме своих обычных параметров принимает один дополнительный – продолжение, к которому переходит управление после окончания функции (хотя в общем случае не после окончания, когда вызвали продолжение, тогда и перешло). Мы предлагаем вам собственную реализацию CPS в JavaScript – _chain.js. С помощью этого скрипта цепочки функций вызываются следующим образом:

$$._chain( func1 [, args1] )
    ._chain( func2 [, args2] )
    ...
    ._chain( funcN [, argsN] )
    .run();

Метод _chain добавляет звено цепочки, funcX – функция, argsX – опциональный массив передаваемых ей параметров, метод _run запускает цепочку. Для того, чтоб передать управление следующему звену цепочки, внутри текущей функции достаточно вызвать метод this.next();

_chain.js

Скачать _chain.js можно здесь >>

Зачем это нужно?

_chain.js прежде всего применим в Rich Internet Applications построенных на JavaScript, и вот почему:

  1. В js существуют отсроченные во времени функции, т.е. мы не можем сказать точно, когда они закончат работу и соответственно, если после отсроченной функции вызвать что – то ещё ( funcWithDelay(); func(); ), то что – то ещё ( func() ) выполнится быстрее. В js отсроченные во времени функции это прежде всего это AJAX запросы и функции анимации.
  2. Если несколько функций анимации выполняются параллельно, это может существенно тормозить работу веб – приложения, поэтому в некоторых случаях имеет смысл создать цепочку и выполнять следующую функцию только после завершения предыдущей.
  3. Чем масштабней веб – приложение, тем длинее цепочки могут быть в нём. Гораздо удобней, когда вызов всех функций цепочки перед глазами в одном месте.
  4. Внутри каждой функции цепочки следующее звено анонимно и скрыто в методе this.next(), это даёт сразу два преимущества по сравнению с явным вызовом функции: во-первых одну и ту же функцию можно использовать в нескольких цепочках, во-вторых при таком подходе легче разделить презентационный слой приложения и слой бизнес-логики.
Но давайте перейдём уже от слов к делу. Для наглядности примеров напишем несколько простых анимационных функций.

function jumpTop(){
    this.arguments.obj.animate({ marginTop: 0}, 300, this.next );
} 
 
function jumpBottom(){
    this.arguments.obj.animate({ marginTop: 180}, 300, this.next );
}   
 
function jumpLeft(){
    this.arguments.obj.animate({ marginLeft: '-=300'}, 300, this.next );
}     
 
function jumpRight(){
    this.arguments.obj.animate({ marginLeft: '+=300'}, 300, this.next );
}   
 
function getBigger(){
    this.arguments.obj.animate({ width: '+=50', height : '+=50'}, 300, this.next );
}  
 
function getSmaller(){
    this.arguments.obj.animate({ width: '-=50', height : '-=50'}, 300, this.next );
}

Если у вас хорошая фантазия, можете представить, например, что jumpTop – это AJAX запрос, jumpBottom – обработка полученного json объекта, getBigger – плавное появление всплывающего окошка и т.п.

Осторожно, jQuery!
Хоть _chain.js и является самостоятельным скриптом и не использует в личных целях ни один из фреймворков, в примерах данной статьи мы используем jQuery (главным образом из-за его замечательного метода animate). Если ваши религиозные воззрения не позволяют вам пользоваться jQuery, не читайте дальше.

Первое знакомство

В каждой функции цепочки в переменной this доступно свойство arguments, в котором хранится объект, доступный для всех функций цепочки. Для того, чтоб задать или изменить свойства arguments в _chain.js предусмотрен метод _arguments( obj ). Функции цепочки могут вообще не пользоваться переменными массива args, переданного в _chain(func, args ) и использовать только объект this.arguments.
Для вызова следующей функции цепочки нужно воспользоваться методом this.next( obj ). Здесь obj – опциональный объект, расширяющий this.arguments свойствами obj для всех последующих функций цепочки.

Исходный код

var elem0   = $('#elem0');
var test0  = {};
var chain0 = $$._arguments({ obj : elem0 })
    ._chain( jumpRight )
    ._chain( jumpTop )            
    ._chain( jumpRight )                       
    ._chain( jumpBottom )
    ._chain( jumpLeft )
    ._chain( jumpTop )
    ._chain( jumpLeft )
    ._chain( jumpBottom );
 
$('#test0_run').bind('click', function(){
    if (! test0.running ) test0 = chain0._run();
});
Хозяйке на заметку:
Для того, чтоб предотвратить одновременное выполнение одной и той же цепочки несколько раз, можно воспользоваться свойством running, которое есть у объекта, возвращаемого методом _run() и которое равно true пока цепочка не закончит работу. Именно поэтому в большинстве примеров этой статьи вы встретите конструкции вида:

if (! test.running ) test = chain._run();

Объединение цепочек

Опишем пару цепочек и попробуем их объединить.

Исходный код

var test1_1Obj   = $('#elem1');
var test1_2Obj  = $('#elem1_1');
 
var chain1_1Obj = {};
var chain1_1 = $$._chain( jumpTop )
    ._chain( getBigger )            
    ._chain( jumpRight )
    ._chain( getSmaller )                            
    ._chain( jumpBottom )
    ._chain( jumpLeft );
 
$('#test1_1_run').bind('click', function(){
    if ( (! chain1_1Obj.running) && (! chain1_3Obj.running ) ) 
        chain1_1Obj = $$._arguments({ obj : test1_1Obj })._chain( chain1_1 )._run();
}); 
 
var chain1_2 = $$._chain( jumpRight )
    ._chain( jumpTop )   
    ._chain( getBigger )                              
    ._chain( jumpRight )  
    ._chain( getSmaller )                               
    ._chain( jumpBottom )
    ._chain( jumpLeft )                         
    ._chain( jumpLeft );  
 
$('#test1_2_run').bind('click', function(){
    if ( (! chain1_1Obj.running) && (! chain1_3Obj.running ) ) 
        chain1_1Obj = $$._arguments({ obj : test1_1Obj })._chain( chain1_2 )._run();
});   
 
var chain1_2Obj = {};    
var chain1_3 = $$._arguments({ obj : test1_2Obj })
    ._chain( chain1_1 )
    ._chain( chain1_2 );
 
$('#test1_3_run').bind('click', function(){
    if ( (! chain1_2Obj.running ) && (! chain1_3Obj.running ) ) 
        chain1_2Obj = chain1_3._run();
});
 
var chain1_3Obj = {};  
var chain1_4 = $$._arguments({ obj : test1_2Obj })
    ._chain( chain1_1 )
    ._arguments({ obj : test1_1Obj })
    ._chain( chain1_2 );        
 
$('#test1_4_run').bind('click', function(){
    if ( (! chain1_1Obj.running ) && (! chain1_2Obj.running ) && (! chain1_3Obj.running ) ) 
        chain1_3Obj = chain1_4._run();
});

Цепочка1 – цепочка функций, заставляющая двигаться синий квадратик.
Цепочка2 – другая последовательность действий, применённая к синему квадратику.
Цепочка3 – последовательное выполнение сначала первой, потом второй цепочки, применяется уже к красному квадратику.
Цепочка4 – применение Цепочки1 к красному квадратику, затем Цепочки2 к синему.

Добавление элементов в работаущую цепочку

В данном случае квадратик прыгнет ровно столько раз, сколько мы ему скажем (кликнем). Если цепочка не запущена – запускаем её, если же запущена – добавляем в очередь на выполнение ещё одну итерацию прыгания.

Исходный код

var test2Obj   = $('#elem2'),
    chain2Obj  = {},
    chain2     = $$._arguments({ obj : test2Obj });    
 
$('#test2_run').bind('click', function(){
    if (! chain2Obj.running){
        chain2Obj = chain2._chain( jumpTop )._chain( jumpBottom )._run();
    } else {
        chain2Obj = chain2Obj._chain( jumpTop )._chain( jumpBottom );
    }
});

Передача параметров в функции цепочки

Как мы уже говорили, в звено цепочки можно передать массив параметров, которые функция получит на входе. В следующем примере при описании цепочки передадим имя css класса в функцию, которая добавит нашему скачашему объекту этот класс, и добавит его в arguments.classToRemove, для того, чтоб последняя функция цепочки убрала из объекта этот css класс.

Исходный код

var test3Obj   = $('#elem3'),
    chain3Obj  = {},
    chain3     = $$._arguments({ obj : test3Obj })
    ._chain( jumpTop )
    ._chain( jumpRight )
    ._chain( function( className ){
        this.arguments.obj.addClass( className );
        this.next({ classToRemove : className });
    }, [ 'clone_obj' ])
    ._chain( jumpBottom )
    ._chain( jumpLeft )
    ._chain( function(){
        this.arguments.obj.removeClass( this.arguments.classToRemove );  
        this.next();
    });    
 
$('#test3_run').bind('click', function(){
    alert(chain3Obj.running)
    if (! chain3Obj.running) chain3Obj = chain3._run();
});
Хозяйке на заметку:
Никогда не пренебрегайте вызовом this.next() в функциях цепочки, даже если вы точно знаете, что данная функция – последнее звено.

Примеры из жизни

Мы столкнулись с необходимостью использовать CPS когда начали заниматься разработкой AJAX сайтов.
Нарастающее количество действий, которое нужно было выполнить с помощью js, требовало от нас всё больше концентрации. Допустим, находясь в разделе «компания»->«новости»->«теперь мы торгуем бананами!» пользователь нажал на контентную ссылку «купите банан»,
ведущую на пункт меню «продукция»->«бананы». Какие при этом действия должен выполнить js?

  1. Скрыть контент и показать слой загрузки;
  2. Сделать неактивным пункт меню «новости» и закрыть подпункты пункта «компания»;
  3. Послать на сервер AJAX запрос страницы /production/banana/ и дождаться ответа;
  4. Сделать активным пункт меню «продукция», открыть его подпункты, cделать активным подпункт «бананы»;
  5. Заменить url в адресной строке с www.somecompany.ru/#company/news/we-trade-bananas/ на www.somecompany.ru/#production/banana/;
  6. Скрыть слой загрузки и отобразить контент страницы «купите банан»;

При этом выпонление пунктов 1, 2, 3, 4, 6 отсрочено во времени, т.к. 1, 2, 4, 6 – анимация, а 3 – AJAX запрос. К тому же результат выполнения 3 очень нужен в 6, поэтому просто вызвать все эти функции в строчку не получится. С помощью _chain.js вызов приведённой выше цепочки выглядит примерно следующим образом:

$$._chain( $p._hideContent )
  ._chain( $p._hideCurrentMenuLevels )
  ._chain( $p._askContent, newPageUrl )
  ._chain( $p._showNewMenuLevels )
  ._chain( $p._changePageUrl, newPageUrl )
  ._chain( $p._showContent )
  ._run();

Кстати, впервые мы упомянули эту проблему и концепцию CPS в нашей статье «ActionWeb. Асинхронный интернет».

Итак, примеры из жизни:
www.akomarov.com
www.stroydelo-kuban.ru
www.viprieltyug.ru
www.rudent.info
www.nikitaeremin.com

Заключение

Надеемся _chain.js найдёт своего пользователя. В следующей статье мы расскажем о том, как менять цепочку по ходу её выполнения. И да пребудет с вами Сила!

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

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

*

Можно использовать следующие HTML-теги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="">