当“循环+递归+异步”,常见错误写法和正确写法讨论

先不考虑异步,先准备一个多维数组

今天我们准备递归查找一个多维数组里的某个元素。现在假定这个多维数组如下:

let array = [
  1,
  2,
  [
    3,
    4
  ],
  5,
  6
];

一个常识

当循环套循环递归时,会先递归到深层,直到不能再递归,然后进行上一层的下一个循环体,这跟我们关于循环嵌套循环的常识是一致的。

所以,也就是说,上面的数组的递归顺序就是1-2-3-4-5-6。

错误的递归函数写法

错误写法1:

function recur(arr) {
  for(let i = 0; i < arr.length; i++) {
    console.log('this is', arr[i]);
    if (Array.isArray(arr[i])) {
      return recur(arr[i]);
    } else {
      if (arr[i] === 6) {
        console.log('result is:', arr[i]);
        return arr[i];
      }
    }
  }
}

console.log(recur(array)); // undefined
image.png

可以看到,根本没有遍历到元素6。原因是for循环出现return会终止循环。

错误写法2:

去掉上例return recur(arr[i]);return。也就是:

function recur(arr) {
  for(let i = 0; i < arr.length; i++) {
    console.log('this is', arr[i]);
    if (Array.isArray(arr[i])) {
      recur(arr[i]);
    } else {
      if (arr[i] === 6) {
        console.log('result is:', arr[i]);
        return arr[i];
      }
    }
  }
}

console.log(recur(array)); // undefined
image.png

成功了?!No,如果把6换成4试试:

function recur(arr) {
  for(let i = 0; i < arr.length; i++) {
    console.log('this is', arr[i]);
    if (Array.isArray(arr[i])) {
      recur(arr[i]);
    } else {
      if (arr[i] === 4) {
        console.log('result is:', arr[i]);
        return arr[i];
      }
    }
  }
}

console.log(recur(array)); // undefined
image.png

为什么命中了4,依然最终得不到4?因为命中4的这条执行,是在内层recur得到的4,在外层并没有继续return这个4。但是如果在外层return,就又成了错误写法1的写法。这个悖论真的没办法了吗?

错误写法3

索性把2个return都去掉?更扯淡,毫无疑问,这样一定不会返回值了。现在我灵机一动,我定义一个result变量来存储深层命中的4,可不可以呢?试试:

function recur(arr) {
  var result;
  for(let i = 0; i < arr.length; i++) {
    console.log('this is', arr[i]);
    if (Array.isArray(arr[i])) {
      recur(arr[i]);
    } else {
      if (arr[i] === 4) {
        console.log('result is:', arr[i]);
        result = arr[i];
      }
    }
  }
  return result;
}

console.log(recur(array)); // undefined
image.png

可以看到,依然获取不到!为什么?

因为在执行recur([3,4])的时候,这个深层作用域的result虽然确实得到了4,也返回给了上层的作用域,但是,上层作用域虽然能拿到这个返回值,但是它根本没有拿,当然不可能输出最终结果。

正确写法1

所以现在,我把recur(arr[i]);的结果赋值给result,也就是result = recur(arr[i]);,再试试:

function recur(arr) {
  var result;
  for(let i = 0; i < arr.length; i++) {
    console.log('this is', arr[i]);
    if (Array.isArray(arr[i])) {
      result = recur(arr[i]);
    } else {
      if (arr[i] === 4) {
        console.log('result is:', arr[i]);
        result = arr[i];
      }
    }
  }
  return result;
}

console.log(recur(array));
image.png
function recur(arr) {
  var result;
  for(let i = 0; i < arr.length; i++) {
    console.log('this is', arr[i]);
    if (Array.isArray(arr[i])) {
      result = recur(arr[i]);
    } else {
      if (arr[i] === 6) {
        console.log('result is:', arr[i]);
        result = arr[i];
      }
    }
  }
  return result;
}

console.log(recur(array));
image.png

可见,无论是想获取4或是6,都可以正确找到。

正确写法2

那么,我觉得result这个变量累赘,不想用这个变量,可不可以呢?也是可以的,这就需要你在本次递归中,要获取到下一次递归的值,然后将值的判断提前到本次。有点拗口,具体请看:

function recur(arr) {
  for(let i = 0; i < arr.length; i++) {
    console.log('this is', arr[i]);
    if (Array.isArray(arr[i])) {
      const result = recur(arr[i]);
      if (result === 4) {
        return result;
      }
    } else {
      if (arr[i] === 4) {
        return arr[i];
      }
    }
  }
}

console.log(recur(array));
image.png
function recur(arr) {
  for(let i = 0; i < arr.length; i++) {
    console.log('this is', arr[i]);
    if (Array.isArray(arr[i])) {
      const result = recur(arr[i]);
      if (result === 6) {
        return result;
      }
    } else {
      if (arr[i] === 6) {
        return arr[i];
      }
    }
  }
}

console.log(recur(array));
image.png

可见,无论是查找4还是查找6,都可以成功找到。而且,有一个细节你有没有发现:

正确写法1中,如果查找4,也依然会遍历到6,而本例(正确写法2)中,却不会遍历到6。原因也是for中写了return,中断了循环。所以:

  • 正确写法1的优势是代码简单,理解容易。缺点是由于return放在最后,意味着所有递归都会执行一遍,存在无意义执行。

  • 正确写法2的优势是由于提前判断,提前return,所以不存在无意义执行,缺点是代码繁复,因为要提前判断,也就是说,if (xxx === 4) {}你要写两遍。

所以结论就是,如果无意义执行很少,那么两种都可以用,如果无意义执行很多,最好还是用正确写法2。

当循环遇到异步递归,也就是“循环+异步+递归”,怎么办?

我们依然假设一个多维数组,每一层有数字元素,也有数组,进入数组,里面又可能有数字元素,也有可能有子数组,如此往复。

按说读取元素,直接用下标即可获得,但是今天我们为了模拟异步操作,不用arr[i]来获取元素,而是用一个Promise来获取,也就是:

function getElement(el) {
  return new Promise(resolve=> {
    setTimeout(() => {
      resolve(el);
    }, Math.random() * 100)
  })
}

也就是说,我们假设取元素值是异步操作,会延迟0-100毫秒才获取到值,值依然是元素值本身。

方法1:async/await方法

let array = [
  [
    1,
    [
      2,
      3
    ],
    4
  ],
  5,
  6,
  [
    7,
    [
      8,
      9,
      [
        10,
        11
      ],
      [
        12,
        13
      ],
      14
    ],
    15,
    [
      16,
      17
    ],
    18
  ],
  19,
  20
];

function getElement(el) {
  return new Promise(resolve=> {
    setTimeout(() => {
      resolve(el);
    }, Math.random() * 100)
  })
}

async function recur(arr) {
  for(let i = 0; i < arr.length; i++) {
    const thisEl = await(getElement(arr[i]));
    console.log('this is', thisEl);
    if (Array.isArray(thisEl)) {
      const result = await(recur(thisEl));
      if (result === 15) {
        return result;
      }
    } else {
      if (thisEl === 15) {
        return thisEl;
      }
    }
  }
}

async function consoleResult() {
  const result = await(recur(array));
  console.log('result', result);
}

consoleResult();

主要操作:

  1. 使用const thisEl = await(getElement(arr[i]));获取元素。

  2. 遇到元素是数组的,就递归这个数组,用const result = await(recur(thisEl));获取结果。

  3. 直接打印recur(array)只可得到一个未决的Promise,所以必须再创建一个异步函数来获取recur(array)的返回结果。

  4. 执行这个异步函数。你会发现,在经过1秒多之后,控制台打印了结果,而且,打印this is xxx的过程是有延迟的。

image.png
  1. 也可以用recur(array).then(value => {console.log(value);});来获取结果,这样就没必要创建consoleResult函数了。

方法2:纯Promise方法

let array = [
  [
    1,
    [
      2,
      3
    ],
    4
  ],
  5,
  6,
  [
    7,
    [
      8,
      9,
      [
        10,
        11
      ],
      [
        12,
        13
      ],
      14
    ],
    15,
    [
      16,
      17
    ],
    18
  ],
  19,
  20
];

function getElement(el) {
  return new Promise(resolve=> {
    setTimeout(() => {
      resolve(el);
    }, Math.random() * 100)
  })
}

