【问题标题】:Nested Execution Flow Control嵌套执行流控制
【发布时间】:2014-05-31 15:52:58
【问题描述】:

我已经阅读了几十个与回调、承诺和其他控制流程的方法相关的答案,但我仍然无法完全理解这项任务,显然是因为我缺乏能力。

我有一个嵌套问题:

  1. test_1()(和其他函数)中,我想确保根据元素在对象中的顺序将行添加到表中;
  2. 我想仅在 test_1 完全完成后执行 test_2 或 test_3 (或两者之后)。实际上正确的序列只有在运行时才能知道(会有一个带有可能序列的开关,如 1,2,3 或 1,3,2 或 1,2,1,3 或 1,3,3,2,等等...)

代码:

$(function () {

        // create table
        tbl = document.createElement('table');
        tbl.className = "mainTbl";
        $("body").append(tbl);    

    });

    function test_1() {
            $.each(obj, function () {
                var img = new Image();

                img.onload = function () {
                    // add row of data to table
                    var row = tbl.insertRow(-1);
                    var c1 = row.insertCell(0);
                    c1.innerHTML = "loaded";
                };

                img.onerror = function () {
                    // add row of data to table
                    var row = tbl.insertRow(-1);
                    var c1 = row.insertCell(0);
                    c1.innerHTML = "not loaded";
                };

                img.src = this.url;
            });
    }

function test_2() {
            $.each(obj, function () {
                var img = new Image();

                img.onload = function () {
                    // add row of data to table
                    var row = tbl.insertRow(-1);
                    var c1 = row.insertCell(0);
                    c1.innerHTML = "loaded";
                };

                img.onerror = function () {
                    // add row of data to table
                    var row = tbl.insertRow(-1);
                    var c1 = row.insertCell(0);
                    c1.innerHTML = "not loaded";
                };

                img.src = this.url;
            });
    }
function test_3() {
            $.each(obj, function () {
                var img = new Image();

                img.onload = function () {
                    // add row of data to table
                    var row = tbl.insertRow(-1);
                    var c1 = row.insertCell(0);
                    c1.innerHTML = "loaded";
                };

                img.onerror = function () {
                    // add row of data to table
                    var row = tbl.insertRow(-1);
                    var c1 = row.insertCell(0);
                    c1.innerHTML = "not loaded";
                };

                img.src = this.url;
            });
    }

我知道按顺序调用这些函数是行不通的,因为它们不会互相等待……我认为 promises 是他们的路要走,但我找不到正确的组合,文档也是如此对我的技能来说很复杂。

构建代码以使其按正确顺序执行的最佳方式是什么?

【问题讨论】:

  • 这段代码在我看来都是同步的,你能指出为什么它会异步运行
  • @RossBille - 在图像的异步加载处理程序中插入行。
  • 如果您立即插入行然后让图像加载,则不会出现异步问题。导致异步问题的原因是您仅在图像加载时才插入行。
  • 可以存储所有中间结果,并且只按顺序添加行,而不管图像加载的顺序是什么,同时保留 iamges 的并行加载,并且可以通过 Promise 完成所有这些操作,但是对于一些可以重新考虑为更简单的解决方案的东西似乎需要做很多工作。但是,我们必须更多地了解您所做工作的实际要求,才能提升到更高的水平,为您考虑更好的解决方案。
  • 另外,为什么test_1()test_2()test_3() 的代码相同?

标签: javascript jquery promise


【解决方案1】:

您使这个问题很难回答,因为您的代码非常抽象,并且缺少大量相关细节,例如您如何称呼test_1()test_2(),等等...,您在做什么图片,obj 是什么,等等...我知道你试图通过省略细节来简化事情,但实际上你只是把它太抽象了,不知道如何回答或你真正想解决什么问题。

在 JS 中,你不能调用某些东西并告诉它等到其他东西完成。当test_1() 完成时,您可以告诉test_2()。或者,您可以注册一个函数序列,并且任何时候任何test_n() 完成,它都可以调用序列中的下一个函数。或者,您可以切换到使用 Promise 并使用 Promise 功能来安排异步事物按顺序运行。

