【问题标题】:How do you know when an indefinitely long promise chain has completely finished?你怎么知道一个无限长的承诺链何时完全结束?
【发布时间】:2015-10-26 08:12:42
【问题描述】:

我试图使用 Promise 来强制序列化一系列 Ajax 调用。每次用户按下按钮时,都会进行一次这些 Ajax 调用。我可以成功序列化这样的操作:

// sample async function
// real-world this is an Ajax call
function delay(val) {
    log("start: ", val);
    return new Promise(function(resolve)  {
        setTimeout(function() {
            log("end: ", val); 
            resolve();
        }, 500);
    });
}

// initialize p to a resolved promise
var p = Promise.resolve();
var v = 1;

// each click adds a new task to 
// the serially executed queue
$("#run").click(function() {
    // How to detect here that there are no other unresolved .then()
    // handlers on the current value of p?
    p = p.then(function() {
        return delay(v++);
    });
});

工作演示:http://jsfiddle.net/jfriend00/4hfyahs3/

但是,这会构建一个可能永无止境的承诺链,因为存储最后一个承诺的变量 p 永远不会被清除。每一个新的操作都只是链接到先前的承诺。所以,我在想,为了良好的内存管理,我应该能够检测到何时没有更多的.then() 处理程序可以在p 的当前值上运行,然后我可以重置p 的值,使确保之前的 Promise 处理程序链可能在闭包中持有的任何对象都符合垃圾回收条件。

所以,我想知道如何在给定的.then() 处理程序中知道在此链中没有更多的.then() 处理程序可以调用,因此,我可以执行p = Promise.resolve() 来重置p 和释放之前的 Promise 链,而不是不断地添加它。

