Skip to content
Go back

彻底搞懂 forEach 与 for...of 在循环体中执行 await 时的区别

Updated:

循环的差异

最近在处理一组需要严格 串行执行 的异步任务时,又撞上了一个老生常谈却总被忽略的坑:当循环体里出现 await,不同的迭代方式表现完全不同。有的会等上一个任务结束再继续;有的则把所有异步任务触发后不等待,导致“串行”不再串行。

先给结论

我原本以为forforEachwhile这几个基础方法的表现应该是一样的,要么这几个都能”串行”,要么这几个都不能”串行”。

但是结果让我大吃一惊,你没看错,我们经常使用的forwhile方法的表现跟for...of一样,都能让异步任务串行!只有forEach做不到这一点。

下面分别从场景复现、规范条文以及编译产物三个角度,把差异讲透。

场景复现

先准备一组会返回 Promise 的任务:

let list = [];
for (let i = 0; i < 3; i++) {
  list.push(() => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(i);
      }, 1000);
    });
  });
}
  1. 使用 for...of 串行(whilefor 等同理):
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
  1. 使用 forEach
const test = () => {
  list.forEach(async (task) => {
    console.log(`forEach 循环体内执行`);
    const data = await task();
    console.log(data);
  });
};

test();

// 输出顺序:回调被同步触发,无法等待前一个任务完成
// forEach 循环体内执行
// forEach 循环体内执行
// forEach 循环体内执行
// 0
// 1
// 2

forEachfor...of 一类都是用来遍历集合,为啥连最基础的 forwhile 都能串行,forEach 却偏偏不行?答案写在 ECMAScript 规范里。

从 ECMAScript 规范看差异

for...of 这一类能“等”的根本原因

for of类在ECMAScript中的定义

从图中就能明细看出,forwhile 是跟 for...of “混”一起的,且并不包含forEach

ECMAScript 规范中,Iteration Statement(迭代语句)包含 do...whilewhileforfor...offor...in。它们的执行流程会配合 迭代器与生成器 来管理循环:next 决定迭代推进,yield 描述等待点。

在异步函数中,async 可以类比为 function*await 则好比 yield。当执行到 await 时,上下文被挂起,迭代暂停,待 Promise 完成后再恢复执行。因此,在当前迭代体中的 await 结束前,下一次迭代根本不会开始。

forEach 为什么“不能等”

forEach在ECMAScript中的定义 Array.prototype.forEach ( callbackfn [ , thisArg ] ) 的规范算法(节选,意译):

对数组的每一个有效索引 k:

  • 取出元素值 kValue
  • 执行 Call(callbackfn, thisArg, « kValue, k, O »)
  • 继续下一项;
  • 最终 Return undefined

注意整个流程没有任何 await/yieldcallbackfn 即便返回了 Promise,forEach 也不会关心结果。它不过是同步地挨个触发回调,然后立刻返回 undefined

换个视角:看代码转译

把前面的示例交给 SWC(或 Babel)转译到不支持原生 async/await 的环境,差异会更形象。以下是去掉样板代码后的核心结构:

// 源码
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 的值来调度执行。这形成了一个状态机,模拟了循环的行为。

  1. 单一的 generator:整个函数被包装成一个 generator_async_to_generator),既掌控 await 的暂停与恢复,也管理 for...of 的迭代进度。
  2. 遇到 await 就暂停:执行到 case 2return [4, task()] 时,相当于碰到 await task()generator 在此暂停,等待 task() 的 Promise settle。
  3. Promise 完成再继续:Promise 完成后,在 case 3 处恢复,_state.sent() 接收结果。随后 case 4 通过 return [3, 2] 回到 case 2,开启下一轮循环。
// 源码
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 转换后立即执行 !!!
  });
};
  1. forEach 把回调转换成了 generator(通过 _async_to_generator)。
  2. 每次循环都会创建一个新的 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();

小结

当循环体里执行异步任务时:

ECMAScript 规范明确区分了 forEachfor...of 一类迭代的执行模型,SWC 的编译结果则从另外一个角度验证了这一点:for...of 天生会等待当前迭代结束,而 forEach 只负责触发回调,不负责等待。


Share this post on:

Next Post
安全可靠的前端大文件分片上传