先不考虑异步,先准备一个多维数组
今天我们准备递归查找一个多维数组里的某个元素。现在假定这个多维数组如下:
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
可以看到,根本没有遍历到元素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
成功了?!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
为什么命中了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
可以看到,依然获取不到!为什么?
因为在执行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));
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));
可见,无论是想获取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));
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));
可见,无论是查找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();
主要操作:
使用
const thisEl = await(getElement(arr[i]));
获取元素。遇到元素是数组的,就递归这个数组,用
const result = await(recur(thisEl));
获取结果。直接打印
recur(array)
只可得到一个未决的Promise,所以必须再创建一个异步函数来获取recur(array)
的返回结果。执行这个异步函数。你会发现,在经过1秒多之后,控制台打印了结果,而且,打印
this is xxx
的过程是有延迟的。
- 也可以用
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);});
主要操作:
recur函数本身返回一个Promise对象,这个对象只干一件事:resolve(13),或者什么都不返回,不返回意味着永远是“pending”,但是我们恰恰希望永远pending,这样省去处理结果。
return recur(el)
的递归,会保证:resolve会将13发送给上层作用域的recur(el)
,直到发送到顶层recur(el)
,作为顶层recur(el)
的已决的值。顶层
recur(el)
拿到13,交给then,由then打印结果。
神奇的事情又发生了!
对比async/await方法和Promise方法的运算结果,你会发现:
前者打印顺序是1 2 3 4 …… 20,后者的打印顺序是乱序!
我们知道,异步的优势在于并发,而前者似乎并不是“并发”,而是“按顺序串行”检索,而后者,在打印完成之前,正确结果就已经被找到了!也就是说,后者是并发,而且后者的效率更高!
怎么回事?
这是因为,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);});
主要操作:
getElement
函数中加一步语句:将timer放到数组里。定义一个清除所有定时器的函数。
一旦找到13,就立即清除其他的定时器。
结果可以看到:少打印了好几行,找到13就立即停了: