Promise到底解決了什麼問題?

磚用冰西瓜發表於2019-03-02

我的github部落格 github.com/zhuanyongxi…

大家都知道Promise解決了回撥地獄的問題。說到回撥地獄,很容易想到下面這個容易讓人產生誤解的圖片

Promise到底解決了什麼問題?

可回撥地獄到底是什麼?它到底哪裡有問題?是因為巢狀不好看還是讀起來不方便?

首先我們要想想,巢狀到底哪裡有問題?

舉個例子:

function a() {
  function b() {
    function c() {
      function d() {}
      d();
    }
    c();
  }
  b();
}
a();
複製程式碼

這也是巢狀,雖然好像不是特別美觀,可我們並不會覺得這有什麼問題吧?因為我們經常會寫出類似的程式碼。

在這個例子中的巢狀的問題僅僅是縮排的問題,而縮排除了會讓程式碼變寬可能會造成讀程式碼的一點不方便之外,並沒有什麼其他的問題。如果僅僅是這樣,為什麼不叫“縮排地獄”或“巢狀地獄”?

把回撥地獄完全理解成縮排的問題是常見的對回撥地獄的誤解。要回到“回撥地獄”這個詞語上面來,它的重點就在於“回撥”,而“回撥”在JS中應用最多的場景當然就是非同步程式設計了。

所以,“回撥地獄”所說的巢狀其實是指非同步的巢狀。它帶來了兩個問題:可讀性的問題和信任問題。

可讀性的問題

這是一個在網上隨便搜尋的關於執行順序的面試題:

for (var i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(new Date, i);
  }, 1000);
}

console.log(new Date, i);
複製程式碼

答案是什麼大家自己想吧,這不是重點。重點是,你要想一會兒吧?

一個整潔的回撥:

listen( "click", function handler( evt){ 
  setTimeout( function request(){ 
    ajax( "http:// some. url. 1", function response( text){ 
      if (text == "hello") { 
        handler(); 
      } else if (text == "world") { 
        request(); 
      } 
    }); 
  }, 500); 
});
複製程式碼

如果非同步的巢狀都是這樣乾淨整潔,那“回撥地獄”給程式猿帶來的傷害馬上就會減少很多。

可我們實際在寫業務邏輯的時候,真實的情況應該是這樣的:

listen( "click", function handler(evt){ 
  doSomething1();
  doSomething2();
  doSomething3();
  doSomething4();
  setTimeout( function request(){ 
    doSomething8();
    doSomething9();
    doSomething10();
    ajax( "http:// some. url. 1", function response( text){ 
      if (text == "hello") { 
        handler(); 
      } else if (text == "world") { 
        request(); 
      } 
    }); 
    doSomething11();
    doSomething12();
    doSomething13();
  }, 500); 
  doSomething5();
  doSomething6();
  doSomething7();
});
複製程式碼

這些“doSomething”有些是非同步的,有些是同步。這樣的程式碼讀起來會非常的吃力,因為你要不停的思考他們的執行順序,並且還要記在腦袋裡面。這就是非同步的巢狀帶來的可讀性的問題,它是由非同步的執行機制引起的。

信任問題

這裡主要用非同步請求討論。我們在做AJAX請求的時候,一般都會使用一些第三方的工具庫(即便是自己封裝的,也可以在一定程度上理解成第三方的),這就會帶來一個問題:這些工具庫是否百分百的可靠?

一個來自《YDKJS》的例子:一個程式設計師開發了一個付款的系統,它良好的執行了很長時間。突然有一天,一個客戶在付款的時候信用卡被連續刷了五次。這名程式設計師在調查了以後發現,一個第三方的工具庫因為某些原因把付款回撥執行了五次。在與第三方團隊溝通之後問題得到了解決。

故事講完了,可問題真的解決了嗎?是否還能夠充分的信任這個工具庫?信任依然要有,可完善必要的檢查和錯誤處理勢在必行。當我們解決了這個問題,由於它的啟發,我們還會聯想到其他的問題,比如沒有呼叫回撥。

再繼續想,你會發現,這樣的問題還要好多好多。總結一下可能會出現的問題:

  • 回撥過早(一般是非同步被同步呼叫);
  • 回撥過晚或沒有回撥;
  • 回撥次數過多;
  • 等等

