概要
- 学习一门技术,最好的方式就是先了解这门技术是如何诞生的,及它解决了什么问题?
- 接下来将从一下几个方面介绍
Promise
:- 异步编程的问题:代码逻辑不连续;
- 回调地狱:嵌套了太多的回调函数;
Promise
:消灭嵌套调用;Promise
:合并多个任务的错误处理;Promise
与微任务的关系;
- 首先明确一下,
Promise
解决的是异步编码风格的问题,而不是一些其他的问题;
异步编程的问题:代码逻辑不连续
- 假设有一个请求,使用
XMLHttpRequest
来实现,代码如下:// 执行状态 function onResolve(response) { console.log(response); } function onReject(error) { console.log(error); } let xhr = new XMLHttpRequest(); xhr.ontimeout = function(e) { onReject(e); }; xhr.onerror = function(e) { onReject(e); }; xhr.onreadystatechange = function () { onResolve(xhr.response); }; // 设置请求类型,请求URL,是否同步信息 let URL = 'https://localhost:8080/getList'; xhr.open('Get', URL, true); // 设置参数 xhr.timeout = 3000; // 设置xhr请求的超时时间 xhr.responseType = "text"; // 设置响应返回的数据格式 xhr.setRequestHeader("X_TEST","time.geekbang"); // 发出请求 xhr.send();
- 可见上述这短短的一段代码里面竟然出现了五次回调,这么多的回调会导致代码的逻辑不连贯、不线性,非常不符合人的直觉,这就是异步回调影响到我们的编码方式;
回调地狱:嵌套了太多的回调函数
-
当然也可以把上述的异步代码封装成一个函数,调用的时候传入相应的请求参数、回调函数,就可以让处理流程变得线性,代码如下:
// request,请求信息,请求头,延时值,返回类型等 // resolve, 执行成功,回调该函数 // reject 执行失败,回调该函数 function XFetch(request, resolve, reject) { let xhr = new XMLHttpRequest(); xhr.ontimeout = function (e) { reject(e); }; xhr.onerror = function (e) { reject(e); }; xhr.onreadystatechange = function () { if (xhr.status = 200) resolve(xhr.response); }; xhr.open(request.method, request.url, request.sync); xhr.timeout = request.timeout; xhr.responseType = request.responseType; // 补充其他请求信息 // ... xhr.send(); } // 调用封装的异步代码 XFetch( { method: 'GET', url: 'https://localhost:8080/getList', sync: true }, function resolve(data) { console.log(data); }, function reject(e) { console.log(e); } );
-
封装异步代码在一些简单逻辑下可以满足需求,但如果逻辑复杂一点,会产生嵌套很多回调函数,从而陷入了回调地狱,代码如下:
XFetch( { method: 'GET', url: 'https://localhost:8080/getList', sync: true }, function resolve(response) { console.log(response); XFetch( { method: 'GET', url: 'https://localhost:8080/getList1', sync: true }, function resolve(response) { console.log(response); XFetch( { method: 'GET', url: 'https://localhost:8080/getList2', sync: true }, function resolve(response) { console.log(response); }, function reject(e) { console.log(e); }) }, function reject(e) { console.log(e); }) }, function reject(e) { console.log(e); } );
-
上述代码看上去很乱,归根结底有两点原因:
- 嵌套调用:下一个任务依赖上一个任务的请求结果,并在上一个任务的回调函数内部执行新的业务逻辑,当嵌套层次多了之后,代码的可读性就变得非常差;
- 任务的不确定性:执行每个任务都有两种可能的结果(成功或者失败),所以对每个任务都要进行一次额外的错误处理的方式,其明显增加了代码的混乱程度;
-
原因分析出来后,那么就需要解决这两个问题:
- 消灭嵌套调用;
- 合并多个任务的错误处理;
-
ES6引入
Promise
,就是为了解决上述这两个问题,下面具体介绍;
Promise
:消灭嵌套调用
Promise
主要通过下面两步解决嵌套回调问题:
-
Promise
实现了回调函数的延时绑定:// 创建Promise对象promise1,并在executor函数中执行业务逻辑 function executor(resolve, reject){ resolve(100); } let promise1 = new Promise(executor); // promise1延迟绑定回调函数onResolve function onResolve(value){ console.log(value); } promise1.then(onResolve);
- 如上所示:
- 回调函数的延时绑定在代码上体现就是先创建
Promise
对象promise1
; - 通过
Promise
的构造函数executor
来执行业务逻辑; - 创建好
Promise
对象promise1
之后,再使用promise1.then()
来设置回调函数;
- 回调函数的延时绑定在代码上体现就是先创建
- 总之,
Promise
实现回调函数的延时绑定,能把原来的回调写法分离出来,在异步操作执行完后,用链式调用的方式执行回调函数;
- 如上所示:
-
Promise
将回调函数onResolve
的返回值穿透到最外层:// 创建Promise对象promise1,并在executor函数中执行业务逻辑 function executor(resolve, reject) { resolve(100); } const promise1 = new Promise(executor); // promise1延迟绑定回调函数onResolve function onResolve(value) { console.log(value); function executor2(resolve, reject) { resolve(value + 1); } return new Promise(executor2); } // promise2为内部返回值穿透到了最外层 const promise2 = promise1.then(onResolve); promise2.then((value) => { console.log(value); });
- 如上所示,把
onResolve
函数内部创建好的Promise
对象返回到最外层,这样就可以摆脱嵌套循环了;
- 如上所示,把
Promise
:合并多个任务的错误处理
function executor(resolve, reject) {
let rand = Math.random();
console.log(1);
console.log(rand);
if (rand > 0.5)
resolve();
else
reject();
}
var p0 = new Promise(executor);
var p1 = p0.then((value) => {
console.log("succeed-1");
return new Promise(executor);
});
var p3 = p1.then((value) => {
console.log("succeed-2");
return new Promise(executor);
});
var p4 = p3.then((value) => {
console.log("succeed-3");
return new Promise(executor);
});
p4.catch((error) => {
console.log("error");
})
console.log(2);
- 上述代码,链式调用了四个
Promise
对象:p0~p4
,无论哪个对象里面抛出异常,都可以通过最后一个对象p4.catch
来捕获异常; - 通过这种方式可以将所有
Promise
对象的错误合并到一个函数来处理,这样就解决了每个任务都需要单独处理异常的问题; - 之所以可以使用最后一个对象来捕获所有异常,是因为
Promise
对象的错误具有“冒泡”性质,会一直向后传递,直到被onReject
函数处理或catch
语句捕获为止; - 具备了这样“冒泡”的特性后,就不需要在每个
Promise
对象中单独捕获异常了;
Promise
与微任务的关系
function executor(resolve, reject) {
resolve(100)
}
let demo = new Promise(executor)
function onResolve(value){
console.log(value)
}
demo.then(onResolve)
-
如上代码:
- 首先执行
new Promise
时,Promise
的构造函数会被执行; - 接下来,
Promise
的构造函数会调用Promise
的参数executor
函数; - 然后在
executor
中执行了resolve
, - 执行
resolve
函数,会触发demo.then
设置的回调函数onResolve
;- 所以可以推测,
resolve
函数内部调用了通过demo.then
设置的onResolve
函数;
- 所以可以推测,
- 首先执行
-
注:由于
Promise
采用了回调函数延迟绑定技术,所以在执行resolve
函数的时候,回调函数还没有绑定,那么只能推迟回调函数的执行; -
为了方便理解,这里实现了一个简单的Promise对象:
function Bromise(executor) { var onResolve_ = null; var onReject_ = null; //模拟实现resolve和then,暂不支持rejcet this.then = function (onResolve, onReject) { onResolve_ = onResolve; }; function resolve(value) { //setTimeout(()=>{ onResolve_(value); // },0) } executor(resolve, null); }
-
调用上述定义的Bromise对象:
function executor(resolve, reject) { resolve(100); } // 调用Bromsie let demo = new Bromise(executor); function onResolve(value) { console.log(value); } demo.then(onResolve);
- 这时会报错,因为在执行
executor
函数的时候,还没有通过demo.then()
设置回调函数,Bromise
中的onResolve_
还为空,所以就报错了;
- 这时会报错,因为在执行
-
这时就需要改造
Bromise
中的resolve
方法,让resolve
延迟调用onResolve_
:- 要让
resolve
中的onResolve_
函数延后执行,可以在resolve
函数里面加上一个定时器,让其延时执行onResolve_
函数,代码如下:function Bromise(executor) { var onResolve_ = null; var onReject_ = null; // 模拟实现resolve和then,暂不支持rejcet this.then = function (onResolve, onReject) { onResolve_ = onResolve; }; function resolve(value) { // 使用setTimeout定时器来延时`onResolve_`函数的执行 setTimeout(() => { onResolve_(value); }, 0); } executor(resolve, null); }
- 上面采用了定时器来推迟
onResolve
的执行,不过使用定时器的效率并不是太高;
- 要让
-
所以
Promise
又把这个定时器改造成了微任务了,这样既可以让onResolve_
延时被调用,又提升了代码的执行效率,这就是Promise
中使用微任务的原因;