带着承诺走一棵树

Posted

技术标签:

【中文标题】带着承诺走一棵树【英文标题】:walking a tree with Promises 【发布时间】:2016-04-14 13:09:15 【问题描述】:

我有一个要使用 Promises 遍历的树形结构,但我还没有找到正确的代码模式。

假设我们得到了节点名称,并且将节点名称转换为节点的行为是一个异步过程(例如,涉及 Web 访问)。还假设每个节点都包含一个(可能为空的)子名称列表:

function getNodeAsync(node_name) 
    // returns a Promise that produces a node


function childrenNamesOf(node) 
    // returns a list of child node names

我想用这个签名的方法结束:

function walkTree(root_name, visit_fn) 
   // call visit_fn(root_node), then call walkTree() on each of the
   // childrenNamesOf(root_node), returning a Promise that is fulfilled
   // after the root_node and all of its children have been visited.

它返回一个在根节点及其所有子节点都被访问后实现的 Promise,因此它可能被如下调用:

walkTree("grandpa", function(node)  console.log("visiting " + node.name); )
  .then(function(nodes)  console.log("found " + nodes.length + " nodes."));

更新

I've create a gist that shows my first attempt。我对 walkTree() 的(有点错误的)实现是:

function walkTree(node_name, visit_fn) 
    return getNodeAsync(node_name)
        .then(function(node) 
            visit_fn(node);
            var child_names = childrenNamesOf(node);
            var promises = child_names.map(function(child_name) 
                walkTree(child_name, visit_fn);
            );
            return Promise.all(promises);
        );
;

这会以正确的顺序访问节点,但最外层的 Promise 在所有子节点都被访问之前解析。 See the gist for full details.

正如@MinusFour 指出的那样,使用这种技术来展平节点列表是毫无意义的。事实上,我真的只想在所有节点都被访问后触发最终的承诺,所以一个更现实的用例是:

walkTree("grandpa", function(node)  console.log("visiting " + node.name); )
  .then(function()  console.log("finished walking the tree"));

【问题讨论】:

你的想法是好的和直截了当的。 walkTree 方法可以批量处理从访问每个节点返回的承诺,然后当所有这些承诺都已实现时,它会使用承诺数组或您决定它应该解决的任何内容来解决(这样您就可以访问长度,不确定您还想用它做什么)。这将做类似于RSVP.all 可能做的事情(在等待一批承诺解决或如果其中一个拒绝时拒绝承诺)。 您的要点正是正确的方法。唯一的问题是您在 map 回调中错过了 return 声明! 【参考方案1】:

嗯,处理每个节点的函数调用不是什么大问题,但是收集节点值是个问题。走那棵树有点困难,最好的办法是将它映射到没有最终值的树上。你可以使用类似的东西:

function buildTree(root_name) 
    var prom = getNodeAsync(root_name);
    return Promise.all([prom, prom.then(function(n)
        return Promise.all(childrenNamesOf(n).map(child => buildTree(child)))
    )]);

从那里开始:

var flatTree = buildTree(root_name).then(flatArray);
flatTree.then(nodes => nodes.forEach(visit_fn));
flatTree.then(nodes => whateverYouWantToDoWithNodes);

要展平您可以使用的数组:

function flatArray(nodes)
    if(Array.isArray(nodes) && nodes.length)
        return nodes.reduce(function(n, a)
                return flatArray(n).concat(flatArray(a));
        );
     else 
        return Array.isArray(nodes) ? nodes : [nodes];
    

老实说,如果你想要一个节点列表,那么使用 tree walker 是没有意义的,你最好将它展平然后迭代元素,但是如果你愿意,你可以遍历数组树。

【讨论】:

你是对的:这是建立列表的折磨方式。事实上,我真的不打算使用最终的 Promise() 作为一种扁平化结果的方式——我只想要一个在遍历整个树后完成的 Promise。你的回答就是这样。 (不过,我确实打算访问沿途的每个节点。)打勾。 Hrm,之前的数组展平功能有一些问题,这个应该可以工作。 不用担心。 FWIW,您可以消除 buildTree 方法中的 if 子句,因为“一个空的承诺就是一个履行的承诺”。请参阅下面的答案。 @fearless_fool,我不确定当它没有孩子时,你会从childrenNamesOf 获得什么样的价值。如果它返回一个空数组,那么可以确定,你可以把它拿走。如果它返回 undefinednull 或不是可迭代的东西,那么它将拒绝承诺。【参考方案2】:

尽管我在 O.P 中说过,我并不真正关心最终 Promise 的返回值,但我确实想等到所有节点都被遍历完。

最初尝试的问题只是 map() 函数中缺少返回语句。 (尽管看起来,这在结构上与@MinusFour 的答案基本相同。)以下更正形式:

function walkTree(node_name, visit_fn) 
    return getNodeAsync(node_name)
        .then(function(node) 
            visit_fn(node);
            var child_names = childrenNamesOf(node);
            var promises = child_names.map(function(child_name) 
                return walkTree(child_name, visit_fn);
            );
            return Promise.all(promises);
        );
;

下面是 walkTree() 的两个用例。第一个简单地按顺序打印节点,然后在树遍历完成时宣布:

walkTree("grandpa", function(node)  console.log("visiting " + node.name); )
  .then(function()  console.log("finished walking the tree"));

第二个创建一个平面节点列表,在树遍历完成时可用:

var nodes = [];
walkTree("grandpa", function(node)  nodes.push(node) )
  .then(function()  console.log('found', nodes.length, 'nodes);
                     console.log('nodes = ', nodes); );

【讨论】:

以上是关于带着承诺走一棵树的主要内容,如果未能解决你的问题,请参考以下文章

Axios 承诺解决/待处理承诺

猫鼬承诺文档说查询不是承诺?

返回使用另一个 PromiseKit 承诺的承诺

承诺 - 是不是可以强制取消承诺

JS 承诺里面的承诺

猫鼬承诺和Q承诺