加上了這些檢查,強壯之後的程式碼可能是這樣的:

listen( "click", function handler( evt){ 
  check1();
  doSomething1();
  setTimeout( function request(){ 
    check2();
    doSomething3();
    ajax( "http:// some. url. 1", function response( text){ 
      if (text == "hello") { 
        handler(); 
      } else if (text == "world") { 
        request(); 
      } 
    }); 
    doSomething4();
  }, 500); 
  doSomething2();
});
複製程式碼

我們都清楚的知道,實際的check要比這裡看起來的複雜的多,而且很多很難複用。這不但使程式碼變得臃腫不堪,還進一步加劇了可讀性的問題。

雖然這些錯誤出現的概率不大,但我們依然必須要處理。

這就是非同步巢狀帶來的信任問題,它的問題的根源在於控制反轉。控制反轉在物件導向中的應用是依賴注入,實現了模組間的解耦。而在回撥中,它就顯得沒有那麼善良了,控制權被交給了第三方,由第三方決定什麼時候呼叫回撥以及如何呼叫回撥。

一些解決信任問題的嘗試

加一個處理錯誤的回撥

function success(data) { 
  console. log(data); 
} 
function failure(err) { 
  console. error( err ); 
} 
ajax( "http:// some. url. 1", success, failure );
複製程式碼

nodejs的error-first

function response(err, data) { 
  if (err) { 
    console. error( err ); 
  } 
  else { 
    console. log( data ); 
  } 
} 
ajax( "http:// some. url. 1", response );
複製程式碼

這兩種方式解決了一些問題,減少了一些工作量, 但是依然沒有徹底解決問題。首先它們的可複用性依然不強,其次,如回撥被多次呼叫的問題依然無法解決。

Promise如何解決這兩個問題

Promise已經是原生支援的API了,它已經被加到了JS的規範裡面,在各大瀏覽器中的執行機制是相同的。這樣就保證了它的可靠。

如何解決可讀性的問題

這一點不用多說,用過Promise的人很容易明白。Promise的應用相當於給了你一張可以把解題思路清晰記錄下來的草稿紙,你不在需要用腦子去記憶執行順序。

如何解決信任問題

Promise並沒有取消控制反轉,而是把反轉出去的控制再反轉一次,也就是反轉了控制反轉。

這種機制有點像事件的觸發。它與普通的回撥的方式的區別在於,普通的方式,回撥成功之後的操作直接寫在了回撥函式裡面,而這些操作的呼叫由第三方控制。在Promise的方式中,回撥只負責成功之後的通知,而回撥成功之後的操作放在了then的回撥裡面,由Promise精確控制。

Promise有這些特徵:只能決議一次,決議值只能有一個,決議之後無法改變。任何then中的回撥也只會被呼叫一次。Promise的特徵保證了Promise可以解決信任問題。

對於回撥過早的問題,由於Promise只能是非同步的,所以不會出現非同步的同步呼叫。即便是在決議之前的錯誤,也是非同步的,並不是會產生同步(呼叫過早)的困擾。

var a = new Promise((resolve, reject) => {
  var b = 1 + c;  // ReferenceError: c is not defined,錯誤會在下面的a列印出來之後報出。
  resolve(true);
})
console.log(1, a);
a.then(res => {
  console.log(2, res);
})
.catch(err => {
  console.log(err);
})
複製程式碼

對於回撥過晚或沒有呼叫的問題,Promise本身不會回撥過晚,只要決議了,它就會按照規定執行。至於伺服器或者網路的問題,並不是Promise能解決的,一般這種情況會使用Promise的競態APIPromise.race加一個超時的時間:

function timeoutPromise(delay) {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      reject("Timeout!");
    }, delay);
  });
}

Promise.race([doSomething(), timeoutPromise(3000)])
.then(...)
.catch(...);
複製程式碼

對於回撥次數太少或太多的問題,由於Promise只能被決議一次,且決議之後無法改變,所以,即便是多次回撥,也不會影響結果,決議之後的呼叫都會被忽略。

參考資料:

相關文章