function recur(arr) {
  return new Promise(resolve => {
    for(let i = 0; i < arr.length; i++) {
      getElement(arr[i]).then(el => {
        console.log('this is', el);
        if (Array.isArray(el)) {
          return recur(el).then(result => {
            if (result === 13) {
              resolve(result);
            }
          });
        } else {
          if (el === 13) {
            resolve(el);
          }
        }
      });
    }
  });
}

recur(array).then(value => {console.log(value);});

主要操作:

  1. recur函数本身返回一个Promise对象,这个对象只干一件事:resolve(13),或者什么都不返回,不返回意味着永远是“pending”,但是我们恰恰希望永远pending,这样省去处理结果。

  2. return recur(el)的递归,会保证:resolve会将13发送给上层作用域的recur(el),直到发送到顶层recur(el),作为顶层recur(el)的已决的值。

  3. 顶层recur(el)拿到13,交给then,由then打印结果。

image.png

神奇的事情又发生了!

对比async/await方法Promise方法的运算结果,你会发现:

  1. 前者打印顺序是1 2 3 4 …… 20,后者的打印顺序是乱序!

  2. 我们知道,异步的优势在于并发,而前者似乎并不是“并发”,而是“按顺序串行”检索,而后者,在打印完成之前,正确结果就已经被找到了!也就是说,后者是并发,而且后者的效率更高!

怎么回事?

这是因为,async/await和Promise的根本区别在于await fn()暂停当前函数的执行,而promise.then(fn)在将fn调用添加到回调链后,继续执行当前函数。在async函数中,await有一个锁死执行的操作,从字面意思也可以看到,await意思就是等待。既然锁死了操作,即便是for循环,也被中止了循环,所以,肯定是等第一个元素判断完了,才轮到第二个元素,然后依次往下判断。

这就涉及到await的特点了,async/await是Generator的语法糖,await代替的就是yield的作用,而Generator跟Promise是不同的东西,Generator讲究的是顺序递归执行每一个异步,Promise讲究的是并发执行异步。

如果强行希望将方法1改成“异步并行执行”,可以使用Promise.all(arr.map(el => return ...)),但是显然,这样比方法2更难以理解,更繁琐。

因此:

你希望得到“循环且串行执行”,那么请一定使用async/await方法,如果你希望“循环且并行执行”,请一定使用Promise方法。

最后一个小问题,在找到13之后,如何阻止其他的异步任务的执行?

这个想法是很好的,毕竟浏览器消耗能小一点就小一点,但是,异步任务可并不是所有的任务都能中止,比如setTimeout是可以取消的,ajax在高级浏览器里也是可以取消的,具体要看能不能取消。今天我们看看如何取消setTimeout。

let array = [
  [
    1,
    [
      2,
      3
    ],
    4
  ],
  5,
  6,
  [
    7,
    [
      8,
      9,
      [
        10,
        11
      ],
      [
        12,
        13
      ],
      14
    ],
    15,
    [
      16,
      17
    ],
    18
  ],
  19,
  20
];

let timerArr = [];

function getElement(el) {
  return new Promise(resolve=> {
    const timer = setTimeout(() => {
      resolve(el);
    }, Math.random() * 100);
    timerArr.push(timer);
  })
}

function clearAllTimer() {
  timerArr.forEach(timer => {
    clearTimeout(timer);
  })
}

function recur(arr) {
  return new Promise(resolve => {
    for(let i = 0; i < arr.length; i++) {
      getElement(arr[i]).then(el => {
        console.log('this is', el);
        if (Array.isArray(el)) {
          return recur(el).then(result => {
            if (result === 13) {
              resolve(result);
              clearAllTimer();
            }
          });
        } else {
          if (el === 13) {
            resolve(el);
            clearAllTimer();
          }
        }
      });
    }
  });
}

recur(array).then(value => {console.log(value);});

主要操作:

  1. getElement函数中加一步语句:将timer放到数组里。

  2. 定义一个清除所有定时器的函数。

  3. 一旦找到13,就立即清除其他的定时器。

结果可以看到:少打印了好几行,找到13就立即停了:

image.png

你可能感兴趣的:(当“循环+递归+异步”,常见错误写法和正确写法讨论)