[译]为何前端初学者必需要明白发布订阅模式

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!

相关文章
相关标签/搜索