谁说forEach不支持异步代码,只是你拿不到异步结果而已

在前面探讨 forEach 中异步请求后端接口时,很多人都知道 forEach 中 async/await 实际是无效的,很多文章也说:forEach 不支持异步,forEach 只能同步运行代码,forEach 会忽略 await 直接进行下一次循环…

当时我的理解也是这样的,后面一细想好像不对,直接上我前面一篇文章用到的示例代码:

async function getData() {
    const list = await $getListData()

    // 遍历请求
    list.forEach(async (item) => {
        const res = await $getExtraInfo({
            id: item.id
        })
        item.extraInfo = res.extraInfo
    })

    // 打印下最终处理过的额外数据
    console.log(list)
}

上面 $getListData、$getExtraInfo 都是 promise 异步方法,按照上面说的 forEach 会直接忽略掉 await,那么循环体内部拿到的 res 就应该是 undefined,后面的 res.extraInfo 应该报错才对,但是实际上代码并没有报错,说明 await 是有效的,内部的异步代码也是可以正常运行的,所以 forEach 肯定是支持异步代码的。

手写版 forEach

先从自己实现的简版 forEach 看起:

Array.prototype.customForEach = function (callback) {
  for (let i = 0; i < this.length; i++) {
    callback(this[i], i, this)
  }
}

里面会为数组的每个元素执行一下回调函数,实际拿几组数组测试和正宗的 forEach 方法效果也一样。可能很多人还是会有疑问你自己实现这到底靠不靠谱,不瞒你说我也有这样的疑问。

MDN 上关于 forEach 的说明

先去 MDN 上搜一下 forEach,里面的大部分内容只是使用层面的文档,不过里面有提到:“forEach() 期望的是一个同步函数,它不会等待 Promise 兑现。在使用 Promise(或异步函数)作为 forEach 回调时,请确保你意识到这一点可能带来的影响”。

ECMAScript 中 forEach 规范

继续去往 javascript 底层探究,我们都知道执行 js 代码是需要依靠 js 引擎,去将我们写的代码解释翻译成计算机能理解的机器码才能执行的,所有 js 引擎都需要参照 ECMAScript 规范来具体实现,所以这里我们先去看下 ECMAScript 上关于 forEach 的标准规范:
在这里插入图片描述

谷歌 V8 的 forEach 实现

常见的 js 引擎有:谷歌的 V8、火狐 FireFox 的 SpiderMonkey、苹果 Safari 的 JavaScriptCore、微软 Edge 的 ChakraCore…后台都很硬,这里我们就选其中最厉害的谷歌浏览器和 nodejs 依赖的 V8 引擎,V8 中对于 forEach 实现的主要源码:

transitioning macro FastArrayForEach(implicit context: Context)(
    o: JSReceiver, len: Number, callbackfn: Callable, thisArg: JSAny): JSAny
    labels Bailout(Smi) {
  let k: Smi = 0;
  const smiLen = Cast<Smi>(len) otherwise goto Bailout(k);
  const fastO = Cast<FastJSArray>(o) otherwise goto Bailout(k);
  let fastOW = NewFastJSArrayWitness(fastO);
  // Build a fast loop over the smi array.
  for (; k < smiLen; k++) {
    fastOW.Recheck() otherwise goto Bailout(k);
    // Ensure that we haven't walked beyond a possibly updated length.
    if (k >= fastOW.Get().length) goto Bailout(k);
    const value: JSAny = fastOW.LoadElementNoHole(k)
        otherwise continue;
    Call(context, callbackfn, thisArg, value, k, fastOW.Get());
  }
  return Undefined;
}

源码是 .tq 文件,这是 V8 团队开发的一个叫 Torque 的语言,语法类似 TypeScript,所以对于前端程序员上面的代码大概也能看懂,想要了解详细的 Torque 语法,可以直接去 V8 的官网上查看。

从上面的源码可以看到 forEach 实际还是依赖的 for 循环,没有返回值所以最后 return 的一个 Undefined。看完源码是不是发现咱上面的手写版也大差不差,只不过 V8 里实现了更多细节的处理。

结论:forEach 支持异步代码

最后的结论就是:forEach 其实是支持异步的,循环时并不是会直接忽略掉 await,但是因为 forEach 没有返回值,所以我们在外部没有办法拿到每次回调执行过后的异步 promise,也就没有办法在后续的代码中去处理或者获取异步结果了,改造一下最初的示例代码:

async function getData() {
    const list = await $getListData()

    // 遍历请求
    list.forEach(async (item) => {
        const res = await $getExtraInfo({
            id: item.id
        })
        item.extraInfo = res.extraInfo
    })

    // 打印下最终处理过的额外数据
    console.log(list)
    setTimeout(() => {
        console.log(list)
    }, 1000 * 10)
}

你会发现 10 秒后定时器中是可以按照预期打印出我们想要的结果的,所以异步代码是生效了的,只不过在同步代码中我们没有办法获取到循环体内部的异步状态。

如果还是不能理解,我们对比下 map 方法,map 和 forEach 很类似,但是 map 是有返回值的,每次遍历结束之后我们是可以直接 return 一个值,后续我们就可以接收到这个返回值。这也是为什么很多文章中改写 forEach 异步操作时,使用 map 然后借助 Promise.all 来等待所有异步操作完成后,再进行下面的逻辑来实现同步的效果。

