前面两章我们介绍了回调的两个主要问题:顺序不定与缺乏验证机制。因此我们面对的主要问题就是控制权的交换问题,我们需要一种方式,能够明确的知道这一步调用完成时,接下来要发生什么,而这种编程范式就叫做“承诺”(Promises)。

Promises 在将来会变得越来越普及,很多新的异步API都开始采用这种方式了,因此你也有必要跟上时代,学习一下了。

注:本章会经常使用“随即”(immediately)一词。它的意思是指在 工作队列 (Job queue)的末尾执行。

什么是 Promises

程序员在学习新的技术时,最喜欢的事件就是“Don't BiBi , show me the code”,然而 Promise 则是那种你要是不理解其工作原理,而直接开始学习 API 就会完全被绕晕的技术,因此还是先随我了解 Promise 的原理比较好,这里会用两种不同的类比解释 Promise:

1. 未来的价值

你来到一家开封菜,想要买汉堡,于是先交钱了,收银员给了你一张等餐票,让你找个座位等一下,你知道手上的 “票”等于“未来的汉堡”。这时可以先刷个微博发个朋友圈什么的,过了一会你听到 “113号”!然后你就可以把手中的“价值承诺”换成真正的价值——“汉堡”。当然你也可能被告知“对不起先生,今天的汉堡卖完了,您只能点别的了”,然后你愤然退款并选择去吃沙县。

“票” 就是 promise,你手上拿着票就知道未来你可能有汉堡或是等到一个坏消息。当然这个比喻与JS代码有点出入,因为你在JS里可能永远也等不到叫号,后面会介绍这个问题。

2. 现在和未来的值

 var x , y = 2;
 console.log(x+y); //  NaN

买汉堡的例子怎么在代码中体现呢,看上面的代码,在进行 x + y 的运算前,程序就应该假定 x 与 y 是“已知”(Resolved)的,但是此时 x 并没有被赋值,对 undefinded 操作运算会得到什么? 没错 NaN, + 号并不能神奇的判断出 x 或 y 此时有没有被赋值,如果你希望“ + ” 号有这种神奇的能力,那以后写代码可就乱套了。

现在是重点了,你怎么才能让第二行的表达式依赖于第一行,也就是先拿到票,等汉堡做好了你才能吃到呢?就像是上面的代码要求是“请运算 x + y,但是要在 x 和 y 都有值的情况下再执行哦”。代码要怎么写?还记得第一章的“城门”吗?你肯定已经想到了使用回调函数。

function add(getX, getY, addXY) {
    var x,y;
    getX( function( xVal) { 
       x = xVal;
       if (y) addXY( x+ y);
    });
    getY( function( yVal) { 
       y = yVal;
       if (x) addXY( x+ y);
    });
}

add( AjaxGetX, AjaxGetY , function( addSum) {
    console.log( addSum);    
})

你看这个代码简直美(chou)到不能取消,如果我们想处理现在和未来的值,所有的函数都只能变成异步的去处理未来的值。利用回调的确可以解决这个问题,不过你依然要处理时间顺序问题,如果不需要处理先后顺序是不是更棒呢?

3. Promise 的值

我们单刀直入,使用 Promise 改写上面的函数,虽然你可能不理解,先不用关心具体的语法:

说实话我觉得 Promise 的语法一点都不简洁

function add( xPromise , yPromise ) {
  return Promise.all ( [ xPromise , yPromise ]).then( function( valuesArray ) { 
           return valuesArray[0] + valuesArray[1];
       } );
}


function fetchAsync( x ) {

  x == 'x' ? x = 1 : x = 2;

  return new Promise( function(resolve , reject) {
           setTimeout( function() { resolve( x ) } , 5000);       
    } );
};

function fetchSync( y ) {

  y == 'y' ? y = 2 : y = 3;

  return new Promise( function(resolve , reject) {
          resolve( y );       
    } );
};

add ( fetchAsync('x') , fetchSync('y') ).then( function( sum ) { console.log(sum);}) // 5秒后  =>3

