前言
为什么for和forEach循环一个异步函数效果会不同?
正文
当for
和forEach
在使用async、await
循环一个异步函数并不是说结果不同,而是效果不同,准确来说应该是得到结果的时机不相同,会有一丝丝差异,为什么会出现这种情况呢?
看例子:
js
//定义一个数组数据
const arr = ["苹果", "香蕉", "橘子", "梨", "葡萄", "西瓜", "草莓", "火龙果", "车厘子"];
//定义一个导出promise的异步函数,异步时间是一秒钟
const p = (v, c) =>
new Promise((resolve, reject) => {
setTimeout(() => {
console.log(v, c);
resolve();
}, 1000);
});
下面分别使用for
和foreach
遍历这个异步函数
js
//foreach接收一个回调函数,并使用async和await关键字让遍历的函数'同步执行'
arr.forEach(async (element) => {
await p(element, "foreach");
});
//同foreach一样的操作使用for循环
const inits = async () => {
for (let i = 0; i < arr.length; i++) {
await p(arr[i], "for");
}
};
inits();
上面两个循环按我们正常的思路第一时间想到的应该就是数组中的数据会依次打印出来,并且每一个数据打印两遍,但是实际的结果如我们想象的一样吗?
可以看出,使用foreach
循环的异步函数虽然使用async、await
关键字,但依旧是在一秒后数组中的数据全部加载了出来。而for
循环的异步函数确实是我们想象中的那种,在使用了async、await
关键字之后,每隔一秒才会显示数组中的下一个元素。
是什么导致了foreach
循环异步函数的时候超出我们的预期了呢?
首先要看一下foreach
的实现,这里大致写一下foreach
的实现代码
js
//类似 foreach的源码实现 其实还是很简单的
Array.prototype.myForEach = function (callback) {
//判断确实传进来的一个函数
if (typeof callback !== 'function') {
throw new Error('Callback must be a function');
}
//判断数组长度,如果为0直接return
if (this.length === 0) return;
for (let i = 0; i < this.length; i++) {
//在这里,callback函数是在数组的每个元素上调用的,所以需要将当前元素作为this参数传递给callback函数
callback.call(this[i],this[i], i, this);
}
};
//调用一下看看是否正确
arr.myForEach((element, index, arr) => {
console.log(element, index, arr);
});
从最终打印的结果来看foreach
的实现是正确的。
看完foreach
源码之后是不是就可以理解为,foreach
循环就是for
循环的时候又多了一层函数掉调用。
理念有了,开始尝试
js
//可以理解为在这个函数调用的异步函数加了await 他这里'同步执行'了
const each = async (v) => {
await p(v, "for");
};
//但是在for循环的时候并没有加await 在for循环的时候直接将这个没有进行'同步执行'的函数一次经循环完了
//中间不会有阻塞,导致最终打印的显示时机与直接在for循环内await 一个异步函数不同
const inits = async () => {
for (let i = 0; i < arr.length; i++) {
each(arr[i]);
}
};
inits();
这样多加了一层没有'阻塞'函数之后,最终的效果和foreach
展示的是一样的。
如果想要foreach
循环同for
循环一样循环一个异步函数,每隔一定的时间才会显示下一个,也可以自己写一个'同步阻塞'的foreach
js
//重写一个同步foreach 在循环的时候加上await
Array.prototype.syncForEach = async function (callback) {
if (typeof callback !== "function") return;
if (this.length === 0) return;
for (let i = 0; i < this.length; i++) {
await callback.call(this[i], this[i], i, this);
}
};
//当这里调用的时候如果是个异步函数,syncForEach里的await也会生效,进行阻塞同步执行
arr.syncForEach(async (element) => {
await p(element, "foreach");
});
最终得到的结果也是和我们预想用的一样了。
最后
其实for
和foreach
的区别在于中间多了一个函数调用,并且这个函数是普通函数,导致在遍历异步函数的时候会先将这个函数全部调用,在函数内才使用async、await
进行阻塞函数执行,这个时候函数已经多次调用,所以最终结果会一次性全部显示出来。
使用回调函数、Promise
、async
和await
可以产生异步传染性 的效果,只有当这个异步执行完毕之后才会执行下一个异步函数,所以在自定义一个阻塞foreach
的时候添加上async、await
就达到了与直接使用for
遍历一个异步函数的效果。