是否可以利用Promises / A + promises来实现同步时已经解析的语义?

时间:2017-05-20 10:34:49

标签: javascript promise es6-promise

我在内存中实现数据结构 阴影部分大型数据结构存储在网络上的某个地方。 让我们说有问题的数据结构是二叉树。 我希望内存中的树最初只包含根节点, 当用户(或算法)探索时,它应该根据需要从网络中提取节点而变得懒散。

执行此操作的一种自然方法是树节点数据类型提供方法getLeftChild()getRightChild(), 每个子节点立即返回 promise 。 在左子节点已在内存中的树节点上调用getLeftChild()时, 它返回一个已经使用缓存子进行解析的promise; 否则它会启动一个孩子的获取(如果尚未通过之前的调用启动)并返回一个承诺, 当被抓住的孩子最终从网上回来时,被取出的孩子将被保存在记忆中,以便将来用来解决这个承诺。

因此,要在左侧分支下打印节点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);
})();

这一切都很好,在任何一种情况下,调用代码看起来都不太可能。

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

对于每个then回调执行,

Promises/A+确实需要一个新的微任务(至少):

  

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

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

我想知道的是:什么是最简单/最干净的方法 一个类似Promise的对象,其行为与Promises / A + promise完全相同,除了以用于第2.2.4节? 即我希望它有一个then(或then - 类似)方法,即"同步时可用",以便上面的第一个代码段 将在不产生执行上下文的情况下一次执行。

为了避免命名问题/混淆,我很高兴不要在我的同步访问时调用then (由于Promises / A +,现在实际上是一个保留词);相反,我会称之为thenOrNow。 我会调用我的假设类型/实现PromiseOrNow

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

请注意,由于我不打算捣乱任何名为" then"的任何内容, PromiseOrNow偶然可能是Promises / A +,如果这是一个很好的方式。 也许它将是原生Promise.prototype的原型。 这些属性在某些方面会很好,但它们不是要求。

3 个答案:

答案 0 :(得分:1)

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

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构造函数,而不是使用包装器,但主要逻辑是相同的。

关于立即解决的承诺

如果promise以异步方式解析 (即在您对原始承诺调用thenOrNow之后),addThenOrNow的上述实现将只能同步执行回调,例如会是你的情况(假设您的http请求是异步执行的)。但是,如果promise立即(同步)解析,thenOrNow将无法通过本机Promise接口同步获取值。

bluebird等其他库为synchronous inspection提供了方法,因此,如果您包含bluebird,则可以提供一种解决方案,该解决方案也可用于立即解决承诺:

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);
<script src="https://cdnjs.cloudflare.com/ajax/libs/bluebird/3.5.0/bluebird.min.js"></script>

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

答案 1 :(得分:0)

  

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

他们被称为&#34; microtask&#34;因为它们有微观的开销。微任务队列非常快,你不必担心。更好keep consistency而不是为Zalgo而堕落。

  

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

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

  

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

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

  

是否有一种利用现有Promise / A +实施的简洁可靠的方法,如本机Promise?

不,或者至少不是来自其公共API,因为它没有同步检查功能。

答案 2 :(得分:0)

抱歉延迟,但我很忙。如何通过不同的方法来解决实际问题?与其尝试以同步方式解析Promise,而不是几分钟,如何分离任务的同步和异步部分。

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

因此,我们可以从异步数据加载中去除树的转换和延迟生成。

//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));

我不擅长编写例子。与课程一起玩,并根据需要修改/扩展