【问题标题】:can Promises/A+ promises be leveraged to implement synchronous-when-already-resolved semantics?可以利用 Promises/A+ 承诺来实现同步时已经解决的语义吗?
【发布时间】:2017-10-20 10:57:46
【问题描述】:

我正在内存中实现一个数据结构 它掩盖了存储在网络上某处的大型数据结构的一部分。 假设有问题的数据结构是二叉树。 我希望内存树最初只包含根节点, 随着用户(或算法)的探索,它应该通过按需从网络上获取节点来惰性增长。

做到这一点的一种自然方法是为树节点数据类型提供方法getLeftChild()getRightChild()、 每个都立即为各自的子节点返回一个 promise。 当 getLeftChild() 在其左子节点已在内存中的树节点上调用时, 它返回一个已经用缓存的孩子解决的承诺; 否则它会启动对孩子的获取(如果尚未由先前的调用启动)并为它返回一个承诺, 当获取的孩子最终从网络上回来时,获取的孩子将被保存在内存中以备将来使用并用于解决承诺。

所以,要打印左分支下 5 层的节点,我会说:

root.getLeftChild()
    .then(child0 => child0.getLeftChild())
    .then(child00 => child00.getLeftChild())
    .then(child000 => child000.getLeftChild())
    .then(child0000 => child0000.getLeftChild())
    .then(child00000 => {
  console.log("child00000 = ", child00000);
});

或(感谢@Thomas):

const lc = node => node.getLeftChild();
Promise.resolve(root)
    .then(lc).then(lc).then(lc).then(lc).then(lc)
    .then(child00000 => {
  console.log("child00000 = ", child00000);
});

或者,同样的事情使用async/await

(async()=>{
  let child0 = await root.getLeftChild();
  let child00 = await child0.getLeftChild();
  let child000 = await child00.getLeftChild();
  let child0000 = await child000.getLeftChild();
  let child00000 = await child0000.getLeftChild();
  console.log("child00000 = ",child00000);
})();

这一切都很好,调用代码在任何一种情况下都不会太糟糕。

我唯一的疑虑是,在二叉树(或任何类似的链接数据结构)中探索时 已经在内存中,我不想承受启动新微任务的开销 每次我想从内存数据结构中的一个节点到邻居时。 想象一个算法,其核心计算执行数百万次这样的链接跟踪操作。

Promises/A+ 对于每个then 回调执行确实需要一个新的微任务(至少):

2.2.4 在执行上下文堆栈仅包含平台代码之前,不得调用 onFulfilled 或 onRejected。 [3.1]。

我相信async/await 也有类似的要求。

我想知道的是:最简单/最干净的制作方法是什么 一个类似 Promise 的对象,其行为与 Promises/A+ 承诺完全相同,except 用于第 2.2.4 条? IE。我希望它有一个“同步时可用”的then(或then-like)方法,以便上面的第一个代码sn-p 将在不产生执行上下文的情况下一次性执行。

为避免命名问题/混淆,我很乐意避免调用我的同步可用访问器 then (由于 Promises/A+,这实际上是一个保留字);相反,我将其称为thenOrNow。 我将把我假设的类型/实现称为PromiseOrNow

我是否必须从头开始编写 PromiseOrNow,或者是否有一种简洁可靠的方法来利用现有的 Promises/A+ 实现,例如原生 Promise

请注意,因为我不打算弄乱任何名为“then”的东西, PromiseOrNow 可能顺带符合 Promises/A+,如果结果证明这是一种好方法的话。 也许它是从原生Promise.prototype 中继承的原型。 这些属性在某些方面会很好,但它们不是必需的。

【问题讨论】:

  • 关于你的 sn-p 的一件事,避免回调地狱:root.getLeftChild().then(child0 => child0.getLeftChild()).then(child00 => child00.getLeftChild()).then(child000 => child000.getLeftChild()).then(child0000 => child0000.getLeftChild()).then(child00000 => { console.log("child00000 = ",child00000); }); 甚至更好,DRY:const lc = node => node.getLeftChild(); 然后Promise.resolve(root).then(lc).then(lc).then(lc).then(lc).then(lc).then(child00000 => { console.log("child00000 = ",child00000); });
  • @Thomas,谢谢!你的重写很棒。显然我仍然是一个有承诺的新手,否则我会注意到回调地狱。我会将这些编辑到问题中并归因于您。关于 async/await 版本的任何类似见解?
  • 好的,我已经编辑了@Thomas 的改进。我看不出如何重写 await/async 版本,否则它会变成某种嵌套地狱,所以我将保持原样。

标签: javascript promise es6-promise


【解决方案1】:

抱歉耽搁了,但我很忙。解决实际问题的不同方法怎么样?与其尝试以同步的方式解决 Promises,而是节省几毫秒,不如将任务的同步和异步部分分开。

准确地说:这里的异步部分是加载二叉树中特定节点的数据。即使树是懒惰构建的,遍历也不必是异步的。

