Javascript:如何使用异步递归树遍历来控制流?

时间:2013-03-28 15:18:36

标签: javascript asynchronous recursion tree control-flow

我需要在树上进行递归,以使用异步操作在特定节点上执行操作。如何控制流量,以便在完成后可以访问节点?

以下是一个示例情况:

data = {
  name: "deven",
  children: [
    { name: "andrew" },
    { name: "donovan" },
    { name: "james",
      children: [
        { name: "donatello" },
        { name: "dan" }
      ]
    },
    { name: "jimmy",
      children: [
        { name: "mike" },
        { name: "dank" }
      ]
    }
  ]
};

我有一个功能,它的目标是遍历树并将所有以' d开头的名称大写。之后,我想将树传递给另一个函数来做更多工作(可能删除名称以' a'开头的所有节点),但只有在完成初始处理之后:

function capitalize_d(node) {
    if(node.name === "d") {
        node.name = node.name.toUpperCase();
    }

    if(node.children != null) {
        for(var i = 0; i < node.children.length; i++) {
            capitalize_d(node.children[i]);
        }
    }
}

function remove_a(node) {
}

capitalize_d(data);

// Should only get called after all the d's have been capitalized.
remove_a(data);

上面的代码工作正常,因为capitalize_d是阻塞的。如果capitalize_d异步递归,我们如何保证remove_a在完成后被调用?请注意setTimeout中的capitalize_d来电。

function capitalize_d(node) {
    setTimeout(function() {

        if(node.name === "d") {
            node.name = node.name.toUpperCase();
        }

        if(node.children != null) {
            for(var i = 0; i < node.children.length; i++) {
                capitalize_d(node.children[i]);
            }
        }

    }, 1);
}

function remove_a(node) {
}

capitalize_d(data);

// Should only get called after all the d's have been capitalized.
remove_a(data);

问题是我们对树的不同分支进行处理同时被解雇了,并且无法判断它何时最终处理完树。

我该如何解决这个问题?

2 个答案:

答案 0 :(得分:3)

让我总结一下我对你的要求的理解:

  • 您有一个数据树,其中每个节点可以通过一组操作异步更改(在您的示例中为capitalize_dremove_a),
  • 您希望在允许下一个节点之前确保每个节点都经过了给定的操作。

我花了10年或更长时间设计实时嵌入式软件,并且相信我,这个领域的要求比大多数网络程序员在他们的整个生活中经历的任何事情都更加苛刻和苛刻。这让我警告你,你似乎在这里走错了路。

我可以想象,你的问题是在某种有意义的结构中组织一组个人数据。某些进程会收集随机信息(您在“示例”中称为“节点”),并且在某些时候您希望将所有这些节点放入一致的单一数据结构(示例中的分层树)

换句话说,您手头有三个任务:

  • 将异步收集节点的数据采集过程
  • 将呈现一致,精炼的数据树的数据生成过程
  • 一个控制器进程,它将同步数据采集和生产(如果上述两个进程足够智能,可能直接用户界面,但不要太依赖它)。

我的建议: 不要尝试同时进行收购和制作

只是为了让您了解您前往的噩梦:

  • 取决于操作的触发方式,树可能永远不会被给定的操作完全处理。我们假设控制软件忘记在几个节点上调用capitalize_dremove_a将永远不会获得绿灯

  • 相反,如果你随机地在树上开火,很可能一些节点会被多次处理,除非你跟踪操作范围以防止对给定节点应用两次相同的转换< / p>

  • 如果您希望remove_a处理能够启动,您可能必须阻止控制软件再发送capitalize_d个请求,否则灯光可能会永远停留在红色状态。你最终会以这种或那种方式对你的请求进行流量控制(或者更糟糕的是:你不会做任何事情,如果操作流程远离你偶然遇到的最佳位置,你的系统很可能会冻死)。

  • 如果操作改变树的结构(显然remove_a),则必须防止并发访问。至少,您应该从节点remove_a开始锁定子树,否则您将允许处理可能异步更改和/或销毁的子树。

这是可行的。我看到很好的男人赚大钱做这个主题的变化。他们通常每周都会花几个晚上在电脑前吃比萨饼,但是,嘿,这就是你怎么能告诉那些吃乳蛋饼的黑客,对吧?...

我假设您在此处发布此问题意味着您并非真的想要这样做。现在,如果你的老板确实引用了一个着名的机器人,我就不能骗你的机会,但是......你有我的同情心。

