【问题标题】:Recursively add property to every node in a tree-like structure and return modified tree递归地将属性添加到树状结构中的每个节点并返回修改后的树
【发布时间】:2018-11-04 03:24:28
【问题描述】:

对于线程中的 cmets 树,我有以下数据结构。此结构包含在单个对象中。

comment {
    id: 1,
    text: 'foo',
    children: [
        comment {
            id: 2,
            text: 'foo-child',
            children: []
        },
        comment {
            id: 3,
            text: 'foo-child-2',
            children: []
        }
    ]
},
comment {
    id: 4,
    text: 'bar',
    children: []
}

这是由后端 API 提供的,没有问题。我想要做的是递归地探索这棵树,并且对于每个节点(根节点或子节点)我想执行一个 API 调用并获取一些额外的数据为每个单个节点,拍一些额外的属性,并返回整个树以及添加到每个节点的新键。

function expandVoteData(comments) {
    return new Promise((resolve, reject) => {
        let isAuth = Auth.isUserAuthenticated();
        // 'this' is the vote collection
        async.each(comments, (root, callback) => {
            // First get the vote data
            async.parallel({
                votedata: function(callback) {
                    axios.get('/api/comment/'+root.id+'/votes').then(votedata => {
                        callback(null, votedata.data);
                    });
                },
                uservote: function(callback) {
                    if(!isAuth) {
                        callback(null, undefined);
                    } else {
                        axios.get('/api/votes/comment/'+root.id+'/'+Auth.getToken(), { headers: Auth.getApiAuthHeader() }).then(uservote => {
                            callback(null, uservote.data); // Continue
                        });
                    }
                }
            }, function(error, data) {
                if(error) {
                    console.log('Error! ', error);
                } else {
                    // We got the uservote and the votedata for this root comment, now expand the object
                    root.canVote = isAuth;
                    root.totalVotes = data.votedata.total;
                    root.instance = 'comment';

                    if(data.uservote !== undefined) {
                        root.userVote = data.uservote;
                    }

                    if(root.children && root.children.length > 0) {
                        // Call this function again on this set of children
                        // How to "wrap up" this result into the current tree?
                        expandVoteData(root.children);
                    }
                    callback(); // Mark this iteration as complete
                }
            });
        }, () => {
            // Done iterating
            console.log(comments);
            resolve();
        });
    })
}

它的作用是:接受一个 'cmets' 参数(它是整个树对象),创建一个 Promise,遍历每个叶节点,并在异步请求中执行相应的 API 调用。如果叶节点有任何子节点,则对每个子节点重复该函数。

理论上这在同步世界中可以完美运行,但我需要做的是在每个节点都经过处理后获取新树以进行进一步处理,作为单个对象,就像它作为输入一样。事实上,我为树中的每个单独节点获得了多个控制台打印,证明代码按照编写的方式工作......虽然我不想要单独的打印,我想将整个结果集包装在一个对象中.理想情况下,函数应该这样调用:

expandVoteData(comments).then(expanded => {
    // yay!
});

关于如何执行此操作的任何提示?提前谢谢你。

【问题讨论】:

  • 不要将 Promise 与 async.js 一起使用。
  • 为什么不建议将 Promise 与 async.js 一起使用?
  • 因为回调样式不能很好地与 promise 语法一起使用。来回的转换导致到处都是微妙的错误。例如,您实际上并没有在任何地方处理错误。

标签: javascript node.js asynchronous recursion tree


【解决方案1】:

如果您将代码分成多个函数并使用酷炫的async / await 语法,它会变得更容易。首先定义一个异步函数来更新一个节点而不关心子节点:

async function updateNode(node) {
 const [votedata, uservote] = await Promise.all([
   axios.get('/api/comment/'+root.id+'/votes'),
    axios.get('/api/votes/comment/'+root.id+'/'+Auth.getToken(), { headers: Auth.getApiAuthHeader() })
 ]);

 node.totalVotes = votedata.total;
 node.instance = 'comment';

 if(uservote)
   node.userVote = uservote;
}

要递归更新所有节点,那么简单:

