[譯]爲何前端初學者必需要明白髮布訂閱模式

By Hubert Zub | Oct 3, 2018

原文html

當你將關注點從樣式,美學和網格系統轉移到邏輯,框架和編寫JavaScript代碼時。一切都開始了,你會發現你處於你的web開發歷程中最激動人心的那一刻。git

<center>開始的時候像這樣子…</center>github

在這個很是時刻你會發現,當涉及到JS時,它不只僅是幾個簡單的jQuery技巧和視覺效果。你的視野是一整個web應用,而再也不僅僅是侷限於頁面。web

當你把更多的精力投入到寫js代碼時,你會開始考慮交互、你的子模塊和邏輯。事情開始奏效,你感受到你的app有了生命。一個全新的、使人興奮的世界出如今你眼前,一樣,也出現了不少全新的、棘手的問題。編程

<center>這僅是開始</center>redux

你並不氣餒,並想出來各類各樣的辦法,代碼也寫的愈來愈多。嘗試某些博客文章中那各類各樣的技術,不斷地完善本身解決問題的方法。api

而後,你開始以爲有些不對路。數組

你的腳本文件慢慢變大,一小時前才200行的,如今已經500行了。「嘿」——你想——「這沒什麼大不了的」。隨後,你開始閱讀關於代碼維護的相關文章,並着手實現它。開始分離你的邏輯代碼,並把它們分塊、組件。事情開始又變好了點。代碼像圖書館藏書那樣分類存放。你感受良好,由於各類各樣的文件被以正確的命名放置在合適的目錄裏。代碼變得模塊化,更易於維護了。promise

然而,你又感受不對路了,可是不知道哪裏有問題。網絡

<center> * </center>

web應用的行爲不多是線性的。事實上,web應用的許多行爲應該是瞬時發生(有時候應該是出乎意料或是自發地)。

應用須要正確併合適響應各類網絡請求、用戶操做、計時事件和各類延時動做。名爲「異步」和「race condition」的怪物無時不刻在敲你的腦門。

你須要將你帥氣的模塊化結構與醜陋的新娘結合 - 異步代碼。一個棘手的問題來了:我應該把這段代碼放在哪裏?

你會把你的app精心地劃分紅一個個構建塊。導航和內容組件被整齊地放置在合適的目錄中,較小的輔助腳本文件包含了執行普通任務的重複代碼。一切都經過app.js這個文件來調度,一切都從這裏開始。完美。

可是,你的目標是在app中的某個地方調用異步代碼,運行後把它放在一旁。

異步代碼應該放在ui組件麼?或者放在主文件裏?app的哪一個構建塊負責響應呢?哪個構建塊負責開始運行?錯誤處理呢?你在腦海裏考慮着各類方法——可是你仍是愁眉不解——你意識到若是想要拓展或維護這些代碼,那難度是至關大的,問題還沒解決。你須要理想的一勞永逸的方案。

放鬆一下,這對你來講沒有問題。事實上,你的思惟越有條理,這種煩惱就會越強烈。

你開始閱讀有關處理此問題的信息並尋求即用型解決方案。一開始,你瞭解到promises優於回調的地方。隨後,你開始試圖瞭解什麼是RxJS(而且爲何網上的一些人說這是解決網絡異步請求的惟一解決方案)。通過一些閱讀以後,你試着去理解,爲何一個博客寫道沒有redux-thunk的redux沒有意義,可是另外一我的認爲redux-saga也是如此。

一天結束後,你疲憊的大腦充斥着各類詞。閱讀完大量可行的方法後,你的想法噴涌出來。爲何會有這麼多呢?那麼複雜?人們怎麼喜歡在互聯網上爭論,不去開發一個好的模式?

由於這些都不重要

不管使用哪一種框架,異步代碼都不可能被正確地存放好。並無一個單1、通用、既定的解決方案,要根據具體的開發環境、需求來採起不一樣的方案。

而且,這篇文章也不會提供解決全部問題的方案。可是它能夠給你提供一個好的思路,讓你處理好你的異步代碼——由於它都基於一個很是基本的原則。

<center> * </center>

通用部分

從某些角度來看,編程語言的結構並不複雜。畢竟,它們只是相似於計算機的愚蠢東西,可以在各類盒子裏儲存值而已,而且經過if或函數調用改變程序執行流程。做爲一種命令式和略微面向對象的語言,js在這裏也是相似的。

這意味着究其本質,來自各路大神寫的各類宇宙級異步庫(不管是redux-saga、RxJS、觀察者或者其餘奇奇怪怪的庫)都依賴相同的基本原理。它們並無那麼神奇——它必須讓你們學習它的概念,這裏並無新發明。

爲何這個事實如此重要?讓咱們來考慮這樣的一個例子。

<center> * </center>

Let’s do (and break) something

先來個簡單的app,這個app可讓咱們在地圖上標記咱們喜歡的地方。沒有什麼花哨的東西:只是右側的地圖視圖和左側的簡單側邊欄。單擊地圖應在地圖上保存新標記。

固然,咱們須要一個不同凡響的特性:咱們須要它用local storage記住咱們標記好的地方列表。

綜上所述,咱們能夠畫一個流程圖出來

看,並非很複雜

爲簡潔起見,下面的示例將在不使用任何框架或UI庫的狀況下編寫 - 僅涉及vanilla js。此外,咱們將使用谷歌地圖API的一小部分 - 若是你想本身建立相似的應用程序,你應該註冊你的API密鑰[https://cloud.google.com/maps...](https://cloud.google.com/maps...
).

快速分析一下

  • init方法用google地圖api初始化地圖組件,註冊地圖點擊事件而且嘗試從local storage加載數據。
  • addPlace方法處理地圖點擊事件——把新地點加在列表裏而且更新ui
  • renderMarkers方法迭代地點列表,清除地圖後,將標記放在其上。

忽略一些不完善的地方(沒有錯誤處理之類的)—— 它將做爲原型提供足夠好的服務。完美。讓咱們寫一些html:

假設咱們寫了一些樣式(咱們不會在這裏介紹它,由於它不相關),無論你信不信 - 它其實是這樣作的:

儘管它很醜,可是管用。不過可拓展性很差。

首先,咱們的代碼責任分割不明確。若是你據說過SOLID)原則,你應該清楚咱們已經打破了第一條規則:單一責任原理。在咱們的例子中——儘管很簡單——一個js文件包含了全部,包括處理用戶響應的代碼和數據轉換和異步代碼。「爲何這樣很差,運行起來不是棒棒的麼?」——你可能會這麼說。確實運行起來棒棒的,可是若是要加新特性那就不棒棒了——可維護性低。

我用一個例子讓你完全心服口服:

首先,咱們想要側邊欄加標記列表。第二,咱們想要用googleAPI實如今地圖上看到城市名的功能——這就引入了異步代碼。

好了,咱們的新流程圖畫出來了:

<center>提示:城市名稱查找不是很複雜,谷歌地圖爲此提供了很是簡單的API。 你能夠本身檢查一下! </center>

既然你調用別人的接口,那確定不是同步代碼而是異步代碼啦。它首先要調用google的js庫,而且回覆過來須要必定時間。雖然有點複雜,可是用於教學剛恰好。

讓咱們回到ui代碼這裏而且這裏有個明顯的事實。咱們的頁面分兩大塊,側邊欄和主要內容區。咱們絕對不能把它們兩的代碼放在一塊兒。緣由很明顯——咱們未來有四個組件怎麼辦?六個呢?一百個呢?咱們須要把咱們的代碼分開——咱們須要有兩個獨立的js文件。一個是側邊欄,一個是主要內容區塊。問題來了,哪個應該存放地方標記列表的數組呢?

哪個正確呢?哪一個都不對。還記得單一責任原則麼?爲了下降代碼冗餘度,咱們應該以某種方式分離關注點並將咱們的數據邏輯保存在其餘地方。看吧:

代碼分離萬金油:咱們能夠把進行數據操做的代碼放到另外一個文件裏,這個文件集中處理數據。這個servce文件將負責那些與本地存儲同步的問題和機制。相反,組件將僅僅提供接口。這符合SOLID原則。讓咱們介紹下這個模式:

Service code

Map component code

Sidebar component code:

好了,一個大問題已經解決。代碼整齊擺放在它們該待的位置。但在咱們感受良好以前,運行下這個。

。。。oops。

在作任何動做以後,app沒有交互了。

爲何? 好吧,咱們沒有實現任何同步手段。使用導入的方法添加地點後,咱們不會在任何地方發出任何信號。在調用addPlace()以後,咱們甚至沒法在下一步調用getPlaces()方法,由於城市查找是異步的,須要時間來完成。

程序在後臺進行,可是並無反應到界面上——在地圖上添加標記後,咱們沒有看到側邊欄的更新。怎麼解決?

一個簡單的方法就是,使用定時器輪詢咱們的服務,例如:

它有用麼?emm。。有,但不是最佳方案。大多數狀況下咱們並不須要這個服務。

畢竟,你也不會定時去看你的包裹有沒到達。一樣地,若是你把汽車丟去維修,你也不會每半小時給修車師傅打電話詢問工做是否完成(至少但願你不是這種人)。正常的狀況應該是這樣的,修車師傅修好了,天然會打電話給你。固然,咱們事先留電話了。

如今,咱們在js中嘗試下這種「留電話」的方式。

<center> * </center>

js是一門很是神奇的語言——它的一個古怪的特徵就是能夠把函數視爲其餘值。形象點表示就是,「函數是一等公民」。這意味着任何函數均可以分配給變量或做爲參數傳遞給另外一個函數。事實上你已經接觸過了:還記得setTimeout,setInterval和各類事件監聽器回調嗎? 它們經過將函數做爲參數來使用。

這種特性在異步場景中是基礎

咱們能夠定義一個更新咱們的UI的函數 - 而後將它傳遞給另外一部分的代碼,在那裏它將被調用。

