前言
很多人都知道JS中的回调地狱(毁灭金字塔)一说,认为回调带来的缺陷就是那一层又一层的嵌套和缩进,但实际上回调地狱与嵌套,缩进几乎没有关系,回调所引起的问题要远远比这二者严重的多,下面将一一分析
回调地狱
代码示例
举一个书中的关于嵌套的回调代码示例
listen( 'click', function handler(evt) {
setTimeout( function request () {
ajax( 'http://some.url.1', function response (text) {
if (test == 'hello') {
handler();
}else if (text == 'world') {
request();
}
});
}, 500);
});
以上示例就是人们口中常说的回调地狱,下面描述一下该代码示例所大致的执行流程
-
等待一个click事件
-
当click事件发生时,等待定时器500ms
-
等待Ajax相应返回,之后当返回值为'world'时,可能会重新请求
不得不说,为了看懂以上代码示例,我们的大脑需要努力的分析才能知道整个异步流程执行的顺序是什么,这便是我所说的关于第一个回调带来的缺陷,即:难以理解代码意图
信任问题
书中举了一个很好的关于回调带来的信任问题,我们知道关于JavaScript中异步回调的代码将会在将来的某个时刻由第三方回调我们,那既然是由第三方回调我们,就会带来一些隐藏的严重的问题,它什么时候回调?会不会出现过早/过晚回调?它会回调多次吗?它会不会干脆不回调我们等等
而出现这一系列不确定问题的关键则在于我们将自己程序中一部分代码的执行控制权交给了某个第三方,可能是函数ajax,也肯能是你对接的第三方客户等
五个回调的故事
书中描述了一个关于五个回调的小故事,大概的意思是这样的,某售卖昂贵电视的公司有一个在线售卖电视的网站,该网站有一个功能是当用户点击 '确认' 购买电视时,网站脚本需要调用某第三方公司提供的用于跟踪该比交易的函数,而该第三方公司为了提高调用性能,采用了异步回调的形式,即网站脚本在调用第三方函数时需要传入一个用于回调的函数,真正出问题的就在这个用于回调的函数中,网站开发人员在回调函数中去收取客户费用和展示感谢购买的页面
然而某一天出现了一个严重的线上问题,某客户在购买电视时费用被扣除了五次,造成了严重的生产事故
而最终的原因是第三方连续调用了五次回调函数,造成客户被扣款五次,很多人可能会说,这应该是你自己的问题,是你没有做好回调函数的幂等,没有做多次调用的处理
那么如果我们仔细的分析以上的小故事,就会发现,这就是回调交给外部代码所带来的严重缺陷,既然是回调,就会存在很多的情况,比如:回调过早,过晚,次数太多,太少,不回调等等
如何挽救回调
如果我们不把自己程序的将来部分(continuation)传给第三方,而是希望第三方给我们提供一种了解其任务何时结束的能力,然后由我们自己的代码来决定下一步做什么
这种范式就称为 Promise
Promise 承诺
分析Promise如何解决回调引起的信任问题之前先回顾一下回调可能引起的信任问题有哪些
-
调用过早
-
调用过晚或不调用
-
调用回调次数过多或过少
-
吞掉可能出现的错误和异常
-
...
下面一一分析
调用过早
即使是立即完成的Promise,也无法被同步观察到,也就是说对一个Promise调用then(...)的时候,即使该Promise已经决议,提供给then(...)回调也总会被异步调用
调用过晚
当Promise创建对象调用resolve(...)或reject(...)时,该promise的then(...)注册的观察回调就会被自动调度,可以确信,这些被调度的回调在下一个异步事件点上一定会被触发
回调未调用
首先,没有任何东西(甚至是JavaScript 错误)能阻止Promise向你通知它的决议
调用次数过多或过少
由于Promise只能被决议一次,所以任何通过then(...)注册的(每一个)回调就只会被调用一次
吞掉错误或者异常
如果在Promise的创建过程中或在查看其决议结果过程中的任何时间点上出现了一个JavaScript 异常错误,比如一个TypeError 或 ReferenceError 那这个异常就会被捕获,并且会使这个Promise被拒绝(决议失败)
举个书中的例子
var p = new Promise((resolve, error) => {
foo.bar();
resolve(1);
});
p.then((value) => {
console.log(value);
}, (error) => {
console.log('异常: ' + error);
});
// 结果值
异常: ReferenceError: foo is not defined
如果Promise完成决议后再其then(...) 回调中出现了JavaScript异常错误会怎么样呢?会被吞掉吗?
下面看一个例子
var p = new Promise((resolve, reject) => {
resolve(1);
});
p.then(
function fulfilled(msg) {
foo.bar();
console.log(msg);// 代码会执行到这里吗?不会
},
function rejected (error) {
console.log('异常:' + error);// 代码会执行到这里吗?不会
}
);
以上代码貌似会被吞掉,其实不然,如果我们这样改一下代码
var p = new Promise((resolve, reject) => {
resolve(1);
});
var pp = p.then(
function fulfilled(msg) {
foo.bar();
console.log(msg);// 代码会执行到这里吗?不会
},
function rejected (error) {
console.log('异常:' + error);// 代码会执行到这里吗?不会
}
);
pp.then(
null,
(error) => {
console.log('异常:' + error);// 代码执行到了这里
}
);
通过以上示例我们发现,如果在then(...)回调中发生了异常,看似异常信息会被吞掉,其实异常是导致了then(...)本身返回的另外一个promise对象的拒绝,只要在新的promise对象的then方法中去捕获就可以了
相信通过以上的分析,你已经主要到Promise并没有完全摆脱回调,当promise决议后,还是会回调then(...)指定的回调函数,也就是说我们并没有把回调函数传给第三方,而是我们自己编写的then函数,相反我们从第三方得到了某个东西(Promise承诺),然后把回调传给这个承诺
这个Promise可信任吗?
如何能够确定返回的这个东西实际上就是一个可信任的Promise呢?为什么这就比使用单纯的回调更值得信任呢?
Promise.resolve()
关于Promise的很重要但也是常被忽略的一个细节是:Promise的可信任的解决方案就是Promise.resolve(...)
如果向Promise.resolve(...)传递一个非Promise,非thenable的立即值,就会得到用这个值填充的promise,如果传递的就是一个真正的Promise,那么得到的就是它本身
代码示例:
var p1 = new Promise((resolve, error) => {
resolve(42);
});
var p2 = Promise.resolve(42);
var p3 = Promise.resolve(42);
var p4 = Promise.resolve(p3);
p3 === p4;// true
通过Promise.resolve(...)可以定义一个规范良好的异步任务,同时也可以保证返回值是一个可信任的行为良好的Promise
Promise 链式流
关于Promise链式流模型只需要记住两点即可
-
每次你对Promise调用then(...),它都会创建并返回一个新的Promise,我们可以将其链接起来
-
不管从then(...)调用的完成回调(第一个参数)返回的值是什么,它都会被自动设置为被链接Promise(第一个点中的)的完成
这里举个例子来说明Promise链式流
var p = Promise.resolve(21);
var p2 = p.then(v => {
console.log(v);
return v * 2;
});
p2.then(v => {
console.log(v);
});
// 结果值
21
42
可以看到p.then 创建了一个新的Promise,其返回值成为了新Promise的决议值
如果我们在p.then中引入异步呢?其会带来什么影响呢?
var p = Promise.resolve(21);
var p2 = p.then(v => {
console.log(v);
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(v * 2);
}, 1000);
});
});
p2.then(v => {
console.log(v);
});
// 结果值
21
...等待1000ms后
42
我们构建的Promise链不仅是一个表达多步异步序列的流程控制,更是从一个步骤到下一个步骤传递消息的通道
链中出现异常
默认情况下,如果你的promise.then(...)没有指定拒绝处理函数,那么一个默认的拒绝处理函数就会顶替,而默认的拒绝处理函数就是将错误信息重新抛出,直到遇到显示定义的拒绝处理函数为止
默认完成处理函数
如果then函数没有指定完成处理函数的话,即传null,那默认的完成处理函数会顶替,并将接收到的任何值传递给下一个步骤的Promise
then(null, error => {
...
})
这个模式,虽然表面上只是处理了拒绝,但是实际上还是会把完成值传递下去
看个例子
var p = Promise.resolve(21);
var p2 = p.then(null, error => {
console.log(error);
});
p2.then(v => {
console.log('p2: ' + v);
});
// 结果值
p2: 21
这里可以看出,虽然p.then(...)表面上没有显示传递完成值,但是配p2的完成处理函数仍然获取到了p的完成值
优雅捕获链式流错误
前文我们知道在链式流Promise中,如果then(...)中抛出异常信息(非决议前抛出的异常),这些异常信息需要在下一个新的Promise中才能处理,那么循环往复,就需要在最后一次的then(...)之后调用Promise API catch一下,但是如果在我们Promise链末尾的catch内部有错误怎么办?
我们没有捕获这个最终Promise的结果,也没有为其注册拒绝处理函数,异常是不是就会被吞掉了?
书中建议采用defer的方式处理,感兴趣同学的可以自己研究下
Promise 模式
Promise.all([...]) 门模式
在异步序列Promise链中,任意时刻执行有一个异步任务正在执行,但是如果我们想要同时执行两个或更多步骤,要怎么实现呢?
考虑这样一种场景,如果我需要同时发送两个Ajax请求,但他们不管谁先完成,你都不关心,只要他们两个都成功完成,你在去请求第三个Ajax请求
Promise.all([p1, p2])
其中p1,p2通常是Promise的实例,也可以是立即值或thenable,因为进过all方法之后,每一个参数都会被Promise.resolve(...)过滤,规范化为真正的Promise实例
Promise.all(...)的返回值是一个主Promise实例,其完成注册函数的参数是其传入的每一个Promise的完成消息组成的数组,这里注意一下的是完成消息的数组顺序与指定的顺序有关,与每个Promise完成的顺序无关
特性:
-
所有传入的Promise都决议完成,主Promise才会决议完成
-
若其中任何一个Promise决议拒绝,主Promise会立即决议拒绝,并丢弃来自其他所有Promise的全部结果
-
永远要记住为每一个传入的Promise关联一个拒绝/错误处理函数,尤其是从Promise.all([...])返回的那个
Promise.race([...]) 竞态模式
如果你正在观看一场110米跨栏比赛,那么你只会关注那第一个跨过终点线的刘翔,其余选手在你眼里可能并不重要
Promise.race([p1, p2])
由于只有一个Promise可以取胜,所以完成值是单个消息,并不是一个数组
特性:
-
一旦有任何一个Promise决议完成,Promise.race([...])就会完成
-
一旦有任何一个Promise决议拒绝,Promise.race([...])就会拒绝
即Promise.race([...])决议取决于传入的第一个决议的Promise
Promise API 简述
new Promise(...)构造器
构造函数必须传入一个函数回调,该函数回调是同步的或立即调用的,该函数有两个参数(函数回调),用以支持Promise的决议,通常这两个函数被称为resolve(...) 和 reject(...)
这里需要注意一点的是resolve(...) 既可能决议完成,也可能决议拒绝,而reject(...)则只是决议拒绝
-
如果传给resolve(...)的是一个thenable值,这个值就会被递归展开,要构造的Promise则会取用其最终的决议值或状态,这也是为什么说resolve(...)可能决议完成或拒绝
-
如果传给resolve(...)的是一个真正的Promise,直接返回,什么也不做
-
如果传给resolve(...)的值是一个非Promise,非thenable的立即值,要构造的Promise就会用这个值决议完成
下面举一个例子
var fulfilled = {
then: (cb) => {
cb(42);
}
}
var rejected = {
then: (cb, errCb) => {
errCb(42);
}
}
var p1 = Promise.resolve(fulfilled);
var p2 = Promise.resolve(rejected);
// 结果值
// p1 Promise {<resolved>: 42}
// p2 Promise {<rejected>: 42}
以上就是传给resolve(...)一个thenable值,构造的Promise的状态就是采用传入的thenable值的最终决议值和状态的
then() 和 catch()
每一个Promise实例都有then(...)和catch(...)方法,用来注册决议完成和决议拒绝后的回调处理函数
我们知道当Promise决议之后,总是会立即以异步的方式调用这两个处理函数之一
- then(...)
接受一个或两个参数,第一个用于完成完成回调,第二个用于拒绝回调
若两者中的任何一个省略或作为非函数类型值传入的话,就会被替换为相应的默认回调
默认完成回调:把决议完成消息传递下去
默认拒绝回调:重新抛出其接收到的出错原因
- catch(...)
只接受一个拒绝回调函数作为参数,并自动替换默认完成回调
等价于 then(null, ...)
- 相同点
-
都会创建并返回一个新的Promise实例,用于实现Promise链式控制流
-
如果在完成或拒绝回调中发生异常,那么会导致新生成并返回的Promise是被决议拒绝的
-
如果任意一个回调返回非Promise,非thenable的立即值,该值会被用作新生成并返回的Promise的完成值,注意是决议完成状态的值
-
如果任意一个回调返回Promise,thenable,那么该值会被递归展开,最终新生成并返回的Promise会采用展开的决议值
总结
-
Promise解决了我们因只用回调的代码而备受困扰的控制反转的问题
-
Promise并没有摈弃回调,只是把回调的安排转交给了一个位于我们和其他工具(第三方)之间的可信任的中介机制
-
提供了以顺序的方式表达异步流的更好的办法,让代码更清晰易懂
以上就是我对Promise的理解,记录下来,每天进步一点点
网友评论