深入理解我最喜欢的设计模式,以及它为什么会在响应式设计中如此的重要。
很多开发者喜欢往前端框架加入一些神秘的「面纱」,比如React
,它能够直接地看到数据的流向,但这一切跟他们之前所了解的完全不一样。如果不知道这里面的实现,看起来确实比较神奇,就如 Arthur C. Clarke 说的:
任何足够先进的技术都与魔法无异。
不过在了解响应式背后的基本原理后,就会发现这其实没什么神奇的,并且还能够帮助你理解框架本身。
提醒一下,本文不打算模拟实现或精简一个React
框架,来帮助你理解整个框架的工作原理。本文只想谈的是一种被广泛用于前端框架的设计模式。
什么是观察者模式
首先第一件事,你需要理解它本身是一种设计模式,不用担心,一旦你了解它之后,你会发现根本没有神奇。
这种所谓的「行为设计模式」,它负责处理对象的行为,以及在某种特定情况下,对象之间是如何通信的。也就是说,该设计模式是表示当一组对象(观察者)关注到另一个对象(被观察者)的状态变化,以及如何建立起观察者-被观察者之间的关系。
有个关键点,观察者并不需要时刻关注着被观察者对象,它们而是会以订阅的方式,一旦被观察者发生了某些事件后就会通知到观察者。这个细节相当关键,因为如果观察者一直处理关注,就意味着需要有个不断循环的程序来检查这些变化。虽然单个对象下,这无关紧要,但如果扩展到数百个甚至数千个,性能问题就会凸显出来。
假设观察者能够独立运行或处理闲置,而不需要另外作循环检查,那么就能够解决性能上的问题了。
举个例子,假如你是一个上班族,你每天都需要看报纸,此时有两种选择,一种是在订阅报纸后,它每天都会自动送到你家门口,另外一种是,你需要花费时间和精力亲自去拿报纸。
上图不是设计模式的 UML,而是以一个简单的例子来说明 3 个观察者是如何与被观察者进行通信的。
从图的左侧可以看出,每个观察者是如何调用addSubscriber
,以及notifySubscribers
方法是如何通过传递 event 参数,来调用update
方法。虽然你也有可以让观察者直接访问被观察者对象的状态,但我认为这种设计模式则会更加清晰,因为这里的被观察者能够直接展示了变化(即触发了通知)。
可以看到这种模式的背后其实没有什么神奇的,因为这种设计太优雅,以至于会让开发者看起来像是一种「魔法」。
使用 Javascript 实现观察者模式
接下来,我们用代码实现一个例子,在一个for
循环内不断地遍历变量,并且在遍历到某个特定值时,去执行「反应」事件(原文是react
)。
举个例子,假设有一个从 1 到 1000 的for
循环,并且希望在遍历到奇数项时,执行「反应」事件,就像这样的:
现在我们开始写 Javascritpt 代码,不过有一点需要注意,当前 Javascript 没有私有方法和属性,甚至没有虚类和抽象方法,所以我们需要根据实际情况去实现它。
代码实现如下:
1 | const Looper = require('./looper'); |
代码非常简单,Lopper
类负责实现 for 循环,并且run
方法可以运行这个循环,而前面的addObserver
方法则是允许我们加入一些观察者。在每个观察者的内部都实现了WHEN
(什么时候)和HOW
(做什么)的逻辑,在我们这个例子中,观察者就是OddNotifier
。
运行代码后,会得到以下结果:
正如你所见,我们实现了一个「相当啰嗦」的观察者。
接下来,我们来看下观察者OddNotifier
的实现:
1 | const Observer = require('./observer'); |
上面的代码,我们重写了父类Observer
的eventIsRelevant
和reactToEvent
方法。注意,这里我们并没有实现update
方法,因为它在父类已经被实现:
1 | class Observer { |
这里定义了一个update
方法去接收event
参数,如果满足触发条件,那么就会调用reactToEvent
,而具体是什么条件,则是留给每个具体的观察者去定义。
我们注意到Lopper
类是继承了另外一个Subject
类,不过它只关心如何变量变量值,在每次变量中,它都会触发一个新的event
去通知观察者:
1 | const Subject = require('./subject'); |
上面的Lopper
类不会关心如何添加和通知观察者,它只需要在特定时机,调用notifyObservers
方法,在这个例子中,这个时机被定义为「每一次循环产生新的变量值」,需要视具体情况而定。实际上,如果实现逻辑更复杂一些,我们也可以触发多个事件,但是否触发,是根据观察者内部实现确定的。
最后来看下Subject
的实现,也非常简单,它只关心收集(collect)和通知(notifying)观察者:
1 | class Subject { |
是不是很简单?现在我们再深入一些,继续看另外一个例子,特别是React
开发者。
假设有以下一段代码:
1 | let [looper, increaseLooper] = useState(1); |
这里创建了一个hook
,looper
变量的初始值是 1,并且返回了一个用于递增变量值的函数increaseLooper
,代码运行结果:
hook
的实现如下:
1 | function useState(start) { |
increase
的内部实现是更新state
的变化和通知所有的观察者:
1 | //.... |
至此,观察者模式的「面纱」已经被揭开了,我理解hook
的神秘行为只不过是一组预先设置好的观察者。
[本文谢绝转载,谢谢]