forEach同/异步问题_song854601134的博客

时间:2022-10-20    作者:悬浮的青春    分类:


请不要在说 foreach 是一个异步的了!

forEach 执行异步代码
可能你遇到的情况是 forEach 中执行的都是异步函数,你想在里面逐个执行出来!但是不行!比如下面的代码

const sleep = (ms) => {
    return new Promise((resolve) => {
        setTimeout(resolve, ms)
    })
}
const arr = [
    () => console.log("start"),
    () => sleep(1000),
    () => console.log(1),
    () => sleep(1000),
    () => console.log(2),
    () => sleep(1000),
    () => console.log(3),
    () => sleep(1000),
    () => console.log("end")
]
arr.forEach(async fn => {
    await fn()
})
console.log("for循环执行完")
这里 await 的 ‘跳出当前线程的操作,因为在for循环里,
所以每次跳出也是跳出当前这次循环,但并没有跳到for循环外面。

期待的结果是先打印 start, 然后每隔一秒往下执行一次,直到 end,但是你执行会发现,这些 console 会一次瞬间执行完成!

那为什么会这样呢?我们可以去 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
  };
}

关键代码在这里

foreach 内部呢是一个 while 循环,然后再内部去执行回调函数!你可以看到并没有对内部异步进行什么处理~

async fn => {
    await fn()
}

相当于每次循环,都只是回调执行了外层的 fn, 执行就完事了,而没有对内部的 await 做一些操作,其实就是在循环中没有去等待执行 await 的结果,所以里面的异步 sleep, 还是放到异步队列去等待同步执行完成后再去执行,也就是先打印再去 sleep, 所以没有 sleep 的效果

如果直接用 for 循环去处理,那么就是针对每一次循环去做了一个异步的 await,

const sleep = (ms) => {
    return new Promise((resolve) => {
        setTimeout(resolve, ms)
    })
}
const arr = [
    () => console.log("start"),
    () => sleep(1000),
    () => console.log(1),
    () => sleep(1000),
    () => console.log(2),
    () => sleep(1000),
    () => console.log(3),
    () => sleep(1000),
    () => console.log("end")
]

async function run(arr) {
    for (let i = 0; i < arr.length; i++) {
        await arr[i]()
    }
}
run(arr)

(async function(){
})()//或者使用匿名函数!!!

写一个自己的 forEach
其实就是把 forEach 内部实现改成 for 循环,让每次循环都能捕捉执行 await, 而不是外层的 async 函数

Array.prototype.asyncForEach = async function (callback, args) {
    const _arr = this, // 因为调用的方式 [1,2,3].asyncForEach this指向数组
        isArray = Array.isArray(_arr), //判断调用者是不是数组
        _args = args ? Object(args) : window //对象化
    if (!isArray) {
        throw new TypeError("the caller must be a array type!")
    }
    for (let i = 0; i < _arr.length; i++) {
        await callback.call(_args,_arr[i])
    }
}

const sleep = (ms) => {
    return new Promise((resolve) => {
        setTimeout(resolve, ms)
    })
}
const arr = [
    () => console.log("start"),
    () => sleep(1000),
    () => console.log(1),
    () => sleep(1000),
    () => console.log(2),
    () => sleep(1000),
    () => console.log(3),
    () => sleep(1000),
    () => console.log("end")
]
arr.asyncForEach(async(fn)=>{
    await fn()
})

测试一下,成功的!




一、forEach外部等待forEach执行完成

    let arr = [1, 2, 3, 4, 5, 6, 7];
    let arr2 = [];
    arr.forEach((item) => {
      setTimeout(() => {
        arr2.push(item);
      }, 1000);
    });
    console.log(JSON.parse(JSON.stringify(arr2))); // []

如上:当forEach内部处理异步操作时,则forEach同时也处于异步状态,并不会阻塞进程,而是让下面的语句先执行

即如果在使用forEach遍历数组时,forEach内有异步操作,那么后面的代码执行是不会等待forEach的执行结果,但是很明显我们想要拿到的是forEach结束后的数据,此时我们只需要将forEach写到promise里,即使forEach处于同步状态。