本着您抽象讨论的精神,这是一种对事物进行排序的抽象方式。我做了两个主要更改:

  1. 在您迭代 obj 时会同步添加行和单元格,以保证它们以正确的顺序添加。

  2. 单独的函数被调度,test_2() 在所有调度函数完成之前不会被调用。这是在不调用特定函数名称的情况下进行编排的,方法是创建一个排序的函数列表,然后让每个函数使用 seq.increment()seq.decrement() 让排序器跟踪它何时完成,以便可以调用下一个函数。

我的猜测是,实际的实现不必如此通用,更具体的解决方案可能更简单,但由于您的讨论非常抽象,这是一种按顺序插入行的特定方法以及一种确保函数按顺序调用的抽象方法,并且在所有图像从test_1() 完成之前不会调用test_2()

// an object to maintain a list of functions that can be called in sequence
// and to manage a completion count for each one
function Sequencer() {
    this.list = [];
    this.cnt = 0;
}

Sequencer.prototype.add = function(/* list of function references here */) {
    this.list.push.apply(this.list, arguments);
}

Sequencer.prototype.next = function() {
    var fn = this.list.shift();
    if (fn) {
        fn(this);
    }
}

Sequencer.prototype.increment = function(n) {
    n = n || 1;
    this.cnt += n;
}

// when calling .decrement(), if the count gets to zero
// then the next function in the sequence will be called
Sequencer.prototype.decrement = function(n) {
    n = n || 1;
    this.cnt -= n;
    if (this.cnt <= 0) {
        this.cnt = 0;
        this.next();
    }
}

// your actual functions using the sequencer object
function test_1(seq) {
    seq.increment();
    $.each(obj, function () {
        seq.increment();
        var img = new Image();
        var row = tbl.insertRow(-1);
        var c1 = row.insertCell(0);

        img.onload = function () {
            // add row of data to table
            c1.innerHTML = "loaded";
            seq.decrement();
        };

        img.onerror = function () {
            // add row of data to table
            c1.innerHTML = "not loaded";
            seq.decrement();
        };

        img.src = this.url;
    });
    seq.decrement();
}

function test_2(seq) {
    seq.increment();
    $.each(obj, function () {
        seq.increment();
        var img = new Image();
        var row = tbl.insertRow(-1);
        var c1 = row.insertCell(0);

        img.onload = function () {
            // add row of data to table
            c1.innerHTML = "loaded";
            seq.decrement();
        };

        img.onerror = function () {
            // add row of data to table
            c1.innerHTML = "not loaded";
            seq.decrement();
        };

        img.src = this.url;
    });
    seq.decrement();
}

function test_3(seq) {
    seq.increment();
    $.each(obj, function () {
        seq.increment();
        var img = new Image();
        var row = tbl.insertRow(-1);
        var c1 = row.insertCell(0);

        img.onload = function () {
            // add row of data to table
            c1.innerHTML = "loaded";
            seq.decrement();
        };

        img.onerror = function () {
            // add row of data to table
            c1.innerHTML = "not loaded";
            seq.decrement();
        };

        img.src = this.url;
    });
    seq.decrement();
}

// code to run these in sequence
var s = new Sequencer();

// add all the functions to the sequencer
s.add(test_1, test_2, test_3);

// call the first one to initiate the process
s.next();

仅供参考,我个人永远不会有看起来几乎相同的代码 test_1、test_2 和 test_3,因为我会将它分解为一个我可以传递参数的通用部分,但你已经做了这个抽象,所以我不知道如何考虑你没有提供任何差异或细节的东西。

【讨论】:

  • 太棒了,非常感谢。并为“保持简单”的麻烦感到抱歉......我花了一段时间来欣赏这个解决方案背后的所有推理。它真的很聪明。谢谢!
  • 将其应用到实际代码后,我注意到它有时仍会跳到下一个函数,而无需等待当前函数完成。似乎test_1 的外部seq.decrement() 在第一个内部seq.decrement 更改为触发之前被触发。我在外部 seq.decrement() 中添加了 1 秒的超时时间,它可以解决问题,但我想知道我是否做错了什么。
  • @chris - 出了点问题 - 不需要计时器。外部seq.decrement() 应该在内部之前触发。当图像完成加载时,内部的最后一个。你能告诉我你的代码并解释它什么时候不适合你吗?
  • 这里是代码:pastebin.com/eyGYfEh3 该脚本将尝试从讨厌的网站下载微小的 img 文件以了解它们是否可访问(作为测试 pr0n 阻止程序等的一种方式)。因此,如果您看到与这些网站的连接,我深表歉意...
