使用延迟在循环内按顺序链接AJAX调用,避免回调地狱

时间:2017-05-12 10:39:24

标签: jquery ajax promise deferred sequential

我有我认为是一个非常基本的问题,其解决方案出于某种原因完全无法解决。

我有一个HTML表单#sendform,其中包含许多复选框,每个复选框都有一个类.userCheckbox。复选框的数量是不可预测的;它可以是从一到两到几千的任何地方。每个框代表一个用户,如果选中,则该用户应在提交表单时收到电子邮件。

预期的结果是提交表单会向PHP后端发送一个AJAX请求,在发送请求时向父容器(显示一个小轮子)添加一个类loading,然后替换它在请求完成时,相关的successerror类。我不想立刻发送整个大小的复选框,因为(如果我错了,请纠正我),这需要在页面上获得任何视觉反馈之前等待处理所有行。

所以非常基本的骨架是这样的:

$(document).on('submit', "#sendform", function(e) {
    e.preventDefault();
    var $form = $(this);

    $form.find('input.userCheckbox').each(function(i) {
        var $this = $(this);

        if (this.checked) {
            $.ajax({
                url: $form.prop('action'),
                type: $form.prop('method'),
                dataType: 'json',
                data: { /* ... stuff here ... */ }
                beforeSend: function() { /* Add loading class to parent */ },
                success: function(data, textStatus, jqXHR) { /* Add success/fail class to parent depending on data returned */ },
                error: function(jqXHR, textStatus, errorThrown) { /* Add fail class to parent */ }
            });
        }
    });
});

这当然在理论上有效,但在实践中,所有同时被发送的AJAX请求都会造成严重破坏。服务器(一个标准的低端共享服务器)似乎可以处理大约二十个或多或少同时发生的AJAX请求,然后放弃,其余的调用失败,textStatus “错误”和errorThrown“请求太多”。

所以我需要的是一次一个地触发AJAX请求,循环的每次迭代等到上一次AJAX调用完成后再发送另一个 - 但最好也是在没有锁定整个浏览器的情况下,如async: false那样。输入回调地狱,或者至少是(对我而言)在AJAX请求的complete回调中调用的递归函数的非常不优雅的解决方案。

当然,这一定是一个非常普遍的问题,我想,并且正在寻找更好的选择。果然,关于这个主题的变化有a plethora of StackOverflow questions,并且随机互联网人员的几个 1 也提供了各种解决方案。

绝大多数解决方案的共同点是使用延迟对象和承诺是The Way To Go。非常好 - 我从来没有真正使用延迟对象,所以我认为这是一个很好的时间,以确定他们是什么以及它们是如何工作的。事实证明这比预期更棘手,我仍然不确定我是否已经得到它,这可能就是我没有得到它的原因。

我已尝试在此处和其他地方实施至少五种不同的基于承诺的解决方案,我的问题是没有一种可行。部分是因为绝大多数都是六七岁,从那时起,延迟和承诺发生了很大的变化,我无法完全跟踪。

目前,我已在this solution实施了变体,基本上是这样的:

$(document).on('submit', "#sendform", function(e) {
    e.preventDefault();
    var $form = $(this);
    var promise = $.when({});

    $form.find('input.userCheckbox').each(function(i) {
        var $this = $(this);

        if (this.checked) {
            promise = promise.then(sendmail($this, $form));
        }
    });
});

