promise vs Observable(js小筆記)

JayJunG發表於2017-12-25

1.promise

在傳統的解決方案中,js實現非同步程式設計採用的方法是回撥函式和事件監聽(事件釋出訂閱),但是當應用很複雜很龐大時,大量的回撥會讓除錯程式變得舉步維艱,成為開發者的噩夢。

promise是在es6標準中的一種用於解決非同步程式設計的解決方案,由於在語言級別上,不同於Java、Python等多執行緒語言,js是單執行緒的,所以在node.js中大量使用了非同步程式設計的技術,這樣做是為了避免同步阻塞。

promise意為承諾,擬定一個承諾,當承諾實現時即返回結果,不受其他操作的影響,可以把它理解為一個簡單的容器,裡面存放著一個將來會結束的事件返回結果(即非同步操作)。不同於傳統的回撥函式,在promise中,所有的非同步操作的結果都可以通過統一的方法處理。promise有三種狀態:

pending(進行中),resolved(成功),rejected(失敗),非同步操作的結果決定了當前為哪一種狀態,promise的狀態只有兩種改變情況,且僅改變一次:由pending轉變為resolved,由pending轉變為rejected,結果將會保持不變。

下面程式碼是一個簡單的promise例項:

const promise = new Promise(function(resolve, reject){
//some code
if(/*非同步操作成功*/){
    resolve(value);
} else {
    reject(error);
}
});複製程式碼

Promise建構函式接受一個函式作為引數,該函式的兩個引數分別是resolvereject。它們是兩個函式。promise例項生成後,可以用then方法分別指定resolvedrejected的回撥函式。其中第二個函式是可選的,下面是一個Promise物件的簡單例子:

function timeout(ms){
    return new Promise((resolve, reject) => {
    setTimeout(resolve, ms, "done");
    });
}

timeout(100).then((value) => {
    console.log(value);
});複製程式碼

timeout方法返回一個Promise例項,表示一段時間以後才會發生的結果,過了指定的時間後,Promise狀態變為resolved,觸發then方法繫結的回撥函式。

let promise = new Promise(function(resolve, reject){
    console.log("Promise");
    resolve();
});

promise.then(function(){
    console.log("resolved.");
});
console.log("Hi!");
// Promise
// Hi!
// resolved.複製程式碼

上面程式碼中,Promise新建後立即執行,所以首先返回的是Promise,然後,then方法指定的回撥函式,將在當前指令碼所有同步任務執行完才會執行,所以resolved最後輸出。

以上簡單介紹了promise的基本特性:一旦建立立即執行;三種狀態:執行中,成功,失敗;結果不受其他操作影響,結果不可取消;當非同步操作完成或失敗時,Promise會處理一個單個事件。

2.Observable

Observable即可觀察物件,在很多軟體程式設計任務中,或多或少你都會期望你寫的程式碼能按照編寫的順序,一次一個的順序執行和完成。但是在ReactiveX(基於一系列可觀察的非同步和基礎事件程式設計組成的一個庫)中,很多指令可能是並行執行的,之後他們的執行結果才會被觀察者捕獲,順序是不確定的。為達到這個目的,你定義一種獲取和變換資料的機制,而不是呼叫一個方法。在這種機制下,存在一個可觀察物件(Observable),觀察者(Observer)訂閱(Subscribe)它,當資料就緒時,之前定義的機制就會分發資料給一直處於等待狀態的觀察者哨兵。

ReactiveX 可見模式允許你使用陣列等資料項的集合來進行些非同步事件流組合操作。它使你從繁瑣的web式回撥中解脫,從而能使得程式碼可讀性大大提高,同時減少bug的產生。

  • observable資料是很靈活的,不同於promise只能處理單個值,observalbe支援多值甚至是資料流。
  • 當observalbes被建立時,它是不會立即執行的(lazy evaluation),只有當真正需要結果的時候才會去呼叫它。例如下面的程式碼,對於promise而言,無論是否呼叫then,promise都會被執行;而observables卻只是被建立,並不會執行,而只有在真正需要結果的時候,如這裡的foreach,才會被執行。

var promise = new Promise((resolve) => {      setTimeout(() => {          resolve(42);      }, 500);      console.log("promise started");  });    //promise.then(x => console.log(x));    var source = Rx.Observable.create((observe) => {      setTimeout(() => {          observe.onNext(42);      }, 500);      console.log("observable started");  });    //source.forEach(x => console.log(x));  複製程式碼

  • observables是可以取消的(dispose),observables能夠在執行前或執行中被取消,即取消訂閱。下面的例子中,observable在0.5秒的時候被dispose,所以日誌“observable timeout hit”不會被列印。

var promise = new Promise((resolve) => {      setTimeout(() => {          console.log("promise timeout hit")          resolve(42);      }, 1000);      console.log("promise started");  });    promise.then(x => console.log(x));    var source = Rx.Observable.create((observe) => {      id = setTimeout(() => {          console.log("observable timeout hit")          observe.onNext(42);      }, 1000);      console.log("observable started");        return () => {          console.log("dispose called");          clearTimeout(id);      }  });    var disposable = source.forEach(x => console.log(x));    setTimeout(() => {      disposable.dispose();  }, 500);  複製程式碼

  • observables可以多次呼叫(retry),對於一個observable物件返回的結果,可以被多次呼叫處理,能夠觸發多次非同步操作,在observables中封裝了很多工具方法可以用來操作observable結果,對其進行組合變換。在上面的程式碼中,可以拿到promise和observable的變數。對於promise,不論在後面怎麼呼叫then,實際上的非同步操作只會被執行一次,多次呼叫沒有效果;但是對於observable,多次呼叫forEach或者使用retry方法,能夠觸發多次非同步操作。

下面再通過一個angular2例項場景瞭解promise和observable在實際應用中的區別:

首先,我們來定義一下問題的場景。假設我們要實現一個搜尋功能,有一個簡單的輸入框,當使用者輸入文字的時候,實時的利用輸入的文字進行查詢,並顯示查詢的結果。

在這個簡單的場景當中,一般需要考慮3個問題:

  • 不能在使用者輸入每個字元的時候就觸發搜尋。
    如果使用者輸入每個字元就觸發搜尋,一來浪費伺服器資源,二來客戶端頻繁觸發搜尋,以及更新搜尋結果,也會影響客戶端的響應。一般這個問題,都是通過加一些延時來避免。
  • 如果使用者輸入的文字沒有變化,就不應該重新搜尋。
    假設使用者輸入了’foo’以後,停頓了一會,觸發了搜尋,再敲了一個字元’o’,結果發現打錯了,又刪掉了這個字元。如果這個時候使用者又停頓一會,導致觸發了搜尋,這次的文字’foo’跟之前搜尋的時候的文字是一樣的,所以不應該再次搜尋。
  • 要考慮伺服器的非同步返回的問題。
    當我們使用非同步的方式往伺服器端傳送多個請求的時候,我們需要注意接受返回的順序是無法保證的。比如我們先後搜尋了2個單詞’computer’, ‘car’, 雖然’car’這個詞是後來搜的,但是有可能伺服器處理這個搜尋比較快,就先返回結果。這樣頁面就會先顯示’car’的搜尋結果,然後等收到’computer’的搜尋結果的時候,再顯示’computer’的結果。但是,這時候在使用者看來明明搜尋的是’car’,卻顯示的是另外的結果。
首先是promise的初始版本:

import { Injectable } from '@angular/core';
import { URLSearchParams, Jsonp } from '@angular/http';
@Injectable()
export class WikipediaService {  

constructor(private jsonp: Jsonp) {}  

search (term: string) {    
    var search = new URLSearchParams()    
    search.set('action', 'opensearch');    
    search.set('search', term);    
    search.set('format', 'json');    
    return this.jsonp                
    .get('http://en.wikipedia.org/w/api.php?callback=JSONP_CALLBACK', { search })       
    .toPromise()             
    .then((response) => response.json()[1]);  
            }
        }複製程式碼

在上面程式碼中,使用Jsonp模組來請求api結果,它的結果應該是一個型別為Observable<Response>的物件,我們把返回的結果從Observable<Response> 轉換成 Promise<Response>物件,然後使用它的then方法把結果轉成json。這樣,這個search方法的返回型別為Promise<Array<string>>。這樣雖然查詢功能基本實現了,但是前面提到的三個問題都沒有解決。

下面應用observable實現功能:

(1)控制使用者輸入延時

export class AppComponent { 
      items: Array<string>;  
      term = new FormControl();  
      constructor(private wikipediaService: WikipediaService) { 
      this.term.valueChanges        
      .debounceTime(400)          
      .subscribe(term => this.wikipediaService.search(term)
      .then(items => this.items = items));  
        }
    }複製程式碼

這裡的this.term.valueChanges是一個Observable<string>物件,通過debounceTime(400)我們設定它的事件觸發延時是400毫秒。這個方法還是返回一個Observable<string>物件。然後我們就給這個物件新增一個訂閱事件

subscribe(term => this.wikipediaService.search(term).then(items => this.items = items));複製程式碼

這樣就解決了第一個問題,通過控制使用者輸入延時來解決每次輸入一個字元就觸發一次搜尋的問題。

(2)防止觸發兩次

this.term.valueChanges           .debounceTime(400)           .distinctUntilChanged()           .subscribe(term => this.wikipediaService.search(term).then(items => this.items = items));複製程式碼

上面的程式碼解決了第二個問題,就是經過400ms的延時以後,使用者輸入的搜尋條件一樣的情況。Observable有一個distinctUntilChanged的方法,他會判斷從訊息源過來的新資料跟上次的資料是否一致,只有不一致才會觸發訂閱的方法。

(3)處理返回順序

上面描述了伺服器端非同步返回資料的時候,返回順序不一致出現的問題。對於這個問題,我們的解決辦法就比較直接,也就是對於之前的請求返回的結果,直接忽略,只處理在頁面上使用者最後一次發起的請求的結果。我們可以利用Observabledispose()方法來解決。實際上,我們是利用這種’disposable’特性來解決,而不是直接呼叫dispose()方法。(實在不知道該怎麼翻譯’disposable’,它的意思是我可以中止在Observable物件上的訊息處理,字面的意思是可被丟棄的、一次性的。)

search (term: string) {
  var search = new URLSearchParams()
  search.set('action', 'opensearch');
  search.set('search', term);
  search.set('format', 'json');
  return this.jsonp             
 .get('http://en.wikipedia.org/w/api.php?callback=JSONP_CALLBACK', { search })              
 .map((response) => response.json()[1]);
}複製程式碼

注意這個方法最後用.map((response) => response.json()[1]),意思是對於原先的Response型別的結果,轉換成實際的搜尋結果的列表,即利用Observable的特性去丟棄上一個未及時返回的結果。

總結:在處理某些複雜非同步應用中,observable比promise更受開發者青睞,因為使用Observable建立的非同步任務,可以被處理,而且是延時載入的。而promise設計的初衷只是為了解決大量的非同步回撥所造成的難以除錯問題,observable封裝了大量的方法供我們使用以處理複雜的非同步任務。

以上內容部分摘自阮一峰阮老師的《es6入門》,關於promise和observable的區別,還有一個較好的視訊:egghead.io上的這個7分鐘的視訊(作者Ben Lesh)。第一次在掘金上寫文章,算是對個人知識的總結,難免會有錯漏,歡迎大家指點。



相關文章