递归地向树状结构中的每个节点添加属性并返回修改后的树

时间:2018-05-24 19:51:38

标签: javascript node.js asynchronous recursion tree

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

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();
        });
    })
}

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

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

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

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

2 个答案:

答案 0 :(得分:0)

如果将代码分成多个函数并使用酷async / await语法,则会变得更加容易。 Furst定义了一个异步函数,它可以更新一个节点而无需照顾孩子:

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));
}

答案 1 :(得分:0)

串行请求

下面,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"
// }




现在我们使用axios.get代替fetchExtra。鉴于第一条评论为输入

,我们可以看到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
// }

由于您有一系列评论,我们可以map使用addExtra每个

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

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

&#13;
&#13;
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' ] // <--
//   }
// ]
&#13;
&#13;
&#13;

添加多个字段

以上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 => ({ 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 })))
        )
    )