для циклов с функциями обратного вызова в jQuery или как их избежать

#javascript #jquery

#javascript #jquery

Вопрос:

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

 var arr = ['GOAT', 'BEAVER', 'TIGER', 'ELEPHANT', 'FOX', 'BEAR', 'BEE', 'CAT', 'DOG', 'MOUSE', 'LION', 'FISH', 'SHRIMP', 'HEN', 'GOOSE', 'COW', 'CROCODILE', 'DEER', 'MOOSE', 'HIPPOPOTAMUS', 'WOLF', 'RACCOON', 'HARE', 'OTTER', 'DOLPHIN', 'WHALE', 'CHICK'];

var narr = ['REQXARDE', 'YORSTDAJ', 'AWQPQQQR', 'FJSJAJAA', 'QQWPEEET', 'ALALOIYE', 'BOUILAARW', 'NVOSAQEWW', 'WARTYDIOS', 'SUPARWLISS', 'WQQQAPXXX', 'OOOSAAOEA', 'SSIUDHFWW', 'AWWWEIPP', 'AAZXDOUP', 'SURPAAARJ', 'AALDJWWA', 'WEEJSYSJ', 'REQXARDE', 'YORSTDAJ', 'AWQPQQQR', 'FJSJAJAA', 'QQWPEEET', 'ALALOIYE', 'BOUILAARW'];

var key = ['jQuery', 'Javascript', 'css3', 'stackoverflow', 'html5', 'animation'];

/* This selects a random value from each array */
function narr_val() { return narr[Math.floor(Math.random() * narr.length)]; }
function arr_val() { return arr[Math.floor(Math.random() * arr.length)]; }
function key_val() { return key[Math.floor(Math.random() * key.length)]; }

$( "#foo" ).bind("click tap", function(){

$("#foo").unbind( "click" ); 

//Block 0
$('#foo').fadeIn(1).delay(500).html('Attention!').fadeOut(1,function(){
$('#foo').fadeIn(1).delay(500).html('In 3...').fadeOut(1,function(){
$('#foo').fadeIn(1).delay(500).html('2...').fadeOut(1,function(){
$('#foo').fadeIn(1).delay(500).html('1...').fadeOut(1,function(){
$('#foo').fadeIn(1).delay(500).html('Go!').fadeOut(1,function(){

//Block 1
$('#foo').fadeIn(1).delay(175).html(narr_val()).fadeOut(1,function(){
$('#foo').fadeIn(1).delay(40).html(key_val()).fadeOut(1,function(){
$('#foo').fadeIn(1).delay(175).html(narr_val()).fadeOut(1,function(){
$('#foo').fadeIn(1).delay(320).html(arr_val()).fadeOut(1,function(){
$('#foo').fadeIn(1).delay(40).html(narr_val()).fadeOut(1,function(){

//Block 2
$('#foo').fadeIn(1).delay(175).html(narr_val()).fadeOut(1,function(){
$('#foo').fadeIn(1).delay(40).html(key_val()).fadeOut(1,function(){
$('#foo').fadeIn(1).delay(175).html(narr_val()).fadeOut(1,function(){
$('#foo').fadeIn(1).delay(320).html(arr_val()).fadeOut(1,function(){
$('#foo').fadeIn(1).delay(40).html(narr_val()).fadeOut(1,function(){
 

и т.д. … есть версия с 27 блоками, а другая с 40. Рабочий пример здесь

 }); }); }); }); }); }); }); }); }); }); }); }); }); }); }); });
 

Мои вопросы:
1) Любой другой способ сделать это вместо использования встроенных функций обратного вызова?
2) Любой способ написать этот синтаксис с помощью цикла for или чего-то, что позволило бы избежать записи 27 блоков функций обратного вызова?

Комментарии:

1. Да, вероятно, есть. Вы смотрели на систему очередей? Я не думаю, что вам нужны какие-либо из этих обратных вызовов.

2. Не публикую это как ответ, поскольку я уверен, что есть лучший способ, но здесь это может вам помочь: jsfiddle.net/hnkhZ/1

3. Downvoter, потрудитесь объяснить?

Ответ №1:

Другим подходом было бы использовать встроенную возможность постановки в очередь путем создания вызова .html, который можно поставить в очередь (см. jQuery .queue для получения дополнительной информации):

 function queuedHtml(html) {
    return function () {
        $(this).html(html);
        $(this).dequeue();
    };
}
 

Это можно настроить для использования именованной очереди, а не очереди по умолчанию.

Как только вы это сделаете, вы можете настроить свой код примерно так:

 $("#foo").bind("click tap", function () {
   var $foo = $("#foo");
   $foo.unbind("click");

   $foo.fadeIn(1).delay(500).queue(queuedHtml('Attention!')).fadeOut(1)
       .fadeIn(1).delay(500).queue(queuedHtml('In 3...')).fadeOut(1)
       .fadeIn(1).delay(500).queue(queuedHtml('2...')).fadeOut(1)
       .fadeIn(1).delay(500).queue(queuedHtml('1...')).fadeOut(1)
       .fadeIn(1).delay(500).queue(queuedHtml('Go!')).fadeOut(1);

   for (var i = 0; i < 27;   i) {
       $foo.fadeIn(1).delay(175).queue(queuedHtml(narr_val())).fadeOut(1)
           .fadeIn(1).delay(40).queue(queuedHtml(key_val())).fadeOut(1)
           .fadeIn(1).delay(175).queue(queuedHtml(narr_val())).fadeOut(1)
           .fadeIn(1).delay(320).queue(queuedHtml(arr_val())).fadeOut(1)
           .fadeIn(1).delay(40).queue(queuedHtml(narr_val())).fadeOut(1);
   }

   $foo.fadeIn(1).queue(queuedHtml('Terminated'));
});
 

демонстрационная скрипка

Вы можете еще немного сократить код, проведя рефакторинг некоторой повторяющейся логики — например:

 function addToQueue($el, html, delay) {
    $el.fadeIn(1).delay(delay).queue(queuedHtml(html)).fadeOut(1);
}

$("#foo").bind("click tap", function () {
   var $foo = $("#foo");
   $foo.unbind("click");

   addToQueue($foo, 'Attention!', 500);
   addToQueue($foo, 'In 3...', 500);
   addToQueue($foo, '2...', 500);
   addToQueue($foo, '1...', 500);
   addToQueue($foo, 'Go!', 500);

   for (var i = 0; i < 27;   i) {
       addToQueue($foo, narr_val(), 175);
       addToQueue($foo, key_val(), 40);
       addToQueue($foo, narr_val(), 175);
       addToQueue($foo, arr_val(), 320);
       addToQueue($foo, narr_val(), 40);
   }

   $foo.fadeIn(1).queue(queuedHtml('Terminated'));
});
 

демонстрационная скрипка

или уменьшите его еще больше, проведя немного больший рефакторинг:

 $("#foo").bind("click tap", function () {
   var $foo = $("#foo");
   $foo.unbind("click");

   $.each(['Attention', 'In 3...', '2...', '1...', 'Go!'], function(idx, val) {
       addToQueue($foo, val, 500);
   });

   var delays = [175, 40, 175, 320, 40];
   for (var i = 0; i < 27;   i) {
       $.each([narr_val(), key_val(), narr_val(), arr_val(), narr_val()], function(idx, val) {
           addToQueue($foo, val, delays[idx]);
       });
   }

   $foo.fadeIn(1).queue(queuedHtml('Terminated'));
});
 

демонстрационная скрипка

Комментарии:

1. @dc5: Это потрясающее сокращение длины кода! Также спасибо за пошаговую демонстрацию. Однако остается одна проблема, и я не понимаю почему, заключается в том, что сроки, похоже, не соблюдаются. Самая длинная задержка, 320 мс, возникает для слова animal. Это означает, что это слово должно восприниматься на экране. Скорее, более длительная задержка возникает для не-слова. Похоже, что задержка не применяется к правильному значению массива.

2. В настоящее время логика добавляет задержку перед установкой html, поэтому задержка 320 происходит непосредственно перед установкой animal word. Слово animal отображается в течение 40 мс (последнее значение задержки). Если это не то, что вы хотите, вы можете переключить вызов в очередь html с задержкой: $el.fadeIn(1).queue(queuedHtml(html)).delay(delay).fadeOut(1);

3. хорошо, понял, спасибо! Просто измените переменную delays на: var delays = [40, 175, 40, 175, 320]; делает его в точности похожим на то, что было у меня.

4. Если подумать об этом подробнее, вы можете получить лучший эффект от этой цепочки: fadeOut -> update html -> fadeIn -> delay с более длинными значениями fadeIn / Out, подобными этому: $el.fadeOut(100).queue(queuedHtml(html)).fadeIn(100).delay(delay); В нынешнем виде значения fadeIn / Out на 1 мс, вероятно, не нужны — вы также можете удалить их, если хотите резкого перехода.

5. @Fred — Я вижу, где я ошибся — в вашей версии обратного .html вызова вызов произошел сразу после вызова .delay , поскольку эта версия не находится в очереди. Я не учел это при создании версии в очереди.

Ответ №2:

При работе с асинхронными материалами вам нужно использовать рекурсивные функции вместо циклов for для выполнения итерации. Без использования каких-либо библиотек одним из возможных подходов было бы что-то вроде

 var aims = [
    {delay:500, html:'Attention!'},
    {delay:500, html:'In 3...'},
    ...
];

function loop(i){
    if(i >= anims.length){
        //ANIMATION DONE
    }else{
        var a = anims[i];
        $('#foo').fadeIn(1).delay(a.delay).html(a.html).fadeOut(1,function(){
            loop(i 1);
        });
    }
}

loop(0);
 

Самый прямой способ избежать вложенности обратного вызова в Javascript — использовать именованные обратные вызовы вместо анонимных. Например:

 function f1(){ $('#foo').fadeIn(1).delay(500).html('Attention!').fadeOut(1, f2); }
function f2(){ $('#foo').fadeIn(1).delay(500).html('In 3...').fadeOut(1,f2); }
function f3(){ ... }
 

Конечно, наличие набора жестко закодированных имен f1, f2, f3, fn … подвержено ошибкам и вообще не поддерживается. Что вы можете сделать, так это предоставить каждой пошаговой функции ее продолжение в качестве параметра

 var steps = [
    function (next){ $('#foo').fadeIn(1).delay(500).html('Attention!').fadeOut(1, next); },
    function (next){ $('#foo').fadeIn(1).delay(500).html('In 3...').fadeOut(1, next); },
    ...
];
 

А затем используйте функцию цепочки для последовательного вызова каждого шага, передавая следующий шаг в качестве параметра:

 function chain(steps, onDone){
    function loop(i){
        if(i >= steps.length){
            onDone();
        )else{
            steps[i](function(){
                loop(i 1);
            });
        }
     }
     loop(0);
 }

 chain(steps, function(){ console.log("all done") });
 

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