前端小白手动实现Promise.all

最近面试了拼多多和美团,两次被问到能不能自己实现一个Promise.all.

很可惜,我都没答上来。

面拼多多的时候,答不上来Promise.all使用方法.

面美团的时候,答不上来手动实现Promise.all

因此,突然之间感觉到了【源码】的重要性。对于程序员来说:

  • 要学会看源码。这个没什么可解释的,就像公理一样,每天被灌输着。

  • 尝试自己去实现一个功能的源码。个人感觉这部分就比较考验数据结构和算法了,虽然不至于像LeetCode 那么变态,但是此时却是反映出了一个程序员的功底。是否具备【逆向工程】的思维。哪怕写的再烂,但是你思考了,尝试着去实现,就会有进步。

好了,扯了一些个人感悟。开始正题。


1- Promise.all 的用法

逆向的去实现功能,最关键的前提是准确了解API,输入、输出、和注意事项。

这里直接引用MDN:

Promise.all(iterable) 方法返回一个 Promise 实例,此实例在 iterable 参数内所有的 promise 都“完成(resolved)”或参数中不包含 promise 时回调完成(resolve);如果参数中 promise 有一个失败(rejected),此实例回调失败(reject),失败的原因是第一个失败 promise 的结果。

MDN后面也给出了详细说明:

此方法在集合多个promise的返回结果时很有用。

完成(Fulfillment):

  • 如果传入的可迭代对象为空,Promise.all会同步地返回一个已完成(resolved)状态的promise。

  • 如果所有传入的promise都变为完成状态,或者传入的可迭代对象内没有promise,Promise.all返回的promise异步地变为完成。

  • 在任何情况下,Promise.all返回的promise的完成状态的结果都是一个数组,它包含所有的传入迭代参数对象的值(也包括非promise值)。

失败/拒绝(Rejection):

如果传入的promise中有一个失败(rejected),Promise.all异步地将失败的那个结果给失败状态的回调函数,而不管其它promise是否完成。

个人感觉MDN解释的比较清楚了,还是云里雾里的话,可以反复细品一下上面的说明。或者结合下面的代码去理解。


2 - 手动实现Promise.all

面试美团的时候,面试官看我写不出来,就说“既然你知道了输入和输出是什么,应该能写出来了....”。

面试官其实不是在鄙视我“我不行”,而是在试图引导我的思路,只是当时自己编程思路太差,最后还是没写出来。

但是面试官的提示,确实是一个很好的思考思路。 先不管完整的Promise.all代码是什么样子,甚至包括优化啥的。先想想"Promise.all(iterable) 方法返回一个 Promise实例",就这么简单的一句话怎么写呢?

function myPromiseAll(arr) {  // 参数是一个iterable对象,一般是数组
   // 返回一个Promise实例
    return new Promise((resolve, reject) => {
        resolve("面试官让我写一个Promise.all");
        // 或者
        // reject("我太笨了,写不出来");
        
    });
}


let pResult = myPromiseAll([]);  // 先不要去想数组有没有元素
pResult.then(value=>{
    console.log(value);  // 输出: 面试官让我写一个Promise.all
}, err=>{
    console.log(err);
}) 

好了,如过看懂了,那么最重要的一步就完成了。是不是很简单。

接下来,只要根据MDN的说明,一步步完善内部函数的功能就行了。

我们先从“完成”情况下手:
完成(Fulfillment)

  • A. 如果传入的可迭代对象为空,Promise.all会同步地返回一个已完成(resolved)状态的promise。
  • B. 如果所有传入的promise都变为完成状态,或者传入的可迭代对象内没有promise,Promise.all返回的promise异步地变为完成。
  • C. 在任何情况下,Promise.all返回的promise的完成状态的结果都是一个数组,它包含所有的传入迭代参数对象的值(也包括非promise值)。

请先看C,在完成情况下,会始终返回一个数组.

function myPromiseAll(arr) {
    // 定义一个数组
    let result = [];

    return new Promise((resolve, reject) => {

        // 现在只考虑 “在完成情况下” ,会返回一个数组
        resolve(result);
       
        
    });
}

let pResult = myPromiseAll([]);
pResult.then(value=>{
    console.log(pResult);  // 输出 Promise { : "fulfilled", : [] }
    console.log(value); // 输出:[]
})

那么下面来实现B,B里有分两种情况:

  • 元素是Promise实例
  • 元素不是Promise实例

那先考虑元素不是Promise实例,从简单的开始

function myPromiseAll(arr) {
    let result = [];

    return new Promise((resolve, reject) => {

        for(let i = 0; i < arr.length; i++) {
            result.push(arr[i]);
        }

        resolve(result);
       
        
    });
}


let pResult = myPromiseAll([1,2,3]);  // 元素不是Promise实例
pResult.then(value=>{
    console.log(pResult); // 输出:  Promise { : "fulfilled", : (3) […] }
    console.log(value); // 输出: Array(3) [ 1, 2, 3 ]
})

最难的来了,元素都是Promise实例呢?
别慌,先写顶层设计,再想细节(自上向下编程)

function myPromiseAll(arr) {
    let result = [];

    return new Promise((resolve, reject) => {


        for(let i = 0; i < arr.length; i++) {
            
            if(/*如果是Promise实例*/) {
                
            } else {
                result.push(arr[i]);
            }
 

        }


        // 先想想,resolve放在这里,对不对?
        resolve(result);
       
        
    });
}

继续完善

function myPromiseAll(arr) {
    let result = [];

    return new Promise((resolve, reject) => {

        // 数组为空,直接resolve了
        if(arr.length == 0) {
            resolve(result);
        }


        for(let i = 0; i < arr.length; i++) {
            
            if(arr[i].then) { // 若元素是Promise实例,则会有then函数,这里只是简单的作为判断标准
                
                // 元素是Promise
                arr[i].then(value => {
                    console.log(value);
                    result.push(value);

                    // 想一想什么时候resolve呢?--- 所有Promise实例都完成了
                    if(result.length == arr.length) {
                       console.log("所有都完成了")
                        resolve(result);
                    }

                })
                
            } else {
                result.push(arr[i]);

                // 这段代码跟上面重复,想想,能不能提取放到外面,会出现什么情况呢?
                if(result.length == arr.length) {
                    resolve(result);
                }

            }
        } 

    });
}

let p1 = new Promise((resolve, reject)=> {
    setTimeout(resolve, 2000, "P1 resolved");
})

let p2 = new Promise((resolve, reject)=> {
    setTimeout(resolve, 3000, "P2 resolved");
})

let p3 = new Promise((resolve, reject)=> {
    setTimeout(resolve, 4000, "P3 resolved");
})


let pResult = myPromiseAll([p1,p2,p3]);
pResult.then(value=>{
    console.log(pResult);
    console.log(value);
})

// 输出
// P1 resolved 
// P2 resolved
// P3 resolved
// 所有都完成了
// Promise { : "fulfilled", : (3) […] }
// Array(3) [ "P1 resolved", "P2 resolved", "P3 resolved" ]

完成情况写完了,还剩失败情况:

  • 如果传入的 promise 中有一个失败(rejected),Promise.all 异步地将失败的那个结果给失败状态的回调函数,而不管其它 promise 是否完成
function myPromiseAll(arr) {
    let result = [];

    return new Promise((resolve, reject) => {

        // 如果数组为空,直接返回空数组
        if(arr.length == 0) {
            resolve(result);
        }


        for(let i = 0; i < arr.length; i++) {
            
            if(arr[i].then) { // 若元素是Promise实例,则会有then函数,这里只是简单的作为判断标准
                
                // 元素是Promise
                arr[i].then(value => {
                    console.log(value);
                    result.push(value);

                    // 想一想什么时候resolve呢?
                    if(result.length == arr.length) {
                        console.log("所有都成功了")
                        resolve(result);
                    }

                }, err => {
                    console.log("很不幸,其中一个失败了");
                    // 注意到没, 这里没有像上面的判断 result.length == arr.length, 为什么?
                    // 只要碰到 resolve 或 reject ,就结束了
                    reject(err);
                })
                
            } else {
                result.push(arr[i]);

                // 这段代码跟上面重复,想想,能不能提取放到外面,会出现什么情况呢?
                if(result.length == arr.length) {
                    resolve(result);
                }

            }
        } 

    });
}

let p1 = new Promise((resolve, reject)=> {
    setTimeout(reject, 2000, "P1 rejected");
})

let p2 = new Promise((resolve, reject)=> {
    setTimeout(resolve, 3000, "P2 resolved");
})

let p3 = new Promise((resolve, reject)=> {
    setTimeout(resolve, 4000, "P3 resolved");
})


let pResult = myPromiseAll([p1,p2,p3]);
pResult.then(value=>{
    console.log(pResult);  // 是输出成功
    console.log(value);
}, err => {
    console.log(pResult);   // 还是输出失败呢?
    console.log(err);
})

// 输出
// 很不幸,其中一个失败了
// Promise { : "rejected" }
// P1 rejected
// P2 resolved
// P3 resolved

为什么最后还是输出了 P2 和 P3 的结果呢? 这是因为,尽管遇到了P1就reject了,然而 P2 和 P3 仍在执行。注意MDN说的是“不管其他Promise是否完成”,而不是“其他Promise被stop”。

let p2 = new Promise((resolve, reject)=> {
    setTimeout(resolve, 3000, "P2 resolved");
})

let p3 = new Promise((resolve, reject)=> {
    setTimeout(resolve, 4000, "P3 resolved");
})


let pResult = myPromiseAll([p2,55,p3]);
pResult.then(value=>{
    console.log(pResult);
    console.log(value); // 输出 [55, 'P2 resolved', 'P3 resolved']
}, err => {
    console.log(pResult);
    console.log(err);
})

3 - 总结

首先声明,上面的代码不是最优的,有很多优化的空间。我只是根据MDN说明一步步写下来,帮助自己理解。

也是第一次尝试写“源码实现”,就是想告诉大家,先不要考虑写的烂不烂,什么时间复杂度空间复杂度好不好,只要你尝试去写,都一定会写出来一些东西,哪怕只是实现了其中的一个功能点也会让你进步的。

一个合格的面试官只会问你会的,而且会在这个问题上不断深入,直到达到你的极限。 多多思考,多写写,大家都会进步的。

你可能感兴趣的:(前端小白手动实现Promise.all)