async function updateNodeRecursively(node) {
  await updateNode(node);
  await Promise.all(node.children.map(updateNodeRecursively));
}

【讨论】:

  • 您是在使用Promise.all 来实现并行化吗?
  • 这很好,你能解释一下吗。 1) 如何递归地添加树分支中的所有值,例如我们有嵌套的电子/电流设备。因此,除了显示每个节点/设备消耗的电流之外,b)我如何显示该节点和下面节点的总数。 (当一个节点被移动拖动时有一个刷新事件)
【解决方案2】:

串行请求

下面,addExtra 接受输入 comment 并异步向评论添加附加字段,并且所有评论的 children 递归。

const addExtra = async ({ children = [], ...comment }) =>
  ({ ...comment
   , children: await Promise.all (children.map (addExtra))
   , extra: await axios.get (...)
  })

为了展示这个作品,我们首先介绍一个假数据库。我们可以通过评论id查询评论的额外字段

const DB =
  { 1: { a: "one" }
  , 2: { a: "two", b: "dos" }
  , 4: [ "anything" ]
  }

const fetchExtra = async (id) =>
  DB [id]
  
fetchExtra (2)
  .then (console.log, console.error)
  
// { "a": "two"
// , "b": "dos"
// }

现在我们使用 fetchExtra 代替 axios.get。我们可以看到addExtra 按预期工作,给定第一个评论作为输入

const comments =
  [ /* your data */ ]

const addExtra = async ({ children = [], ...comment }) =>
  ({ ...comment
  , children: await Promise.all (children.map (addExtra))
  , extra: await fetchExtra (comment.id)
  })

addExtra (comments [0])
  .then (console.log, console.error)

// { id: 1
// , text: "foo"
// , children:
//   [ {id: 2
//     , text: "foo-child"
//     , children:[]
//     , extra: { a: "two", b: "dos" } // <-- added field
//     }
//   , { id: 3
//     , text: "foo-child-2"
//     , children:[]
//     }
//   ]
// , extra: { a: "one" } // <-- added field
// }

由于您有一个 cmets 数组,我们可以使用 mapaddExtra 到每一个

Promise.all (comments .map (addExtra))
  .then (console.log, console.error)

// [ { id: 1
//   , text: "foo"
//   , children:
//     [ {id: 2
//       , text: "foo-child"
//       , children:[]
//       , extra: { a: "two", b: "dos" } // <--
//       }
//     , { id: 3
//       , text: "foo-child-2"
//       , children:[]
//       }
//     ]
//   , extra: { a: "one" } // <--
//   }
// , { id: 4
//   , text: "bar"
//   , children:[]
//   , extra: [ 'anything' ] // <--
//   }
// ]

虽然使用Promise.all 对用户来说是一种负担,所以最好有addExtraAll 之类的东西

const addExtraAll = async (comments) =>
  Promise.all (comments .map (addExtra))

addExtraAll (comments)
  .then (console.log, console.error)

// same output as above

重构和启发

您是否注意到代码重复?你好,mutual recursion...

const addExtraAll = async (comments) =>
  Promise.all (comments .map (addExtra))

const addExtra = async ({ children = [], ...comment }) =>
  ({ ...comment
  , children: await Promise.all (children .map (addExtra))
  , children: await addExtraAll (children)
  , extra: await fetchExtra (comment.id)
  })

addExtra (singleComment) // => Promise

addExtraAll (manyComments) // => Promise

在下面您自己的浏览器中验证结果

const addExtraAll = async (comments) =>
  Promise.all (comments .map (addExtra))

const addExtra = async ({ children = [], ...comment }) =>
  ({ ...comment
  , children: await addExtraAll (children)
  , extra: await fetchExtra (comment.id)
  })

const DB =
  { 1: { a: "one" }
  , 2: { a: "two", b: "dos" }
  , 4: [ "anything" ]
  }

const fetchExtra = async (id) =>
  DB [id]
  
const comments =
  [ { id: 1
    , text: "foo"
    , children:
      [ {id: 2
        , text: "foo-child"
        , children:[]
        }
      , { id: 3
        , text: "foo-child-2"
        , children:[]
        }
      ]
    }
  , { id: 4
    , text: "bar"
    , children:[]
    }
  ]