因此我们可以将树的遍历和延迟生成与异步数据加载解耦。

//sync traversion:
var node = root.getOrCreate('left', 'right', 'right', 'left', 'right');

//wich is a shorthand for the more verbose:
var child0 = root.getOrCreateLeft();
var child01 = child0.getOrCreateRight();
var child011 = child01.getOrCreateRight();
var child0110 = child011.getOrCreateLeft();
var node = child0110.getOrCreateRight();

到目前为止,一切都是(虽然很懒)老式的同步代码。现在是异步部分,访问节点的数据。

node.then(nodeData => console.log("data:", nodeData));
//or even
var nodeData = await node;
console.log(nodeData);
//or
var data = await root.getOrCreate('left', 'right', 'right', 'left', 'right');

到实现:

class AsyncLazyBinaryTree {
    constructor(config, parent=null){
        if(typeof config === "function")
            config = {load: config};

        //tree strucute
        this.parent = parent;
        this.left = null;
        this.right = null;

        //data-model & payload
        this.config = config;
        this._promise = null;

        //start loading the data
        if(config.lazy || config.lazy === undefined) 
            this.then();
    }

    get root(){
        for(var node = this, parent; parent=node.parent; node = parent);
        return node;
    }       

///// These methods are responsible for the LAZY nature of this tree /////

    getOrCreateLeft(){ return _goc(this, "left") }

    getOrCreateRight(){ return _goc(this, "right") }

    getOrCreate(...args){
        if(args.length === 1 && Array.isArray(args[0]))
            args = args[0];

        var invalid = args.find(arg => arg !== "left" && arg !== "right");
        if(invalid)
            throw new Error("invalid argument "+ invalid);

        for(var node = this, i=0; i<args.length; ++i)
            node = _goc(node, args[i]);

        return node;
    }

///// These methods are responsible for the ASYNC nature of this tree /////

    //If this node looks like a promise, quacks like a promise, walks like a promise, ... 
    //you can use it as a Promise of the data they represent
    then(a,b){ 
        if(!this._promise){
            this._promise = Promise.resolve().then(() => this.config.load(this));
        }

        return this._promise.then(a,b);
    }

    catch(fn){ return this.then(null, fn); }    

    //to force the node to reload the data
    //can be used as `node.invalidate().then(...)`
    //or `await node.invalidate()`
    invalidate(){
        this._promise = null;
        return this;
    }

}

//private
function _goc(node, leftOrRight){
    if(!node[leftOrRight])
        node[leftOrRight] = new AsyncLazyBinaryTree(node.config, node);
    return node[leftOrRight];
}

还有一个基本的例子

//A utility to delay promise chains.
/* use it as:   somePromise.then(wait(500)).then(...)
    or          wait(500).then(...);
    or          wait(500).resolve(value).then(...)
    or          wait(500).reject(error).then(...);
*/
var wait = ((proto) => delay => Object.assign(value => new Promise(resolve => setTimeout(resolve, delay, value)), proto))({
    then(a,b){ return this().then(a,b) },
    resolve(value){ return this(value) },
    reject(error){ return this(error).then(Promise.reject.bind(Promise)) }
});




//initializing a tree
var root = new AsyncLazyBinaryTree({
    //load the data as soon as the Node is generated
    lazy: true, 

    //this method will be called (once) for each node that needs its data.
    load(node){
        var path = this.getPath(node);

        console.log('load', path, node);

        //create an artificial delay, then return the payload
        return wait(1500).resolve({
            ts: new Date(),
            path: path
        });

        //but maybe you need some data from the parentNode, to actually load/generate the current data:

        //node.parent is `null` for the root node, 
        //that's why I wrap that into a Promise.resolve()
        //so for the rootNode, parentData is now null;
        return Promise.resolve(node.parent)
            .then(parentData => {
                //do something with the parentData
                return wait(500).resolve({
                    ts: new Date(),
                    path: path,
                    parent: parentData,
                });
            });
    },


    //an utility to be used by load(). 
    //the tree doesn't care if you add methods or data to the config
    //it's all passed through the whole tree.
    getPath(node){
        var path = "";
        for(var n = node, p; p = n.parent; n=p){
            var leftOrRight = n === p.left? "left": n === p.right? "right": "";
            if(!leftOrRight) throw new Error("someone messed up the tree");
            path = "." + leftOrRight + path;
        }
        return "root" + path;
    },
});

var node = root.getOrCreate("left", "right", "left");

//just to be perfectly clear
//the config is simply passed through the tree, and you can (ab)use it to store additional data about the tree.
console.log("same config", root.config === node.config);

node.then(nodeData => console.log("data:", nodeData));

我不擅长编造例子。玩一下这个类并根据需要修改/扩展它

