forEach循环中的同步和异步问题

业务

最近做了一个需求,首页弹窗,当我们从服务端拿到一组图片数据时(可能有好几张),先确保该图加载完成,再进行弹窗,关闭后继续下一个;
实际的业务代码
可以看出遍历图片数组,每一项依次处理;

用forEach能否实现

正好碰到这个需求,是关于循环的,而且循环内还是同步的方法,需要顺序执行,为什么我这里要使用for循环来实现?forEach也能循环遍历啊,用了会发生什么呢?

我觉得有必要写一篇文章来认真分析下。

写一个类似的实例来跑一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 使用promise实现的等待方法
const sleep = (time) => {
const _promise = new Promise((resolve, reject) => {
setTimeout(_ => {resolve()}, time)
})
return _promise
}

const list = ['111', '222', '333', '444', '555']

list.forEach(async it => {
await sleep(2000) //等待2秒
const nowTime = new Date().getSeconds() //获取当前时间的秒
console.log(`第${nowTime}秒,输出${it}`)
})

自己简单写了一个promise实现的等待方法,想达到的目的就是:每次循环,等待2秒钟再打印出内容;即:

1
2
3
4
5
6
7
8
9
10
//等待2秒
//打印111

//等待2秒
//打印222

//等待2秒
//打印333

//......后面以此类推

那么按照forEach的写法,使用asyncawait,同步执行,看起来没有任何问题,那么直接执行,看看结果:
forEach运行结果
和我们预想的结果完全不用,而且是在同一时间打印了所有的信息;

后面我们再详细剖析为什么会这样。

用for循环能否实现

直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 使用promise实现的等待方法
const sleep = (time) => {
const _promise = new Promise((resolve, reject) => {
setTimeout(_ => {
resolve()
}, time)
})
return _promise
}

const list = ['111', '222', '333', '444', '555']

for (let i = 0; i < list.length; i++) {
await sleep(2000)
const nowTime = new Date().getSeconds()
console.log(`第${nowTime}秒,输出${list[i]}`)
}

for循环运行结果
可以从结果看出,真正做到了每次等待2秒,再打印信息,和我们最初预想的完全一致。

forEach的问题

先说结论:准确来讲,forEach其实也是个同步方法,只不过循环内部如果有异步的方法,你想依次按照顺序执行,那么就会出问题;

我们直接查出forEach方法的源码,深入分析下,点击查看MDN详细说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
//MDN上面的forEach源码

// Production steps of ECMA-262, Edition 5, 15.4.4.18
// Reference: http://es5.github.io/#x15.4.4.18
if (!Array.prototype.forEach) {

Array.prototype.forEach = function(callback, thisArg) {

var T, k;

if (this == null) {
throw new TypeError(' this is null or not defined');
}

// 1. Let O be the result of calling toObject() passing the
// |this| value as the argument.
var O = Object(this);

// 2. Let lenValue be the result of calling the Get() internal
// method of O with the argument "length".
// 3. Let len be toUint32(lenValue).
var len = O.length >>> 0;

// 4. If isCallable(callback) is false, throw a TypeError exception.
// See: http://es5.github.com/#x9.11
if (typeof callback !== "function") {
throw new TypeError(callback + ' is not a function');
}

// 5. If thisArg was supplied, let T be thisArg; else let
// T be undefined.
if (arguments.length > 1) {
T = thisArg;
}

// 6. Let k be 0
k = 0;

// 7. Repeat, while k < len
while (k < len) {

var kValue;

// a. Let Pk be ToString(k).
// This is implicit for LHS operands of the in operator
// b. Let kPresent be the result of calling the HasProperty
// internal method of O with argument Pk.
// This step can be combined with c
// c. If kPresent is true, then
if (k in O) {

// i. Let kValue be the result of calling the Get internal
// method of O with argument Pk.
kValue = O[k];

// ii. Call the Call internal method of callback with T as
// the this value and argument list containing kValue, k, and O.
callback.call(T, kValue, k, O);
}
// d. Increase k by 1.
k++;
}
// 8. return undefined
};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//----------
//源码中的循环,为了方便展示我删掉了乱七八糟的注释
//----------

while (k < len) {
var kValue;

if (k in O) {
kValue = O[k];

callback.call(T, kValue, k, O);
}
k++;
}

关键问题就出在里面的循环,可以看到内部只是执行了callback回调函数,没有任何的异步处理,内部的每一次循环,没有等待回调函数执行完毕,只是暴力调用方法,所以等同于:执行后,等待了2秒,同时打印了所有信息;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//自己手写支持同步的forEach

const sleep = (time) => {
const _promise = new Promise((resolve, reject) => {
setTimeout(_ => {
resolve()
}, time)
})
return _promise
}

//自己手写一个支持同步的forEach方法
Array.prototype.myForEach = async function(func, thisArg) { //async
const _arr = this

const _this = thisArg ? Object(thisArg) : window //this指向

for (let i = 0; i < _arr.length; i++) {
await func.call(_this, _arr[i]) //内部函数await,传入this指向,每一项的值
}
}

const list = ['111', '222', '333', '444', '555']

list.myForEach(async it => { //async
await sleep(2000) //await
const nowTime = new Date().getSeconds()
console.log(`第${nowTime}秒,输出${it}`)
})

可以看到,其实forEach的底层逻辑,还是用for循环,只不过外部加了async,每一个回调函数的执行,也是要等待完成才能执行下一个;

和我最原始的for循环写法完全一个道理;
myForEach方法 运行结果

总结

出了问题,只要知道底层的原理,就能知道具体是哪里错了;

forEach本身,等于多返回index值,还有原数组而已,只是为了方便使用,没有什么神秘的地方。


forEach循环中的同步和异步问题
https://liujiaweb.cn/posts/39893.html
作者
Liu Jia
发布于
2022年5月31日
许可协议