promise
回调地狱
定义:在一个回调函数中嵌套另外一个回调函数,导致出现所谓的回调地狱。
原因:每次调用都依赖于上一次调用的结果,需要嵌套一系列的回调函数。
示例
setTimeout(function () {
console.log('第一层');
setTimeout(function () {
console.log('第二层');
setTimeout(function () {
console.log('第三层');
}, 1000)
}, 2000)
}, 3000)
缺点
-
不利于阅读(缩进太过频繁);
-
还不利于异常处理(可能会重复写某些异常处理的代码)。
promise
-
Promise功能
-
Promise 是 JS 中进行异步编程的新解决方案;
-
从语法上来说:Promise 是一个构造函数;
-
从功能上来说:Promise 对象用来封装一个异步操作并可以获取其成功/失败的结果值。
-
promise接受一个执行器作为参数、这个执行器是同步的
-
-
Promise状态改变
- Promise 对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。
- Promise对象的状态改变,只有两种可能:从pending变为fulfilled,称之为resolve;从pending变为rejected,称之为reject。
- 一个Promise对象只能发生一次状态改变。
-
Promise实例代码
// promise返回一个任务,该任务在指定的时间后完成 function promiseDemo() { return new Promise((resolve) => { setTimeout(() => { resolve(111); }, 1000); }) } // 利用delay函数,等待一秒钟,输出finish promiseDemo().then( (value) => { console.log(value) }, () => { console.log('失败的回调') } )
Promise优势
- 避免了回调地狱
Promise通过链式调用 .then() 方法来处理异步任务的结果,避免了过多的嵌套回调,大大提高了代码的可读性和可维护性
-
更好的异步流程控制
-
顺序执行异步任务:通过在
.then()方法中返回一个新的Promise,每个异步任务依赖于前一个异步任务的结果,这样可以将多个异步任务按顺序串联起来,同时可以确保异步任务按照特定的顺序依次完成 -
并行执行异步任务:除了顺序执行,Promise还提供了
Promise.all()和Promise.race()等方法来实现多个异步任务的并行执行。Promise.all()可以同时发起多个异步任务,并在所有任务都完成后统一处理结果;Promise.race()则是多个异步任务中只要有一个完成,就立即返回其结果,这些方法为不同的异步场景提供了灵活的解决方案
-
-
增强了代码的可读性
类似同步代码的编写风格:使用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输出结果原理
- 新任务的状态取决于后续处理
-
- 若没有后续的相关处理,新任务的状态和前任务一样,数据为前任务的数据
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的一样
-
- 若有后续处理但还未执行,新任务挂起
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
-
- 若后续处理执行了,则根据后续处理的情况确定新任务的状态
-
后续处理执行无错,新任务的状态为完成,数据为后续数据的返回值
-
后续处理执行有错,新任务的状态为失败,数据为异常对象
-
后续执行返回的是一个任务对象,新任务的状态和数据与该任务对象一致
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
输出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()里面的函数是否是异步执行的');//(同步执行)
结果:
4. 异步编程的解决方案
1、回调函数 的方式:使用回调函数方式的一个缺点是,多个回调函数嵌套的时候会造成回调函数地狱,上下两层的回调函数间的代码耦合度太高,不利于代码的可维护。
2、Promise 的方式:使用 Promise 的方式可以将嵌套的回调函数作为链式调用。但是使用这种方法,有时会造成多个 then 的链式调用,可能会造成代码的语义不够明确。
3、generator 的方式:Generator 函数最大特点就是可以交出函数的执行权(即暂停执行),异步操作需要暂停的地方都用 yield语句注明。yield命令表示执行到此处时,执行权交给其他协程。等到执行完之后再从暂停的地方继续往后执行。
4、async 函数 的方式:async用于声明一个函数是异步的,await 用于等待一个异步方法执行完成。async 函数是 generator 和 promise 实现的一个自动执行的语法糖,它内部自带执行器,当函数内部执行到一个 await 语句的时候,如果语句返回一个 promise 对象,那么函数将会等待 promise 对象的状态变为 resolve 后再继续向下执行。因此可以将异步逻辑,转化为同步的顺序来书写,并且这个函数可以自动执行。
5. 解决异步编程代码
- 回调地狱
弊端: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)
结果:
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
- 如果有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. 输出结果练习题
错题总结1:
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(console.log)
输出
1
原因
-
Promise.resolve(1):返回一个解析为数字1的Promise对象。 -
.then(2):这里有一个常见的误解。.then()方法期望两个参数:一个是处理Promise成功(fulfilled)状态的函数,另一个是处理失败(rejected)状态的函数。如果传递给.then()的不是一个函数,那么它会被忽略,并且.then()会返回一个新的Promise对象,这个新的Promise对象会采用前一个Promise对象的状态和值。因此,这里的2被忽略,这一步相当于.then(undefined),它不会改变Promise的状态或值,只是简单地传递了上一个Promise的结果(即数字1)给下一个.then()。 -
.then(Promise.resolve(3)):同样地,.then()方法期望一个函数作为参数。但这里传递的是Promise.resolve(3),它返回一个解析为数字3的Promise对象。由于.then()期望一个函数而不是一个Promise对象,这个Promise对象会被忽略 -
.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'
原因
在async1中await后面的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);
});