【讨论】:

    【解决方案2】:

    我不想承受每次启动新微任务的开销

    它们被称为“微任务”,因为它们的开销很小。微任务队列真的很快,你不用担心。更好的 keep consistency 而不是爱上 Zalgo。

    除了第 2.2.4 条之外,制作行为与 Promises/A+ 承诺完全相同的类似 Promise 的对象的最简单/最干净的方法是什么?

    使用具有此功能的现有实现。例如,Q v0.6 contained an asap method

    我是否必须从头开始编写 PromiseOrNow

    不,您可以从适合您的 Promise 库开始。

    是否有一种简洁可靠的方式来利用现有的 Promise/A+ 实现,例如原生 Promise?

    没有,或者至少在它不具备同步检查功能时不会来自其公共 API。

    【讨论】:

    • 关于微任务的微观性:我在一个简单的链表中运行了一些简单的链接计时,在 macbook 上的 chrome 59 中。看起来我每秒可以做 90,000 node = node.nexts,而每秒可以做 80 node = await node.nexts。这是一个超过 1000 倍的减速,这对我来说并不奇怪,因为我对微任务创建和上下文切换所涉及的内容有一个粗略的了解。有没有让你吃惊?
    • 是的,这确实让我感到惊讶。我原本预计可能会放缓 2 到 10 倍。然而,我猜最大的开销是promise 创建本身——await 中隐含的Promise.resolve 调用——在当前的实现中没有被优化掉。您需要与创建承诺对象但使用同步检查的测试用例进行比较,就像您的 thenOrNow 方法一样。
    【解决方案3】:

    您可以使用 thenOrNow 方法和以下包装函数扩展标准承诺:

    function addThenOrNow(p) {
        let value, resolved;
        p.then( response => (value = response, resolved = 1) )
         .catch( err => (value = err, resolved = -1) );
        p.thenOrNow = (fulfilled, rejected) => 
            resolved > 0 ? Promise.resolve(fulfilled ? fulfilled(value) : value)
            : resolved   ? Promise.reject (rejected  ? rejected (value) : value)
                         : p.then(fulfilled, rejected); // default then-behaviour
        return p;
    }
    
    // Demo 
    const wait = ms => new Promise( resolve => setTimeout(resolve, ms) );
    const addSlow = (a, b) => wait(100).then(_ => a + b);
    const prom = addThenOrNow(addSlow(2, 3));
    
    prom.then(value => console.log('promise for adding 2 and 3 resolved with', value));
    setTimeout(_ => {
        // At this time the promise has been resolved.
        let sum;
        prom.then(response => sum = response);
        // above callback was executed asynchronously
        console.log('sum after calling .then is', sum); 
        prom.thenOrNow(response => sum = response);
        // above callback was executed synchronously
        console.log('sum after calling .thenOrNow is', sum); 
    }, 200);

    您可以创建自己的 myPromise 构造函数,而不是使用包装器,但主要逻辑是相同的。

    关于立即解决的承诺

    thenOrNow 的上述实现只有在承诺异步解决时才能同步执行回调(即在您在原始承诺上调用 addThenOrNow 之后),就像您的情况一样(假设您的 http 请求是异步执行的)。但是,如果 Promise 立即(同步)解析,thenOrNow 将无法通过本机 Promise 接口同步获取值。

    其他库,如bluebird 提供synchronous inspection 的方法,因此如果您包含bluebird,您可以提供一个同样适用于立即解决promise 的解决方案:

    function addThenOrNow(p) {
        p.thenOrNow = (fulfilled, rejected) => 
            p.isFulfilled() ? Promise.resolve(fulfilled ? fulfilled(p.value()) : p.value())
            : p.isRejected()? Promise.reject (rejected  ? rejected (p.reason()) : p.reason())
                            : p.then(fulfilled, rejected); // default then-behaviour
        return p;
    }
    
    // Demo 
    const prom = addThenOrNow(Promise.resolve(2+3));
    
    let sum;
    prom.then(response => sum = response);
    console.log('sum after calling then is', sum);
    prom.thenOrNow(response => sum = response);
    console.log('sum after calling thenOrNow is', sum);
    &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/bluebird/3.5.0/bluebird.min.js"&gt;&lt;/script&gt;

    但同样,由于您的场景本质上是异步的(从 HTTP 请求中获取响应),您可以使用任一解决方案。

    【讨论】:

    • 问题是当使用then设置resolved(应该叫“fulfilled”顺便说一句)时,它仍然只异步发生;所以addThenOrNow(Promise.resolve(…)).thenOrNow(…) 不会同步运行回调。
    • 确实,对于立即解决的承诺,这是不合适的。
    • @Bergi 这是一个好点,但如果第一个 thenOrNow() 被延迟到下一个微任务,我认为这不是问题。我更关心以下 999,999 个电话。也许我可以在问题中更清楚地说明这一点。
    猜你喜欢
    • 2019-01-02
    • 2013-05-15
    • 2021-10-01
    • 2016-05-25
    • 2022-01-21
    • 2018-04-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多