【解决方案2】:

克里斯,我知道你有一个答案,但还有其他方法可以做这种事情,特别是编写你的 test_ 函数,以便它们返回所有图像已加载或未加载的承诺,并利用这些承诺实现异步序列。

理想情况下,我们应该有一个可用的$.allSettled() 方法,但由于 jQuery 没有这样的方法,我们需要一个解决方法。幸运的是,在这种情况下,解决方法非常简单。

为了紧凑,我尽可能地利用 jQuery 并最终得到以下结果:

$(function () {
    var $tbl = $('<table class="mainTbl"/>').appendTo("body");
    function test_1() {
        //First, use $.map() to build an array of promises from `obj`.
        var promises = $.map(obj, function() {
            return $.Deferred(function(dfrd) {
                var $c1 = $("<td/>").appendTo($("<tr/>").prependTo($tbl));//a concise one-liner to make the required table elements and insert them in the DOM 
                var img = new Image();
                img.onload = dfrd.resolve;//a jQuery Deferred's resolve method is "detachable"
                img.onerror = dfrd.reject;//a jQuery Deferred's reject method is "detachable"
                img.src = this.url;
            }).then(function() {//the workaround for lack of an allSettled method is to return a resolved promise from both the done and fail callbacks of a .then() .
                $c1.html("loaded");
                return $.when(1);//a resolved promise (resolved with 1, though it's not used)
            }, function() {
                $c1.html("not loaded");
                return $.when(0);//a resolved promise (resolved with 0, though it's not used)
            });
        });
        //Now return a new promise, which will be resolved when all images are "settled" (loaded state or  error state).
        return $.when.apply(null, promises);
    }

    function test_2() {
        // identical to test_1(), or a variant as required
    }
    function test_3() {
        // identical to test_1(), or a variant as required
    }

    // Now, build your sequence(s) as array(s). The exact way in which you do this will depend on your application.
    var sequence = [test_1, test_2, test_1, test_3];//some arbitrary sequence of functions, each of which, when executed, will return a promise. 

    // To run the seqnence, you simply exploit `array.reduce()` to build and execute a `.then()` chain, as follows :
    sequence.reduce(function(promise, fn) {
        return promise.then(function(result) {
            return fn();
        });
    }, $.when());

});

所有未测试

自从.reduce() 出现以来,这正在迅速成为此类问题的事实上的解决方案,并且可能更像您最初设想的那样。无论如何,这就是它的本质。实际上,代码可能会稍微复杂一些,例如:

  • 如果您需要将参数传递给 test_ 函数
  • 如果您需要处理错误

最后说明:javascript 对象是一个无序的属性集合。如果您的obj 中的顺序很重要,那么请使用元素数组,而不是属性对象。

【讨论】:

  • jQuery 的$.when() 的一个问题是,一旦给$.when() 的第一个承诺失败,它就会导致主承诺失败。它不会等待所有这些都完成然后传达失败状态。因此,如果一个图像在此处失败,则以下函数调用可能会在前一个函数中加载其他图像之前开始。 $.when() 的这种二进制行为降低了它对许多类型的所需错误处理的有用性(在我看来)。
  • @jfriend00,是的,当然,$.when() 就是这样设计的,但它甚至不是一个遥远的问题。 $.when() 在这里有两种使用方式。首先,表达式$.when()$.when(1)$.when(0) 保证生成已解决的承诺。其次,在表达式$.when.apply(null, promises)中,promises是一个保证被解析的Promise数组;作为缺少 .allSettled() 方法的解决方法,表达式永远不会出现错误。
  • 谢谢,不错的选择。这就是我的想法,尽管 jfriend00 的解决方案更容易理解,至少对我来说是这样。但是您呈现它的方式使其非常清晰和有用。谢谢!
  • 更容易理解!我认为陪审团不在那个问题上。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-08-23
  • 2012-08-13
  • 1970-01-01
  • 2018-09-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多