Promise.all 链式函数调用执行顺序问题

handleGroups() {
            Promise.all(
                this.groups.map(g => {
                    g.videoDtos = []
                    g.videos.split(',').map(async (id) => {
                        let v = await this.getVideoById(Number(id));
                        if (v) g.videoDtos.push(v)
                    })
                })
            ).then(res => {
                this.playGroups()
            })
        }

这里理想的执行顺序是当 g.videoDtos得到所有的结果,再去执行playGroups(),但是事实是当执行到g.videos.split(',').map(async (id)这一步,就直接跳到了playGroups(),然后再去执行getVideoById(Number(id))

getVideoById(id: number) {
            return new Promise((resolve, reject) => {
                getVideoDto(id).then(res => {
                    if (res.data.code == 200) {
                        let video: Video = res.data.data
                        resolve(video)
                    } else {
                        reject();
                    }
                })
            })
        }
playGroups() {
            let videos = this.groups[this.playGroupsIndex].videoDtos;
            // 放一堆视频
            if (videos.length > 0) 
                this.$refs.videoBoxRef.setVideos(videos);

            this.playGroupsTimer = setTimeout(()=>{
                this.playGroupsIndex = (this.playGroupsIndex + 1) % this.groups.length;
                this.playGroups();
            }, this.roundTime * 1000);
        }

想问下,如何才能让这个链式调用起作用,等getVideoById(Number(id))都执行完之后,再去执行.then中的playGroups()

阅读 3.7k
4 个回答

先看注释

function handleGroups() {
    Promise.all(
        this.groups
            // 看看,这个 .map() 的回调中没有返回值,
            // 所以 map 的结果是一个[undefined, ..., undefiend]数组
            // Promise.all([undefined, ...]) 当然不会有等待
            // 直接就会执行后面的 .then()
            .map(g => {
                g.videoDtos = [];
                g.videos.split(",")
                    // 这个 map 得到的是一个 Promise[]
                    // 虽然也没有返回值,但是 `async` 函数会自动用 Promise 封装结果
                    // 哪怕是 undefined
                    .map(async (id) => {
                        let v = await this.getVideoById(Number(id));
                        if (v) g.videoDtos.push(v);
                    });
                // 虽然在 map 回调的每次调用都会有 await 等待
                // 但是并没有依次等待,也没有 Promise.all 之类的方法来等待全部完成
                // 所以这里仍然是多 videos 并发的
            })
    ).then(res => {
        this.playGroups();
    });
}

然后一步步分析

在分析之前 ,我觉得可能需要介绍一篇文章给你了解下基础知识:理解 JavaScript 的 async/await

首先,既然内部用到了 await,那外面也没必要客气,用 await 可以逻辑更容易懂,所以

async handleGroups() {
    const res = await Promise.all( ... );  // 这里的省略号接着就说
    this.playGroups();
}

不过 res 拿到之后并没有使用,不需要,可以直接把 const res = 丢掉。

接下来 Promise.all() 的参数是一个数组,如果数组的元素是 Promise 对象,会等待它 resolve();如果是普通对象或 undefined 等,就不需要等待。Promise.all() 会返回一个 Promise对象,它会在参数数组中所有 Promise 对象都 resolved 之后 resolve()

不妨拆一下这个调用,变成

const groupPromises = this.group.map( ... );
await Promise().all(groupPromises);

现在的问题是 groupPromises 是什么东西。它既然是从 this.group.map(fn) 返回的,那一定是一个数组,而其中有元素,是 fn 执行后的返回值。这里 fng => { ... },很显然,没有返回值,所以 groupPromises 现在是由一堆 undefined 组成的数组。

显然这不是需要的结果,应该它 g => { ... } 加上返回值,而且这个返回值得是 Promise 对象才对得起 Promise.all()

那该返回什么?接下来看 g => { ... } 的内部逻辑。它的内部是要异步处理某个 group 中的若干 videos。

同上面类似的分析,可以知道 g.videos.split(",").map(async () => {}) 返回的是一个由若干 Promise 对象组成的数组。需要注意的是 this.getVideoById(Number(id)) 虽然返回 Promise 对象,但是已经有 await 等过了;但是它外面包的 async () => {} 会产生另一个 Promise 对象,也就是 map 的返回值,并没有谁在等。当然可以用 for 循环来等(注意不是 .forEach())不过既然是并发处理,可以用 Promise.all() 组织起来一起等,所以可以改为

const videoDtos = await Promise.all(
    g.videos.split(",")
        // 返回都是返回 Promise 对象
        // async id => await this.get...
        // 和直接 id => this.get...
        // 是一样的
        .map(id => this.getVideoById(Number(id)))
);
// 由于 map 没有过滤无效返回值,所以这里专门过滤一遍
g.videoDtos = videoDtos.filter(v => v);

好了,注意,这里的 await 不是修饰的 map() 回调,它的外层函数需要使用 async,所以 g => { ... } 要变成 async g => { ... }。是不是顺手就把 g => { ... } 的返回值变成 Promise 对象了(前面说过需要返回 Promise 对象的)?

所有逻辑都分析过多了,把整个过过程连起来,就是正确的结果

async handleGroups() {
    await Promise.all(
        this.groups
            .map(async g => {
                const videoDtos = await Promise.all(
                    g.videos.split(",")
                        .map(id => this.getVideoById(Number(id)))
                );
                g.videoDtos = videoDtos.filter(v => v);
            })
    );

    this.playGroups();
}

已参与了 SegmentFault 思否「问答」打卡,欢迎正在阅读的你也加入。

Promise.all是接受一个promise的列表,你map出来的并不是一个promise实例数组,在你的基础上给你改下

handleGroups() {
            Promise.all(
                this.groups.map(g => {
                    g.videoDtos = []
                    return g.videos.split(',').map(async (id) => {
                        let promise = this.getVideoById(Number(id))
                        let v = await promise;
                        if (v) g.videoDtos.push(v)
                        return promise
                    })
                }).flat()
            ).then(res => {
                this.playGroups()
            })
        }
已参与了 SegmentFault 思否「问答」打卡,欢迎正在阅读的你也加入

首先指出你的问题,

Promise.all(
  this.groups.map(g => {
    g.videoDtos = []
    g.videos.split(',').map(async (id) => {
      let v = await this.getVideoById(Number(id));
      if (v) g.videoDtos.push(v)
    })
  })
)

这里这一块内容你出现了歧义,应该就是你出现问题的部分。就算如你意愿,最后 this.groups.map 的返回值会是这样的 [[promise0-0,pormise0-1,promise0-2],[promise1-0,pormise1-1,promise1-2],...]
所以应该要把外部包裹的数组给推平,大概实现demo:

const groups = ['0-1,0-2,0-3,0-4,0-5', '1-1,1-2,1-3', '2-1,2-2,2-3,2-4']
function demo(){
  const promiseList = groups.map((g) => {
    g.split(",").map(id => getVideoById(id))
  }).flat()
  Promise.all(promiseList).then((res) => {
    console.log('then',res)
  })
}

function getVideoById(index){
  return new Promise((resolve) => {
    setTimeout(() => { resolve('当前ID:' + index) }, 300)
  })
}
demo()
已参与了 SegmentFault 思否「问答」打卡,欢迎正在阅读的你也加入。

因为在你的第一层 this.groups.map中回调函数全部默认返回了undefined,传入Promise.all中的是一个undefined数组,Promise.all会对其中的每一个值使用Promise.resolve()进行处理,全部返回fullfilled状态的Promise,Promise.all onFullfilled成立。 因此直接执行then语句了。
推测你的this.groups数据结构如下:

{
  //...
  videos:'id1,id2,id3....'
}

将你代码拆了下,逻辑变清晰了,你可以试试:

handleGroups() {
    // 取到所有的 videos
    const allVideos = this.groups.reduce((pre,current)=>pre += current.videos,'')
    // id 去重,减少http请求
    const videoIds = Array.from(new Set(allVideos.split(',')))
    Promise.all(
        videoIds.map(id => this.getVideoById(Number(id));)
    ).then(res => {
        this.playGroups()
    })
}

希望能解答你的问题~

已参与了 SegmentFault 思否「问答」打卡,欢迎正在阅读的你也加入。
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
logo
Microsoft
子站问答
访问
宣传栏