参考文档

  • MDN forEach 文档:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
  • ECMAScript 中 forEach 规范:https://tc39.es/ecma262/#sec-array.prototype.foreach
  • 谷歌 V8 中 forEach 源码:https://chromium.googlesource.com/v8/v8.git/+/refs/heads/9.0-lkgr/src/builtins/array-foreach.tq#70
  • 谷歌 V8 中 map 源码:https://chromium.googlesource.com/v8/v8.git/+/refs/heads/9.0-lkgr/src/builtins/array-map.tq#192
  • 谷歌 V8 官网:https://v8.dev
  • 谷歌 V8 源码:https://github.com/v8/v8
### 回答1: 手写Promise异步的方法如下: ``` // 定义Promise构造函数 function MyPromise(fn) { // 状态机,初始化为pending var state = 'pending'; // 成功结果 var value = null; // 失败原因 var reason = null; // 成功回调函数队列 var successCallback = []; // 失败回调函数队列 var failCallback = []; // resolve方法 function resolve(newValue) { if (state === 'pending') { state = 'fulfilled'; value = newValue; successCallback.forEach(function(cb) { cb(value); }); } } // reject方法 function reject(newReason) { if (state === 'pending') { state = 'rejected'; reason = newReason; failCallback.forEach(function(cb) { cb(reason); }); } } // then方法 this.then = function(onFulfilled, onRejected) { if (state === 'fulfilled') { onFulfilled(value); } if (state === 'rejected') { onRejected(reason); } if (state === 'pending') { successCallback.push(onFulfilled); failCallback.push(onRejected); } } // 执行fn函数 fn(resolve, reject); } ``` 使用方式为: ``` var promise = new MyPromise(function(resolve, reject) { setTimeout(function() { resolve('成功'); }, 1000); }); promise.then(function(value) { console.log(value); }, function(reason) { console.log(reason); }); ``` 这样就实现了一个简单的promise异步. ### 回答2: 手写Promise异步的实现: Promise是一种用于处理异步操作的机制,通过它可以更加方便地处理异步代码,避免了回调地狱问题。 下面是一个手写的简单Promise异步实现例子: ```javascript class MyPromise { constructor(executor) { this.status = 'pending'; // 初始状态为pending this.value = undefined; // 存储Promise的值 this.callbacks = []; // 存储then方法的回调函数 const resolve = (value) => { if (this.status === 'pending') { // 只能由pending状态转换为fulfilled this.status = 'fulfilled'; this.value = value; this.callbacks.forEach(callback => callback(value)); } }; const reject = (reason) => { if (this.status === 'pending') { // 只能由pending状态转换为rejected this.status = 'rejected'; this.value = reason; this.callbacks.forEach(callback => callback(null, reason)); } }; try { executor(resolve, reject); // 执行executor函数 } catch (error) { reject(error); // 若存在错误则reject } } then(onFulfilled, onRejected) { return new MyPromise((resolve, reject) => { const fulfilled = (value) => { try { const result = onFulfilled(value); if (result instanceof MyPromise) { result.then(resolve, reject); } else { resolve(result); } } catch (error) { reject(error); } }; const rejected = (reason) => { try { const result = onRejected(reason); if (result instanceof MyPromise) { result.then(resolve, reject); } else { resolve(result); } } catch (error) { reject(error); } }; if (this.status === 'fulfilled') { setTimeout(() => fulfilled(this.value)); // 异步执行onFulfilled函数 } else if (this.status === 'rejected') { setTimeout(() => rejected(this.value)); // 异步执行onRejected函数 } else { // 当前状态是pending时,将回调函数存储到callbacks中 this.callbacks.push(fulfilled); this.callbacks.push(rejected); } }); } } // 使用例子: const promise = new MyPromise((resolve, reject) => { setTimeout(() => { resolve('success'); }, 1000); }); promise.then((value) => { console.log(value); // 输出'success' }).catch((reason) => { console.error(reason); }); ``` 这个简单的Promise实现例子中,我们根据Promise的状态定义了resolve和reject函数,同时使用了setTimeout模拟异步处理。在then方法中,根据当前Promise的状态执行相应的回调函数,并处理返回值为Promise的情况。最后使用该Promise实例的时候,可以通过then方法获取resolve的值,或者通过catch方法处理reject的情况。 ### 回答3: 手写Promise异步,可以通过自己实现Promise的构造函数和其原型方法来完成。 首先定义一个Promise的构造函数,接收一个执行函数(executor)作为参数。在构造函数中,需要定义三个状态常量:PENDING、FULFILLED和REJECTED,分别代表Promise的三种状态。同时还需要定义一个变量存储Promise的结果。构造函数中,需要定义一个resolve函数和一个reject函数,用于改变Promise的状态,并传递结果。 接下来,需要定义Promise的原型方法,包括then方法和catch方法。then方法接收两个参数,分别是onFulfilled和onRejected,分别代表Promise状态变为FULFILLED和REJECTED时的回调函数。在then方法中,根据当前Promise的状态,执行相应的回调函数,并将结果传递给下一个Promise。 catch方法用于捕获Promise链中的错误,当之前的Promise状态变为REJECTED时,会执行catch方法中的回调函数。 最后,在手动实现的Promise中,任意时间只能处于一种状态,一旦Promise的状态确定,就不可以再次改变。 总的来,手写Promise异步,需要定义Promise的构造函数,实现resolve和reject函数来改变Promise的状态。然后定义then方法和catch方法来处理结果或错误。这样就能实现一个简单的手动Promise异步函数了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值