《JavaScript设计模式与开发实践》读书笔记
突然意识到设计模式的重要性,于是找了《JavaScript设计模式与开发实践》PDF来看。总的来说,不是很喜欢。示例都有点老旧,观点不一定正确(没有区分观察者模式和发布订阅模式)。但我可以用其结构,结合现在、系统的梳理一遍前端里用到的设计模式。
单例模式
单例模式的定义是;保证一个类仅有一个实例,并提供一个访问它的全局访问节点
最常见的例子就是toast。之前封装Vue的toast和modal组件时就是个典型例子。toast应该始终只有一个,如果toast多次,始终只toast一个内容。只是替换了文字和重置关闭时间。因为用户不太希望toast n多层内容,导致文字重叠。
model就不是单例。可以叠多层上去。
策略模式
策略模式的定义是:定义一系列的算法,把他们一个个封装起来,并且使他们可以互相替换。
个人理解,策略模式就类似React根据tag类型去做对应操作的方式。如beginWork的mount阶段,根据tag的类型创建不同的fiber。layout阶段commitLayoutEffectOnFiber会根据tag去执行不同的commit操作。
个人开发中也有,全员的海报需求时,合并公文包与内容集的分享弹窗合并小程序码和二维码海报时,重构前是四个接口,合并后是一个接口。四个接口参数都不相同。其实可以先制定好四种策略
,在实际调用中连续调用四个策略即可。
但网上说的,微微有点不一样。
1 | // 客户端代码会选择具体策略并将其传递给上下文。客户端必须知晓策略之间的差 |
不同的策略通过setStrategy注入context,然后执行executeStrategy即可。JS里,setStrategy的参数,可以是个方法,而不是new对象
况且,与其先set一遍strategy再excuteStrategy,还不如直接在条件内excute对应的strategy。所以我理解为,我理解的与网上的是一致的。
代理模式
代理模式就太多了。
ES6的proxy
、debounce
、throttle
。对真正执行操作的方法前加一层代理,可以做到,真正剥离纯逻辑与访问控制。是一个很良好的思想。
其实实际开发中,应该常用代理模式。
如很多方法其实系统已经提供,但业务中不应该直接用。如this.$router.replace、Taro.navigateTo、document.cookie、axios()等。最好业务中应该使用封装过后在调用。一旦之后需要加什么功能,就可以在代理中加,而不是全局替换一遍方法。
遇到一个坑是,工作台的路由跳转一直用的是react-router-dom原生的跳转方法。但是后来需要兼容腾讯云,不得不封装了一个setLink方法,然后手动的一个一个替换过来。
发布订阅与观察者
这™应该是最争议的模式了。到底发布订阅模式与观察者模式是不是一种模式?本书与Refactoring.Guru都认为是一种。但是民间都认为是两种。
不过我个人也认为,是两种。
观察者模式
观察者模式是一对多的关系,最常见的方法是MutationObserver和ResizeObserver
1 | const observer = new MutationObserver(callback); |
observer对象作为观察者,观察targetNode,当targetNode有改变时,做出响应。
发布订阅模式
发布订阅之余观察者多了一个事件中心
的概念。是一个事件对多个订阅者、多个发布者。
发布订阅,最常见的是Vue的eventBus、DOM的事件了(很多事件的概念都如此)。
总结
发布订阅的三个角色能更好的解耦,其实更多情况,发布订阅优于观察者模式。
工厂模式
工厂模式说简单也简单,说复杂也很复杂。
简单说,工厂模式就如其名,给一个工厂函数传入指定类型参数,即可返回期望类型的内容。
工厂模式行为类似类,但不仅限于类。类更多的是创建一个对象,或者根据参数创建一个对象。
工厂模式更多是,根据类型
直接生成一个期望类型的内容。
简单的实现方法
1 | //User类 |
如果说复杂的,工厂模式还分为简单工厂模式
、工厂方法模式
、抽象工厂模式
。后两者还有抽象类
的概念,在JS的摇篮里待久了,不太能理解。。
所幸,前端一般就用到简单工厂模式
。暂时不考虑另外两种。
实际场景
本段末尾贴的链接给得挺好。例子提到的Vue-router根据用户权限动态配置路由。
那么就可以通过传入用户类型
,生产出路由列表,然后动态创建路由即可
命令模式
个人理解,命令模式就是希望把逻辑解耦,然后逻辑通过命令调用。
那么这个模式应该早已深入骨髓。
实际场景
在编写React组件时,往往要绑定事件回调,那么可以这么写:
1 | function Copy(props){ |
而更好的做法是(命令模式)把逻辑与UI分离
1 | function Copy(props){ |
copy可能多个地方会用,如此解耦,就不用多次编写逻辑代码了。
外观模式
外观模式(Facade Pattern)隐藏系统的复杂性,并向客户端提供了一个客户端可以访问系统的接口。这种类型的设计模式属于结构型模式,它向现有的系统添加一个接口,来隐藏系统的复杂性。
个人理解,各种库、sdk的的设计就是外观模式。比如说微信jssdk,没人知道如何与微信浏览器打交道,使用着只管调用sdk提供的api就能完成一系列的功能
组合模式
按照我个人理解,组合模式就是把一系列操作组合成一个树。要进行操作时,只需对数的根节点触发,组合模式则应该遍历树,对每个叶子节点执行操作方法。
就这么简单。。。
模板方法模式
书上描述的模板方法模式是:模板方法模式建议将算法分解为一系列步骤, 然后将这些步骤改写为方法, 最后在 “模板方法” 中依次调用这些方法
《JavaScript设计模式与开发实践》的例子是,煮咖啡与泡茶。
煮咖啡与泡茶都有类似的步骤:煮水、冲泡、倒进杯子、加料。
尽管咖啡与茶有不同的操作,如,加料部分、咖啡是糖和牛奶,茶是柠檬。但是步骤都是一样的。
所以可以把冲泡每一步骤分解成不同的算法。然后再执行方法里,组合这些算法
1 |
|
init方法就是模板方法模式。由子类去实现不同的算法
,init拼接模板执行。
书中还说道,Vue、React的生命周期的钩子,也是模板方法模式。
享元模式
享元模式英文名是flyweight,fly是苍蝇、意思是蝇量级
,也就是轻量级
的意思(不要去记中文名!!!)
refactoringguru网站的定义是:它摒弃了在每个对象中保存所有数据的方式, 通过共享多个对象所共有的相同状态, 让你能在有限的内存容量中载入更多对象
。
中间慢慢推论就不说了,实际应用场景是:XX池
实际场景
比如做一个滚动的日历组件,每一日的小块都是一个对象(或者说组件),实际情况中,页面同时应该只会存在两个月,也就是62个日组件,这样,就可以在加载下一个月的时候,复用被滚出的日组件。内存中始终存一个“日组件池”,这样防止无限滚动时,内存中无限存储日组件。
这其实是个时间换空间的模式。这样一来,加载下一个月时,需要牺牲一定的性能来换取空间。一般是日历这种可能会无限加载的内容会用到享元模式。
职责链模式
refactoringguru的解释是:允许你将请求沿着处理者链进行发送。 收到请求后, 每个处理者均可对请求进行处理, 或将其传递给链上的下个处理者
其实也很好理解,JavaScript中的作用域链、原型链、DOM的事件冒泡就是最经典的职责链模式。
实际场景
手写表单验证时,一般我们会对一个输入进行多重验证。每一重就是链上的一个节点。如果节点能处理,则return false,如果校验成功,则交给链的下一个节点
中介者模式
书与网站都提到了一个例子:机场的塔台与飞机。
机场飞机起落需要所有飞机协同好时间,一次只能起降一辆飞机,否则事故会非常的严重。
如果没有中介者,则需要飞机与附近的所有飞机都进行沟通、协商。而实际上,所有飞机都只与塔台联系,省去了与多个飞机沟通的难题。
塔台则是中介者。
中介者模式使用场景是,如果遇到多个对象,多个对象之间互相都有一些联系。如果某些情况需要改动代码,则牵一发而动全身,要改所有有关联的对象。这就会带来隐患。
多个对象网状的关系这种情况,可以引入一个中介。所有对象都通过中介来沟通,所有需要与其他对象的操作,与所有其他对象对当前对象的造成的改变的方法都放到中介那。中介协调各个对象之间的关系。
现实中,全局store,如Redux、mobx这些应该就是中介的角色
中介模式也会带来一些问题,就是,需要引入一个额外中介对象。而且随着中介逻辑的增多,中介对象也会变得难以维护。
装饰者模式
这个也是深入骨髓的模式了,ES6的装饰器就是最经典的例子
1 | function testable(isTestable) { |
在ES6出来之前也有装饰器模式:
1 | window.onload = function() { |
状态模式
状态模式的定义:允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类
定义有点晦涩难懂。书中用个例子来解释,
假设我们有个台灯,开关可以控制台灯不同亮度:off -> 1 -> 2 ->3 -> 4 -> off
一般情况下,我们需要写一堆if else来判断,什么状态下,需要跳转到什么状态,并且要做什么操作。
利用状态模式改造,需要把每个状态都抽象为一个类,状态类定义点击事件,决定下一步是什么状态。
emmm
这么说,有点像策略模式?确实有点像。
状态模式还可以深入一点,需要理解有限状态机的概念。
有限状态机,大概意思就是,维护一个二维表,分别表示状态之间切换到条件。在状态切换时,从表里找到对应的操作。这样非常清晰明了。(详细了解看阮一峰的文章)
实际场景
一般用在状态比较多,需要写一堆if else的情况,如ajax的状态,文件上传的状态等。暂时没有在生产中用到。
适配器模式
这个也是深入骨髓的模式了。一般用在兼容旧接口
的情况下用到。
原则
单一职责原则(SRP)
就一个类而言,应该有一个引起它变化的原因。如果我们有两个动机去改写一个方法,那么这个方法就具有两个职责。每个职责都是变化的一个轴线,如果一个方法承担太多职责,那么在需求的变迁过程中,需要改写这个方法的可能性就越大,产生bug的可能性就越大。
实际中,方法、类、以及React hooks的依赖
,都应该这样设计,否则方法太大,非常难改
反例:内容营销小程序
最少知识原则(LKP)
最少知识原则说的是一个软件实体
应当尽可能少的与其他实体发生相互作用。这里的软件实体是一个广义的概念,不仅包括对象
,还包括系统、类、模块、函数、变量
等。
其实也与SRP
目的类似,就是软件实体尽量减少与其他实体的关联,也就减少了复杂性、减少需求变更时被修改的概率。从而减少bug的产生。
实际中,方法、模块的引用都应该尽量减少网状的引用。保持树状的引用关系,修改起来比较方便。
反例:全员与获客通相关的页面。全都柔和在一起,分包时没法拆开。
开放-封闭原则
其定义是:软件实体(类、模块、函数)等应该是可以扩展的,但是不可修改。
书中举例:扩展onload
假设onload已经绑定了一个巨型函数,要添加功能的时候,修改这个巨型函数肯定是非常不理智的。正确的做法是,用装饰者模式,灵活的给其添加功能
我个人理解是,一个良好的设计就是:shell命令那种。linux由一堆灵活的命令组成,那么扩展起来,就非常的方便。开发者可以随意组合这些命令以实现强大的功能。
反之,windows,根本没法扩展。。。
分类
创建型模式
创建型模式提供了创建对象的机制, 能够提升已有代码的灵活性和可复用性。
如:单例模式、工厂模式
结构型模式
结构型模式介绍如何将对象和类组装成较大的结构, 并同时保持结构的灵活和高效。
如:适配器模式、组合模式、装饰器模式、代理模式、享元模式等。
行为模式
行为模式负责对象间的高效沟通和职责委派。
如:状态模式、中介者模式、责任链模式、观察则模式、策略模式等。