Понимание замыканий: создание метафункции, которая объединяет функции в очередь

Что касается решения проблемы, у меня есть полностью работающее решение, которое я только что закончил здесь:

// synchronous dynamic script loading. 
// takes an array of js url's to be loaded in that specific order. 
// assembles an array of functions that are referenced more directly rather than 
// using only nested closures. I couldn't get it going with the closures and gave up on it. 

function js_load(resources, cb_done) {
    var cb_list = []; // this is not space optimal but nobody gives a damn 
    array_each(resources, function(r, i) {
        cb_list[i] = function() {
            var x = document.body.appendChild(document.createElement('script'));
            x.src = r;
            console.log("loading "+r);
            x.onload = function() { 
                console.log("js_load: loaded "+r); 
                if (i === resources.length-1) {
                    cb_done();
                } else {
                    cb_list[i+1]();
                }
            }; 
        };
    });
    cb_list[0]();
}

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

Но чего я не могу понять, так это того, почему я никогда не мог заставить его работать.

Это выглядело примерно так.

function js_load(resources, cb_done) {
    var cur_cont = cb_done;
    // So this is an iterative approach that makes a nested "function stack" where 
    // the inner functions are hidden inside the closures. 
    array_each_reverse(resources, function(r) {
        // the stack of callbacks must be assembled in reverse order
        var tmp_f = function() {
            var x = document.body.appendChild(document.createElement('script'));
            x.src = r;
            console.log("loading "+r);
            x.onload = function() { console.log("js_load: loaded "+r); cur_cont(); }; // TODO: get rid of this function creation once we know it works right 
        };
        cur_cont = tmp_f; // Trying here to not make the function recursive. We're generating a closure with it inside. Doesn't seem to have worked :(
    });
    cur_cont();
}

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

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

Мой вопрос таков: возможно ли создать функцию Javascript, которая может принимать функцию в качестве аргумента и дополнять ее списком других функций, создавая замыкания, которые обертывают функции, которые она создает сама?

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

P.S. В приведенном выше коде на эти подпрограммы ссылаются

// iterates through array (which as you know is a hash), via a for loop over integers
// f receives args (value, index)
function array_each(arr, f) {
    var l = arr.length; // will die if you modify the array in the loop function. BEWARE
    for (var i=0; i<l; ++i) {
        f(arr[i], i);
    }
}

function array_each_reverse(arr, f) {
    var l = arr.length; // will die if you modify the array in the loop function. BEWARE
    for (var i=l-1; i>=0; --i) {
        f(arr[i], i);
    }
}

person Steven Lu    schedule 24.03.2013    source источник
comment
К вашему сведению: в JavaScript есть встроенная функция [1, 2, 3].forEach.   -  person Eric    schedule 25.03.2013
comment
Ваша функция рекурсивна, потому что вы не создаете замыкание для value cur_cont. Когда обработчики выполняются, все они используют последнее значение cur_cont.   -  person Eric    schedule 25.03.2013


Ответы (1)


Проблема в том, как вы устанавливали значение cur_cont для каждой новой созданной вами функции и вызывали cur_cont в обратном вызове onload. Когда вы выполняете замыкание, подобное tmp_f, любые свободные переменные, такие как cur_cont, не "замораживаются" до их текущих значений. Если cur_cont вообще изменится, любая ссылка на него из tmp_f будет ссылаться на новое, обновленное значение. Поскольку вы постоянно меняете cur_cont на новую функцию tmp_f, которую вы только что создали, ссылка на другие функции теряется. Затем, когда cur_cont выполняется и завершается, снова вызывается cur_cont. Это точно та же самая функция, которая только что завершила выполнение — отсюда и бесконечный цикл!

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

function js_load(resources, cb_done) {
    var cur_cont = cb_done;
    array_each_reverse(resources, function(r) {
        // the stack of callbacks must be assembled in reverse order

        // Make a new function, and pass the current value of the `cur_cont`
        // variable to it, so we have the correct value in later executions.
        // Within this function, use `done` instead of `cur_cont`;
        cur_cont = (function(done) {

            // Make a new function that calls `done` when it is finished, and return it.
            // This function will become the new `cur_cont`.
            return function() {

                var x = document.body.appendChild(document.createElement('script'));
                x.src = r;
                console.log("loading "+r);
                x.onload = function() {
                    console.log("js_load: loaded "+r);
                    done();
                };
            };
        })(cur_cont);

    });

    // Start executing the function chain
    cur_cont();
}

EDIT: На самом деле это можно сделать еще проще, используя функцию Array.reduce. Концептуально вы берете массив и создаете одну функцию из этого массива, и каждая последующая сгенерированная функция должна зависеть от последней сгенерированной функции. Это проблема, для решения которой было разработано сокращение:

function js_load(resources, done) {
    var queue = resources.reduceRight(function(done, r) {
        return function() {
            var x = document.body.appendChild(document.createElement('script'));
            x.src = r;
            console.log("loading "+r);
            x.onload = function() {
                console.log("js_load: loaded "+r);
                done();
            };
        };
    }, done);

    queue();
};

Обратите внимание, что reduce и reduceRight недоступны для старых браузеров (‹= IE8). Реализация JavaScript находится на странице MDN.

person Tim Heap    schedule 24.03.2013
comment
Сладкий! спасибо за совет по Array.reduce. Здесь работают старые добрые концепции FP: я определенно выбрал трудный путь для решения этой проблемы. - person Steven Lu; 25.03.2013