第三章 Promises

王工發表於2016-05-09

前面兩章我們介紹了回撥的兩個主要問題:順序不定與缺乏驗證機制。因此我們面對的主要問題就是控制權的交換問題,我們需要一種方式,能夠明確的知道這一步呼叫完成時,接下來要發生什麼,而這種程式設計正規化就叫做“承諾”(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。

相關文章