[RxJS] 認識非同步 (Asynchronous) 及串流 (Stream)

|

非同步 (asynchronous) 與串流 (stream),是程式開發時經常必須面對的議題,各自有各自解決的問題,也各自都有其帶來的延伸問題,今天就先來對這兩個東西有基礎的理解吧!

認識非同步

在 JavaScript 中有個 setTimeout 方法,會在指定的時間(毫秒) 後執行裡面的程式碼邏輯,請問以下程式有兩個 console.log 會分別印出 A 和 B,請問印出的順序為?

setTimeout(() => {
  console.log('A');
}, 1000);
console.log('B');
  1. 一秒後印出 A,然後再印出 B
  2. 先印出 B,一秒後再印出 A

相信使用過 setTimeout() 的人都知道答案是 2,而其中的原因就是「非同步」。

到底什麼是非同步呢?我們先來說明一下「同步」是什麼?

先想下一下以下程式印出的順序為何?

console.log('A');
console.log('B');

相信所有人都會毫不猶豫地回答「先印出 A 在印出 B」,因為我們通常在閱讀程式碼時候是按照前後順序閱讀的,自然而然會認為程式碼是按照前後順序執行的,也就是先發生的程式碼先處理,而這個按照前後發生順序執行的行為我們就稱為「同步」行為。

因此,不一定按照這種順序的行為我們就稱為「非同步」行為。那麼為什麼要有「非同步」這種行為?全部照順序執行不是很好嗎?我們可以想像一下 setTimeout(() => console.log('A'), 1000) 這樣的程式碼,如果一定「等待」一秒鐘,在畫面操作上會發生什麼事情?

最簡單的理解就是在這一秒鐘畫面因為全心等待要去執行 console.log('A') 而導致其他互動完全無法處理,一秒鐘聽起來還好,但如果換成是 AJAX 呼叫後端 API 的請求,偏偏伺服器處理又要等待比較長的時間,數十秒甚至數分鐘都有可能的時候呢?我們真的能接受畫面完全卡住那麼長的一段時間卻什麼都不做嗎?

相信大多數人的答案應該都是「不能」。

這種情況下「非同步」的設計就變得非常重要,當程式並沒有任何其他的運算,只是單純的「等待」時,我們就把它丟到一個暫存區內,讓畫面可以繼續處理其他的行為,直到等待到我們要的資料時,在拿回處理的所有權,繼續後續的程式運算。

當然這只是很粗淺的說明非同步存在的必要性跟流程,它的背後原理有很多東西可以說,但我們的目標是學習 RxJS,因此在這裡只需要知道基本的觀念就好,關於非同步處理的深入原理,網路上有非常多深度介紹的文章,有興趣可以自行搜尋一下。

透過非同步我們可以避免等待時間畫面卡住的時間浪費,也就是俗稱的「non blocking I/O」,然而明顯的缺點是這樣的處理方式把程式邏輯變得更複雜了,畢竟這跟多數人閱讀程式碼的習慣不同,但只要理解什麼時候應該是非同步處理,其實習慣後也不會造成太大困擾,且 JavaScript 也提供了 Promise 這個好用的非同步處理 API 來簡化一些複雜的問題。而另一個問題是,一般的非同步就是「等待完成」後就結束了,若有一系列的等待和先後順序等行為,就需要搭配到串流了。

認識串流

相信大家都有在網路上看過線上影片的經驗,如果是高畫質影片,通常都要數十 MB 甚至數 GB 以上,如果每個次都要把整個影片檔案下載完才能播放,那麼體驗之差相信難以想像。

但假設我們將影片依照時間切成一小段一小段,只需要數秒鐘就能載入完成的影片片段,當播放器快要播放到某個時間點時,再去下載這個時間點對應的片段呢?是不是就不用等那麼長的時間啦!

這就是串流背後做的事情,將資料分成小小的片段,再「串」起來分段「流」向同一個地方,以上述的例子來說就是播放影片的邏輯。

以播放影片的例子來說,大概會寫出像這樣的程式碼:

// 建立一個 stream 物件
const videoPlayStream = {
  // 用來存放每個時間片段的影片內容
  videoObject: [],
  downloadVideo: minute => {
    // 如果影片片段已存在,直接回傳
    if (videoPlayStream.videoObject[minute]) {
      return Promise.resolve(videoPlayStream.videoObject[minute]);
    } else {
      // 如果影片片段不存在,則下載
      return fetch(`...?minute=${minute}`).then(video => {
        videoPlayStream.videoObject[minute] = video;
        return video;
      });
    }
  },
  // 跳到指定的時間
  jumpTo: minute => {
    // 如果影片片段影存在,直接播放
    if (videoPlayStream.videoObject[minute]) {
      videoPlayStream.play(minute);
    } else {
      // 如果影片片段不存在,先進行下載,然後再播放
      videoPlayStream.downloadVideo(minute).then(video => {
        videoPlayStream.play(minute);
      });
    }
  },
  // 播放指定時間影片片段
  play: minute => {
    // 實際播放影片的邏輯
    // 同時預先下載下一個時間點的片段
    videoPlayStream.downloadVideo(minute + 1);
  }
};

// 從頭開始播放
videoPlayStream.jumpTo(0);

// 跳到第 45 分鐘播放
videoPlayStream.jumpTo(45);

當然以上程式碼還有很大的優化空間,也有很多未處理的細節,實際上也絕對跑不動,但作為範例,大概就會像這樣的概念去處理串流。

ReactiveX 與串流

在 ReactiveX 的觀念中,我們會將所有發生的事情都視為串流!

以網頁為例子,滑鼠的點擊事件可以視為一連串事件的串流,除非網頁關閉,否則這個事件持續可能會發生。

當 HTTP 請求呼叫時(也就是 AJAX),也是一種串流,只是這種串流事件只發生一次就會結束

既然所有行為都可以視為串流,如何整合這些串流就變得非常重要,若沒有妥善的設計,很容易就會發生串流包串流這種巢狀地獄的發生,任何有經驗的開發人員應該想盡辦法避免這種狀況!而 ReactiveX 就是結合了多種程式設計的觀念和技巧,漂亮的解決了這些問題!關於這些觀念和技巧,我們之後再來說明。現在我們只需要先有一個觀念,就是盡量以串流的方式思考就好,未來我們會學習到各種組合串流的方式。

本日小結

今天我們介紹了「非同步」與「串流」的基本觀念與問題。

非同步的主要目標是不要為了等待而造成後續程式無法進行的問題,但非同步還是一段執行完就結束的程式碼,因此比較無法處理連續性的資料。

這時候就要搭配串流的概念來設計,而比起非同步處理完就結束,串流相對比較難以掌控及預測,在開發上需要更多的技巧來輔助以避免程式太過複雜難以維護。

ReactiveX 的出現就是為了解決的這個問提,在接下來幾天的文章,我們將一一介紹 ReactiveX 背後組合的各種觀念和程式技巧,掌握這些技巧後,就能更加靈活的組合出強固的串流處理邏輯囉!

如果您覺得我的文章有幫助,歡迎免費成為 LikeCoin 會員,幫我的文章拍手 5 次表示支持!