你如何知道一个无限长的承诺链何时已经完全结束?

How do you know when an indefinitely long promise chain has completely finished?

本文关键字:何时 承诺 结束 何知道 无限 一个      更新时间:2023-09-26

我试图使用承诺来强制序列化一系列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的值,确保之前的承诺处理程序链可能在闭包中持有的任何对象都有资格进行垃圾收集。

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

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

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

在这种情况下,这里是我的代码的一个实现,在这方面似乎是"安全的"。它只是为每个.then()处理程序保存全局最后承诺变量的本地副本,当.then()处理程序运行时,如果全局承诺变量仍然具有相同的值,那么我的代码没有将任何更多的项目链接到它上,所以这必须是当前最后的.then()处理程序。它似乎在这个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;
    });
});

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

。已经执行的承诺处理程序(当承诺已经解决时)不再需要,并且隐式地符合垃圾收集的条件。已解决的承诺除了分辨率值之外不会保留任何东西。

你不需要为承诺(异步值)做"良好的内存管理",你的承诺库会自己处理。它必须自动"释放之前的承诺链",如果它不这样做,那就是一个bug。你的模式完全可以正常工作。


你如何知道承诺链何时已经完全完成?

我将采用纯粹的递归方法:

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();
}

(在jsfiddle.net的演示)

当没有添加更多的处理程序时是不可能检测到的。

这实际上是一个无法确定的问题。要显示停止(或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()不会节省内存。如果一个承诺没有做到这一点,那么它就是内存泄漏,你应该针对它提交一个bug。

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

  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;
    });
  }
<<h3>小提琴演示/h3>

您可以将承诺推入数组并使用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);
});

Edit:如果数组可能会变得很长,那么您应该使用第一个选项b/c来移动数组是O(N)。

Edit:正如您所注意到的,没有理由保留数组。一个计数器就可以了。

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++;
});