循环的差异
最近在处理一组需要严格 串行执行 的异步任务时,又撞上了一个老生常谈却总被忽略的坑:当循环体里出现 await
,不同的迭代方式表现完全不同。有的会等上一个任务结束再继续;有的则把所有异步任务触发后不等待,导致“串行”不再串行。
先给结论:
- 需要 串行执行 时,用
do...while
、while
、for
、for...of
、for...in
、for await...of
(下文统称为 “for...of
一类”)。 - 需要 并发执行 时,用
forEach
(或Promise.all
这类聚合器)。
我原本以为for
、forEach
、while
这几个基础方法的表现应该是一样的,要么这几个都能”串行”,要么这几个都不能”串行”。
但是结果让我大吃一惊,你没看错,我们经常使用的for
、while
方法的表现跟for...of
一样,都能让异步任务串行!只有forEach
做不到这一点。
下面分别从场景复现、规范条文以及编译产物三个角度,把差异讲透。
场景复现
先准备一组会返回 Promise 的任务:
let list = [];
for (let i = 0; i < 3; i++) {
list.push(() => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(i);
}, 1000);
});
});
}
- 使用
for...of
串行(while
、for
等同理):
const test = async () => {
for (const task of list) {
console.log(`for of 循环体内执行`);
const data = await task();
console.log(data);
}
};
test();
// 输出顺序:严格串行,等待当前任务结束后才进入下一轮
// for of 循环体内执行
// 0
// for of 循环体内执行
// 1
// for of 循环体内执行
// 2
- 使用
forEach
:
const test = () => {
list.forEach(async (task) => {
console.log(`forEach 循环体内执行`);
const data = await task();
console.log(data);
});
};
test();
// 输出顺序:回调被同步触发,无法等待前一个任务完成
// forEach 循环体内执行
// forEach 循环体内执行
// forEach 循环体内执行
// 0
// 1
// 2
forEach
和 for...of
一类都是用来遍历集合,为啥连最基础的 for
、while
都能串行,forEach
却偏偏不行?答案写在 ECMAScript 规范里。
从 ECMAScript 规范看差异
for...of
这一类能“等”的根本原因
从图中就能明细看出,
for
、while
是跟for...of
“混”一起的,且并不包含forEach
在 ECMAScript 规范中,Iteration Statement
(迭代语句)包含 do...while
、while
、for
、for...of
、for...in
。它们的执行流程会配合 迭代器与生成器 来管理循环:next
决定迭代推进,yield
描述等待点。
在异步函数中,async
可以类比为 function*
,await
则好比 yield
。当执行到 await
时,上下文被挂起,迭代暂停,待 Promise 完成后再恢复执行。因此,在当前迭代体中的 await
结束前,下一次迭代根本不会开始。
forEach
为什么“不能等”
Array.prototype.forEach ( callbackfn [ , thisArg ] )
的规范算法(节选,意译):
对数组的每一个有效索引 k:
- 取出元素值
kValue
;- 执行
Call(callbackfn, thisArg, « kValue, k, O »)
;- 继续下一项;
- 最终
Return undefined
。
注意整个流程没有任何 await
/yield
,callbackfn
即便返回了 Promise,forEach
也不会关心结果。它不过是同步地挨个触发回调,然后立刻返回 undefined
。
换个视角:看代码转译
把前面的示例交给 SWC(或 Babel)转译到不支持原生 async/await
的环境,差异会更形象。以下是去掉样板代码后的核心结构:
for...of
编译后:
// 源码
const test = async () => {
for (const task of list) {
console.log(`for of 循环体内执行`);
const data = await task();
console.log(data);
}
};
test();
// SWC编译后的代码示意
function _async_to_generator(fn) { ... }
function _ts_generator(thisArg, body) {...}
var test = function () {
return _async_to_generator(function () { // 整个函数是一个generator, 由 async 转化
// ...
return _ts_generator(this, function (_state) {
switch (_state.label) {
// ...
case 2: // 循环开始/继续
if (!!(_iteratorNormalCompletion = (_step = _iterator.next()).done))
return [3, 5];
task = _step.value;
console.log("for of 循环体内执行");
return [4, task()]; // 遇到 await,暂停
case 3: // Promise resolve 后的恢复点
data = _state.sent(); // 获取 Promise 的结果
console.log(data);
_state.label = 4;
case 4:
_iteratorNormalCompletion = true;
return [3, 2]; // 跳回到 case 2,开始下一次循环
// ...
}
});
})();
};
重点:在 SWC 编译后的代码中,原本线性的 for…of 循环被拆解成了多个状态,由 switch 语句根据 _state.label 的值来调度执行。这形成了一个状态机,模拟了循环的行为。
- 单一的
generator
:整个函数被包装成一个generator
(_async_to_generator
),既掌控await
的暂停与恢复,也管理for...of
的迭代进度。 - 遇到
await
就暂停:执行到case 2
的return [4, task()]
时,相当于碰到await task()
,generator
在此暂停,等待task()
的 Promise settle。 - Promise 完成再继续:Promise 完成后,在
case 3
处恢复,_state.sent()
接收结果。随后case 4
通过return [3, 2]
回到case 2
,开启下一轮循环。
forEach
编译后:
// 源码
const test = () => {
list.forEach(async (task) => {
console.log(`forEach 循环体内执行`);
const data = await task();
console.log(data);
});
};
test();
// SWC编译后的代码示意
function _async_to_generator(fn) { ... }
function _ts_generator(thisArg, body) {...}
var test = function () {
list1.forEach(function (task) { // 循环方法在外面
return _async_to_generator(function () { // !!! async 的转换发生在循环内 !!!
var data;
return _ts_generator(this, function (_state) {
// ...
});
})(); // !!! async 转换后立即执行 !!!
});
};
forEach
把回调转换成了generator
(通过_async_to_generator
)。- 每次循环都会创建一个新的
generator
并立刻执行,主流程并不会等待它完成。
回调确实是异步的,但 forEach
只是把它们同步地触发调用。外层既不收集,也不等待这些 Promise,流程自然不会串行。
编译后的完整代码
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
try {
var info = gen[key](arg);
var value = info.value;
} catch (error) {
reject(error);
return;
}
if (info.done) {
resolve(value);
} else {
Promise.resolve(value).then(_next, _throw);
}
}
function _async_to_generator(fn) {
return function () {
var self = this,
args = arguments;
return new Promise(function (resolve, reject) {
var gen = fn.apply(self, args);
function _next(value) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
}
function _throw(err) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
}
_next(undefined);
});
};
}
function _ts_generator(thisArg, body) {
var f,
y,
t,
_ = {
label: 0,
sent: function () {
if (t[0] & 1) throw t[1];
return t[1];
},
trys: [],
ops: [],
},
g = Object.create(
(typeof Iterator === "function" ? Iterator : Object).prototype
);
return (
(g.next = verb(0)),
(g["throw"] = verb(1)),
(g["return"] = verb(2)),
typeof Symbol === "function" &&
(g[Symbol.iterator] = function () {
return this;
}),
g
);
function verb(n) {
return function (v) {
return step([n, v]);
};
}
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while ((g && ((g = 0), op[0] && (_ = 0)), _))
try {
if (
((f = 1),
y &&
(t =
op[0] & 2
? y["return"]
: op[0]
? y["throw"] || ((t = y["return"]) && t.call(y), 0)
: y.next) &&
!(t = t.call(y, op[1])).done)
)
return t;
if (((y = 0), t)) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0:
case 1:
t = op;
break;
case 4:
_.label++;
return {
value: op[1],
done: false,
};
case 5:
_.label++;
y = op[1];
op = [0];
continue;
case 7:
op = _.ops.pop();
_.trys.pop();
continue;
default:
if (
!((t = _.trys), (t = t.length > 0 && t[t.length - 1])) &&
(op[0] === 6 || op[0] === 2)
) {
_ = 0;
continue;
}
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) {
_.label = op[1];
break;
}
if (op[0] === 6 && _.label < t[1]) {
_.label = t[1];
t = op;
break;
}
if (t && _.label < t[2]) {
_.label = t[2];
_.ops.push(op);
break;
}
if (t[2]) _.ops.pop();
_.trys.pop();
continue;
}
op = body.call(thisArg, _);
} catch (e) {
op = [6, e];
y = 0;
} finally {
f = t = 0;
}
if (op[0] & 5) throw op[1];
return {
value: op[0] ? op[1] : void 0,
done: true,
};
}
}
var _loop = function (i) {
list1.push(function () {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve(i);
}, 1000);
});
});
list2.push(function () {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve(i);
}, 1000);
});
});
};
var list1 = [];
var list2 = [];
for (var i = 0; i < 3; i++) _loop(i);
var test1 = function () {
list1.forEach(function (task) {
return _async_to_generator(function () {
var data;
return _ts_generator(this, function (_state) {
switch (_state.label) {
case 0:
console.log("forEach 循环体内执行");
return [4, task()];
case 1:
data = _state.sent();
console.log(data);
return [2];
}
});
})();
});
};
var test2 = function () {
return _async_to_generator(function () {
var _iteratorNormalCompletion,
_didIteratorError,
_iteratorError,
_iterator,
_step,
task,
data,
err;
return _ts_generator(this, function (_state) {
switch (_state.label) {
case 0:
((_iteratorNormalCompletion = true),
(_didIteratorError = false),
(_iteratorError = undefined));
_state.label = 1;
case 1:
_state.trys.push([1, 6, 7, 8]);
_iterator = list2[Symbol.iterator]();
_state.label = 2;
case 2:
if (!!(_iteratorNormalCompletion = (_step = _iterator.next()).done))
return [3, 5];
task = _step.value;
console.log("for of 循环体内执行");
return [4, task()];
case 3:
data = _state.sent();
console.log(data);
_state.label = 4;
case 4:
_iteratorNormalCompletion = true;
return [3, 2];
case 5:
return [3, 8];
case 6:
err = _state.sent();
_didIteratorError = true;
_iteratorError = err;
return [3, 8];
case 7:
try {
if (!_iteratorNormalCompletion && _iterator.return != null) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
return [7];
case 8:
return [2];
}
});
})();
};
test1();
test2();
小结
当循环体里执行异步任务时:
- 要串行:使用
do...while
、while
、for
、for...of
、for...in
、for await...of
。 - 要并发:使用
forEach
(或显式的Promise.all
)。
ECMAScript 规范明确区分了 forEach
与 for...of
一类迭代的执行模型,SWC 的编译结果则从另外一个角度验证了这一点:for...of
天生会等待当前迭代结束,而 forEach
只负责触发回调,不负责等待。