8

我有一个网页,其中异步触发了某个 Ajax 事件。这个 Ajax 部分可以被调用一次或多次。我无法控制触发此事件的次数,也无法控制时间。

此外,该 Ajax 部分中的某些代码应该作为临界区运行,这意味着,当它运行时,不应运行该代码的其他副本。

这是一个伪代码:

  1. 运行 JavaScript 或 jQuery 代码
  2. 进入临界区即Ajax(当某个进程在等待响应回调时,不要再次进入该区,直到这个过程完成)
  3. 运行更多 JavaScript 或 jQuery 代码

我的问题是,如何按照上述方式运行第 2 步?如何使用 JavaScript 或 jQuery 创建/保证互斥部分。

我了解理论(信号量、锁等),但我无法使用 JavaScript 或 jQuery 实现解决方案。

编辑

如果您建议使用布尔变量进入临界区,这将不起作用,下面的行将解释原因。

关键部分的代码如下(使用布尔变量建议):

load_data_from_database = function () {

    // Load data from the database. Only load data if we almost reach the end of the page
    if ( jQuery(window).scrollTop() >= jQuery(document).height() - jQuery(window).height() - 300) {

        // Enter critical section
        if (window.lock == false) {

            // Lock the critical section
            window.lock = true;

            // Make Ajax call
            jQuery.ajax({
                type:        'post',
                dataType:    'json',
                url:         path/to/script.php,
                data:        {
                    action:  'action_load_posts'
                },
                success:     function (response) {
                    // First do some stuff when we get a response

                    // Then we unlock the critical section
                    window.lock = false;
                }

            });


            // End of critical section
        }
    }
};

// The jQuery ready function (start code here)
jQuery(document).ready(function() {
    var window.lock = false; // This is a global lock variable

    jQuery(window).on('scroll', load_data_from_database);
});

