繁体   English   中英

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

[英]Recursively add property to every node in a tree-like structure and return modified 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();
        });
    })
}

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

从理论上讲,这在同步世界中将是完美的,但是我要做的是将每个节点都作为单个对象进行了进一步处理后,得到一棵新树,就像它作为输入一样。 实际上,我为树中的每个节点获得了多个控制台打印结果,证明了代码在编写时就可以正常工作了……我不希望有单独的打印结果,而是要将整个结果集包装在一个对象中。 理想情况下,该函数应这样调用:

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

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

如果将代码分成多个函数并使用凉爽的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));
}

串行请求

在下面, 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用户带来负担,因此最好使用Promise.all类的addExtraAll

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

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

// same output as above

重构和启发

您是否注意到代码重复? 您好, 相互递归 ...

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM