promise与异步编程核心知识总结

57 阅读21分钟

promise

回调地狱

定义:在一个回调函数中嵌套另外一个回调函数,导致出现所谓的回调地狱。

原因:每次调用都依赖于上一次调用的结果,需要嵌套一系列的回调函数。

示例

setTimeout(function () {  
    console.log('第一层');
    setTimeout(function () {  
        console.log('第二层');
        setTimeout(function () {   
            console.log('第三层');
        }, 1000)
    }, 2000)
}, 3000)

缺点

  1. 不利于阅读(缩进太过频繁);

  2. 还不利于异常处理(可能会重复写某些异常处理的代码)。

promise

  • Promise功能

    • Promise 是 JS 中进行异步编程的新解决方案;

    • 从语法上来说:Promise 是一个构造函数;

    • 从功能上来说:Promise 对象用来封装一个异步操作并可以获取其成功/失败的结果值。

    • promise接受一个执行器作为参数、这个执行器是同步的

  • Promise状态改变

    1. Promise 对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。
    2. Promise对象的状态改变,只有两种可能:从pending变为fulfilled,称之为resolve;从pending变为rejected,称之为reject。
    3. 一个Promise对象只能发生一次状态改变。
  • Promise实例代码

    // promise返回一个任务,该任务在指定的时间后完成
    function promiseDemo() { 
        return new Promise((resolve) => { 
            setTimeout(() => { 
                resolve(111); 
            }, 1000); 
        }) 
    }
    
    
    
    // 利用delay函数,等待一秒钟,输出finish 
    promiseDemo().then( (value) => { 
        console.log(value) 
    }, () => { 
        console.log('失败的回调') 
    } )
    

Promise优势

  1. 避免了回调地狱

Promise通过链式调用 .then() 方法来处理异步任务的结果,避免了过多的嵌套回调,大大提高了代码的可读性和可维护性

  1. 更好的异步流程控制

    • 顺序执行异步任务:通过在 .then() 方法中返回一个新的Promise,每个异步任务依赖于前一个异步任务的结果,这样可以将多个异步任务按顺序串联起来,同时可以确保异步任务按照特定的顺序依次完成

    • 并行执行异步任务:除了顺序执行,Promise还提供了 Promise.all() 和 Promise.race() 等方法来实现多个异步任务的并行执行。Promise.all() 可以同时发起多个异步任务,并在所有任务都完成后统一处理结果;Promise.race() 则是多个异步任务中只要有一个完成,就立即返回其结果,这些方法为不同的异步场景提供了灵活的解决方案

  2. 增强了代码的可读性

    类似同步代码的编写风格:使用Async/Await与Promise结合时,可以使异步代码看起来更像同步代码,进一步提高了代码的可读性。开发者可以使用 try/catch 块来处理异步操作中的错误,与同步代码的错误处理方式一致,降低了理解和编写异步代码的难度。

Promise链式调用

  • promise为什么支持链式调用

    核心原因在于其设计上的两个关键特性:then 方法和返回值自动解析为 Promise。

  • then()方法

    Promise 对象提供了一个 then 方法,这个方法接受两个可选的回调函数作为参数:onFulfilled 和 onRejected。这两个函数分别对应于 Promise 成功(fulfilled)和失败(rejected)时的处理逻辑。

  • 返回值自动解析为 Promise

    then 方法本身会返回一个新的 Promise 对象。这个新 Promise 的结果由 then 方法中回调函数的返回值决定:

    • 如果回调函数返回了一个值(非 Promise),则新 Promise 的状态为“成功”,并且结果就是回调函数返回的值

    • 如果回调函数抛出了一个错误,则新 Promise的状态为“失败”,并且将以这个错误作为原因。

    • 如果回调函数返回了一个 Promise,则新 Promise 将等待这个返回的 Promise 解决,并以其结果作为自己的结果。

  • 总结

    由于 then 方法返回一个新的 Promise,并且这个新 Promise 的结果依赖于前一个 then 方法中回调函数的返回值,因此可以连续调用 then 方法,形成链式调用。

示例

new Promise((resolve, reject) => {  
    resolve(1);  
})  
.then(result => {  
    console.log(result); // 输出 1  
    return result * 2; // 返回 2  
})  
.then(result => {  
    console.log(result); // 输出 2  
    return result * 2; // 返回 4  
})  
.then(result => {  
    console.log(result); // 输出 4  
});

promise输出结果原理

  1. 新任务的状态取决于后续处理
    1. 若没有后续的相关处理,新任务的状态和前任务一样,数据为前任务的数据
    const p1 = new Promise((resolve) => { 
        console.log('学习') 
        resolve(1) 
    }) 
    const p2 = p1.catch(() => { 
        console.log("学习失败") 
    })
    
    setTimeout(() => { 
        console.log(p2) 
    })
    

结果:

p1: fulfilled 1
p2: fulfilled 1

原因:由于p2没有针对p1的成功的处理,所以p2的状态和p1一样,并且p2的数据和p1的一样

    1. 若有后续处理但还未执行,新任务挂起
    const p1 = new Promise((resolve) => {
            console.log('学习')
            setTimeout(() => {
                resolve(1)
            }, 2000)
        })
    const p2 = p1.then(() => {
        console.log("考试")
    })
    
    setTimeout(() => {
        console.log(p2)
    }, 1000)
    

结果:

学习
Promise { <pending> }
考试

原因:p2的状态依赖于p1,但是p1的状态在2s后才改变,所以p2的状态为pending

    1. 若后续处理执行了,则根据后续处理的情况确定新任务的状态
    • 后续处理执行无错,新任务的状态为完成,数据为后续数据的返回值

    • 后续处理执行有错,新任务的状态为失败,数据为异常对象

    • 后续执行返回的是一个任务对象,新任务的状态和数据与该任务对象一致

const p1 = new Promise((resolve) => { 
    console.log('学习') 
    resolve(1) 
}) 
const p2 = p1.then(() => { 
    // 1、若执行无错,p2的状态为完成,数据为后续数据的返回值, 如下,p2的状态为成功,数据为100 
    return 100 
    // 2、执行有错,新任务的状态为失败,数据为异常对象,如下,p2的状态为失败 
    throw new Error('错误') 
    // 3、返回的是一个任务对象,新任务的状态和数据与该任务对象一致, 如下,p2的状态依赖于下面的promise的状态 
    return new Promise(() => {
    }) 
}) 
setTimeout(() => { console.log(p2) }, 1000)

练习代码

const p = new Promise((resolve, reject) => {
    resolve(1)
}).then((res) => {
    console.log(res)
    return new Error('2')
}).catch((err) => {
    throw err
    return 3
}).then((res) => {
    console.log(res)
})

setTimeout(() => {
    console.log(p)
}, 1000)

结果:

1
Error(2)

过程:

pro1 fulfilled 1 //因为resolve(1) ,所以状态为fulfilled,数据为1
pro2 fulfilled Error(2) //因为return new Error(2)只是返回了error,并没有抛出error
pro3 fulfilled Error(2) //因为pro2成功,pro3是catch,没有针对成功做处理,所以状态和数据和pro2一样
pro4 fulfilled Error(2) //因为pro3成功,且返回结果为Error(2),所以pro4的状态为成功,且数据为Error(2)

总结: 整个过程其实是promise的异常穿透过程:当使用promise的then链式调用时, 可以在最后指定失败的回调, 前面任何操作出了异常, 都会传到最后失败的回调中处理
中断promise链:返回状态为pedding的promise对象

静态方法

  • promise.all:

将多个promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。需要注意,Promise.all获得的成功结果的数组里面的数据顺序和Promise.all接收到的数组顺序是一致的,这样当遇到发送多个请求并根据请求顺序获取和使用数据的场景,就可以使用Promise.all来解决。

let p1 = new Promise((resolve, reject) => {
    resolve('OK');
})
let p2 = Promise.reject('Error');
let p3 = Promise.resolve('Oh Yeah');

const result = Promise.all([p1, p2, p3]);
  • Promise.race:

就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。当要做一件事,超过多长时间就不做了,可以用这个方法来解决:

注意,如果p1执行最快,那么promise.race()的结果为p1的结果,但是p2和p3也是会执行的

let p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('OK');
    }, 1000);
})
let p2 = Promise.reject('Error');
let p3 = Promise.resolve('Oh Yeah');
//调用,结果为Error
const result = Promise.race([p1, p2, p3]);
  • Promise.any:

接收一个可迭代对象作为参数,当其中任意一个promise成功后,就返回这个已经成功的promise,如果可迭代对象中没有成功的promise,则返回一个失败的promise和AggregateError类型的实例

const promise1 = new Promise((resolve, reject) => {
    setTimeout(reject, 100, 'promise 1 rejected');
});

const promise2 = new Promise((resolve, reject) => {
    setTimeout(resolve, 400, 'promise 2 resolved at 400 ms');
});

const promise3 = new Promise((resolve, reject) => {
    setTimeout(resolve, 700, 'promise 3 resolved at 800 ms');
});

(async () => {
    try {
        let value = await Promise.any([promise1, promise2, promise3]);
        // 结果为promise2,因为promise.any会忽略失败的promise1,返回第一个成功的promise2
        console.log(value);
    } catch (error) {
        console.log(error);
    }
})();
  • Promise.allsettled():

接收一个可迭代对象(比如数组)作为参数,该可迭代对象包含多个 Promise 实例。当所有的 Promise 实例都已经成功(fulfilled)或失败(rejected)时,Promise.allSettled() 返回的新的 Promise 实例才会解决(resolve)

与promise.all()的区别

与 Promise.all() 不同的是,Promise.all() 会在所有传入的 Promise 实例都成功时才解决,并且如果有一个 Promise 实例失败,它就会立即拒绝(reject)并返回那个失败的 Promise 的结果。而 Promise.allSettled() 则不管传入的 Promise 实例是成功还是失败,都会等待它们全部完成后才解决,并且它会以一个对象数组的形式返回每个 Promise 实例的结果。

const promise1 = Promise.resolve(42);
const promise2 = new Promise((resolve, reject) => setTimeout(reject, 100, 'error'));
const promise3 = Promise.resolve('Hello World');
 
Promise.allSettled([promise1, promise2, promise3])
  .then((results) => results.forEach((result) => {
    if (result.status === 'fulfilled') {
      // 处理成功的情况
    } else if (result.status === 'rejected') {
      // 处理失败的情况
    }
  }));

在这个例子中,promise1 和 promise3 会成功,而 promise2 会失败。但是 Promise.allSettled() 会等待所有三个 Promise 都完成后才解决,并且它会返回一个包含三个结果对象的数组。然后,我们可以遍历这个结果数组,并根据每个结果对象的 status 属性来处理成功或失败的情况。

async和await

  • 功能

    async关键字用于修饰函数,被它修饰的函数,一定返回promise
    await关键字表示等待某个promsie完成,它必须用于async函数中

    • 知识点1: async用于声明一个函数是异步的,await 用于等待一个异步方法执行完成,await 只能出现在 async 函数中

    • 知识点2: await是.then的语法糖。await A函数表示先执行A函数、A函数返回一个promise对象,通过.then获取promise的结果。 函数的执行是同步的,但是获取promise的结果是异步的。

    • 知识点3: await是一个让出线程的标志, await函数后面的代码,需要等到await函数执行完毕后才执行。await修饰的函数执行完毕后,会跳出async修饰的函数,执行其他代码

  • 将async/await改写成promise async function async1() { await async2(); console.log('async1 end') } async function async2() { console.log('async2 end') } async1();

    //改写后
    function async1() {
        new Promise((resolve) => {
            console.log('async2    end')
        }).then(res => {
            console.log('async1 end')
        })
    }
    

面试题(输出题)

1. 改变promise状态和指定回调函数谁先谁后
都有可能,常规是先指定回调再改变状态,但也可以先改状态再指定回调.
1.1 先指定回调函数,再改变状态

new Promise((resolve,reject) => {
    setTimeout(() => { // 后改变状态,同时指定了数据,异步执行回调函数
            resolve(1)
    },1000)
}).then( // 这种情况是先指定回调函数,保存当前指定的回调函数
    value => {},
    reason => {}
)

因为promise的状态改变是在resolve或者reject函数执行过程中进行的。上述代码把resolve放在定时器中,因此会在下一个事件循环中执行。在此之前then中的回调函数(onResolved、onRejected)已指定,会保存在一个数组中。等resolve执行时候再取出来执行。
1.2 先改变状态,再执行回调函数

new Promise((resolve,reject) => {
    // 先改变状态,同时指定了数据
    resolve(1)
}).then( // 这种情况是后指定回调函数,异步执行回调函数
    value => {},
    reason => {}
)

上述代码先执行resove(1)改变promise的状态,再指定并执行回调函数。
什么时候才能得到数据?
①如果先指定的回调, 那当状态发生改变时, 回调函数就会调用, 得到数据
②如果先改变的状态, 那当指定回调时, 回调函数就会调用, 得到数据

2. 如何先改状态再指定回调?
(1)在执行器中直接调用 resolve()/reject()

const p2 = new Promise((resolve,reject) => {//执行器函数(同步执行)
    resolve('Success Data');//修改状态(同步执行)
});
p2.then(//.then()同步执行
    value => {console.log('value: ' + value);} //成功的回调函数(异步执行)
);

(2)延迟更长时间才调用 then()

 const p3 = new Promise((resolve,reject) => {
    setTimeout(() => {
        resolve('Success Data!!');
    })
},1000);
setTimeout(() => {
    p3.then(
        value => {console.log('value: ' + value);}
    )
},2000)

3. 判断输出结果

(1)then方法返回一个promise

image.png

image.png
输出p2的状态为pending的原因是console.log()为同步代码,所以会先执行console.log(p2),此时p2的状态为pending;接着执行resolve()改变p1的状态;接着执行then()中的回调,p2的状态改变为fulfilled

(2)下面的任务最终状态是什么,相关的数据或失败原因是什么,最终输出什么

new Promise((resolve, reject) => {
    console.log("任务开始")
    resolve(1)
    reject(2)
    resolve(3)
    console.log("任务结束")
})

最终状态:fulfilled(因为resolve最先被调用,所以状态更改为fulfilled后,不作改变)
相关的数据是:1(因为resolve(1)调用resolve()函数时传入1)
最终输出:

任务开始  
任务结束

(3)输出结果

 const p1 = new Promise((resolve,reject) => {
     //执行器函数(同步执行)
     console.log('执行器函数开始执行');
     setTimeout(() => {
        //执行异步操作,并在里面进行状态的修改(异步执行)
        console.log('异步任务setTime开始执行---------------');
        if(3 == 3){
            console.log('1');
            resolve('异步任务执行成功!!');//此时状态发生改变
            console.log('2');
        }else{
            reject('异步任务执行失败!!')
        }
         console.log('异步任务setTime即将结束---------------');
     },0)

});
 //指定回调(因为状态的改变是在异步操作内完成,因此执行器函数执行完就会立即执行p1.then();而状态的改变是在其之后进行执行)
p1.then(
    //成功的回调(异步执行)
    value => {console.log('状态发生改变后获取的value :' + value);},
    //失败的回调(异步执行)
    reason => {console.log('状态发生改变后获取的reason:' + reason);}
)
console.log('测试.then()里面的函数是否是异步执行的');//(同步执行)

结果:

image.png 4. 异步编程的解决方案
1、回调函数 的方式:使用回调函数方式的一个缺点是,多个回调函数嵌套的时候会造成回调函数地狱,上下两层的回调函数间的代码耦合度太高,不利于代码的可维护。

2、Promise 的方式:使用 Promise 的方式可以将嵌套的回调函数作为链式调用。但是使用这种方法,有时会造成多个 then 的链式调用,可能会造成代码的语义不够明确。

3、generator 的方式:Generator 函数最大特点就是可以交出函数的执行权(即暂停执行),异步操作需要暂停的地方都用 yield语句注明。yield命令表示执行到此处时,执行权交给其他协程。等到执行完之后再从暂停的地方继续往后执行。

4、async 函数 的方式:async用于声明一个函数是异步的,await 用于等待一个异步方法执行完成。async 函数是 generator 和 promise 实现的一个自动执行的语法糖,它内部自带执行器,当函数内部执行到一个 await 语句的时候,如果语句返回一个 promise 对象,那么函数将会等待 promise 对象的状态变为 resolve 后再继续向下执行。因此可以将异步逻辑,转化为同步的顺序来书写,并且这个函数可以自动执行。

5. 解决异步编程代码

  1. 回调地狱
    弊端:data1,data2,data3这三个数据不能重名,调试起来很不方便。2、需要的操作多时,回调会一直往里塌陷。
const fs = require("fs")
fs.readFile("./one.txt", (err, data1) => {
    fs.readFile("./two.txt", (err, data2) => {
        fs.readFile("./three.txt", (err, data3) => {
            console.log(data1 + "\n" + data2 + "\n" + data3)
        })
    })
})

2. promise

const fs = require("fs")

const p = new Promise((resolve, reject) => {
    fs.readFile("./one.txt", (err, data) => {
        resolve(data)
    })
})
p.then(value => {
    return new Promise((resolve, reject) => {
        fs.readFile("./two.txt", (err, data) => {
            resolve([value, data])
        })
    })
}).then(value => {
    return new Promise((resolve, reject) => {
        fs.readFile("./three.txt", (err, data) => {
            value.push(data)
            resolve(value)
        })
    })
}).then(value => {
    let str = value.join("\n")
    console.log(str)
})

3. async和await

const fs = require("fs")
function readOne() {
    return new Promise((resolve, reject) => {
        fs.readFile("./one.txt", (err, data) => {
            if (err) {
                reject(err)
            }
            resolve(data)
        })
    })
}
function readTwo() {
    return new Promise((resolve, reject) => {
        fs.readFile("./two.txt", (err, data) => {
            if (err) {
                reject(err)
            }
            resolve(data)
        })
    })
}
function readThree() {
    return new Promise((resolve, reject) => {
        fs.readFile("./three.txt", (err, data) => {
            if (err) {
                reject(err)
            }
            resolve(data)
        })
    })
}

async function test() {
    let one = await readOne()
    let two = await readTwo()
    let three = await readThree()
    console.log(one + '\n' + two + '\n' + three)
}
test()

6. 输出结果题
1.

async function m1() {
    return 1;
}
async function m2() {
    // 等同于const n = await Promise.resolve(1)
    const n = await m1();
    console.log(n);
    return 2;
}
async function m3() {
    const n = m2();
    console.log(n);
    return 3;
}
m3().then((n) => {
    console.log(n);
})
m3();
console.log(4)

image.png

结果:

Promise { <pending> }  // 先是15行调用m3,因为11行没有await m2() 所以12行直接输出n,其中n为pending的promise
Promise { <pending> }  // 18行调用m3,12行接着输出pending的promise
4 //同步代码19行输出4
1 //15行调用m3时,第7行输出的微队列中的1
3 //15行调用m3时,第16行输出的微队列中的3
1 //18行调用m3时,第7行输出的微队列中的1

2.

Promise.resolve(1).then(2).then(Promise.resolve(3)).then(console.log);

结果:

1

3.

var a;
var b = new Promise((resolve, reject) => {
    console.log('promise1');
    setTimeout(() => {
        resolve()
    }, 1000);
})
.then(() => {
    console.log('promise2')
})
.then(() => {
    console.log('promise3')
})
.then(() => {
    console.log('promise4')
})

a = new Promise(async (resolve, reject) => {
    console.log(a);
    await b;
    console.log(a);
    console.log('after1');
    await a;  //等待自己完成,永远完成不了,所以下面代码永远不执行
    resolve(true);
    console.log('after2');
})

console.log('end');

结果:

promise1
undefined // 返回undefined的原因是a被重新赋值为promise,但在这个节点还没有赋值完成
end
promise2
promise3
promise4
Promise { <pending> }
after1

4.

async function async1() {
    console.log('async start1');
    await async2();
    console.log('async1 end');
}
async function async2() {
    console.log('async2');
}
console.log('script start');
setTimeout(function () {
    console.log('setTimeout');
}, 0);
async1();

new Promise(function (resolve) {
    console.log('promise1');
    resolve();
}).then(function () {
    console.log('promise2');
});
console.log('script end');

结果:

script start
async start1
async2
promise1
script end
async1 end
promise2
setTimeout

5.

Promise.resolve().then(() => {
    console.log(0)
    return Promise.resolve(4)
}).then((res) => {
    console.log(res)
})

Promise.resolve().then(() => {
    console.log(1)
}).then(() => {
    console.log(2)
}).then(() => {
    console.log(3)
    // return Promise.resolve(5)
}).then((res) => {
    // console.log(res)
    console.log(6)
}).then(() => {
    console.log(7)
})

结果

0
1
2
3
4
5
6

解释

首先执行第一行的Promise.resolve(),将第一行的then()方法放入微队列

接着执行第八行的Promise.resolve(),将第八行的then()方法放入微队列

接着执行第一个微队列中的代码(也就是第2,3行), 输出0,并且将第4行的then()方法放入微队列

接着执行第第二个微队列中的代码(即第9行),输出1,并且将第10行的then()方法放入微队列

接着后面几个then()方法依次执行,输出2,3,4,5,6

至于为啥4在2,3后输出,是因为第3行返回的是一个promise,并且then()方法返回的也是一个promise,在原生 Promise 中 return Promise.reolve(4) 会多创建 2 次微任务进入队列,这就造成了 4 的位置排到了3的后面,如果第4行不是Promise.resolve(4),而是return 4,那总体返回结果就是0,1,4,3,5,6

  1. 如果有100个请求,如何使用promsie控制并发
    思路:使用promise.all()实现并发,将100个url分为10组,每组10个请求,使用promise.all()一次发送10个请求
const urls = [url1, url2, ... url100]
const maxConcurrentNum = 10
//数组分块
function chunk(arr, chunk) {
    let result = []
    for(let i=0;i<arr.length;i+=chunk) {
        result.push(arr.slice(i, i+chunk))
    }
    return result
}

// 异步请求方法
function fetchUrl(url) {
    return new Promise((resolve, reject) => {
        fetch(url)
        .then(res => resolve(res))
        .catch(err => reject(err))
    })
}

// 对url数组分块处理
const chunkedUrls = chunk(urls, maxConcurrentNum)
(async function() {
    try {
        for (let urls of chunkedUrls) {
            const promise = urls.map(url => fetchUrl(url))
            const results = await Promise.all(promise)
        }
    }
})();

7. 输出结果练习题

juejin.cn/post/684490…

错题总结1:

Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(console.log)

输出

1

原因

  1. Promise.resolve(1) :返回一个解析为数字1的Promise对象。

  2. .then(2) :这里有一个常见的误解。.then() 方法期望两个参数:一个是处理Promise成功(fulfilled)状态的函数,另一个是处理失败(rejected)状态的函数。如果传递给 .then() 的不是一个函数,那么它会被忽略,并且 .then() 会返回一个新的Promise对象,这个新的Promise对象会采用前一个Promise对象的状态和值。因此,这里的 2 被忽略,这一步相当于 .then(undefined),它不会改变Promise的状态或值,只是简单地传递了上一个Promise的结果(即数字1)给下一个 .then()

  3. .then(Promise.resolve(3)) :同样地,.then() 方法期望一个函数作为参数。但这里传递的是 Promise.resolve(3),它返回一个解析为数字3的Promise对象。由于 .then() 期望一个函数而不是一个Promise对象,这个Promise对象会被忽略

  4. .then(console.log) :这里,console.log 被用作处理函数。由于前面的步骤中Promise的值一直是数字1(没有被任何 .then() 中的非函数参数改变),所以这里 console.log 会接收到数字1作为参数,并将其打印到控制台。

错题总结2:

async function async1 () {
  console.log('async1 start');
  await new Promise(resolve => {
    console.log('promise1')
  })
  console.log('async1 success');
  return 'async1 end'
}
console.log('srcipt start')
async1().then(res => console.log(res))
console.log('srcipt end')

输出

'script start'
'async1 start'
'promise1'
'script end'

原因

async1await后面的Promise是没有返回值的,也就是它的状态始终是pending状态。

所以在await之后的内容是不会执行的,也包括async1后面的 .then

8. 大厂面试题(来自7中的链接)

8.1 使用Promise实现每隔1秒输出1,2,3

(考察异步任务按顺序执行)

function delayLog(value, delay) {
    return new Promise(() => {
        setTimeout(() => {
            console.log(value)
            resolve()
        }, delay)
    })
}

delayLog(1, 1000)
.then(() => {
    delayLog(2, 2000)
})
.then(() => {
    delayLog(3, 3000)
})

8.2 封装一个异步加载图片的方法

function loadImg(url) {
    return new Promise((resolve, reject) => {
        const img = new Image();
        img.onload = function () {
            console.log("一张图片加载完成");
            resolve(img);
        };
        img.onerror = function () {
            reject(new Error('Could not load image at' + url));
        };
        img.src = url;
    });
}

8.3 限制异步操作的并发个数并尽可能快的完成全部

数组urls中存储8个图片资源的url,而且已经有一个函数function loadImg,输入一个url链接,返回一个Promise,该Promise在图片下载完成的时候resolve,下载失败则reject

要求,任何时刻同时下载的链接数量不可以超过3个,并尽可能快速地将所有图片下载完成

var urls = [
  "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting1.png",
  "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting2.png",
  "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting3.png",
  "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting4.png",
  "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting5.png",
  "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/bpmn6.png",
  "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/bpmn7.png",
  "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/bpmn8.png",
];
function loadImg(url) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = function() {
      console.log("一张图片加载完成");
      resolve(img);
    };
    img.onerror = function() {
    	reject(new Error('Could not load image at' + url));
    };
    img.src = url;
  });

思路1:

  • 拿到urls,然后将这个数组每3个url一组创建成一个二维数组

  • 然后用Promise.all()每次加载一组url(也就是并发3个),这一组加载完再加载下一组

缺点:每次都要等到上一组全部加载完之后,才加载下一组,那如果上一组有2个已经加载完了,还有1个特别慢,还在加载,要等这个慢的也加载完才能进入下一组。这明显会照常卡顿,影响加载效率

function limitLoad (urls, handler, limit) {
  const data = []; // 存储所有的加载结果
  let p = Promise.resolve();
  const handleUrls = (urls) => { // 这个函数是为了生成3个url为一组的二维数组
    const doubleDim = [];
    const len = Math.ceil(urls.length / limit); // Math.ceil(8 / 3) = 3
    console.log(len) // 3, 表示二维数组的长度为3
    for (let i = 0; i < len; i++) {
      doubleDim.push(urls.slice(i * limit, (i + 1) * limit))
    }
    return doubleDim;
  }
  const ajaxImage = (urlCollect) => { // 将一组字符串url 转换为一个加载图片的数组
    console.log(urlCollect)
    return urlCollect.map(url => handler(url))
  }
  const doubleDim = handleUrls(urls); // 得到3个url为一组的二维数组
  doubleDim.forEach(urlCollect => {
    p = p.then(() => Promise.all(ajaxImage(urlCollect))).then(res => {
      data.push(...res); // 将每次的结果展开,并存储到data中 (res为:[img, img, img])
      return data;
    })
  })
  return p;
}
limitLoad(urls, loadImg, 3).then(res => {
  console.log(res); // 最终得到的是长度为8的img数组: [img, img, img, ...]
  res.forEach(img => {
    document.body.appendChild(img);
  })
});

思路2:

先请求urls中的前面三个(下标为0,1,2),并且请求的时候使用Promise.race()来同时请求,三个中有一个先完成了(例如下标为1的图片),我们就把这个当前数组中已经完成的那一项(第1项)换成还没有请求的那一项(urls中下标为3)。

直到urls已经遍历完了,然后将最后三个没有完成的请求(也就是状态没有改变的Promise)用Promise.all()来加载它们。

function limitLoad(urls, handler, limit) {
  let sequence = [].concat(urls); // 复制urls
  // 这一步是为了初始化 promises 这个"容器"
  let promises = sequence.splice(0, limit).map((url, index) => {
    return handler(url).then(() => {
      // 返回下标是为了知道数组中是哪一项最先完成
      return index;
    });
  });
  // 注意这里要将整个变量过程返回,这样得到的就是一个Promise,可以在外面链式调用
  return sequence
    .reduce((pCollect, url) => {
      return pCollect
        .then(() => {
          return Promise.race(promises); // 返回已经完成的下标
        })
        .then(fastestIndex => { // 获取到已经完成的下标
        	// 将"容器"内已经完成的那一项替换
          promises[fastestIndex] = handler(url).then(
            () => {
              return fastestIndex; // 要继续将这个下标返回,以便下一次变量
            }
          );
        })
        .catch(err => {
          console.error(err);
        });
    }, Promise.resolve()) // 初始化传入
    .then(() => { // 最后三个用.all来调用
      return Promise.all(promises);
    });
}
limitLoad(urls, loadImg, 3)
  .then(res => {
    console.log("图片全部加载完毕");
    console.log(res);
  })
  .catch(err => {
    console.error(err);
  });