现在这是使用布尔变量建议的锁定部分的代码。这将无法按以下建议工作:

  1. 用户向下滚动,(并且基于关联jQuery(window).on('scroll', load_data_from_database);触发了多个滚动事件。

  2. 假设几乎同时触发了两个滚动事件

  3. 两者都调用load_data_from_database函数

  4. 第一个事件检查是否window.lock(答案为真,所以如果陈述正确)

  5. 第二个事件检查是否window.lock(答案为真,所以如果陈述正确)

  6. 第一个事件进入if语句

  7. 第二个事件进入if语句

  8. 第一条语句设置window.locktrue

  9. 第二个语句设置window.locktrue

  10. 第一条语句运行 Ajax 临界区

  11. 第二条语句运行 Ajax 临界区。

  12. 两者都完成了代码

正如您所注意到的,这两个事件几乎同时被触发,并且都进入了临界区。所以锁是不可能的。

4

3 回答 3

9

我认为您上面提供的最有用的信息是您对锁定的分析。

  1. 用户向下滚动,(并且基于关联jQuery(window).on('scroll', load_data_from_database);触发了多个滚动事件。

  2. 假设几乎同时触发了两个滚动事件

  3. 两者都调用load_data_from_database函数

  4. 第一个事件检查是否window.lock(答案为真,所以如果陈述正确)

  5. 第二个事件检查是否window.lock(答案为真,所以如果陈述正确)

这立即告诉我,您遇到了一个常见的(并且非常直观的)误解。

Javascript 是异步的,但异步代码与并发代码不同。据我了解,“异步”意味着函数的子例程不一定按照我们在同步代码中所期望的深度优先顺序进行探索。一些函数调用(您调用“ajax”的那些)将被放入队列并稍后执行。这可能会导致一些令人困惑的代码,但没有什么比认为您的异步代码同时运行更令人困惑的了。“并发”(如您所知)是指来自不同函数的语句可以相互交错。

锁和信号量之类的解决方案不是思考异步代码的正确方法。承诺是正确的方式。这就是让网络编程变得有趣和酷的东西。

我不是承诺大师,但这是一个(我认为)演示修复的工作小提琴。

load_data_from_database = function () {

    // Load data from the database. Only load data if we almost reach the end of the page
    if ( jQuery(window).scrollTop() >= jQuery(document).height() - jQuery(window).height() - 300) {

        console.log(promise.state());
        if (promise.state() !== "pending") {
            promise = jQuery.ajax({
                type:        'post',
                url:         '/echo/json/',
                data: {
                    json: { name: "BOB" },
                    delay: Math.random() * 10
                },
                success:     function (response) {

                    console.log("DONE");
                }
            });
        }
    }
};

var promise = new $.Deferred().resolve();

// The jQuery ready function (start code here)
jQuery(document).ready(function() {

    jQuery(window).on('scroll', load_data_from_database);
});

我使用全局承诺来确保事件处理程序的 ajax 部分只被调用一次。如果您在小提琴中上下滚动,您会看到在处理 ajax 请求时,不会发出新请求。ajax 请求完成后,可以再次发出新请求。运气好的话,这就是您正在寻找的行为。

然而,我的回答有一个非常重要的警告:jQuery 的 promises 实现是出了名的被破坏了。这不仅仅是人们说听起来很聪明的东西,它实际上非常重要。我建议使用不同的 Promise 库并将其与 jQuery 混合使用。如果您刚刚开始学习 Promise,这一点尤其重要。

编辑:就个人而言,我最近和你在同一条船上。就在 3 个月前,我认为我使用的一些事件处理程序是交错的。当人们开始告诉我 javascript 是单线程的时,我感到震惊和难以置信。帮助我的是了解触发事件时会发生什么。

在同步编码中,我们习惯于“帧”的“堆栈”,每个帧都代表一个函数的上下文。在 javascript 和其他异步编程环境中,堆栈由队列扩充。当您在代码中触发事件或使用类似该$.ajax调用的异步请求时,您会将事件推送到此队列。下次清除堆栈时将处理该事件。例如,如果您有以下代码:

function () {
    this.on("bob", function () { console.log("hello"); })

    this.do_some_work();
    this.trigger("bob");
    this.do_more_work();
}

这两个函数do_some_workdo_more_work立即一个接一个地触发。然后函数将结束,您入队的事件将开始一个新的函数调用,(在堆栈上)并且“hello”将出现在控制台中。如果您在处理程序中触发事件,或者如果您在子例程中触发和事件,事情会变得更加复杂。

这一切都很好,但是当您想要处理异常时,事情开始变得非常糟糕。当你进入异步领域的那一刻,你就留下了“一个函数应该返回或抛出”的美丽誓言。如果你在一个事件处理程序中,你抛出一个异常,它会在哪里被捕获?这个,

function () {

    try {
        $.get("stuff", function (data) {
            // uh, now call that other API
            $.get("more-stuff", function (data) {
                // hope that worked...
            };
        });
    } catch (e) {
        console.log("pardon me?");
    }
}

现在不会救你了。Promise 允许您通过将回调链接在一起并控制它们返回的时间和地点来收回这个古老而强大的誓言。因此,使用一个不错的 Promise API(不是 jQuery),您可以将这些回调链接起来,让您以您期望的方式冒泡异常,并控制执行顺序。在我看来,这就是承诺的美丽和魔力。

如果我完全离开,有人会阻止我。

于 2014-03-03T17:19:00.633 回答
2

我会推荐一个队列,它一次只允许一个项目运行。这将需要对您的关键功能进行一些修改(尽管不多):

function critical(arg1, arg2, completedCallback) {
    $.ajax({
        ....
        success: function(){
            // normal stuff here.
            ....

            // at the end, call the completed callback
            completedCallback();
        }
    });
}

var queue = [];
function queueCriticalCalls(arg1, arg2) {
    // this could be done abstractly to create a decorator pattern
    queue.push([arg1, arg2, queueCompleteCallback]);

    // if there's only one in the queue, we need to start it
    if (queue.length === 1) {
        critical.apply(null, queue[0]);
    }


    // this is only called by the critical function when one completes
    function queueCompleteCallback() {
        // clean up the call that just completed
        queue.splice(0, 1);

        // if there are any calls waiting, start the next one
        if (queue.length !== 0) {
            critical.apply(null, queue[0]);
        }
    }
}

更新:使用jQuery 的 Promise 的替代解决方案(需要 jQuery 1.8+)

function critical(arg1, arg2) {
    return $.ajax({
        ....
    });
}

// initialize the queue with an already completed promise so the
// first call will proceed immediately
var queuedUpdates = $.when(true);

function queueCritical(arg1, arg2) {
    // update the promise variable to the result of the new promise
    queuedUpdates = queuedUpdates.then(function() {
        // this returns the promise for the new AJAX call
        return critical(arg1, arg2);
    });
}

是的,实现了更简洁代码的承诺。:)

于 2014-03-03T16:32:19.037 回答
-2

您可以将关键部分包装在一个函数中,然后交换该函数,以便在第一次运行后它什么也不做:

  // this function does nothing
function noop() {};

function critical() {
    critical = noop; // swap the functions
    //do your thing 

}

灵感来自用户@I Hate Lazy Function in javascript,只能调用一次

于 2014-03-03T15:58:58.690 回答