使用這種機制,咱們能夠將renderCities方法以某種方式傳遞給dataService。在那裏,它將在必要時被調用:畢竟,服務能準確地知道什麼時候應該將數據傳輸到組件。

試一試,咱們首先在服務端添加這個功能,而後在某個時刻調用它。

如今,在sidebar那裏使用

你知道會發生什麼麼?當在加載咱們的sidebar代碼時,它在dataService註冊了renderCities方法。

在這種狀況下,當咱們的數據發生更改時,dataService就會調用此函數(因爲addPlace()的調用)。

確切地說,咱們的代碼的一部分是事件的SUBSCRIBER,另外一部分是PUBLISHER(服務方法)。咱們已經實現了發佈 - 訂閱模式的最基本形式,這是幾乎全部高級異步概念的基本概念。

還有呢?

請注意,咱們的代碼,僅限於一個監聽組件(即,一位訂閱者)。若是其餘方法也用了這個subscribe方法來傳遞的話,它會覆蓋掉dataService的changeListener變量,爲了解決這個問題,咱們須要用數組來存儲監聽者。

如今,咱們能夠稍微整理一下代碼並編寫一個函數來爲咱們調用全部的監聽者:

這樣咱們也能夠鏈接map.js組件,以便它對服務中的全部操做作出正確的反應:

若是須要傳遞參數怎麼辦?咱們可使用監聽者的參數直接得到。像這樣:

而後,能夠輕鬆地在組件中檢索數據:

這裏還有更多的可能性 - 咱們能夠爲不一樣類型的行爲建立不一樣的主題(或渠道)。此外,咱們能夠提取發佈和訂閱方法到一個文件並從那裏使用它。但就目前而言,還OK啦 - 如下是使用咱們剛剛建立的相同代碼的應用的簡短視頻

(譯者注,你們去原文那裏看吧)

<center> * </center>

(譯者注:接下來的內容是做者關於這個模式的想法,他說,那些組件的概念好比RxjS,雖然它們功能更強大、概念更加地複雜,可是基本概念都是上文講過的。它們搞得太複雜了而已。而且這個模式也能夠套在其餘的地方。如DOM操做。另外,本文只是講了最基本的,還有不少地方能夠拓展。好比取消訂閱、事件訂閱等等。最後做者還建議咱們多點搞優秀的源代碼,down下來用debugger研究源碼。挖掘出它們最基本的思想。多動手、多思考,不要懼怕專有名詞,以爲很高大上、很難理解。其實就是那麼一回事。有些人搞得太複雜了。)
(譯者爲何不翻譯完呢?由於想讀者們本身嘗試去翻譯,最重要的緣由,是由於譯者懶。。。)

Does this whole publish-subscribe thing resemble something you might already know? After giving it some thought, it’s the pretty same mechanism that you use in element.addEventListener(action, callback). You subscribe your function to a particular event, which ich being called when some action is published by element. Same story.

Going back to the title: why is this thing so bloody important? After all, in the long run, there is little sense in holding up to vanilla JavaScript and modifying the DOM manually — same goes with manual mechanisms for passing and receiving events. Various frameworks have their established solutions: Angular uses RxJS, React have state and props management with possibility of boosting it with redux, literally every usable framework or library have its own method of data synchronization.
Well, the truth is that all of them use some variation of publish-subscribe pattern.

As we already said — DOM event listeners are nothing more than subscribing to publishing UI actions. Going further: what is a Promise? From certain point of view, it’s just a mechanism that allows us to subscribe for completion of a certain deferred action, then publishes some data when ready.

React state and props change? Components’ updating mechanisms are subscribed to the changes. Websocket’s on()? Fetch API? They allow to subscribe to certain network action. Redux? It allows to subscribe to changes in the store. And RxJS? It’s a shameless one big subscribe pattern.

It’s the same principle. There are no magic unicorns under the hood. It’s just like the ending of the Scooby-Doo episode.

It’s not a great discovery. But it’s important to know:

No matter what method of solving 
asynchronous problem will you use,
 it will be always some variation of
  the same principle: something 
  subscribes, something publishes.

That’s why it is so essential. You can always think of publish and subscribe. Take note and keep going. Keep building larger and more complex application with many asynchronous mechanisms — and no matter how difficult it may look like, try to synchronize everything with publishers and subscribers.

<center> * </center>

Still, there is a number of topics untouched in this story:

  • Mechanisms of unsubscribing listeners when not needed anymore,
  • Multi-topic subscribing (just like addEventListener allows you to subscribe to different events),
  • Expanded ideas: event buses, etc.

To expand your knowledge, you can review a number of JavaScript libraries that implement publish-subscribe in its bare form:

Go ahead and try to use them, break them and run the debugger in order to see what happens under the hood. Also, there is a number of great articles that describe this idea very well.

You can find the code from this story in the following GitHub repository:

https://github.com/hzub/pubsu...

Keep experimenting and tinkering—and don’t be afraid of the buzz words, they’re usually just regular code in disguise. And keep thinking.

See you!

相關文章
相關標籤/搜索