把每次的循环都包裹到一个Promise里,形成一个Promise队列(asyncFuns),最后使用Promise.all来判断是否全部执行完毕

let arr = [1, 2, 3, 4, 5, 6, 7];
let arr2= [];
function pro(item) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      arr2.push(item);
      resolve();
    }, 10000);
  });
}
let asyncFuns = [];
arr.forEach((item) => {
  asyncFuns.push(pro(item));
});
Promise.all(asyncFuns).then(() => {
  console.log('res', arr2); // 十秒钟后打印:res (7) [1, 2, 3, 4, 5, 6, 7]
});

或者使用原生的for循环

(async function () {
  for (let index = 0; index < flisdata.length; index++) {
    const item = flisdata[index];
    if (item.FLIID) {
      let imgdataBase64 = await that.getFileById(item.FLIID);
      that.loading = false;
      if (!imgdataBase64) {
        // return
      } else {
        that.$set(item, 'imgsrc', flisdata[index].FLIOSSKEY + ',' + imgdataBase64);//注意这里不是给vm的一个属性赋值,而只是利用了$set 这个函数的赋值作用。不写这个直接用 点语法也行;
        //Vue.set(vm.items, indexOfItem, newValue) 采用这种直接从vm.到对象 会不会就不用写下面的that.filedata = flisdata了 ??
        that.loadingArray[index] = false;
      }
      // 获取当前页面的路径
      // let pathheader = window.location.protocol;
      // let pathName = window.location.host;
      // let paths = pathheader + '//' + pathName;
    }
    that.filedata = flisdata;//写这就是为了 每次获取item的src后,立刻重新给vm的filedata 赋值,然后更新;这样就做到了每异步获取一个附件轮播图就显示一个附件的效果
  }
})();

二、forEach内部等待异步执行完成

let arr = [1, 2, 3, 4, 5, 6, 7];
let arr2 = [];
function pro(item) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      arr2.push(item);
      console.log(arr2);
      resolve();
    }, 1000);
  });
}
arr.forEach(async (item) => {
  await pro(item);
  console.log('这里是等待每一次循环结束后的操作');
});

三、既需要forEach内部同步执行,又需要forEach外部同步执行

forEach方法用于调用数组的每个元素,并将元素传递给回调函数
map方法返回一个新数组,数组中的元素为原始数组元素调用函数处理后的值
由于forEach执行并不返回任何数据,则无法使用Promise.all方法进行循环是否结束的判断,于是我们想到了使用map+Promise.all来处理这种情况

let arr = [1, 2, 3, 4, 5, 6, 7];
let arr2 = [];
function pro(item) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      arr2.push(item);
      console.log(arr2);
      resolve();
    }, 1000);
  });
}
Promise.all(
  arr.map((item) => {
    return new Promise(async (resolve, reject) => {
      await pro(item);
      console.log('这里是等待每一次循环结束后的操作');
      resolve();
    });
  })
).then(() => {
  console.log('res', arr2);
});

但是实际上forEach也可以手动设定条件来判断是否遍历结束,由于forEach遍历为顺序执行遍历,所以我们可以使用当前项的index值来判断当前项是否为该数组的最后一项,如果为最后一次遍历,那么我们让程序执行下一步的操作即可

// 使用forEach进行数组遍历处理
let arr = [1, 2, 3, 4, 5, 6, 7];
let arr2 = [];
function pro(item) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      arr2.push(item);
      console.log(arr2);
      resolve();
    }, 1000);
  });
}
arr.forEach(async (item, index) => {
  await pro(item);
  console.log('这里是等待每一次循环结束后的操作');
  if (index === arr.length - 1) {
    console.log('res', arr2);
  }
});

$(function() { setTimeout(function () { var mathcodeList = document.querySelectorAll('.htmledit_views img.mathcode'); if (mathcodeList.length > 0) { var testImg = new Image(); testImg.onerror = function () { mathcodeList.forEach(function (item) { $(item).before('\\(' + item.alt + '\\)'); $(item).remove(); }) MathJax.Hub.Queue(["Typeset",MathJax.Hub]); } testImg.src = mathcodeList[0].src; } }, 1000) })

WRITTEN BY

avatar