http://codin.im/2016/09/17/observables-in-angular2-translate/
本文翻譯自TAKING ADVANTAGE OF OBSERVABLES IN ANGULAR 2。
在Angular1.x中,我們使用Promise來處理各種異步。但是在angular2中,使用的是Reactive Extensions (Rx)的Observable
。對於Promise和Observable的區別,網上有很多文章,推薦egghead.io上的這個7分鐘的視頻(作者
Ben Lesh)。在這個視頻的介紹中,主要說的,使用Observable
創建的異步任務,可以被處理,而且是延時加載的。這篇文章裏,我們主要針對一些在跟服務器端交互的時候遇到的問題,來看看Observable
給我們帶來的特性。
實例場景
首先,我們來定義一下問題的場景。假設我們要實現一個搜索功能,有一個簡單的輸入框,當用戶輸入文字的時候,實時的利用輸入的文字進行查詢,並顯示查詢的結果。
問題
在這個簡單的場景當中,一般需要考慮3個問題:
- 不能在用戶輸入每個字符的時候就觸發搜索。
如果用戶輸入每個字符就觸發搜索,一來浪費服務器資源,二來客戶端頻繁觸發搜索,以及更新搜索結果,也會影響客戶端的響應。一般這個問題,都是通過加一些延時來避免。 - 如果用戶輸入的文本沒有變化,就不應該重新搜索。
假設用戶輸入了’foo’以後,停頓了一會,觸發了搜索,再敲了一個字符’o’,結果發現打錯了,又刪掉了這個字符。如果這個時候用戶又停頓一會,導致觸發了搜索,這次的文本’foo’跟之前搜索的時候的文本是一樣的,所以不應該再次搜索。 - 要考慮服務器的異步返回的問題。
當我們使用異步的方式往服務器端發送多個請求的時候,我們需要注意接受返回的順序是無法保證的。比如我們先後搜索了2個單詞’computer’, ‘car’, 雖然’car’這個詞是後來搜的,但是有可能服務器處理這個搜索比較快,就先返回結果。這樣頁面就會先顯示’car’的搜索結果,然後等收到’computer’的搜索結果的時候,再顯示’computer’的結果。但是,這時候在用戶看來明明搜索的是’car’,卻顯示的是另外的結果。
迎接挑戰
在這個實例中,我們使用wikipedia的api接口來開發一個簡單的實例,實現簡單的搜索功能。
實現搜索
由於只是演示,我們的app裏面只包含2個文件: app.ts
和 wikipedia-service.ts
,最終版本的源文件,請參考原文提供的demo鏈接。
我們直接來看最初版本的WikipediaService
是如何實現的:
|
|
在這裏版本中,使用Jsonp
模塊來請求api結果,它的結果應該是一個類型爲Observable<Response>
的對象,我們把返回的結果從Observable<Response>
轉換成
Promise<Response>
對象,然後使用它的then
方法把結果轉成json。這樣,這個search
方法的返回類型爲Promise<Array<string>>
。
注意上面,我們使用response.json()[1]
方式,從原先的結果中,得到我們需要的查詢結果的列表,列表裏面都是string。
這個看起來很簡單,在angular1.x裏面,也基本都是使用$http</code>或<code>$resource
,來返回一個Promise
類型的結果。
下面就是app.ts
的部分內容(因爲這只是演示,所以直接在app.ts裏面直接定義module和component,並且調用service,在真實的app中,應該創建相應的組件來實現):
|
|
從上面的代碼也能看出,AppComponent
有一個search()
方法,它調用wikipediaService.search()
方法,因爲這個方法返回一個Promise<Array<string>>
類型的結果,所以使用then()
,把結果列表賦值給model對象items
。上面的template
裏面的模板內容就是用來以列表顯示查詢的結果。
雖然這個實現滿足了基本的查詢功能,但是對於上面提到的3個問題,都沒有能夠解決。下面就來修改這個實現來解決上面的問題。
控制用戶輸入延時
我們先解決第一個問題:當用戶輸入的時候,不要每次輸入一個字符就觸發一次搜索,而是設置一個時間延時,當用戶停止輸入的時間超過400毫秒,就觸發搜索。如果用戶一直不停的輸入,輸入的時間間隔小於400ms就不觸發。這正是’Observables’能做的事情。
爲此,我們需要一個Observable<string>
對象來保存用戶的輸入,然後就可以用這個對象提供的方法來實現延時觸發的功能。我們可以利用Angular2的指令(directive)formControl
。要用這個指令,需要引入ReactiveFormsModule
模塊。
|
|
引入以後,我們就可以在模板裏面使用FormControl
來創建表單輸入,並給他設置一個變量名term
。
|
|
這樣,這個input組件所綁定的變量term
就是FormControl
的一個實例,它有一個屬性valueChanges
,這個屬性是一個Observable<string>
類型的對象。我們就可以使用Observable<string>
的debounceTime
方法來設置觸發延時。
|
|
我們看到this.term.valueChanges
是一個Observable<string>
對象,通過debounceTime(400)
我們設置它的事件觸發延時是400毫秒。這個方法還是返回一個Observable<string>
對象。然後我們就給這個對象添加一個訂閱事件:
|
|
這是用lambda表達式寫的一個方法。參數term
就是Observable<string>
對象經過400ms的延時設置,產生的一個用戶輸入的字符串。方法體就是用這個參數進行搜索,跟之前版本的處理方式一致。
在這個修改版中,我們把之前的search()
方法去掉,直接在構造函數constructor(...)
裏面添加的,這相當於,用戶在輸入框的輸入,是一個消息源,會經過debounceTime(400)
的處理,然後產生一個消息,這個消息會發送給訂閱的事件處理函數來處理,也就是搜索。所以,我們不需要一個search()方法來控制什麼時候觸發,而是通過類型訂閱的機制來處理用戶輸入。
防止觸發兩次
現在我們再來解決第二個問題,就是經過400ms的延時以後,用戶輸入的搜索條件一樣的情況。有了上面的Observable
,這個就很簡單了,Observable
有一個distinctUntilChanged
的方法,他會判斷從消息源過來的新數據跟上次的數據是否一致,只有不一致纔會觸發訂閱的方法。
|
|
處理返回順序
上面描述了服務器端異步返回數據的時候,返回順序不一致出現的問題。對於這個問題,我們的解決辦法就比較直接,也就是對於之前的請求返回的結果,直接忽略,只處理在頁面上用戶最後一次發起的請求的結果。說道忽略之前的請求,如果你們看了上面的視頻,或者知道Promise
和Observable
的區別的話,就應該想到我們可以利用Observable
的dispose()
方法來解決。實際上,我們是利用這種’disposable’特性來解決,而不是直接調用dispose()
方法。(實在不知道該怎麼翻譯’disposable’,它的意思是我可以中止在Observable對象上的消息處理,字面的意思是可被丟棄的、一次性的。)
上面我們講到,在service的search()
方法裏,我們把Jsonp
返回的結果從Observable<Response>
轉換成
Promise<Response>
對象。爲了利用Observable
的特性去丟棄上一個未及時返回的結果,我們讓這個方法還是返回Observable
類型的結果。下面就是修改後的WikipediaService
裏面的search()
方法。
|
|
注意這個方法最後用.map((response) => response.json()[1])
,意思是對於原先的Response
類型的結果,轉換成實際的搜索結果的列表。
map()
以及後面要說到的flatMap()
之類的方法,是函數式編程裏面常用到的方法,意思就是將原先的數據集裏面的每一條數據,經過一定的處理再返回一個新的結果,也就是把一個數據集轉換成另一個數據集。
現在,我們的WikipediaSerice
的返回結果就不是Promise
了,所以我們就需要修改app.ts,我們不能再使用then()
方法來處理結果,而是使用subscribe()
添加一個消息訂閱方法。
|
|
其中,第一個subscribe()
:
|
|
這個是對輸入框產生的查詢字符串,註冊一個訂閱方法,來處理用戶的輸入。
第二個subscribe()
:
|
|
是對從服務器端返回的數據查詢結果,註冊一個訂閱方法,來將這個數據賦值到model上。
我們也可以用下面的方式,來避免這樣使用多個subscribe
:
|
|
我們在用戶輸入的字符串的Observable<string>
上調用flatMap(...)
方法,相當於,對用戶輸入的每個有效的查詢條件,調用wikipediaService.search()
方法。然後對這個查詢返回的數據,再註冊一個訂閱方法。
費了這麼大的篇幅,希望你明白了Observable
的flatMap
和subscribe
用法,對於沒有接觸過函數式編程的人來說,這確實不好理解,但是在Angular2裏面,我們將會大量使用各種函數式編程的方法。所以還是需要你花時間慢慢理解。
費了這麼大功夫,上面說的似乎跟’忽略之前未及時返回的消息’好像沒什麼關係,那麼上面的修改到底有沒有解決那個問題呢。沒有!確實是沒有。因爲我們使用flatMap
,對用戶輸入的每個有效的查詢字符串,都會調用訂閱的那個處理函數,然後更新model。所以我們的問題還是沒有解決。
但是到了這一步以後,解決辦法就很容易了,我們只需要用switchMap
代理flatMap
就可以。就這麼簡單!這是因爲,switchMap
會在處理每一個新的消息的時候,就直接把上一個消息註冊的訂閱方法直接取消掉。
最後,再優化一下代碼:
|
|
我們直接把switchMap()
的結果,賦給model對象this.items
,也就是一個Observable<Array<string>>
類型的數據。這樣,在模板裏面使用items
的地方也需要修改,使用AsyncPipe
就可以:
|
|
這樣,模板在解析items
這個model的時候,就會自動解析這個Observable
的結果,再渲染頁面。