简体   繁体   English

带有setTimeout的递归JS函数

[英]Recursive JS function with setTimeout

I have the following code 我有以下代码

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

This output is not in order because of the setTimeout 由于setTimeout,此输出不按顺序排列

在此输入图像描述

Question: 题:

  1. How can I change it to output the something like in the image below? 如何更改它以输出如下图所示的内容?
  2. How do I know the last iteration in this recursive function? 我怎么知道这个递归函数的最后一次迭代? I need to run something once the list is exhausted. 列表耗尽后我需要运行一些东西。

I cant use forEach because I have to use setTimeouts for a different reason. 不能使用forEach因为我必须使用setTimeouts的原因不同。 I understand setTimeout does not work properly in a loop. 我知道setTimeout在循环中不能正常工作。 Any ideas???? 有任何想法吗????

Desired output: 期望的输出:

在此输入图像描述

Tangled wires 纠结的电线

Recursion and asynchrony are separate concepts but there's no reason we can't use them together. 递归和异步是单独的概念,但我们没有理由不能一起使用它们。 First, we'll look at some synchronous traversals then add support for asynchrony as we go. 首先,我们将看一些同步遍历,然后添加对异步的支持。 I like this style of answer because we get to see the same program represented in multiple ways. 我喜欢这种答案,因为我们可以看到以多种方式表示的相同程序。 We focus on a small changes that deliver big impact. 我们专注于产生重大影响的变化。

We start with one approach using generators — 我们从使用发电机的一种方法开始 -

 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 

Collect all the id fields in an array 收集数组中的所有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' ]

Alternatively, we can make breadthFirst a higher-order function, much like Array.prototype.map or Array.prototype.reduce 或者,我们可以使breadthFirst成为一个更高阶的函数,就像Array.prototype.mapArray.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' ] 

We could make breadthFirst an asynchronous function using Promise 我们可以使用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' ]

Lastly, we can make the user-supplied function f asynchronous as well 最后,我们也可以使用户提供的函数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' ]

Lastly again lol, use the new async / await syntax 最后再次使用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' ]

Going generic 走向通用

Recursion is a functional heritage and functional programming is all about reusability. 递归是一种功能性遗产,功能性编程就是可重用性。 Above breadthFirst takes on many responsibilities. 以上breadthFirst首先承担了许多责任。 Beyond building the correct sequence the nodes, we need to think about the Promise API and how to wire the sequence together; 除了构建正确的节点序列之外,我们还需要考虑Promise API以及如何将序列连接在一起; this is a burden and it can be lifted. 这是一种负担,可以解除。 Below, we can make the process more generic using a reverse fold – unfold 下面,我们可以使用反向折叠 - 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 takes a collection of values and reduces it to a single value – unfold takes a single value and unfolds it to a collection of values 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 ]

OK, but you wanted asynchronous unfolding - simply add the async and await keywords 好的,但是你想要异步展开 - 只需添加asyncawait关键字即可

const asyncUnfold = async (f, init) =>
  f ( async (x, acc) => [ x, ...await asyncUnfold (f, acc) ]
    , async () => []
    , init
    )

Let's demo this with a less contrived function, such as an asynchronous getChildren . 让我们用一个设计较少的函数来演示它,比如异步的getChildren In a real program, this might take a node or a node id and fetch it from a database returning a Promise of the node's children. 在实际程序中,这可能需要一个节点或节点id,并从数据库中获取它,返回节点子节点的Promise。 Below, we see a dramatic reduction in complexity in breadthFirst . 下面,我们看到breadthFirst复杂性大幅降低。 Note the programmer is not burdened by the complexities of Promise here 请注意,程序员不会受到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' ]

As it turns out, you didn't want a breadth-first traversal, you wanted depth-first . 事实证明,你不想要广度优先遍历,你想要深度优先 An advantage of the approaches used here is that we can utilize the same generic unfold function for various traversals – below we implement depthFirst identical to breadthFirst but this time we make one tiny change 这里使用的方法的一个优点是我们可以为各种遍历使用相同的通用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' ]

remarks 备注

A final comment about your data is a mistake that many people make when modeling hierarchical trees of data. 关于您的data最终评论是许多人在建模分层数据树时所犯的错误。 In your data object, each item is a Node, and each item of children is a Node; 在你的data对象,每一项都是一个节点,而每个项目children是一个节点; however, data itself is not a Node, it is just a plain Array. 但是, data本身不是Node,它只是一个普通的数组。 This inconsistency is a pain point and actually makes your program less versatile. 这种不一致是一个痛点,实际上使你的程序不那么通用。

Remember what I said about fold ( reduce ) and unfold above? 还记得上面关于折叠( reduce )和unfold吗? reduce takes a collection and produces one value, unfold does the opposite. reduce收集并产生一个值, unfold则相反。 When traversing a tree, we start with a single node — not an array of nodes. 遍历树时,我们从单个节点开始 - 而不是节点数组。

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' ]

Complete program demonstration 完成程序演示

 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' ] 

Generally speaking when you want to do breadth-first iteration, you need to use a queue (ie. FIFO). 一般来说,当您想要进行广度优先迭代时,您需要使用队列(即FIFO)。 Javascript doesn't have a native queue data structure, so this just uses an array and shift , but it still gets the job done. Javascript没有本机队列数据结构,所以这只是使用数组和shift ,但它仍然可以完成工作。

Here you just push everything into the queue at each level. 在这里,您只需将所有内容推送到每个级别的队列中。 This insures the children get pushed in after the parents, and therefore you iterate over the parents first. 这可以保证孩子们在父母之后被推进,因此你首先要对父母进行迭代。 Normally with a graph you would also keep track of where you've been, but since this is a tree, there are no loops. 通常使用图表,您还可以跟踪您去过的地方,但由于这是一棵树,因此没有循环。

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

EDIT - FOR DEPTH FIRST: 编辑 - 深度第一:

If you want depth first you basically use a stack rather than a queue. 如果你想要深度优先,你基本上使用堆栈而不是队列。 Here it's a little strange because you care about the order of the children, so we load the stack backwards. 这里有点奇怪,因为你关心孩子的顺序,所以我们向后加载堆栈。

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

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

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