现在认真的人......这就是我解决问题的方法。

1)在给定时间点拍摄数据快照

你可以使用尽可能多的标准清除原始数据(上次数据采集太旧,输入不正确,无论是什么都可以构建最小的树)。

2)使用快照构建树,然后在此给定快照上应用任何capitalize_d,remove_a和camelize_z操作顺序

同时,数据采集过程将继续收集新节点或更新现有节点,准备拍摄下一个快照。

此外,您可以向前移动部分处理。显然capitalize_d没有利用树结构,因此您可以在构建树之前将capitalize_d应用于快照中的每个节点。您甚至可以更早地应用某些转换,即在每个收集的样本上。 这可以为您节省大量处理时间和代码复杂性。

以一点理论上的唠叨结束,

  • 您的方法是将数据树视为应支持来自数据采集和数据生成过程的并发访问的共享对象,
  • 我的方法是让数据采集过程为数据生成过程提供(异步)一致的数据集,然后可以按顺序处理
  • 所述数据集。

数据生成过程可以按需触发(比如最终用户点击&#34;给我看一些东西&#34;按钮),在这种情况下,反应性会相当差:用户会被卡住观看沙漏或任何Web2.0性感的旋转轮,用于构建和处理树所需的时间(让我们说7-8秒)。

您可以定期激活数据生成过程(每隔10秒为其提供一个新快照,安全地高于数据集的平均处理时间) 。 &#34;给我看一些东西&#34;然后按钮将显示最后一组完成的数据。立即回答,但数据可能比最后收到的样本早10秒。

我很少看到这种情况不被认为是可接受的,特别是当您生成一堆复杂数据时,操作员需要几十秒才能消化。

理论上,我的方法会失去一些反应性,因为处理的数据会稍微过时,但并发访问方法可能会导致软件更慢(大多数肯定是5-10倍更大和更笨)。

答案 1 :(得分:1)

我知道这篇文章很老,但它出现在搜索结果中,并且单独的回复没有提供一个有效的例子,所以这里是我最近做过的修改版本......

function processTree(rootNode, onComplete) {

    // Count of outstanding requests.
    // Upon a return of any request,
    // if this count is zero, we know we're done.
    var outstandingRequests = 0;

    // A list of processed nodes,
    // which is used to handle artifacts
    // of non-tree graphs (cycles, etc).
    // Technically, since we're processing a "tree",
    // this logic isn't needed, and could be
    // completely removed.
    //
    // ... but this also gives us something to inspect
    // in the sample test code. :)
    var processedNodes = [];

    function markRequestStart() {
        outstandingRequests++;
    }

    function markRequestComplete() {
        outstandingRequests--;
        // We're done, let's execute the overall callback
        if (outstandingRequests < 1) { onComplete(processedNodes); }
    }

    function processNode(node) {
        // Kickoff request for this node
        markRequestStart();
        // (We use a regular HTTP GET request as a
        // stand-in for any asynchronous action)
        jQuery.get("/?uid="+node.uid, function(data) {
            processedNodes[node.uid] = data;
        }).fail(function() {
            console.log("Request failed!");
        }).always(function() {
            // When the request returns:
            // 1) Mark it as complete in the ref count
            // 2) Execute the overall callback if the ref count hits zero
            markRequestComplete();
        });

        // Recursively process all child nodes (kicking off requests for each)
        node.children.forEach(function (childNode) {
            // Only process nodes not already processed
            // (only happens for non-tree graphs,
            // which could include cycles or multi-parent nodes)
            if (processedNodes.indexOf(childNode.uid) < 0) {
                processNode(childNode);
            }
        });

    }

    processNode(rootNode);
}

以下是使用QUnit的示例用法:

QUnit.test( "async-example", function( assert ) {
    var done = assert.async();

    var root = {
        uid: "Root",
        children: [{
            uid: "Node A",
            children: [{
                uid: "Node A.A",
                children: []
            }]
        },{
            uid: "Node B",
            children: []
        }]
    };

    processTree(root, function(processedNodes) {
        assert.equal(Object.keys(processedNodes).length, 4);
        assert.ok(processedNodes['Root']);
        assert.ok(processedNodes['Node A']);
        assert.ok(processedNodes['Node A.A']);
        assert.ok(processedNodes['Node B']);
        done();
    });
});