我有以下代码
var data = [
{ id: "0" },
{
id: "1",
children: [
{
id: "1.1",
children: [
{
id: "1.1.1",
children: [
{
id: "1.1.1.1",
children: [
{ id: "1.1.1.1.1" },
{ id: "1.1.1.1.2" },
{ id: "1.1.1.1.3" }
]
},
{ id: "1.1.1.2" },
{ id: "1.1.1.3" }
]
},
{ id: "1.1.2" },
{ id: "1.1.3" },
]
},
{ id: "1.2" },
{ id: "1.3" }
]
},
{ id: "2" },
{ id: "3" }
];
function recursive(current) {
var first = current[0];
current.shift();
var remaining = current;
console.log(first.id);
if (first.children) {
setTimeout(function(){
recursive(first.children);
})
}
if (remaining.length) {
setTimeout(function(){
recursive (remaining);
});
}
}
recursive(data);
由于setTimeout
,此输出不按顺序排列问题:
答案 0 :(得分:4)
纠结的电线
递归和异步是单独的概念,但我们没有理由不能一起使用它们。首先,我们将看一些同步遍历,然后添加对异步的支持。我喜欢这种答案,因为我们可以看到以多种方式表示的相同程序。我们专注于小型更改,以提供重大影响。
我们从使用生成器的一种方法开始 -
const Empty =
Symbol ()
const breadthFirst = function* ([ node = Empty, ...nodes ])
{
if (node === Empty)
return
else
(yield node, yield* breadthFirst (nodes.concat (node.children || [])))
}
const data =
[{ id: "0" },{id: "1",children: [{id: "1.1",children: [{id: "1.1.1",children: [{id: "1.1.1.1",children: [{ id: "1.1.1.1.1" },{ id: "1.1.1.1.2" },{ id: "1.1.1.1.3" }]},{ id: "1.1.1.2" },{ id: "1.1.1.3" }]},{ id: "1.1.2" },{ id: "1.1.3" },]},{ id: "1.2" },{ id: "1.3" }]},{ id: "2" },{ id: "3" }]
for (const x of breadthFirst (data))
console.log (x.id)
// 0 1 2 3 1.1 1.2 1.3 1.1.1 ... 1.1.1.1.3
收集数组中的所有id
字段
const values =
Array.from (breadthFirst (data), x => x.id)
console.log (values)
// [ '0', '1', '2', '3', '1.1', '1.2', ... '1.1.1.1.3' ]
或者,我们可以将breadthFirst
设为更高阶函数,非常类似于Array.prototype.map
或Array.prototype.reduce
const Empty =
Symbol ()
const breadthFirst = (f = identity, [ node = Empty, ...nodes]) =>
node === Empty
? []
: [ f (node), ...breadthFirst (f, nodes.concat (node.children || [])) ]
const data =
[{ id: "0" },{id: "1",children: [{id: "1.1",children: [{id: "1.1.1",children: [{id: "1.1.1.1",children: [{ id: "1.1.1.1.1" },{ id: "1.1.1.1.2" },{ id: "1.1.1.1.3" }]},{ id: "1.1.1.2" },{ id: "1.1.1.3" }]},{ id: "1.1.2" },{ id: "1.1.3" },]},{ id: "1.2" },{ id: "1.3" }]},{ id: "2" },{ id: "3" }]
const values =
breadthFirst (x => x.id, data)
console.log (values)
// [ '0', '1', '2', '3', '1.1', '1.2', ... '1.1.1.1.3' ]
我们可以使用Promise
使breadthFirst
成为异步函数
const breadthFirst = (f = identity, [ node = Empty, ...nodes]) =>
node === Empty
? Promise.resolve ([])
: breadthFirst (f, nodes.concat (node.children || [])) .then (answer => [ f (node), ...answer ])
const promiseOfValues =
breadthFirst (x => x.id, data)
promiseOfValues.then (console.log, console.error)
// [ '0', '1', '2', '3', '1.1', '1.2', ... '1.1.1.1.3' ]
最后,我们也可以使用户提供的函数f
异步
const breadthFirst = (f = identity, [ node = Empty, ...nodes]) =>
node === Empty
? Promise.resolve ([])
: Promise.resolve (node) .then (f) .then (value =>
breadthFirst (f, nodes.concat (node.children || [])) .then (answer =>
[ value, ...answer ]))
const promiseOfValues =
breadthFirst (x => new Promise (r => setTimeout (r, 250, x.id)), data)
promiseOfValues.then (console.log, console.error)
// => Promise
// 4 seconds later ...
// [ '0', '1', '2', '3', '1.1', '1.2', ... '1.1.1.1.3' ]
最后再次使用lol,使用新的async
/ await
语法
const breadthFirst = async (f = identity, [ node = Empty, ...nodes]) =>
{
if (node === Empty)
return []
const value =
await Promise.resolve (node) .then (f)
const answer =
await breadthFirst (f, nodes.concat (node.children || []))
return [ value, ...answer ]
}
const promiseOfValues =
breadthFirst (x => new Promise (r => setTimeout (r, 250, x.id)), data)
promiseOfValues.then (console.log, console.error)
// => Promise
// 4 seconds later ...
// [ '0', '1', '2', '3', '1.1', '1.2', ... '1.1.1.1.3' ]
走向通用
递归是一种功能性遗产,功能性编程就是可重用性。高于breadthFirst
承担了许多责任。除了建立正确的节点序列之外,我们还需要考虑Promise API和如何将序列连接在一起;这是一种负担,可以解除。下面,我们可以使用反向折叠使该过程更通用 - unfold
const unfold = (f, init) =>
f ( (x, next) => [ x, ...unfold (f, next) ]
, () => []
, init
)
const nextLetter = c =>
String.fromCharCode (c.charCodeAt (0) + 1)
const alphabet =
unfold
( (next, done, c) =>
c > 'z'
? done ()
: next (c, nextLetter (c))
, 'a'
)
console.log (alphabet)
// [ a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z ]
Array.prototype.reduce
获取一组值,将缩减为单个值 - unfold
只需一个值,将展开到一个集合值
const fib = (n = 0) =>
unfold
( (next, done, [ n, a, b ]) =>
n < 0
? done ()
: next (a, [ n - 1, b, a + b ])
, [ n, 0, 1 ]
)
console.log (fib (20))
// [ 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765 ]
好的,但您希望异步展开 - 只需添加async
和await
关键字
const asyncUnfold = async (f, init) =>
f ( async (x, acc) => [ x, ...await asyncUnfold (f, acc) ]
, async () => []
, init
)
让我们用一个设计较少的函数来演示它,例如异步getChildren
。在实际程序中,这可能需要一个节点或节点id,并从数据库中获取它,返回节点子节点的Promise。下面,我们看到breadthFirst
的复杂性大幅降低。请注意,程序员不会受到Promise复杂性的影响
const getChildren = (node) =>
new Promise ((resolve, _) =>
setTimeout (resolve, 250, node.children || []))
const Empty =
Symbol ()
const breadthFirst = (nodes) =>
asyncUnfold
( async (next, done, [ node = Empty, ...rest ]) =>
node === Empty
? done ()
: next (node.id, [ ...rest, ...await getChildren (node) ])
, nodes
)
breadthFirst (data) .then (console.log, console.error)
// => Promise
// 4 seconds later ...
// [ '0', '1', '2', '3', '1.1', '1.2', ... '1.1.1.1.3' ]
事实证明,你不想要广度优先遍历,你想要深度优先。这里使用的方法的一个优点是我们可以针对各种遍历使用相同的通用unfold
函数 - 下面我们实现与depthFirst
相同的breadthFirst
,但这次我们做了一个微小的改变
const breadthFirst = (nodes) =>
const depthFirst = (nodes) =>
asyncUnfold
( async (next, done, [ node = Empty, ...rest ]) =>
node === Empty
? done ()
// breadth-first
next (node.id, [ ...rest, ...await getChildren (node) ])
// depth-first
: next (node.id, [ ...await getChildren (node), ...rest ])
, nodes
)
depthFirst (data) .then (console.log, console.error)
// => Promise
// 4 seconds later ...
// [ '0', '1', '1.1', '1.1.1', '1.1.1.1', '1.1.1.1.1', '1.1.1.1.2', ..., '2', '3' ]
<强>备注强>
关于您的data
的最终评论是许多人在建模分层数据树时所犯的错误。在data
对象中,每个项目都是一个节点,children
的每个项目都是一个节点;但是,data
本身不是一个节点,它只是一个普通的数组。这种不一致是一个痛点,实际上会使你的程序不那么通用。
还记得上面关于折叠(reduce
)和unfold
的说法吗? reduce
获取一个集合并生成一个值,unfold
则相反。遍历树时,我们从单节点开始 - 而不是节点数组。
const breadthFirst = (nodes) =>
const breadthFirst = (node) =>
asyncUnfold
( async (next, done, [ node = Empty, ...rest ]) =>
node === Empty
? done ()
: next (node.id, [ ...rest, ...await getChildren (node) ])
, nodes
, [ node ]
)
const depthFirst = (nodes) =>
const depthFirst = (node) =>
asyncUnfold
( async (next, done, [ node = Empty, ...rest ]) =>
node === Empty
? done ()
: next (node.id, [ ...await getChildren (node), ...rest ])
, nodes
, [ node ]
)
breadthFirst ({ id: 'root', children: data }) .then (console.log, console.error)
// => Promise
// 4 seconds later ...
// [ 'root', '0', '1', '2', '3', '1.1', '1.2', ... '1.1.1.1.3' ]
depthFirst ({ id: 'root', children: data }) .then (console.log, console.error)
// => Promise
// 4 seconds later ...
// [ 'root', '0', '1', '1.1', '1.1.1', '1.1.1.1', '1.1.1.1.1', '1.1.1.1.2', ..., '2', '3' ]
完成程序演示
const asyncUnfold = async (f, init) =>
f ( async (x, acc) => [ x, ...await asyncUnfold (f, acc) ]
, async () => []
, init
)
const Empty =
Symbol ()
const depthFirst = (node) =>
asyncUnfold
( async (next, done, [ node = Empty, ...rest ]) =>
node === Empty
? done ()
: next (node.id, [ ...await getChildren (node), ...rest ])
, [ node ]
)
const breadthFirst = (node) =>
asyncUnfold
( async (next, done, [ node = Empty, ...rest ]) =>
node === Empty
? done ()
: next (node.id, [...rest, ...await getChildren (node) ])
, [ node ]
)
const getChildren = (node) =>
new Promise ((resolve, _) =>
setTimeout (resolve, 250, node.children || []))
const data =
[{ id: "0" },{id: "1",children: [{id: "1.1",children: [{id: "1.1.1",children: [{id: "1.1.1.1",children: [{ id: "1.1.1.1.1" },{ id: "1.1.1.1.2" },{ id: "1.1.1.1.3" }]},{ id: "1.1.1.2" },{ id: "1.1.1.3" }]},{ id: "1.1.2" },{ id: "1.1.3" },]},{ id: "1.2" },{ id: "1.3" }]},{ id: "2" },{ id: "3" }]
breadthFirst ({ id: 'foo', children: data }) .then (console.log, console.error)
// => Promise
// 4 seconds later ...
// [ 'foo', '0', '1', '2', '3', '1.1', '1.2', ... '1.1.1.1.3' ]
depthFirst ({ id: 'bar', children: data }) .then (console.log, console.error)
// => Promise
// 4 seconds later ...
// [ 'bar', '0', '1', '1.1', '1.1.1', '1.1.1.1', '1.1.1.1.1', '1.1.1.1.2', ..., '2', '3' ]
答案 1 :(得分:3)
一般来说,当你想进行广度优先迭代时,你需要使用一个队列(即FIFO)。 Javascript没有本机队列数据结构,所以这只是使用一个数组和shift
,但它仍然可以完成工作。
在这里,您只需将所有内容都推送到每个级别的队列中。这可以保证孩子们在父母之后被推进,因此你首先要对父母进行迭代。通常使用图表,您还可以跟踪您去过的地方,但由于这是一棵树,因此没有循环。
var data = [ { id: "0" }, { id: "1", children: [ { id: "1.1", children: [ { id: "1.1.1", children: [ { id: "1.1.1.1", children: [ { id: "1.1.1.1.1" }, { id: "1.1.1.1.2" }, { id: "1.1.1.1.3" } ] }, { id: "1.1.1.2" }, { id: "1.1.1.3" } ] }, { id: "1.1.2" }, { id: "1.1.3" }, ] }, { id: "1.2" }, { id: "1.3" } ] }, { id: "2" }, { id: "3" } ];
function recursive(queue) {
var current = queue.shift();
if (current === undefined) return
console.log(current.id)
if (current.children) {
current.children.forEach(node => {
queue.push(node)
})
}
setTimeout(function() {
recursive(queue)
})
}
recursive(data);
编辑 - 深度:
如果你想要深度优先,你基本上使用堆栈而不是队列。这里有点奇怪,因为你关心孩子的顺序,所以我们向后加载堆栈。
var data = [ { id: "0" }, { id: "1", children: [ { id: "1.1", children: [ { id: "1.1.1", children: [ { id: "1.1.1.1", children: [ { id: "1.1.1.1.1" }, { id: "1.1.1.1.2" }, { id: "1.1.1.1.3" } ] }, { id: "1.1.1.2" }, { id: "1.1.1.3" } ] }, { id: "1.1.2" }, { id: "1.1.3" }, ] }, { id: "1.2" }, { id: "1.3" } ] }, { id: "2" }, { id: "3" } ];
function recursive(stack) {
let current = stack.pop()
if (current === undefined) return
console.log(current.id)
if(current.children) {
stack.push(...current.children.reverse())
}
setTimeout(function(){
recursive(stack)
})
}
recursive(data.reverse());