【问题标题】:search node in a tree using recursive javascript promises使用递归 javascript 承诺在树中搜索节点
【发布时间】:2017-06-22 02:35:47
【问题描述】:

我被困在基于 Javascript 承诺的程序中,无法弄清楚如何让它以承诺格式返回正确的值。

给定一个将节点的子节点作为 PROMISE 返回的 Tree API。例如:

tree.getChildren('/2/4')
.then(function (nodes) {    
    console.log(nodes); // logs 7,8,9 as children of node 4
}

使用tree.getChildren 方法,searchNode 方法应递归尝试在树中查找searchValue,如果找到则返回其路径,否则返回null

下面的方法只是尝试在树路径中搜索节点,但它只是返回undefined,我认为这是由于该方法的异步性质。如何重写代码以兑现承诺并获得所需的行为?

function searchNode(searchValue, path){
    tree.getChildren(path).then(function(children){
        if(children.length>0){
            for(var i = 0; i<children.length; i++){
                if(children[i] == searchValue){
                    return path;
                } else {
                    childPath = searchNode(searchValue, path+"/"+children[i]);
                    if(childPath != null)
                        return childPath;
                }
            }
        }
        else{
            return null;
        }
    })
}

【问题讨论】:

  • 函数searchNode 没有返回任何内容
  • getChildrenthen 承诺总是会返回一些东西,无论多么延迟。我无法将getChildren 的未来返回值与searchNode 的当前返回值联系起来
  • 我认为主要问题在于else 块,其中返回childPath 的代码没有等待对searchNode 的递归调用完成
  • 应该像searchNode(searchValue, path+"/"+children[i]).then(function(childPath){return childPath or null })。但是又一次 searchNode 还没有返回一个承诺,它试图返回一个当前不存在的值

标签: javascript node.js recursion tree promise


【解决方案1】:

当结果异步可用时,函数 searchNode 将需要返回一个 Promise。

但是在两个地方你没有这样做:

  1. 你应该返回第一次调用tree.getChildren

  2. searchNode 的(递归)返回值应该作为一个 Promise 处理,而你却把它当作一个同步结果:

     childPath = searchNode(searchValue, path+"/"+children[i]);
     if (childPath != null) { // ...etc.
    

    这是不可能的。返回值应该是一个promise,所以你需要在它上面调用then方法来获取返回值。

由于您需要迭代几个,也许是所有的孩子,您会得到尽可能多的承诺。但是你可以而且应该只返回一个承诺。

虽然你可以在找不到值的情况下返回null,但在我看来,在这种情况下产生一个被拒绝的承诺更符合承诺的想法。

所以你可以得到第一个承诺的结果,然后如果它解决了,返回那个承诺,但如果它拒绝(即未找到)你应该链接下一个承诺,然后下一个,......直到一个解决,并返回那个。如果它们都没有解决,或者没有孩子,则应返回被拒绝的承诺。

这是建议的代码:

function searchNode(searchValue, path){
    return tree.getChildren(path).then(function(children){
        // First look for the value among the immediate children
        for(let i = 0; i<children.length; i++){
            if(children[i] == searchValue){
                return path;
            }
        }
    // Then look deeper
    return (function loop(i) {
        if (i >= children.length) {
            return Promise.reject("not found"); 
        } else { 
            // after asynchronous result comes in, either
            // continue the loop, or resolve with path
            return searchNode(searchValue, path+"/"+children[i])
                .catch(loop.bind(null, i+1));
        }
    })(0);
}

"use strict";
// Simple implementation of tree
const tree = {
    root: {
        '2': {
            '4': {
                '1': {}
            },
            '7': {}
        },
        '0': {}
    },
    getChildren: function(path) {
        return new Promise(function (resolve) {
            const node = path.split('/').reduce(function (parent, node) {
                return node === '' || !parent ? parent : parent[node];
            }, tree.root);
            resolve(Object.keys(node));
        });
    }
};

function searchNode(searchValue, path){
    return tree.getChildren(path).then(function(children){
        // First look for the value in the immediate children
        for(let i = 0; i<children.length; i++){
            if(children[i] == searchValue){
                return path;
            }
        }
        // Then look deeper
        return (function loop(i) {
            if (i >= children.length) {
                return Promise.reject("not found"); 
            } else { 
                // after asynchronous result comes in, either
                // continue the loop, or resolve with path
                return searchNode(searchValue, path+"/"+children[i])
                    .catch(loop.bind(null, i+1));
            }
        })(0);
    })
}

// Demo
searchNode('1', '').then(function (path) {
    console.log(path);
}, function (reason) {
    console.log(reason);
});

并行搜索替代方法

上述解决方案不会在孩子之间并行查找。相反,它将等待一个节点的搜索结果,然后再决定是否应该为下一个兄弟节点启动搜索。根据tree.getChildren 实现的异步程度,这可能效率低下:

想象一棵树,其中第一个子节点具有 1000 个节点的多级子树,而要查找的值是第二个子节点的直接后代。如果搜索将并行启动,那么您会期望在这种情况下更快地得到结果。另一方面,一旦找到该值,上述代码将不会搜索其他兄弟节点,而通过并行搜索,即使在找到该值并且主要承诺是之后,搜索仍将在“后台”(异步)中继续解决。因此,我们应该确保在找到值后不会启动更深入的搜索。

为了实现这种并行搜索的想法,您将立即在所有子节点上启动 searchNode,并在每个子节点上应用 then 方法来监控哪个是第一个解析的。

为此,您实际上可以定义两个通用实用程序方法——Promise.notPromise.some——就像已经有 Promise.racePromise.all。这些功能可以在"Resolve ES6 Promise with first success?"等其他问答中找到:

// Invert the outcome of a promise
Promise.not = promise => promise.then(x => {throw x}, e => e);
// Resolve when the first promise resolves
Promise.some = iterable => Promise.not(Promise.all(iterable.map(Promise.not)));

或者,您可以为此使用库解决方案,例如 bluebird's Promise.any

然后,您需要添加一些机制,以在主要承诺已经用找到的值解决时停止启动更深入的搜索。为此,只要在它解决时听取主要的承诺和标志就足够了。这可用于停止异步代码以启动任何进一步的搜索。

在您的情况下,您将如何使用 Promise.some 函数:

function searchNode(searchValue, path){
    let resolved = false;

    return (function searchNodeSub(path) {
        return tree.getChildren(path).then(function(children){
            // If we already found the value via another parallel search, quit
            return resolved ? true
                // Otherwise look for the value among the immediate children
                : children.some(child => child == searchValue) ? path
                // If not found there, look deeper -- in parallel
                : Promise.some(children.map(child => searchNodeSub(path+"/"+child)));
        })
    })(path).then( path => (resolved = true, path) ); // register that we have a result
}

注意children.somePromise.some 之间的相似之处。

"use strict";
// Simple implementation of tree
const tree = {
    root: {
        '2': {
            '4': {
                '1': {}
            },
            '7': {}
        },
        '0': {}
    },
    getChildren: function(path) {
        return new Promise(function (resolve) {
            let node = path.split('/').reduce(function (parent, node) {
                return node === '' || !parent ? parent : parent[node];
            }, tree.root);
            resolve(Object.keys(node));
        });
    }
};

// Invert the outcome of a promise
Promise.not = promise => promise.then(x => {throw x}, e => e);
// Resolve when the first promise resolves
Promise.some = iterable => Promise.not(Promise.all(iterable.map(Promise.not)));

function searchNode(searchValue, path){
    let resolved = false;
    
    return (function searchNodeSub(path) {
        return tree.getChildren(path).then(function(children){
            // If we already found the value via another parallel search, quit
            return resolved ? true
                // Otherwise look for the value among the immediate children
                : children.some(child => child == searchValue) ? path
                // If not found there, look deeper -- in parallel
                : Promise.some(children.map(child => searchNodeSub(path+"/"+child)));
        })
    })(path).then( path => (resolved = true, path) ); // register that we have a result
}

// Demo
searchNode('1', '').then(function (path) {
    console.log(path);
}, function (reason) {
    console.log('Not found');
});

【讨论】:

  • @Bergi,感谢您强调这一点。答案已更新。
  • 感谢您的修复,尽管您可能并不是想将字符串 path 作为回调传递。要么将null 传递给then,要么只使用catch
  • 谢谢@Bergi,确实那个非函数参数没有做任何有用的事情。替换为catch
  • @Bergi,我为我的答案添加了一个替代方案,它 确实 使用承诺构造函数来找到解决方案,但我认为在这种情况下,实现预期目标是不可避免的结果。如果您能在方便时对其进行调查,并提供宝贵的反馈意见,我们将不胜感激。
【解决方案2】:

您需要在searchNode 中返回.getChildren() 承诺,以便稍后在返回childPath 时等待它解决:

function searchNode(searchValue, path){
  return tree.getChildren(path).then(function(children){ //return the promise so we can later wait for it to resolve
    if(children.length>0){
        for(var i = 0; i<children.length; i++){
            if(children[i] == searchValue){
                return path;
            } else {
                return searchNode(searchValue, path+"/"+children[i])
                  .then(function(childPath){ //wait for searchNode to complete
                    if (childPath != null) {
                      return childPath;
                    }
                  });
            }
        }
    }
    else{
        return null;
    }
  })
}

【讨论】:

  • 看起来它的方向是正确的。我现在可以写searchNode(42, "/").then(function(value){console.log(value)}) 等待.getChildren() 承诺解决
猜你喜欢
  • 2017-09-26
  • 1970-01-01
  • 2014-03-23
  • 2012-01-26
  • 2017-09-21
  • 1970-01-01
  • 1970-01-01
  • 2015-05-15
  • 1970-01-01
相关资源
最近更新 更多