вторник, 5 марта 2013 г.

Javascript. Остановка setTimeout в рекурсивных функциях

Задача

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

(function () {
    var test = arguments.callee; 
    var t = setTimeout(function () {
        console.log(Math.random()); 
        test()
    }, 3000)
})();
Явный недостаток такого подхода - это невозможность остановить выполнение кода. В данном примере в лог будут сыпаться рандомные числа каждые 3 секунды.

Как все же управлять жизнью таких конструкций?

Варианты решений. 

 

1.Деанонимизация

Самая простая идея - не делать рекурсивную функцию анонимной:

// Сперва объявляем функцию
var test = function () {
    var t = setTimeout(function () {
        console.log(Math.random()); 
        test();
    }, 3000)
};

// Затем, запускаем
(function () {
    test();
})();

Теперь, для остановки просто удаляем нашу функцию:
delete test;

2. Замыкание или инкапсуляция

Основная идея состоит в том, чтобы в выполняемой функции отслеживать состояние некоторой переменной, глобальной по отношению к рекурсивной функции, доступной через замыкание, и, в зависимости от ее состояния, выполнять очередную итерацию или нет. Аналогично можно использовать свойства объекта, если рекурсивная функция является его методом.
var flag = true;
(function () {
    if(flag) {

        var test = arguments.callee;
        var t = setTimeout(function () {
            console.log(Math.random());
            test()
        }, 3000)
;
    }
})();

3. clearTimeout

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

// Объявляем переменную для таймаута
var t; 
(function () {
    var test = arguments.callee; 
    t = setTimeout(function () {
        console.log(Math.random()); 
        test()
    }, 3000)
})();

Останавливаем выполнение:

clearTimeout(t);

Итог.

Конечно, все зависит от конечной реализации, но в приведенных примерах, третий подход гарантированно завершает выполнение кода в момент вызова clearTimeout, в то время, как остальные подходы допускают завершение кода, в теле setTimeout.