如何在递归函数中处理 JavaScript 中的回调?

How to handle callbacks in JavaScript within recursive functions?

试图比较 Chrome 中的两个书签子树,我 运行 遇到了异步调用 API 查询书签文件夹子项的麻烦。

function titleComparator (lhs, rhs) {
  return lhs.title < rhs.title ? -1 : lhs.title > rhs.title ? 1 : 0;
}

// Return whether two bookmark trees have equal content
function compare(lhs, rhs) {
  // Not equal if one is a bookmark and another is a folder
  if (('url' in lhs) != ('url' in rhs))
    return false;
  // If both are bookmarks, compare url and title
  if ('url' in lhs && 'url' in rhs)
    return lhs.title == rhs.title && lhs.url == rhs.url;
  // If both are folders, compare contents
  chrome.bookmarks.getChildren(lhs.id, function (lhsChildren) {
    chrome.bookmarks.getChildren(rhs.id, function (rhsChildren) {
      if (lhsChildren.length != rhsChildren.length)
        return false;  // Want to return from compare()
      lhsChildren.sort(titleComparator);
      rhsChildren.sort(titleComparator);
      for (var i = 0; i < lhsChildren.length; i++)
        if (!compare(lhsChildren[i], rhsChildren[i])
          return false;  // Same here
      return true;  // Same here
    });
  });
}

如何在递归函数中处理 JavaScript 中的回调?

return will only ever exit the callee

您将必须提供一个回调,以便异步吐出答案。

在任何地方你都写了一个 return 语句打算被 callback 闭包使用,你应该标准化而不是将它传递给你的回调。

function compareAsync(lhs, rhs, callback) {
  //…
    callback(false); return;
  //…
    callback(lhs.title == rhs.title && lhs.url == rhs.url); return;
  //…
        callback(false); return;  // Want to return from compare()
  //…

  var finished = 0;
  var whetherSuccess = true;
  lhsChildren.forEach(function(iterand, index) {
    compareAsync(iterand, rhsChildren[index], function(result) {
      whetherSuccess &= result;
      if (++finished === lhsChildren.length) {
        callback(whetherSuccess);
      }
    });
  });
}

因此:在找出 lhsChildren 是什么之后,我们启动了一系列异步函数。他们每个人都会在某个时候增加 finished 计数器。他们每个人都有权通过 &= 将总体 whetherSuccess 降级为 false。发现他们是获得答案的最终功能的消费者是将 whetherSuccess 报告给原始 callback 的人。

as explained in detail here

您需要重构您的代码。

不知何故,在异步(通常是延迟的)函数中使用递归来搜索基于树或分层的数据模型似乎不是正确的方法。

我认为应该这样做:

  1. 将逻辑分成几个函数
  2. 使用"lazy loading"避免重复调用getChilden()
  3. 使用递归并定义新的嵌套函数回调
  4. 也将 for 循环重构为递归

查看我未经测试的代码以说明我的意思:

   function compare(lhs, rhs, callback, index, lhsChilds, rhsChilds){

    // Not equal if one is a bookmark and another is a folder
    if (('url' in lhs) != ('url' in rhs)) {
        callback(false);
        return;
    }
    // If both are bookmarks, compare url and title
    if ('url' in lhs && 'url' in rhs) {
        callback(lhs.title == rhs.title && lhs.url == rhs.url);
        return;
    }

    // If both are folders, check parameters and compare contents


    //First, check if the list has already been loaded (code is running inside a recursive step)
    if(lhsChilds != undefined && rhsChilds != undefined){
        compareTwoChilds(lhs, rhs, callback, index, lhsChilds, rhsChilds);
    }
    else{
        index = 0; //first recursion for this tuple (lhs, rhs)
        chrome.bookmarks.getChildren(lhs.id, function (lhsChildren) {
            chrome.bookmarks.getChildren(rhs.id, function (rhsChildren) {
                compareTwoChilds(lhs, rhs, callback, index, lhsChilds, rhsChilds);
            });
        });
    }

}

function compareTwoChilds(lhs, rhs, callback, index, lhsChilds, rhsChilds){
    if (index < lhsChildren.length){ //just for the safety
        if (lhsChildren.length != rhsChildren.length) {
            callback(false);
            return;
        }
        lhsChildren.sort(titleComparator);
        rhsChildren.sort(titleComparator);

        //compare using recursion, with an emtpy lists of rhs and lhs children
        compare(lhsChildren[index], rhsChildren[index], function(compareResult){
            if(!compareResult){
                callback(false); //if the result is false, no more search needed
            }else{ // use recursion again to loop through the next childs using the already loaded childs
                if (++index < lhsChildren.length){
                    compare(lhsChildren[index], rhsChildren[index], callback, index, lhsChilds, rhsChilds)
                }else{
                    callback(true); // the loop has ended,
                }
            }
        });

    }else{
        callback(false); //this should never happen, so not the same...
    }

}

你可以这样调用比较函数:

compare(lhs,rhs, function(result){
   var compareResult = result;
   //carry on with your code here
});
//and not here :-)

首先,我发现 Chrome 有一个 getSubTree() function as well which makes things considerably easier. So if you just want to get it work, use this instead of asynchronously traversing the tree node by node. However, this remains an interesting problem and thanks to a reddit user,我想出了一个可行的解决方案。

compare()是递归调用自身的main函数。但是,由于内部的异步调用,它无法使用其递归调用的 return 值。

// Pass a boolean to the callback indicating whether the recursive contents of
// both bookmarks folders are equal.
function compare(lhs, rhs, callback) {
    // Compare titles except for the top-level folder
    if (lhs.parent_ && lhs.title !== rhs.title) {
        compare_failure(callback);
        return;
    }
    // Compare urls if at least one of the sides is a bookmark
    if ('url' in lhs || 'url' in rhs) {
        if ((lhs.url || null) === (rhs.url || null))
            compare_respond(lhs.parent_, callback);
        else
            compare_failure(callback);
        return;
    }
    // For both sides being folders, we have to take a look at the contents
    chrome.bookmarks.getChildren(lhs.id, function (lhs_children) {
        chrome.bookmarks.getChildren(rhs.id, function (rhs_children) {
            // Compare amount of children
            if (lhs_children.length != rhs_children.length) {
                compare_failure(callback);
                return;
            }
            // Keep track of how many children already reported back
            lhs.all_children = lhs_children.length;
            lhs.equal_children = 0;
            // Let pairs of children compare each other
            lhs_children.sort(bookmark_comparator);
            rhs_children.sort(bookmark_comparator);
            for (var i = 0; i < lhs_children.length; i++) {
                var lhs_child = lhs_children[i];
                var rhs_child = rhs_children[i];
                // Store parent reference so the deeper function can
                // asynchronously respond with the results once finished.
                lhs_child.parent_ = lhs;
                compare(lhs_child, rhs_child, callback);
            }
        });
    });
};

compare_respond() 是用于传播更深层节点的结果的对应物。在上面的 main 函数中用它代替 return。

// Report comparison results back to the parent node. The parent node waits
// until it collected the results from all its children. Then it reports to
// its parent in turn. At the root node, the user callback is executed.
function compare_respond(node, callback) {
    // Collect child results
    node.equal_children++;
    // Respond upwards if we got results from all
    if (node.equal_children == node.all_children) {
        if ('parent_' in node)
            compare_respond(node.parent_, callback);
        else
            callback(true);
    }
};

compare_failure() 用于在我们发现一对不相等的节点时随时中止整个过程。那样的话我们就不用向上报告了。

// Break out of the recursive function and report failure to the user. It's
// safe against being called multiple times so multiple children can report
// failure and the user will only be notified once.
function compare_failure(callback) {
    if ('called' in callback)
        return;
    callback.called = true;
    callback(false);
};

bookmark_comparator() 是一个小帮手,用于对子书签数组进行排序。需要排序来比较两个文件夹的内容,因为我不想依赖项目顺序。

// Comparator to sort lists of bookmark nodes first by title and second by
// url. Take into that folders have to url.
function bookmark_comparator(lhs, rhs) {
    if (lhs.title != rhs.title)
        return lhs.title < rhs.title ? -1 : 1;
    if (lhs.url || null != rhs.url || null)
        return lhs.url || null < rhs.url || null ? -1 : 1;
    return 0;
};