首页 文章

可以使用Promises / A promises来实现已经解析的同步语义吗?

提问于
浏览
2

我正在内存中实现一个数据结构,它隐藏了存储在Web上某个地方的大型数据结构的一部分 . 假设所讨论的数据结构是二叉树 . 我希望内存中的树最初只包含根节点,并且应该像用户(或算法)探索的那样,通过从Web上按需获取节点来延迟增长 .

一种自然的方法是使树节点数据类型提供方法 getLeftChild() ,_ getRightChild() ,每个方法立即返回相应子节点的promise . 当在其左子节点已在内存中的树节点上调用 getLeftChild() 时,它将返回已使用缓存子节点解析的promise . 否则它会启动一个孩子的获取(如果尚未通过之前的调用启动)并返回一个承诺,当获取的孩子最终从Web返回时,获取的孩子将被保存在内存中以供将来使用解决这个承诺 .

因此,要在左侧分支下打印节点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 有类似的要求 .

除了第2.2.4节之外,我最简单/最干净的方法是制作类似Promise的对象,其行为与Promises / A的承诺完全相同?即我希望它有 then (或 then -like)方法"synchronous-when-available",这样上面的第一个代码片段将在一次性执行而不会产生执行上下文 .

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

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

请注意,由于我不打算弄乱任何名为“ then ”的东西,所以 PromiseOrNow 偶然可能是Promises / A兼容,如果这是一个很好的方法 . 也许它将是原生的 Promise.prototype 原型 . 这些属性在某些方面会很好,但它们不是要求 .

3 回答

  • 0

    您可以使用以下包装函数使用 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以异步方式解析(即在原始promise上调用 addThenOrNow 之后), thenOrNow 的上述实现将只能同步执行回调,就像您的情况一样(假设您的http请求是异步执行的) . 但是,如果promise立即(同步)解析, thenOrNow 将无法通过本机Promise接口同步获取值 .

    其他库,如 bluebirdsynchronous 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

    我不想每次都要承担启动新微任务的开销

    它们被称为"microtask",因为它们具有微观开销 . 微任务队列非常快,你不必担心 . 更好的keep consistency而不是为Zalgo而堕落 .

    制作类似Promise的对象的最简单/最简洁的方法是什么?承诺/承诺,但第2.2.4条除外?

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

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

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

    是否有一种利用现有Promise / A实现(例如本机Promise)的简洁可靠的方法?

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

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

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

相关问题