[英]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.