為什么前端初學(xué)者必須要明白發(fā)布訂閱模式
By Hubert Zub | Oct 3, 2018
當(dāng)你將關(guān)注點(diǎn)從樣式附迷,美學(xué)和網(wǎng)格系統(tǒng)轉(zhuǎn)移到邏輯,框架和編寫JavaScript代碼時(shí)越妈。一切都開始了,你會(huì)發(fā)現(xiàn)你處于你的web開發(fā)歷程中最激動(dòng)人心的那一刻。
<center>開始的時(shí)候像這樣子…</center>
在這個(gè)非常時(shí)刻你會(huì)發(fā)現(xiàn)兽间,當(dāng)涉及到JS時(shí)暂殖,它不僅僅是幾個(gè)簡(jiǎn)單的jQuery技巧和視覺效果价匠。你的視野是一整個(gè)web應(yīng)用,而不再僅僅是局限于頁(yè)面呛每。
當(dāng)你把更多的精力投入到寫js代碼時(shí)踩窖,你會(huì)開始考慮交互、你的子模塊和邏輯晨横。事情開始奏效洋腮,你感覺到你的app有了生命箫柳。一個(gè)全新的、令人興奮的世界出現(xiàn)在你眼前啥供,同樣悯恍,也出現(xiàn)了很多全新的、棘手的問題伙狐。
<center>這僅是開始</center>
你并不氣餒涮毫,并想出來各種各樣的辦法,代碼也寫的越來越多贷屎。嘗試某些博客文章中那各種各樣的技術(shù)罢防,不斷地完善自己解決問題的方法。
然后唉侄,你開始覺得有些不對(duì)路咒吐。
你的腳本文件慢慢變大,一小時(shí)前才200行的属划,現(xiàn)在已經(jīng)500行了恬叹。“嘿”——你想——“這沒什么大不了的”同眯。隨后绽昼,你開始閱讀關(guān)于代碼維護(hù)的相關(guān)文章,并著手實(shí)現(xiàn)它嗽测。開始分離你的邏輯代碼绪励,并把它們分塊、組件唠粥。事情開始又變好了點(diǎn)疏魏。代碼像圖書館藏書那樣分類存放。你感覺良好晤愧,因?yàn)楦鞣N各樣的文件被以正確的命名放置在合適的目錄里大莫。代碼變得模塊化,更易于維護(hù)了官份。
然而只厘,你又感覺不對(duì)路了,但是不知道哪里有問題舅巷。
<center> * * * </center>
web應(yīng)用的行為很少是線性的羔味。事實(shí)上,web應(yīng)用的許多行為應(yīng)該是瞬時(shí)發(fā)生(有時(shí)候應(yīng)該是出乎意料或是自發(fā)地)钠右。
應(yīng)用需要正確并合適響應(yīng)各種網(wǎng)絡(luò)請(qǐng)求赋元、用戶操作、計(jì)時(shí)事件和各種延時(shí)動(dòng)作。名為“異步”和“race condition”的怪物無時(shí)不刻在敲你的腦門搁凸。
你需要將你帥氣的模塊化結(jié)構(gòu)與丑陋的新娘結(jié)合 - 異步代碼媚值。一個(gè)棘手的問題來了:我應(yīng)該把這段代碼放在哪里?
你會(huì)把你的app精心地劃分成一個(gè)個(gè)構(gòu)建塊护糖。導(dǎo)航和內(nèi)容組件被整齊地放置在合適的目錄中褥芒,較小的輔助腳本文件包含了執(zhí)行普通任務(wù)的重復(fù)代碼。一切都通過app.js這個(gè)文件來調(diào)度嫡良,一切都從這里開始锰扶。完美。
但是皆刺,你的目標(biāo)是在app中的某個(gè)地方調(diào)用異步代碼少辣,運(yùn)行后把它放在一旁。
異步代碼應(yīng)該放在ui組件么羡蛾?或者放在主文件里?app的哪個(gè)構(gòu)建塊負(fù)責(zé)響應(yīng)呢锨亏?哪一個(gè)構(gòu)建塊負(fù)責(zé)開始運(yùn)行痴怨?錯(cuò)誤處理呢?你在腦海里考慮著各種方法——但是你還是愁眉不解——你意識(shí)到如果想要拓展或維護(hù)這些代碼器予,那難度是相當(dāng)大的浪藻,問題還沒解決。你需要理想的一勞永逸的方案乾翔。
放松一下爱葵,這對(duì)你來說沒有問題。事實(shí)上反浓,你的思維越有條理萌丈,這種煩惱就會(huì)越強(qiáng)烈。
你開始閱讀有關(guān)處理此問題的信息并尋求即用型解決方案雷则。一開始辆雾,你了解到promises優(yōu)于回調(diào)的地方。隨后月劈,你開始試圖了解什么是RxJS(并且為什么網(wǎng)上的一些人說這是解決網(wǎng)絡(luò)異步請(qǐng)求的唯一解決方案)度迂。經(jīng)過一些閱讀之后,你試著去理解猜揪,為什么一個(gè)博客寫道沒有redux-thunk的redux沒有意義惭墓,但是另一個(gè)人認(rèn)為redux-saga也是如此。
一天結(jié)束后而姐,你疲憊的大腦充斥著各種詞腊凶。閱讀完大量可行的方法后,你的想法噴涌出來。為什么會(huì)有這么多呢吭狡?那么復(fù)雜尖殃?人們?cè)趺聪矚g在互聯(lián)網(wǎng)上爭(zhēng)論,不去開發(fā)一個(gè)好的模式划煮?
因?yàn)檫@些都不重要
無論使用哪種框架送丰,異步代碼都不可能被正確地存放好。并沒有一個(gè)單一弛秋、通用器躏、既定的解決方案,要根據(jù)具體的開發(fā)環(huán)境蟹略、需求來采取不同的方案登失。
并且,這篇文章也不會(huì)提供解決所有問題的方案挖炬。但是它可以給你提供一個(gè)好的思路揽浙,讓你處理好你的異步代碼——因?yàn)樗蓟谝粋€(gè)非常基本的原則意敛。
<center> * * * </center>
通用部分
從某些角度來看馅巷,編程語(yǔ)言的結(jié)構(gòu)并不復(fù)雜。畢竟草姻,它們只是類似于計(jì)算機(jī)的愚蠢東西钓猬,能夠在各種盒子里儲(chǔ)存值而已,并且通過if或函數(shù)調(diào)用改變程序執(zhí)行流程撩独。作為一種命令式和略微面向?qū)ο蟮恼Z(yǔ)言敞曹,js在這里也是類似的。
這意味著究其本質(zhì)综膀,來自各路大神寫的各種宇宙級(jí)異步庫(kù)(無論是redux-saga澳迫、RxJS、觀察者或者其他奇奇怪怪的庫(kù))都依賴相同的基本原理僧须。它們并沒有那么神奇——它必須讓大家學(xué)習(xí)它的概念纲刀,這里并沒有新發(fā)明。
為什么這個(gè)事實(shí)如此重要担平?讓我們來考慮這樣的一個(gè)例子示绊。
<center> * * * </center>
Let’s do (and break) something
先來個(gè)簡(jiǎn)單的app,這個(gè)app可以讓我們?cè)诘貓D上標(biāo)記我們喜歡的地方暂论。沒有什么花哨的東西:只是右側(cè)的地圖視圖和左側(cè)的簡(jiǎn)單側(cè)邊欄面褐。單擊地圖應(yīng)在地圖上保存新標(biāo)記。
當(dāng)然取胎,我們需要一個(gè)與眾不同的特性:我們需要它用local storage記住我們標(biāo)記好的地方列表展哭。
綜上所述湃窍,我們可以畫一個(gè)流程圖出來
看,并不是很復(fù)雜
為簡(jiǎn)潔起見匪傍,下面的示例將在不使用任何框架或UI庫(kù)的情況下編寫 - 僅涉及vanilla js您市。此外,我們將使用谷歌地圖API的一小部分 - 如果你想自己創(chuàng)建類似的應(yīng)用程序役衡,你應(yīng)該注冊(cè)你的API密鑰https://cloud.google.com/maps-platform/#get-started.
快速分析一下
init方法用google地圖api初始化地圖組件茵休,注冊(cè)地圖點(diǎn)擊事件并且嘗試從local storage加載數(shù)據(jù)。
addPlace方法處理地圖點(diǎn)擊事件——把新地點(diǎn)加在列表里并且更新ui
renderMarkers方法迭代地點(diǎn)列表手蝎,清除地圖后榕莺,將標(biāo)記放在其上。
忽略一些不完善的地方(沒有錯(cuò)誤處理之類的)—— 它將作為原型提供足夠好的服務(wù)棵介。完美钉鸯。讓我們寫一些html:
假設(shè)我們寫了一些樣式(我們不會(huì)在這里介紹它,因?yàn)樗幌嚓P(guān))邮辽,不管你信不信 - 它實(shí)際上是這樣做的:
盡管它很丑唠雕,但是管用。不過可拓展性不好逆巍。
首先及塘,我們的代碼責(zé)任分割不明確。如果你聽說過SOLID原則,你應(yīng)該清楚我們已經(jīng)打破了第一條規(guī)則:?jiǎn)我回?zé)任原理锐极。在我們的例子中——盡管很簡(jiǎn)單——一個(gè)js文件包含了所有,包括處理用戶響應(yīng)的代碼和數(shù)據(jù)轉(zhuǎn)換和異步代碼芳肌×樵伲“為什么這樣不好,運(yùn)行起來不是棒棒的么亿笤?”——你可能會(huì)這么說翎迁。確實(shí)運(yùn)行起來棒棒的,但是如果要加新特性那就不棒棒了——可維護(hù)性低净薛。
我用一個(gè)例子讓你徹底心服口服:
首先汪榔,我們想要側(cè)邊欄加標(biāo)記列表。第二肃拜,我們想要用googleAPI實(shí)現(xiàn)在地圖上看到城市名的功能——這就引入了異步代碼痴腌。
好了,我們的新流程圖畫出來了:
<center>提示:城市名稱查找不是很復(fù)雜燃领,谷歌地圖為此提供了非常簡(jiǎn)單的API士聪。 你可以自己檢查一下! </center>
既然你調(diào)用別人的接口猛蔽,那肯定不是同步代碼而是異步代碼啦剥悟。它首先要調(diào)用google的js庫(kù)灵寺,并且回復(fù)過來需要一定時(shí)間。雖然有點(diǎn)復(fù)雜区岗,但是用于教學(xué)剛剛好略板。
讓我們回到ui代碼這里并且這里有個(gè)明顯的事實(shí)。我們的頁(yè)面分兩大塊慈缔,側(cè)邊欄和主要內(nèi)容區(qū)叮称。我們絕對(duì)不能把它們兩的代碼放在一起。原因很明顯——我們將來有四個(gè)組件怎么辦胀糜?六個(gè)呢颅拦?一百個(gè)呢?我們需要把我們的代碼分開——我們需要有兩個(gè)獨(dú)立的js文件教藻。一個(gè)是側(cè)邊欄距帅,一個(gè)是主要內(nèi)容區(qū)塊。問題來了括堤,哪一個(gè)應(yīng)該存放地方標(biāo)記列表的數(shù)組呢碌秸?
哪一個(gè)正確呢?哪個(gè)都不對(duì)悄窃。還記得單一責(zé)任原則么讥电?為了降低代碼冗余度,我們應(yīng)該以某種方式分離關(guān)注點(diǎn)并將我們的數(shù)據(jù)邏輯保存在其他地方轧抗《鞯校看吧:
代碼分離萬金油:我們可以把進(jìn)行數(shù)據(jù)操作的代碼放到另一個(gè)文件里,這個(gè)文件集中處理數(shù)據(jù)横媚。這個(gè)servce文件將負(fù)責(zé)那些與本地存儲(chǔ)同步的問題和機(jī)制纠炮。相反,組件將僅僅提供接口灯蝴。這符合SOLID原則恢口。讓我們介紹下這個(gè)模式:
Service code
Map component code
Sidebar component code:
好了,一個(gè)大問題已經(jīng)解決穷躁。代碼整齊擺放在它們?cè)摯奈恢酶纭5谖覀兏杏X良好之前,運(yùn)行下這個(gè)问潭。
猿诸。。睦授。oops两芳。
在做任何動(dòng)作之后,app沒有交互了去枷。
為什么怖辆? 好吧是复,我們沒有實(shí)現(xiàn)任何同步手段。使用導(dǎo)入的方法添加地點(diǎn)后竖螃,我們不會(huì)在任何地方發(fā)出任何信號(hào)淑廊。在調(diào)用addPlace()之后,我們甚至無法在下一步調(diào)用getPlaces()方法特咆,因?yàn)槌鞘胁檎沂钱惒降募境停枰獣r(shí)間來完成。
程序在后臺(tái)進(jìn)行腻格,但是并沒有反應(yīng)到界面上——在地圖上添加標(biāo)記后画拾,我們沒有看到側(cè)邊欄的更新。怎么解決菜职?
一個(gè)簡(jiǎn)單的方法就是青抛,使用定時(shí)器輪詢我們的服務(wù),例如:
它有用么酬核?emm蜜另。。有嫡意,但不是最佳方案举瑰。大多數(shù)情況下我們并不需要這個(gè)服務(wù)。
畢竟蔬螟,你也不會(huì)定時(shí)去看你的包裹有沒到達(dá)此迅。同樣地,如果你把汽車丟去維修旧巾,你也不會(huì)每半小時(shí)給修車師傅打電話詢問工作是否完成(至少希望你不是這種人)邮屁。正常的情況應(yīng)該是這樣的,修車師傅修好了菠齿,自然會(huì)打電話給你。當(dāng)然坐昙,我們事先留電話了绳匀。
現(xiàn)在,我們?cè)趈s中嘗試下這種“留電話”的方式炸客。
<center> * * * </center>
js是一門非常神奇的語(yǔ)言——它的一個(gè)古怪的特征就是可以把函數(shù)視為其他值疾棵。形象點(diǎn)表示就是,“函數(shù)是一等公民”痹仙。這意味著任何函數(shù)都可以分配給變量或作為參數(shù)傳遞給另一個(gè)函數(shù)是尔。事實(shí)上你已經(jīng)接觸過了:還記得setTimeout,setInterval和各種事件監(jiān)聽器回調(diào)嗎开仰? 它們通過將函數(shù)作為參數(shù)來使用拟枚。
這種特性在異步場(chǎng)景中是基礎(chǔ)
我們可以定義一個(gè)更新我們的UI的函數(shù) - 然后將它傳遞給另一部分的代碼薪铜,在那里它將被調(diào)用。
使用這種機(jī)制恩溅,我們可以將renderCities方法以某種方式傳遞給dataService隔箍。在那里,它將在必要時(shí)被調(diào)用:畢竟脚乡,服務(wù)能準(zhǔn)確地知道何時(shí)應(yīng)該將數(shù)據(jù)傳輸?shù)浇M件蜒滩。
試一試,我們首先在服務(wù)端添加這個(gè)功能奶稠,然后在某個(gè)時(shí)刻調(diào)用它俯艰。
現(xiàn)在,在sidebar那里使用
你知道會(huì)發(fā)生什么么锌订?當(dāng)在加載我們的sidebar代碼時(shí)竹握,它在dataService注冊(cè)了renderCities方法。
在這種情況下瀑志,當(dāng)我們的數(shù)據(jù)發(fā)生更改時(shí)涩搓,dataService就會(huì)調(diào)用此函數(shù)(由于addPlace()的調(diào)用)。
確切地說劈猪,我們的代碼的一部分是事件的SUBSCRIBER昧甘,另一部分是PUBLISHER(服務(wù)方法)。我們已經(jīng)實(shí)現(xiàn)了發(fā)布 - 訂閱模式的最基本形式战得,這是幾乎所有高級(jí)異步概念的基本概念充边。
還有呢?
請(qǐng)注意常侦,我們的代碼浇冰,僅限于一個(gè)監(jiān)聽組件(即,一位訂閱者)聋亡。如果其他方法也用了這個(gè)subscribe方法來傳遞的話肘习,它會(huì)覆蓋掉dataService的changeListener變量,為了解決這個(gè)問題坡倔,我們需要用數(shù)組來存儲(chǔ)監(jiān)聽者漂佩。
現(xiàn)在,我們可以稍微整理一下代碼并編寫一個(gè)函數(shù)來為我們調(diào)用所有的監(jiān)聽者:
這樣我們也可以連接map.js組件罪塔,以便它對(duì)服務(wù)中的所有操作做出正確的反應(yīng):
如果需要傳遞參數(shù)怎么辦投蝉?我們可以使用監(jiān)聽者的參數(shù)直接獲得。像這樣:
然后征堪,可以輕松地在組件中檢索數(shù)據(jù):
這里還有更多的可能性 - 我們可以為不同類型的行為創(chuàng)建不同的主題(或渠道)瘩缆。此外,我們可以提取發(fā)布和訂閱方法到一個(gè)文件并從那里使用它佃蚜。但就目前而言庸娱,還OK啦 - 以下是使用我們剛剛創(chuàng)建的相同代碼的應(yīng)用的簡(jiǎn)短視頻
(譯者注着绊,大家去原文那里看吧)
<center> * * * </center>
(譯者注:接下來的內(nèi)容是作者關(guān)于這個(gè)模式的想法,他說涌韩,那些組件的概念比如RxjS畔柔,雖然它們功能更強(qiáng)大、概念更加地復(fù)雜臣樱,但是基本概念都是上文講過的靶擦。它們搞得太復(fù)雜了而已。并且這個(gè)模式也可以套在其他的地方雇毫。如DOM操作玄捕。另外,本文只是講了最基本的棚放,還有很多地方可以拓展枚粘。比如取消訂閱、事件訂閱等等飘蚯。最后作者還建議我們多點(diǎn)搞優(yōu)秀的源代碼馍迄,down下來用debugger研究源碼。挖掘出它們最基本的思想局骤。多動(dòng)手攀圈、多思考,不要害怕專有名詞峦甩,覺得很高大上赘来、很難理解。其實(shí)就是那么一回事凯傲。有些人搞得太復(fù)雜了犬辰。)
(譯者為什么不翻譯完呢?因?yàn)橄胱x者們自己嘗試去翻譯冰单,最重要的原因幌缝,是因?yàn)樽g者懶。诫欠。狮腿。)
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:
- https://github.com/mroderick/PubSubJS
- https://github.com/Sahadar/pubsub.js
- https://github.com/shystruk/publish-subscribe-js
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/pubsub-demo/
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!