我们使用 Promise 无需关心 fetch 函数是立即执行还是稍后执行,反正他们要等到返回值都齐了才能开始 执行 then 传入的方法。

待续...这样我们不用后验机制就可以处理到齐的问题。

4. 函数完成事件监听

刚才我们用 Promise 完成了对未来值的运算,我们并没有设置先来后到的顺序。而 Promise 的另一种用法就是,我要调用一个函数 foo() ,我不关心 foo 是同步还是异步的,反正就是 foo 执行完了我就要接着执行 bar 函数。你想到了什么?事件监听,对不对,假如我们可以对一个函数的状态进行监听,会不会给人一种钦定的感觉... 怎么突然膜了起来(想想 ES7 好像可以监听对象变动,那岂不是可以直接通过回调改变对象属性进而执行未来值吗?),比如

function foo( x ) { ... return listener } ;

function bar( v ) { ... } ;
function errorHandler ( err ) { ... } ;

var listener = foo( 1 );

listener.ondone = bar ;
listener.onerror = errorHandler ;

你看在 foo 上挂个事件监听器吼不吼啊?

5. Promise 事件

上面的例子就是对 Promise 的模拟,上面的 foo 函数如果使用基于 Promise 方法返回的 listener 实际上是一个 Promise 对象,而这个对象会被传递给 bar 或 errorHandler , Promise 对象没有上面的 done 与 error 事件监听器,它只有一个 then 方法来扮演事件监听器的角色,它监听的事件叫做“fullfillment”(条件满足)和“rejection”(条件拒绝),这个状态保存在 Promise 对象中,我们不会显式的去声明要监听哪个事件。

Promise 的初始状态是“pending”,一旦状态变为“fullfillment”或“rejection”后,Promise 对象的状态就冻结了,无论对它调用多少次 then() 都只会返回最终的值。

new Promise( nowFunction ) 传入的函数会立即被执行,nowFunction 的两个参数是一会要执行的函数 Function,传入的 then( laterFunction ) 的函数就是稍后要做的,这种构造方法称为 启示构造器(Revealing Constructor)

new Promise( nowFunction( laterFuction , laterErrorFunction ) { 
    Ajax('url', function callBack ( value ) {
        laterFunction ( value ); 
     });
}).then( function laterFunction(value) { 
    doneSomeThing with value ....
})

Promise 实际就是在模拟这种对函数的监听,虽然 Promise 实际上返回的是一个对象

Then-able 鸭子类型

如果一个动物看起来像鸭子,又会“嘎嘎”叫,那我们认为这个动物一定是鸭子,这种情况叫做“鸭子类型”。在真正进入 Promise 学习之前先要了解一个小BUG,许多 ES6 之前的库里可能定义过某些 名为 then 的方法,而这些方法可能会干扰 Promise 对象的 then。

Array.prototype.then = function () {  console.log('Array Then') };

function duckType() {
  return new Promise(function(res, rej) {
     res([]);
  });
};

duckType().then( function(v) { console.log(v)}); // 'Array Then'

看到了吧,Array 上的 then 就是鸭子类型。

Promise 的信任机制

已了解 2 ,但还差 1 个 重要的 Trust

1. 过早调用

之前的 Zalgo 不会出现了,因为 Promise 就算传入一个直接运算也要Jobs队列

 function noZalgo( ) {
  return new Promise(function(res, rej) {
     res(1);
  });
};

noZalgo().then( function(v) { console.log(v)});

console.log(2) //=>  2  1

显然 new Promise 没有调用任何异步函数,我们不需要用 setTimeout( res, 0 ) 来人为构造异步了。

2. 过晚调用

Promise 一旦条件实现,马上会调用 then 方法。而且不会被干扰?

3. Promise 的怪癖

p1 嵌套了 p3 ,但代码不清晰

不要用嵌套

function orderPromise() {
   return new Promise(function (res,rej) {
    res();
   })
}

var p = orderPromise();

p.then(function(){
  console.log(1);
  p.then(function(){ 
    console.log(3)
  });
}).then(function(){
  console.log(4)
});