function sendmail($this, $form) {
    var defer = $.Deferred();

    $.ajax({
        url: $form.prop('action'),
        type: $form.prop('method'),
        dataType: 'json',
        data: { /* Stuff here */ },
        beforeSend: function() {
            console.log($this.val() + " start: " + Date.now();
            /* + Add loading class to parent */
        },
        success: function(data, textStatus, jqXHR) { /* Add success/fail class to parent */ },
        error: function(jqXHR, textStatus, errorThrown) { /* Add fail class to parent */ },
        complete: function() {
            console.log($this.val() + " end: " + Date.now());
            defer.resolve();
        }
    });

    return defer.promise();
}

这有效......与顶部代码完全相同。换句话说,它只是一次性地射击所有AJAX请求。在未来冲刺之前,不等待承诺队列中的先前调用完成。我将每个AJAX请求的开始和完成时间记录到控制台,以便检查;当然,这就是我得到的,尝试使用十个复选框: 2

1007 start: 12:12:41.333
1008 start: 12:12:41.341
1009 start: 12:12:41.346
1010 start: 12:12:41.350
1011 start: 12:12:41.355
1012 start: 12:12:41.359
1013 start: 12:12:41.363
1014 start: 12:12:41.367
1015 start: 12:12:41.372
1016 start: 12:12:41.375
1007 end: 12:12:42.140
1008 end: 12:12:42.553
1010 end: 12:12:42.639
1009 end: 12:12:42.772
1011 end: 12:12:42.889
1013 end: 12:12:43.007
1015 end: 12:12:43.157
1016 end: 12:12:43.289
1012 end: 12:12:43.422
1014 end: 12:12:43.570

每个AJAX请求大约需要一秒左右才能完成,但下一个请求会在几毫秒内立即启动。

我明白为什么会这样。

sendform函数返回一个promise对象;但是在AJAX请求的complete回调解析了底层的延迟对象之前,会立即发生这种情况。所以它基本上没有区别:在向队列添加下一次调用之前,没有指示实际等待直到延迟对象被解析。

我在SO和其他地方遇到的所有不同方法都有这个共同点 - 就我所知,它们基本上无法解决实际问题。

所以实际问题是:

是否无法使用延迟/保证对象队列来确保在上一次迭代中发出的请求完成之前,不会在循环迭代中进行AJAX请求?

我是否完全错过了一些令人眼花缭乱的事情,或者误解了延迟和承诺如何运作?

1 是的,我知道 plethora 是女性化的,不是中性的 - 放纵我。

2 实际上并没有使用Date.now()因为Unix时间戳毫秒不是世界上最易读的东西,而只是我在这里遗漏的一个简单的包装函数以免这个答案比现在更长。

4 个答案:

答案 0 :(得分:1)

所以听起来你想要限制并发未完成请求的数量。

在回答之前,有关其他事情的一些想法:

  1. 我怀疑你可以通过" chunking"更好地服务。一组复选框,所以你有(或者说)10或20个请求而不是100个,并以块为单位取回你的结果。

  2. 可能只有一个请求,然后在处理该请求时从服务器逐步获取信息。我已经做了很长时间,以至于当时iframe是最好的方法,但我确信事情已经进行了大约17年。基本上,您发布的资源会将每个结果写入其响应,因为该结果可用,并刷新其输出;服务器将其发送到客户端而不终止连接(因为还有更多的数据),客户端可以读取该部分数据并进行更新。所以在2017年可能值得研究如何做到这一点,而不是狡猾的iframe。 : - )

  3. 另一种方法是发送包含所有数据的单个请求,并使其响应纯粹是已收到请求。然后定期进行代码轮询,以便在结果可用时获得结果。

  4. 有了这个,回到你的实际问题: - )

    从Promise的角度来看,您可以使用我的解决方案in this answer

    const items = /* ...1000 items... */;
    const concurrencyLimit = 10;
    const promise = Promise.all(items.reduce((promises, item, index) => {
        // What chain do we add it to?
        const chainNum = index % concurrencyLimit;
        let chain = promises[chainNum];
        if (!chain) {
            // New chain
            chain = promises[chainNum] = Promise.resolve();
        }
        // Add it
        promises[chainNum] = chain.then(_ => foo(item));
        return promises;
    }, []));
    

    根据你的情况调整,我得到了以下几点:

    $(document).on('submit', "#sendform", function(e) {
        e.preventDefault();
        var $form = $(this);
        var promise = $.when({});
    
        // Get a true array for only the checked checkboxes
        var checkboxes = $form.find('input.userCheckbox:checked').get();
        var concurrencyLimit = 10;
        Promise.all(checkboxes.reduce(function(promises, checkbox, index) {
            // What chain do we add it to?
            var chainNum = index % concurrencyLimit;
            var chain = promises[chainNum];
            if (!chain) {
                // New chain
                chain = promises[chainNum] = Promise.resolve(); // Or $.Deferred().resolve().promise() would work, I think
            }
            // Add it
            promises[chainNum] = chain.then(function() {
                return sendmail($(checkbox), $form);
            });
            return promises;
        }, [])).then(function() {
            // Entire process is done
        });
    });
    

    sendmail进行这些小改动:

    function sendmail($this, $form) {
        return $.ajax({
            url: $form.prop('action'),
            type: $form.prop('method'),
            dataType: 'json',
            data: { /* Stuff here */ },
            beforeSend: function() {
                console.log($this.val() + " start: " + Date.now();
                /* + Add loading class to parent */
            },
            success: function(data, textStatus, jqXHR) { /* Add success/fail class to parent */ },
            error: function(jqXHR, textStatus, errorThrown) { /* Add fail class to parent */ },
            complete: function() {
                console.log($this.val() + " end: " + Date.now());
            }
        });
    }
    

    假设有一个相当最新的jQuery版本,其中$.Deferred的各种问题已被清理并与Promises A / +规范保持一致。

    如果您愿意,我们可以将$.Deferred完全排除在等式之外,通过这些更改sendmail

    function sendmail($this, $form) {
        return new Promise(function(resolve, reject) {
            $.ajax({
                url: $form.prop('action'),
                type: $form.prop('method'),
                dataType: 'json',
                data: { /* Stuff here */ },
                beforeSend: function() {
                    console.log($this.val() + " start: " + Date.now();
                    /* + Add loading class to parent */
                },
                success: function(data, textStatus, jqXHR) {
                    /* Add success/fail class to parent */
                    resolve();
                },
                error: function(jqXHR, textStatus, errorThrown) {
                    /* Add fail class to parent */
                    reject();
                },
                complete: function() {
                    console.log($this.val() + " end: " + Date.now());
                }
            });
        });
    }
    

答案 1 :(得分:1)

HTTP请求很慢。当涉及任何处理时,它在服务器端和客户端都是资源占用。您希望尽可能少地制作它们,同时尽可能快地使服务器响应。

限制请求是一种不得已的方法,而不是随便做的事情。当您的Web应用程序的一个实例因请求过多而导致服务器陷入困境时,您会遇到体系结构问题,而不是处理问题。

通过更改处理方面,您无法真正修复错误的架构。您的代码目前所做的是:

  1. 捕获submit事件
  2. 发出100个Ajax请求,每个复选框一个
  3. ???
  4. 利润
  5. 显然,第二步是错误的,特别是因为唯一的理由似乎是“我希望用户看到中间进展”。这是一个很好的目标,但是让他们等待更长时间并同时锤击服务器不是解决方案。

    按顺序发出100个请求会导致用户等待更长时间,并且只能解决服务器锤击问题。所以这是一个更糟糕的解决方案。

    我的建议是发送表单,因为它将在一个请求中发送 - 并在服务器端工作,以尽可能快地处理该请求。我很确定那里有优化空间。

    在第二步中,您可以考虑如何在等待期间使客户端更具交互性。但请记住 - 发送一个请求并让服务器处理它是您可以获得的最短等待。由于额外的开销,任何发送比一个请求更多的请求当然会更慢。

答案 2 :(得分:0)

尝试这个,但只能使用jQuery 3+,因为回调执行的更改:

$(document).on('submit', "#sendform", function(e) {
    e.preventDefault();
    var $form = $(this);
    var promise = $.when();

    $form.find('input.userCheckbox').each(function(i) {
        var $this = $(this);

        if (this.checked) {
            promise = promise.then(function() {
                return sendmail($this, $form);
            }).then(function(data) {
                //success
            }, function() {
                // fail
            });
        }
    });
});

function sendmail($this, $form) {
    return $.ajax({
        url: $form.prop('action'),
        type: $form.prop('method'),
        dataType: 'json',
        data: { /* Stuff here */ },
        beforeSend: function() {
            console.log($this.val() + " start: " + Date.now();
            /* + Add loading class to parent */
        },
        complete: function() {
            console.log($this.val() + " end: " + Date.now());
        }
    });
}

答案 3 :(得分:-1)

更好的是:做一个ajax请求做PHP处理程序并处理服务器端的数组:

function sendMail(userCheckbox,$from)
{
    $.ajax({
        url: $form.prop('action'),
        type: $form.prop('method'),
        dataType: 'json',
        data: { array:userCheckbox /*and more data here*/ },
        beforeSend: function() {
            console.log($this.val() + " start: " + Date.now();
            /* + Add loading class to parent */
        },
        done: function(response) {
            /*check response and do something like if(response=='success')*/
        },
        complete: function() {
            console.log($this.val() + " end: " + Date.now());
        }
    });
}
$(document).on('submit', "#sendform", function(e) {
    e.preventDefault();
    var $form = $(this);
    var userCheckbox=[];
    $form.find('input.userCheckbox:checked').each(function(i) {
        var $this = $(this);
        if($this.val())
        userCheckbox.push($this.val());
    });
    if(userCheckbox.length>0) sendMail(userCheckbox,$form);
});