【问题讨论】:

    标签: javascript asynchronous promise


    【解决方案1】:

    ... 这样我就可以重置 p 的值,确保之前的承诺处理程序链可能在闭包中持有的任何对象都可以进行垃圾回收。

    没有。已执行的 Promise 处理程序(当 Promise 已解决时)不再需要并且隐式符合垃圾收集的条件。已解决的 Promise 仅保留分辨率值。

    您不需要为 Promise(异步值)执行“良好的内存管理”,您的 Promise 库会自行处理。它必须自动“释放先前的承诺链”,如果没有,那就是一个错误。您的模式完全可以正常工作。


    你怎么知道承诺链何时完成?

    我会为此采取纯粹的递归方法:

    function extendedChain(p, stream, action) {
         // chains a new action to p on every stream event
         // until the chain ends before the next event comes
         // resolves with the result of the chain and the advanced stream
         return Promise.race([
             p.then(res => ({res}) ), // wrap in object to distinguish from event
             stream                   // a promise that resolves with a .next promise
         ]).then(({next, res}) =>
             next
               ? extendedChain(p.then(action), next, action) // a stream event happened first
               : {res, next:stream};                         // the chain fulfilled first
         );
    }
    function rec(stream, action, partDone) {
        return stream.then(({next}) =>
            extendedChain(action(), next, action).then(({res, next}) => {
                partDone(res);
                return rec(next, action, partDone);
            });
        );
    }
    
    var v = 1;
    rec(getEvents($("#run"), "click"), () => delay(v++), res => {
        console.log("all current done, none waiting");
        console.log("last result", res);
    }); // forever
    

    带有事件流的辅助函数,例如

    function getEvents(emitter, name) {
        var next;
        function get() {
            return new Promise((res) => {
                next = res;
            });
        }
        emitter.on(name, function() {
            next({next: get()});
        });
        return get();
    }
    

    (Demo at jsfiddle.net)

    【讨论】:

    • 是否有一个规范说明了这一点,或者它只是“必须”以这种方式工作,我怎么知道所有当前的实现都正确地做到了这一点?
    • @jfriend00:不幸的是,没有任何 js 规范说明内存管理和垃圾收集(除了 那个 ES 是一种垃圾收集语言)。所以只是一种“必须以这种方式工作”:-)
    • 我的猜测是有泄漏的承诺实现,至少在一些 GC 不太好的 JS 实现中,但我认为除了测试之外没有其他方法可以知道这一点.尽管如此,所有严肃的承诺库都会关注这一点,并且会在您检测到泄漏时很乐意修复它。
    • 这就是让我对依赖特定实现行为感到不舒服的原因。我们在许多平台上广泛实现 Promise 相对“早期”,没有规范说它应该以这种方式工作,我正在编写预期可以在任何地方工作的代码。我作为编码员的多年建议我应该自己处理事情,以确保我的代码即使在实现不完美的情况下也能正常工作,直到我有更好的证据证明不需要这种谨慎。
    • 当您处于灰色地带并且不确定平台中可以依靠什么来解决特定问题时,实际尝试针对该问题测试所有可能的平台并进行快速研究是复杂且昂贵的没有提供关于该问题的任何结论性信息,并且只需几行代码即可防御可能的不可靠性,这可能是一个明智的选择。这就是我要说的。
    【解决方案2】:

    有人告诉我,“好的”承诺实现不会导致从无限增长的承诺链中积累内存。但是,显然没有标准要求或描述这一点(除了良好的编程实践),而且我们有很多新手 Promise 实现,所以我还没有决定依赖这种良好行为是否明智。

    我多年的编码经验表明,当实现是新的时,缺乏所有实现都以特定方式运行并且没有规范说明它们应该以这种方式运行的事实,那么将代码编写为“安全”可能是明智的“尽可能的方式。事实上,仅仅围绕不确定的行为编写代码通常比测试所有相关实现以找出它们的行为方式要少。

    在这方面,这是我的代码的一个实现,在这方面似乎是“安全的”。它只是为每个.then() 处理程序保存全局最后一个承诺变量的本地副本,并且当.then() 处理程序运行时,如果全局承诺变量仍然具有相同的值,那么我的代码没有将任何更多的项目链接到它上面所以这必须是当前最后一个 .then() 处理程序。它似乎在this jsFiddle 中工作:

    // sample async function
    // real-world this is an Ajax call
    function delay(val) {
        log("start: ", val);
        return new Promise(function(resolve)  {
            setTimeout(function() {
                log("end: ", val); 
                resolve();
            }, 500);
        });
    }
    
    // initialize p to a resolved promise
    var p = Promise.resolve();
    var v = 1;
    
    // each click adds a new task to 
    // the serially executed queue
    $("#run").click(function() {
        var origP = p = p.then(function() {
            return delay(v++);
        }).then(function() {
            if (p === origP) {
                // no more are chained by my code
                log("no more chained - resetting promise head");
                // set fresh promise head so no chance of GC leaks
                // on prior promises
                p = Promise.resolve();
                v = 1;
            }
            // clear promise reference in case this closure is leaked
            origP = null;
        }, function() {
            origP = null;
        });
    });
    

    【讨论】:

    • 你不应该忘记做origP = null。不要忘记,我们的假设是then 处理程序(以及它们引用的东西)被泄露了! :-) 或者,只测试v 的值似乎更容易。
    • @Bergi - v 只是为了记录日志。这不是正常代码的一部分。您是在谈论清空origP 以清除引用,以便在.then() 引用被泄露的情况下可以对其进行GCed?本着我试图保护的精神,我想我应该这样做。代码已更新。
    • 是的,完全正确。也许它不应该只在 if 情况下完成,而是总是:-)
    • ……哦,如果 promise 拒绝了怎么办?我们也需要在那里清除它吗?
    【解决方案3】:

    无法检测到何时不再添加处理程序。

    这实际上是一个不可判定的问题。显示停止(或Atm 问题)的减少并不是很困难。如果您愿意,我可以添加一个正式的缩减,但要手动添加:给定一个输入程序,在其第一行放置一个承诺,并在每个 returnthrow 处链接它 - 假设我们有一个解决问题的程序您在这个问题中描述 - 将其应用于输入问题 - 我们现在知道它是否永远运行或没有解决停止问题。也就是说,您的问题至少与停机问题一样困难。

    您可以检测承诺何时“解决”并更新新的承诺。

    这在“last”或“flatMap”中很常见。一个很好的用例是自动完成搜索,您只需要最新的结果。这是 [Domenic 的实现 (https://github.com/domenic/last):

    function last(operation) {
        var latestPromise = null; // keep track of the latest
    
        return function () {
            // call the operation
            var promiseForResult = operation.apply(this, arguments);
            // it is now the latest operation, so set it to that.
            latestPromise = promiseForResult;
    
            return promiseForResult.then(
                function (value) {
                    // if we are _still_ the last value when it resovled
                    if (latestPromise === promiseForResult) {
                        return value; // the operation is done, you can set it to Promise.resolve here
                    } else {
                        return pending; // wait for more time
                    }
                },
                function (reason) {
                    if (latestPromise === promiseForResult) { // same as above
                        throw reason;
                    } else {
                        return pending;
                    }
                }
            );
        };
    };
    

    我改编了 Domenic 的代码并为您的问题记录了它。

    你不能放心地优化它

    理智的承诺实现不会遵守“链上”的承诺,因此将其设置为 Promise.resolve() 不会节省内存。如果一个 Promise 没有这样做,那就是内存泄漏,你应该针对它提交一个错误。

    【讨论】:

    • 我不明白你的第一段。但是,您是否明白我并不是要知道整个程序何时完成链接更多.then() 处理程序。我只是在询问如何从给定的.then() 处理程序中知道它当前是最后一个处理程序并且在它之后没有处理程序(此时此刻)。在 Promise 本身内部,这是已知的,因为当这个 .then() 处理程序返回时,Promise 知道是否有下一个要调用。
    • 至于您的最后一段,我怀疑可能是这种情况,但没有描述该规范的规范以及验证所有当前 Promise 实现是否遵循该规范的能力,我认为这不是可以依赖的安全假设,所以我一直在寻找一种方法来保证我的代码不会随着时间的推移导致内存使用量增加。另外,测试不是一件快速的事情,因为您正在尝试查看是否随着时间的推移积累了内存使用量。
    • 您似乎忘记在代码中包含pending,这非常重要。你“适应”了last.js 的哪些部分?
    【解决方案4】:

    您可以将 Promise 推送到数组中并使用 Promise.all:

    var p = Promise.resolve, 
       promiseArray = [], 
       allFinishedPromise;
    
    function cleanup(promise, resolvedValue) {
        // You have to do this funkiness to check if more promises
        // were pushed since you registered the callback, though.
        var wereMorePromisesPushed = allFinishedPromise !== promise;
        if (!wereMorePromisesPushed) {
            // do cleanup
            promiseArray.splice(0, promiseArray.length);
            p = Promise.resolve(); // reset promise
        }
    }
    
    $("#run").click(function() {
        p = p.then(function() {
            return delay(v++);
        });
        promiseArray.push(p)
        allFinishedPromise = Promise.all(promiseArray);
        allFinishedPromise.then(cleanup.bind(null, allFinishedPromise));
    });
    

    或者,由于您知道它们是按顺序执行的,您可以让每个完成回调从数组中删除该承诺,并在数组为空时重置该承诺。

    var p = Promise.resolve(), 
        promiseArray = [];
    
    function onPromiseComplete() {
        promiseArray.shift();
        if (!promiseArray.length) {
            p = Promise.resolve();
        }
    }
    
    $("#run").click(function() {
        p = p.then(function() {
            onPromiseComplete();
            return delay(v++);
        });
        promiseArray.push(p);
    });
    

    编辑:但是,如果数组可能会变得很长,则应该使用第一个选项 b/c 将数组移动为 O(N)。

    编辑: 正如您所指出的,没有理由保留数组。计数器可以正常工作。

    var p = Promise.resolve(), 
        promiseCounter = 0;
    
    function onPromiseComplete() {
        promiseCounter--;
        if (!promiseCounter) {
            p = Promise.resolve();
        }
    }
    
    $("#run").click(function() {
        p = p.then(function() {
            onPromiseComplete();
            return delay(v++);
        });
        promiseCounter++;
    });
    

    【讨论】:

    • 您的第二种方法实际上不需要将承诺存储在数组中,因为您所看到的只是数组何时达到零长度?难道它不能只使用一个递增/递减计数器(递增/递减计数器是我想过的,但希望得到承诺为我维持计数,因为这通常是他们所做的)?
    • 你的第一个选项让我想弄清楚它是如何工作的,或者它是否有效。我还没摸到那个。
    • 如果除了我控制的其他.then() 处理程序链接到给定的承诺之外,您的第二个选项是否可能存在问题?我们的处理程序会被调用,所以我们认为链已经完成,但仍然可能还有其他我们没有明确添加自己的.then() 处理程序仍然处于活动状态,对吧?这就是我担心使用递增/递减计数器的原因。
    • 说得好!您不必保留数组。以增量进行。至于外部回调,您不必担心它们。它们永远不会成为这条链的一部分,它们将成为由外部代码创建的新链的一部分。请记住,每次调用 then 都会创建一个新的承诺。此外,即使对 promise 对象的引用丢失,也保证调用传递给 then 的回调。承诺只是您排队的异步任务的句柄。
    • 我认为您的后两个 sn-ps 不起作用。应该是p = p.then(function() { return delay(v++); }).then(onPromiseComplete);,目前你在delay之前调用onPromiseComplete,这意味着链可能还没有结束。
    【解决方案5】:

    我试图检查我们是否可以在代码中看到承诺的状态,显然这只能从控制台而不是代码中看到,所以我使用了一个标志来监控状态,不确定某处是否存在漏洞:

      var p
        , v = 1
        , promiseFulfilled = true;
    
    
    
      function addPromise() {
        if(!p || promiseFulfilled){
          console.log('reseting promise...');
          p = Promise.resolve();
        }
        p = p.then(function() {
            promiseFulfilled = false;
            return delay(v++);
        }).then(function(){
          promiseFulfilled = true;
        });
      }
    

    fiddle demo

    【讨论】:

    • 这似乎确实有效,尽管我很难理解为什么多个 .then() 处理程序都设置相同的全局变量时没有问题。
    • 这有一个可能的竞争条件,其中addPromise 可能在promiseFulfilled = true 和下一个promiseFulfilled = false 之间被调用。
    猜你喜欢
    • 1970-01-01
    • 2018-08-08
    • 1970-01-01
    • 2017-01-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-07-04
    • 1970-01-01
    相关资源
    最近更新 更多