p.then(function(){
  console.log(2);
  p.then(function(){
   console.log(5);
  });
});

这个顺序也是很烦的

4. 没有调用

JS 中的 Promise 无法取消,如果 条件满足 ,你传入了 resolve 与 rejection 函数,两个都会被执行的。后面再解释。

可以使用 Promise.race() 用来设定一个定时信号,如果超时,则定时信号会先返回然后就可以处理了。

function timeoutPromise(delay, message) {
   return new Promise(function (res,rej) {
    setTimeout( function(){rej( message )}, delay);
   })
}

Promise.race([timeoutPromise( 10000,'tooLong'),timeoutPromise(1000,'error')]).then(null,function(e){console.log(e)}); // 1秒后 'error'

5. 太少或太多

太少的情况就是超时,而太多 Promise 不会出现这种情况,一个 Promise 只能处理一次,一旦完成值就不变了,再对 Promise 对象调用多少次 then 都是一样的结果。

6. 传值

如果没有在 构造 Promise 的函数中给 resolve 或 reject 函数传值,结果就是 undefined,你想传多个值的话,只能用 数组 或 对象进行包装。

7. 内部捕获错误

function errorPromise() {
   return new Promise(function (res,rej) {
    a;
   })
}

errorPromise().then(null,function(e){console.log(e)}); // ReferenceError: a is not defined(…)

在 Promise 中并没地调用 rej,但是我们之前没定义 a ,但 rej 函数依然被调用了。

8. Promise 的可信度

你可能已经发现 Promise 根本没有抛弃回调,只是换了一个调用回调函数的位置。

Promise.resolve() 方法可以将一个传入的 then-able 类型的值取出,再构建一个原生的 Promise 对象?

9. 信任构建

回调函数已经被应用了20多年,但是当你开始考虑信任机制的构建时,回调函数是无比脆弱的。而 Promise 则基于控制权的反转?用可控的语句描述了回调,因此是处理异步的更明智的方法。

链式调用

Promise 不仅能实现简单的 IFTTT(if this then that) ,我们还可以将多个 Promise 串连起来构建一个按顺序执行的异步流程。

可以这样做,基于 Promise 的两个特性 1.每次调用 then 会返回新的 Promise 2. then 接收返回的 Promise 对象的解,而 Promise 构造器中调用的回调函数中的 Resolve 函数的第一个传入值设置为 Promise 对象的条件满足时的最终解。

函数命名:Resolve,Fullfill,Reject

在构造promise的时候需要传入的两个,最好命名为 resolve 与 reject

var p = new Promise( function(resolve , reject ) { 
async( argument, function callBack(d){
     resolve(d);
   });  
});

而在调用 then 方法时,我建议 用 onFullfillment 和 onRejection

p.then( onFullfillment  ).catch( onRejection );

本节大概就讲下这样命名函数的理由好处blahblah的。

错误处理

1. 2. 3.

Promise 的模式

Promise.all( [ ... ] )

Promise.race( [ ... ] )

延时竞赛

多种 all() 与 race()

并发迭代

重述 Promise API

new

resolve() reject()

then() catch()

all() race()

Promise 的局限

线性错误处理

单返回值?

  new Promise(function(r,j){
    setTimeout(r.bind(null,'1','u'),10);
  }).then(function(v,b) {o = v+b});

b不会被传值的,所以复杂值只能包装成 数组 或 对象。

分离值

参数展开

单一解决方案

惯性

无法取消的 Promise

性能

关于性能的问题,既简单又复杂,Promise 与 裸回调 相比,要多处理控制、验证等问题,并且还要协调顺序,自然天生就比直接回调慢。但具体慢多少,这个“不可描述”。

本章小结

使用 Promise 终于斛决了我们之前仅仅使用回调函数时遇到的控制权交换的信任问题。

Promise 并没有抛弃回调,而是通过引入一个管理回调顺序的中间人机制,Promise 的链式调用以一种同步代码风格写出异步的程序,在字面上更符合了我们的线性思维。

当然 Promise 也存在不少缺点,而下一章我们会介绍一种更好的解决方案 Generator。