addExtra (comments [0])
  .then (console.log, console.error)

// { id: 1
// , text: "foo"
// , children:
//   [ {id: 2
//     , text: "foo-child"
//     , children:[]
//     , extra: { a: "two", b: "dos" } // <-- added field
//     }
//   , { id: 3
//     , text: "foo-child-2"
//     , children:[]
//     }
//   ]
// , extra: { a: "one" } // <-- added field
// }

addExtraAll (comments)
  .then (console.log, console.error)

// [ { id: 1
//   , text: "foo"
//   , children:
//     [ {id: 2
//       , text: "foo-child"
//       , children:[]
//       , extra: { a: "two", b: "dos" } // <--
//       }
//     , { id: 3
//       , text: "foo-child-2"
//       , children:[]
//       }
//     ]
//   , extra: { a: "one" } // <--
//   }
// , { id: 4
//   , text: "bar"
//   , children:[]
//   , extra: [ 'anything' ] // <--
//   }
// ]

添加多个字段

上面,addExtra 很简单,只需在您的评论中添加一个extra 字段。我们可以添加任意数量的字段

const addExtra = async ({ children = [], ...comment }) =>
  ({ ...comment
  , children: await addExtraAll (children)
  , extra: await axios.get (...)
  , other: await axios.get (...)
  , more: await axios.get (...)
  })

合并结果

除了将字段添加到comment 之外,还可以将获取的数据合并。但是,您应该在这里采取一些预防措施...

const addExtra = async ({ children = [], ...comment }) =>
  ({ ...await fetchExtra (comment.id)
   , ...comment
   , children: await addExtraAll (children)
  })

addExtra (comments [0])
  .then (console.log, console.error)

// { 
// , a: 1 // <-- extra fields are merged in with the comment
// , id: 1
// , text: "foo"
// , children: [ ... ]
// }

注意上面调用的顺序。因为我们首先调用...await,所以获取的数据不可能覆盖评论中的字段。例如,如果fetchExtra(1) 返回{ a: 1, id: null },我们仍然会得到评论{ id: 1 ... }。如果您希望添加的字段可以覆盖评论中的现有字段,那么您可以更改顺序

最后,您可以根据需要进行多次合并

const addExtra = async ({ children = [], ...comment }) =>
  ({ ...await fetchExtra (comment.id)
   , ...await fetchMore (comment.id)
   , ...await fetchOther (comment.id)
   , ...comment
   , children: await addExtraAll (children)
  })

并行请求

上述方法的一个缺点是对额外字段的请求是按串行顺序完成的。

如果我们可以指定一个函数,该函数将我们的注释作为输入并返回我们希望添加的字段的对象,那就太好了。这次我们跳过 await 关键字,以便我们的函数可以自动为我们并行化子请求

addFieldsAll
  ( c => ({ extra: fetchExtra (c.id), other: fetchOther (c.id) })
  , comments
  )
  .then (console.log, console.error)

// [ { id: 1
//   , children: [ ... ] // <-- fields added to children recursively
//   , extra:  ... // <-- added extra field
//   , other: ... // <-- added other field
//   }
// , ...
// ]

这是实现addFieldsAll 的一种方法。另请注意,由于Object.assign 的参数的顺序,描述符可以指定将覆盖输入注释上的字段的字段——例如c =&gt; ({ id: regenerateId (c.id), ... })。如上所述,可以通过根据需要重新排序参数来更改此行为

const addFieldsAll = async (desc = () => {} , comments = []) =>
  Promise.all (comments .map (c => addFields (desc, c)))

const addFields = async (desc = () => {}, { children = [], ...comment}) =>
  Object.assign
    ( comment
    , { children: await addFieldsAll (desc, children) }
    , ... await Promise.all
        ( Object .entries (desc (comment))
            .map (([ field, p ]) =>
              p.then (res => ({ [field]: res })))
        )
    )

【讨论】:

    猜你喜欢
    • 2019-02-26
    • 1970-01-01
    • 2019-11-17
    • 1970-01-01
    • 1970-01-01
    • 2018-12-31
    • 2018-04-04
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多