你怎么知道一个无限长的承诺链什么时候完成了?

时间:2015-08-04 04:08:48

标签: javascript asynchronous promise

我试图使用promises强制序列化一系列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的当前值上运行,然后我可以重置{的值{1}},确保前一个承诺处理程序链可能在闭包中保留的任何对象都有资格进行垃圾回收。

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

5 个答案:

答案 0 :(得分:3)

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

我多年的编码经验表明,当实现是新的时,缺乏事实,所有实现都以某种方式运行,并且没有规范表明它们应该以这种方式运行,那么将代码编写为“安全”可能是明智之举“尽可能的方式。事实上,围绕不确定行为进行编码往往比测试所有相关实现以了解其行为方式的工作要少。

在这方面,这是我的代码的实现,在这方面似乎是“安全的”。它只保存每个.then()处理程序的全局最后一个promise变量的本地副本,当.then()处理程序运行时,如果全局promise变量仍然具有相同的值,那么我的代码就不再链接了项目到它上面所以这必须是当前最后的.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;
    });
});

答案 1 :(得分:2)

  

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

没有。已经执行的promise处理程序(当promise已经解决时)不再需要并且隐式地有资格进行垃圾收集。除了分辨率值之外,已解决的承诺不会产生任何影响。

您不需要为promises(异步值)执行“良好内存管理”,您的promise库会自行处理。它必须“自动释放以前的承诺链”,如果没有那么那就是一个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();
}

Demo at jsfiddle.net

答案 2 :(得分:1)

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

这实际上是一个不可判定的问题。显示减少停止(或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的代码并记录了它的问题。

您无法安全地优化此

Sane promise实现不会保留“链上”的承诺,因此将其设置为Promise.resolve()将无法节省内存。如果一个promise没有这样做,那就是内存泄漏,你应该提交一个bug来反对它。

答案 3 :(得分:0)

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

  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

答案 4 :(得分:-1)

您可以将promises推送到数组并使用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));
});

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

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