Promise基础
# Promise 特性之 then、catch、finally:
- Promise 的状态一经改变就不能再改变;
const promise = new Promise((resolve, reject) => {
resolve("success1");
reject("error");
resolve("success2");
});
promise
.then((res) => {
console.log("then: ", res);
})
.catch((err) => {
console.log("catch: ", err);
});
// "then: success1"
2
3
4
5
6
7
8
9
10
11
12
13
解析:构造函数中的resolve
或reject
只有第一次执行有效,多次调用没有任何作用;即 Promise 的状态一经改变就不能再改变。
catch
不管被链接到哪里,都能捕获上层未捕获过的错误;.then
和.catch
都会返回一个新的 Promise;
const promise = new Promise((resolve, reject) => {
reject("error");
resolve("success2");
});
promise
.then((res) => {
console.log("then1: ", res);
})
.then((res) => {
console.log("then2: ", res);
})
.catch((err) => {
console.log("catch: ", err);
})
.then((res) => {
console.log("then3: ", res);
});
// "catch: " "error"
// "then3: " undefined
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
解析: catch 不管连接到哪里,都可以捕获上层未捕获过的错误;then3 被执行是因为 catch()也会返回一个 Promise,且由于这个 Promise 没有返回值,所以打印出来的是 undefined。(因为 catch 到了错误,所以返回的 Promise 是成功态,但是没有返回值,故为 undefined)
- 在 Promise 中,返回任意一个非
promise
的值都会包裹成 promise 对象;例如return 2
会被包装成:return Promise.resolve(2)
;
Promise.resolve(1)
.then((res) => {
console.log(res);
return 2;
})
.catch((err) => {
return 3;
})
.then((res) => {
console.log(res);
});
// 1
// 2
2
3
4
5
6
7
8
9
10
11
12
13
解析:Promise 可以链式调用,不过 promise 每次调用.then
或者.catch
都会返回一个新的 promise,从而实现链式调用,它并不像一般我们任务的链式调用一样return this
。
上面的输出结果之所以依次打印出 1 和 2,那是因为 resolve(1)之后走的是第一个 then 方法,并没有走 catch 里,所以第二个 then 中的 res 得到的实际上是第一个 then 的返回值 return 2,且 return 2 会被包装成 resolve(2)。
同理,把上述题目中 Promise.resolve(1)改 Promise.reject(1):
Promise.reject(1)
.then((res) => {
console.log(res);
return 2;
})
.catch((err) => {
console.log(err);
return 3;
})
.then((res) => {
console.log(res);
});
// 1
// 3
2
3
4
5
6
7
8
9
10
11
12
13
14
解析:打印的当然是 1 和 3 啦,因为 reject(1)此时走的就是 catch,且第二个 then 中的 res 得到的就是 catch 中的返回值。
- Promise 的
.then
或者.catch
可以被调用多次;但如果 Promise 内部的状态一经改变,并且有了一个值,那么后续每次调用.then
或者.catch
的时候都会直接拿到该值。
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
console.log("timer");
resolve("success");
}, 1000);
});
const start = Date.now();
promise.then((res) => {
console.log(res, Date.now() - start);
});
promise.then((res) => {
console.log(res, Date.now() - start);
});
// 'timer'
// 'success' 1001
// 'success' 1002
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
解析:Promise 的 .then 或者 .catch 可以被调用多次,但这里 Promise 构造函数只执行一次。或者说 promise 内部状态一经改变,并且有了一个值,那么后续每次调用 .then 或者 .catch 都会直接拿到该值。
- .then 或者.catch 中 return 一个 erro 对象并不会抛出错误,所以不会被后续的.catch 捕获;
Promise.resolve()
.then(() => {
return new Error("error!!!");
})
.then((res) => {
console.log("then: ", res);
})
.catch((err) => {
console.log("catch: ", err);
});
// "then: " "Error: error!!!"
2
3
4
5
6
7
8
9
10
11
解析: 它走的是.then,返回任意一个非 promise 的值都会被包裹成 promise 对象,因此这里的return new Error('error!!!')
也会被包裹成return Promise.resolve(new Error('error!!!'))
。
提示
如果要抛出一个错误的,建议采用:return Promise.reject(new Error('error!!!'));
或throw new Error('error!!!')
。
- .then 或.catch 返回的值不能是 promise 本身,否则会造成死循环。
const promise = Promise.resolve().then(() => {
return promise;
});
promise.catch(console.err);
2
3
4
上述程序会报错:
Uncaught (in promise) TypeError: Chaining cycle detected for promise #<Promise>
- .then 或者 .catch 的参数期望是函数,传入非函数则会发生值透传;
Promise.resolve(1).then(2).then(Promise.resolve(3)).then(console.log);
// 1
2
解析:第一个 then 和第二个 then 中传入的都不是函数,一个是数字类型,一个是对象类型,因此发生了透传,将 resolve(1) 的值直接传到最后一个 then 里。
- .then 函数中存在两个参数,第一个参数是用来处理 Promise 成功的函数,第二个则是处理失败的函数;
Promise.reject("error!!!")
.then(
(res) => {
console.log("success", res);
},
(err) => {
console.log("error", err);
}
)
.catch((err) => {
console.log("catch", err);
});
// 'error' 'error!!!'
2
3
4
5
6
7
8
9
10
11
12
13
解析:它进入了.then()中的第二个参数里面,如果把第二个参数去掉,就会进入 catch()中,因为 catch 会捕获上层所有未捕获的错误。
但是如果在 then 的第一个参数中丢出错误呢?
Promise.resolve()
.then(
function success(res) {
throw new Error("error!!!");
},
function fail1(err) {
console.log("fail1", err);
}
)
.catch(function fail2(err) {
console.log("fail2", err);
});
// fail2 Error: error!!!
// at success (C:\Users\zhang\Desktop\01.js:60:11)
2
3
4
5
6
7
8
9
10
11
12
13
14
解析:由于 Promise 调用的是 resolve(),因此.then()执行的应该是 success()函数,可是 success()函数抛出的是一个错误,它会被后面的 catch()给捕获到,而不是被 fail1 函数捕获。
.finally()
方法特点:
.finally()
方法不管 Promise 对象最后的状态如何都会执行;.finally()
方法的回调函数不接受任何的参数,所以说在.finally()
中的函数中是没办法知道 Promise 最终的状态是 resolved 还是 rejected 的;- 它返回的默认是上一次的 Promise 对象,不过如果抛出的是一个异常则返回的是异常的 Promise 对象。
Promise.resolve("1")
.then((res) => {
console.log(res);
})
.finally(() => {
console.log("finally");
});
Promise.resolve("2")
.finally(() => {
console.log("finally2");
return "我是finally2返回的值";
})
.then((res) => {
console.log("finally2后面的then函数", res);
});
// '1'
// 'finally2'
// 'finally'
// 'finally2后面的then函数' '2'
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
解析:这两个 Promise 的 finally 都会执行,及时 finally2 返回了值,后面 then 函数接收到的结果仍然是'2';finally2 在 finally 之前执行,是因为 finally 需要等到第一个 Promise.then()执行完毕后才会放入到微任务队列中。
如果在 finally 中抛出异常,
Promise.resolve("1")
.finally(() => {
console.log("finally1");
throw new Error("我是finally中抛出的异常");
})
.then((res) => {
console.log("finally后面的then函数", res);
})
.catch((err) => {
console.log("捕获错误", err);
});
// 'finally1'
// '捕获错误' Error: 我是finally中抛出的异常
2
3
4
5
6
7
8
9
10
11
12
13
如果改成 return New Error,打印出来的就是finally后面的then函数 1
。
下面这个例子涉及到多次链式调用:
function promise1() {
let p = new Promise((resolve) => {
console.log("promise1");
resolve("1");
});
return p;
}
function promise2() {
return new Promise((resolve, reject) => {
reject("error");
});
}
promise1()
.then((res) => console.log(res))
.catch((err) => console.log(err))
.finally(() => console.log("finally1"));
promise2()
.then((res) => console.log(res))
.catch((err) => console.log(err))
.finally(() => console.log("finally2"));
// promise1
// 1
// error
// finally1
// finally2
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
解析:这里要注意的点:链式调用后面的内容需要等前一个调用执行完才会加入队列执行。
# 几道常见面试题
- 使用 Promise 实现每隔一秒输出 1,2,3
通过 reduce 使用 Promise.resolve()构造连续 Promise 回调实现系列异步请求按顺序执行:
const arr = [1, 2, 3, 4];
arr.reduce((p, x) => {
return p.then(() => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(console.log(x));
}, 1000);
});
});
}, Promise.resolve());
2
3
4
5
6
7
8
9
10
通过 async、await 和 for 循环来实现
const arr = [1, 2, 3, 4];
async function go(arr) {
for (let i = 0; i < arr.length; i++) {
await new Promise((resolve) => {
setTimeout(() => {
resolve(console.log(x));
}, 1000);
});
}
}
go(arr);
2
3
4
5
6
7
8
9
10
11
- 使用 Promise 实现红绿灯交替重复亮
function red() {
console.log("red 3s");
}
function yellow() {
console.log("yellow 2s");
}
function green() {
console.log("green 1s");
}
function light(timer, callback) {
return new Promise((resolve) => {
return setTimeout(() => {
callback();
resolve();
}, timer);
});
}
function step() {
return Promise.resolve()
.then(() => light(3000, red()))
.then(() => light(2000, yellow()))
.then(() => light(1000, green()))
.then(() => step());
}
setp();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
- 实现 mergePromise 函数
const time = (timer) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, timer);
});
};
const ajax1 = () =>
time(2000).then(() => {
console.log(1);
return 1;
});
const ajax2 = () =>
time(1000).then(() => {
console.log(2);
return 2;
});
const ajax3 = () =>
time(1000).then(() => {
console.log(3);
return 3;
});
function mergePromise(ajaxArr) {
const data = [];
let p = Promise.resolve();
ajaxArr.forEach((ajax) => {
p = p.then(ajax).then((res) => {
data.push(res);
return data;
});
});
return p;
}
mergePromise([ajax1, ajax2, ajax3]).then((data) => {
console.log("done");
console.log(data); // data 为 [1,2,3]
});
// 要求分别输出
// 1
// 2
// 3
// done
// [1,2,3]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
- 封装一个异步加载图片的方法
function loadImg(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
console.log("img loading sucess");
resolve(img);
};
img.onerror = () => {
reject(new Error("Could not load image at " + url));
};
img.src = url;
});
}
2
3
4
5
6
7
8
9
10
11
12
13
- 限制异步操作的并发个数并尽可能快地完成
给定八个图片 url 地址的数组,要求同时加载图片不超过 3 个,并且尽可能快地完成图片的加载;如果采用 Promise.all 的话,有可能单个图片加载很慢,会阻塞整组的加载,从而会影响下一组的加载,可以通过 Promise.race()方法来一直不断更新加载,最后三个用 all 方法。(单个图片加载采用 4 中方法)
function limitLoad(urls, handler, limit) {
const sequence = [].concat(urls);
let promises = sequence.splice(0, limit).map((url, index) => {
return handler(url).then(() => index);
});
return sequence
.reduce((p, url) => {
return p
.then(() => {
return Promise.race(promises);
})
.then((fastestIndex) => {
promises[fastestIndex] = handler(url).then(() => fastestIndex);
})
.catch((err) => {
console.log(err);
});
}, Promise.resolve())
.then(() => {
return Promise.all(promises);
});
}
limitLoad(urls, loadImg, 3)
.then((res) => {
console.log("图片全部加载完毕");
console.log(res);
})
.catch((err) => {
console.error(err);
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
- for + setTimeout 输出问题
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(new Date(), i);
}, 1000);
}
2
3
4
5
在了解了 JS 的事件循环之后,很容易得出答案:5 -> 5,5,5,5,5
,先输出 5,然后隔 1s 输出 5 个 5
如何让输出结果变为5 -> 0,1,2,3,4
呢?可以将 var 换成 let,利用 ES6 的块级作用域,不过第一个 5 就打印不出来了。
① 可以通过立即执行函数来解决闭包造成的问题:
for (var i = 0; i < 5; i++) {
(function (j) {
setTimeout(() => {
console.log(new Date(), i);
}, 1000);
})(i);
}
console.log(new Date(), i);
2
3
4
5
6
7
8
9
② 也可以利用 JS 基本类型的参数传递是按值传递的特征:
var output = (i) => {
setTimeout(() => {
console.log(new Date(), i);
}, 1000);
};
for (var i = 0; i < 5; i++) {
output(i);
}
console.log(new Date(), i);
2
3
4
5
6
7
8
9
10
③ 利用 Promise 来处理异步事件
const tasks = [];
const output = (i) =>
new Promise((resolve) => {
setTimeout(() => {
console.log(new Date(), i);
resolve();
}, 1000 * i);
});
for (var i = 0; i < 5; i++) {
tasks[i] = output(i);
}
Promise.all(tasks).then(() => {
setTimeout(() => {
console.log(new Date(), i);
}, 1000);
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
④ 使用 ES7 async
function sleep = (ms) => new Promise((resolve) => {
setTimeout(()=> {
resolve();
}, ms)
})
( async () => {
for(var i=0;i<5;i++) {
await sleep(1000);
console.log(new Date,i);
}
await sleep(1000);
console.log(new Date,i);
})()
2
3
4
5
6
7
8
9
10
11
12
13
14
参考:
要就来 45 道 Promise 面试题一次爽到底(1.1w 字用心整理) (opens new window) 80% 应聘者都不及格的 JS 面试题 (opens new window)