回调为什么不能完美完成异步
- 复杂的函数追踪与我们大脑平时的思考方式不一致
scss
AudioListener('click',function handler(evt){
setTimeout(() => {
ajax('http://some.url',function res(text){
if(text = 'text'){
text()
}
})
}, 500);
})
这段代码我们很容易想到是先执行click函数,进而调用定时器,再发送ajax请求。但其实这是一种十分偶然的情况,异步的过程中会有相当多的噪音,我们需要在这几个函数之间跳来跳去
- 信任问题
javascript
//A
ajax("...",function(){})
//B
我们知道A和B发生在现在,并且是在JavaScript主程序的控制下,而//C则会延迟到将来发生,并且是在第三方的控制下(在本例中是ajax,即当ajax收到浏览器返回的请求时,就会通知JavaScript引擎执行C代码)。它有一个这样的思路:有时候ajax(也就是交付你回调的第三方)不是你编写的代码,也不在你的直接控制之下。多数情况下是个第三方提供的工具。
我们把这称为控制反转,也就是把自己程序一部分的执行权交给某个第三方。这就是回调的最大问题,它会导致信任链的完全断裂
-
多次调用时的信任问题
-
当通过ajax多次发送请求时,由于response(data)一直可以变化,导致我们可能同时接收到成功和失败的结果
-
当完全没有调用这个回调函数的信任问题 当ajax一直没有接收到值时,回调一直没有办法被调用。可以通过设置计时器的超时来取消这个事件
-
调用过早或过晚导致的信任问题
javascript
function result(data){
console.log(a);
}
var a = 0
ajax("..",result)
a++
这段代码可能输出0(同步回调调用,可以通过async设置)或者1(异步回调调用)。
总结:promise信任问题
- 调用回调过早
- 调用回调过晚或者不调用
- 调用回调次数过多或者过少
- 未能传递所需的环境和参数
- 吞掉可能出现的错误和异常
promise解决信任问题
1.调用过早
造成这个问题的原因,一般是一个任务可能同步完成,可能异步完成,即ajax收到数据之后,就会让ja主引擎执行代码,导致可能某个重要的关键步骤还没执行。但promise就不必担心这个问题,因为即使是立刻完成的promise,也只是会将其加入任务队列,再通过事件循环,先执行完所有同步任务再来执行异步任务
2.调用过晚
一旦promise的状态发生了改变,对应的回调就会被加到任务队列中,这里的回调一定会通过事件循环机制触发,并且这些回调不会影响或者延误其他回调的调用
javascript
p.then(function(){
p.then(function(){
console.log('c');
})
console.log('a');
})
p.then(function(){
console.log('b');
})
//a b c
这里的c不会打断或抢占b,这就是promise的运作方式
javascript
var p3 = new Promise((resolve, reject) => {
resolve('B')
})
var p1 = new Promise((resolve, reject) => {
resolve(p3)
})
p2 = new Promise((resolve, reject) => {
resolve('A ')
})
p1.then(function(v){
console.log(v);
})
p2.then(function(v){
console.log(v);
})
//A B
这里不是B A的原因是,p1不是用立即值而是用另一个promise决议,后者本身决议值为B。规定的行为是将p3展开到p1,这是一个异步的展开,也就是resolve(p3)会进入任务队列,因此p1的then回调排在p2的后面
3.回调未调用
首先,没有任何东西(包括JavaScript错误)能阻止promise向你通知它的决议。如果你对promise注册了一个完成回调和一个拒绝回调,那么promise决议时一定会调用其中一个
但如果promise本身永远不会被决议呢?promise也提供了解决方案,那就是一种叫做竞态的高级抽象机制
javascript
function timeoutPromise(delay){
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('timeout')
}, delay);
})
}
Promise.race([
foo(),
timeoutPromise(3000)
]).then(
function(){
//foo及时完成
},
function(err){
//或者foo被拒绝,或者只是没有按时完成
//查看err是哪种情况
}
)
4.调用次数过多或者过少的问题
一般正确次数是1。过少则是调用0次,和之前说的未调用的情况是一样。
过多的情况则很容易解释。promise只能决议一次,后续对他状态的改变都是无用的,由于只能被决议一次,那么通过then注册的回调自然只能执行一次
5.未能传递参数/环境值
promise至多只能有一个决议值,如果没有用任何值显式决议,那么这个值就是undefined,并且无论这个值是什么,都会传递给注册的回调 并且resolve()和reject()只能有一个参数,当传递多个参数时,第一个参数之后的参数都会被忽略
6.吞掉错误和异常
如果promise在创建过程或者决议过程中的任何一个时间出现JavaScript异常错误,那么这个错误就会被捕捉,并且会使这个promise被拒绝
javascript
var p = new Promise((resolve, reject) => {
foo.bar() //foo未定义,所以会出错
resolve(23) //永远不会到达这里
})
p.then(
function fulfilled(){
//永远不会到达这里
},
function rejected(){
//err将会是一个TypeError异常对象来自foo.bar()这一行
}
)
这里有效解决了链式回调的风险,链式回调可能出错了会引起同步响应,成功了才是异步响应。而promise会把JavaScript异常也变成了异步反应。那么如果是then中注册的回调报错了呢?
javascript
var p = new Promise((resolve, reject) => {
resolve(23)
})
p.then(
function fulfilled(){
foo.bar()
console.log('aaa');//永远不会到达这里
},
function rejected(){
//永远不会到达这里
}
)
看起来我们并没有侦听到这个错误,但实际上不是这样的,p.then()调用本身返回了另一个promise,正是这个promise会因TypeScript异常而被拒绝。为什么不直接调用我们定义的错误处理函数呢?因为这样违背了promise的基本原则,即promise一旦决议就不可再变,p的决议已经是resolve(23)了,再查看promise的决议时,不能因为处理函数出错了而调用rejected函数
是可信任的Promise吗?
我们会遇到无法识别调用的函数是否是Promise,还是只是一个thenable,因此我们可以使用Promise.resolve()将参数转化成一个真正的promise。如果向Promise.resolve()传递一个非promise对象,就会得到这个值填充的Promise对象,如果传递的是Promise对象,那么就返回这个Promise对象本身
javascript
//不要这样做
foo(42)
.then(function(v){
console.log(v);
})
//而是要这样做
Promise.resolve(foo(42))
.then(function(v){
console.log(v);
})
链式流
我们可以将多个Promise连接到一起,以表达一系列异步步骤。这种方法可以实现的关键是因为以下两个Promise固有行为特征
- 每次对Promise调用then方法,它都会创建并返回一个新的Promise,我们可以将其链接起来
- 不管then方法中的回调返回的值是什么,它都会被自动设置为该then方法返回的Promise(第1点中的Promise)的状态
我们可以通过then中显示地返回Promis,控制每一步是异步还是同步
javascript
var p = Promise.resolve(21)
p.then(function(v){
console.log(v);
return new Promise((resolve, reject) => {
resolve(v * 2)
})
})
.then(function(v){
console.log(v);
})
错误处理
错误处理我们一般会想到try...catch,但是try...catch无法捕捉到异步代码的出错,它能捕捉到的异常必须是线程执行已经进入 try catch,但 try catch 未执行完的时候抛出来的 ,而异步代码是交由事件线程,当事件循环将异步事件交给js引擎时,大概率try catch已经执行完毕,因此无法处理异步代码。(参考trycatch 不能捕获运行时异常_面试官:用一句话描述 JS 异常是否能被 try catch 捕获到 ?... - 掘金 (juejin.cn))
Promise的错误处理采用了分离回调风格,一个回调用于完成情况,另一个用于拒绝情况
javascript
var p = Promise.resolve(42)
p.then(function fulfilled(msg){
// 数字没有String函数,所以会抛出错误
console.log(msg.toLowerCase());
},function rejected(err){
//永远不会到达这里
})
这段代码发生错误的时候我们没有通知,因为这个错误应该是由p.then()方法提供的,但是我们此例中没有捕捉。当然,我们在代码的最后加上.catch(handleErrors),但是如果传入的handleErrors也可能失败。任何Promise链的最后一步,不管是什么,总是存在着未被查看的Promise中出现未捕获错误的可能性
Promise模式
1.Promise.all([])
在promise链中,任意时刻都只能有一个异步任务正在进行,步骤2只能等待步骤1完成才能进行,但是如果要同时执行两个或更多步骤,就要用到门这种机制了,门是要等待两个或更多并发/并行的任务都完成才能继续,在Promise API中,这种模式叫做Promise.all([])
javascript
let p1 = request(url1)
let p2 = request(url2)
Promise.all([p1,p2])
.then(function (){},function (){})
Promise.all需要一个参数,是一个数组,可以是Promise、thenable或者立刻值,但是列表中的每一个值都需要通过Promise.resolve()过滤,如果数组是空的,那么Promise.all返回的主Promise就会立刻完成 。Promise.all返回的值是所有传入的Promise的完成消息组成的数组,与指定的顺序一致,和代码的完成顺序无关
Promise.all返回的主Promise在且仅在所有的成员Promise都完成后才会完成,如果这些Promise中有任何一个被拒绝,主Promise就会被立刻拒绝,并丢弃来自其他所有Promise的全部成果。因此要永远记住要为Promise.all返回的Promise关联一个拒绝处理函数
2.Promise.race([])
但有时候只想响应第一个跨过终点线的Promise,而抛弃其他Promise,这种模式叫做,在promise中被称为竞态
Promise.race接受单个数组参数,这个数组可以是一个或多个Promise、thenable或者立即值组成,但是立即值没有意义,因为显然列表中的第一个会获胜,就好像一个选手直接在终点开始比赛一样
与Promise.all类似,一旦有任何一个Promise决议为完成,Promise.race就会完成,一旦决议为拒绝,那么他就会拒绝
和Promise.all不同,如果传入的是一个空数组,Promise将永远不会决议,而不是立刻决议
并发迭代
有时候需要在一列Promise中迭代,并且对Promise都执行某个任务,非常类似于同步数组中的map,但是这些工具也可以有异步版本
javascript
if(!Promise.map)(
Promise.map = function(vals,cb){
// 返回一个等待所有map的promise的新Promise
// 其实就是通过Promise.all方法,将数组里的每一个值到执行一遍函数,cb是传入具体的函数操作d
return Promise.all(
vals.map(function(val){
return new Promise(function(resolve){
cb(val,resolve)
})
})
)
}
)
var p1 = Promise.resolve(21)
var p1 = Promise.resolve(42)
var p1 = Promise.resolve('Ops')
Promise.map([p1,p2,p3],function(pr,done){
Promise.resolve(pr) //将传进来的数组Promise化,确认是Promise对象
.then(
function(){
function(v){
done(v*2)
}
},
done
)
})
.then(
function(vals){
console.log(vals); // [42,84,"Oops"]
}
)
Promise的局限性
1. 顺序错误处理问题
这等同于try catch存在的局限,在Promise链中,我们如果没有为then写上reject函数,则会在链上一直传播,即便最后加了catch(handleError),你也没有办法保证handleError没有错误
2.单一值
Promise只能有一个完成值或一个拒绝理由,在复杂的情景中,这是一种局限。当然,可以通过将返回的多个值封装在数组或者对象中,但更建议通过Promise.all的方式封装,符合'每个Promise一个值'的理念
3.单决议
Promise最本质的一个特征,就是只能被决议一次,但这样会造成问题
javascript
var p = new Promise((resolve, reject) => {
click("#mybtn",resolve)
})
p.then(function(evt){
var btnID = evt.currentTarget.id
request(url + btnID)
})
.then(function(text){
console.log(text);
})
这样的代码在点击第2次的时候就不会成功,因为Promise p已经被决议了,因此第二次的决议就会被忽略。因此我们要为每个事件的发生创建一整个新的Promise链
javascript
click("#mybtn",function(evt){
var btnID = evt.currentTarget.id
request(url + btnID)
.then(function(text){
console.log(text);
})
})
这种方法可以工作,因为针对这个按钮的每个点击事件都会启动一整个新的promise。然而在事件处理函数中定义整个promise链,这很丑陋
3.无法取消的Promise
一旦创建了Promise并为其注册了完成或者拒绝处理函数,如果出现某种情况使得这个任务悬而未决的话,你也没有办法从外部阻止他的进程
考虑到前面的Promise超时场景
less
var p = foo(42)
Promise.race([
p,
timeoutPromise(3000)
])
.then(
doSomeThing,
handleError
)
p.then(function(){
//即使在超时的情况下也会发生
})
这个'超时'相对于Promise p是外部的,所以p是本身还是会继续运行
单独的一个Promise并不是一个真正的流程控制机制,相比之下,集合在一起的Promise链才是一个流程控制的表达,因此取消定义在这个抽象层次上是更合适的,也正因为如此,单个Promise的取消总是让人很别扭
4.Promise的性能
把基于回调的异步任务链与Promise链中需要移动的部分数量进行比较,很显然,Promise进行的动作更多一些,比如任务队列,这自然会慢一点,但是牺牲的一点性能,Promise提供信任性,对回调地狱的避免以及可组合性,这是非常值得的。Promise很好,请使用他。❀❀❀❀