前言
设计模式无论对于前端后端,都是必须理解的一种编码理念,作为前端开发人,其实在日常开发中不知不觉已经采用了设计模式的理念,理解好设计模式,运用设计模式,是一个开发人员质的提升
本文从概念到生活中场景以及编码场景再到示例,解析前端常用9种设计模式,第一步总览设计模式的前传,也就是面向对象概念以及设计原则,然后对每一个设计模式从介绍到UML类图再到场景与示例,最后对设计原则的验证,让各位对设计模式有一个更深入的了解,能够在日常开发中运用
GITHUB: github.com/Jason9708/j…
文章以代码为主,所以阅读本文需要有一定的JavaScript基础(ES6)
面向对象
三要素
- 继承:子类继承父类
- 父类是公共的,不仅仅只服务于一个子类
- 继承可以将公共方法抽离出来,提高复用性,减少冗余
- 封装:数据的权限和保密(
Javascript
,甚至Es6
本身都没有封装这个概念,但Typescript
可实现封装)- 关键字(定义属性/方法)
public
完全开放protected
对子类开放private
对自己开放
- 特点
- 减少耦合,不该暴露的不暴露
- 利于数据,接口的权限管理
- Es不支持,我们可以去约定某种形式( _ 开头)去告诉合作者这是private类型
- 关键字(定义属性/方法)
- 多态:同一接口不同实现
- Js对于多态的应用极少,因为需要结合Java等语言的接口,重写,重载等功能
- 未来Ts的大范围普及,多态的功能也会更加显著
- 特点
- 保持子类的开放性和灵活性
- 面向接口编程(Js使用较少)
Js应用举例
JQuery
就是一个class,而${'p'}
就是JQuery的一个实例
class JQuery {
constructor(selector) {
let slice = Array.prototype.slice
let dom = slice.call(document.querySelectorAll(selector)) // 获取每一个dom元素
let len = dom ? dom.length : 0
for (let i = 0; i < len; i++) {
this[i] = dom[i]
}
this.length = len
this.selector = selector || ''
}
// JQuery自身API
append(node) {
// TODO
}
addClass(name) {
// TODO
}
html(data) {
// TODO
}
// ...
}
window.$ = function(selector) {
return new JQuery
}
使用
var $p = $('p')
$p.append(xxx)
为何要使用面向对象
- 面向对象是为了解决系统的可维护性,可扩展性,可重用性
- 面向对象实现了一种数据的结构化,是为了表述、模拟世间万事
- 面向对象的意义是将平整的数据进行结构化。对于计算机而已,结构化才是最简单的、
- 编程应该做到 简单 & 抽象
好处:
当需求单一,或者简单时我们一步一步操作没有问题,并且效率也挺高。可随着需求的更改,功能的增多,发现于鏊面对每一个步骤很麻烦了,这时就开始思索能不能把步骤和功能进行封装。封装时根据不同的功能,进行不同的封装,功能类似的封装在一起。这样结构就清晰了很多。用的时候找到对应的类就可以了。
设计原则
何为设计?
- 按照哪一种思路或者标准来实现功能
- 功能相同,可以有不同的设计方案来实现
- 随着需求的增加,设计的作用才能体现出来
五大设计原则 (SOLID)
❗ 五大设计原则可以说是设计模式的基础,学好设计模式应该先理解五大设计原则
S - 单一职责原则
- 一个程序只做好一件事
- 如果功能过于复杂就拆分开,每个部分保持独立
O - 开放封闭原则
- 对扩展开放,对修改封闭(重点)
- 增加需求时,尽量做到扩展新代码,而非修改已有代码
L - 李氏置换原则
- 子类能覆盖父类
- 父类能出现的地方子类就能出现
- 由于Js是弱类型,以及继承使用较少,所以在Js中应用较少
I - 接口独立原则
- 保持接口的单一独立,功能统一
- Js中没有接口(TypeScript除外),使用较少
- 类似于单一职责原则,但更关注于接口
D - 依赖导致原则
- 面向接口编程,依赖于抽象而不依赖于具体
- 使用方只关注接口而不关注具体类的实现
- Js中使用较少(没有接口 & 弱类型)
JavaScript中:S O体现较多 L I D体现较少
用 Promise 来说明 S 与 O
// 加载图片
function loadImg(src) {
var promise = new Promise((resolve,reject) => {
var img = document.createElement('img')
img.onload = () => {
resolve(img)
}
img.onerror = () => {
reject('图片加载失败')
}
img.src = src
})
return promise
}
var src = '...'
var result = loadImg(src)
result.then(img => {
console.log('width:',img.width)
return img
}).then(img => {
console.log('height:',img.height)
}).catch( err => {
// 捕获异常
console.log(err)
})
观察上面这个例子,我们可以发现
单一职责原则体现在我们每一个then
只做一件事,如果上面还想再做第三件事,我们就再加一个then
,而不是在第一个或第二个then
中多做一件事
开放封闭原则体现在如果我们有新的需求进来,我们不需要去修改第一第二个then
,而是直接添加一个then
去实现
总结:
单一职责原则:每一个 then 中的逻辑只做好一件事
开放封闭原则:如果新增需求,扩展 then
23种设计模式简介
- 创建型
- 工厂模式(工厂方法模式,抽象工厂模式,建造者模式)
- 单例模式
- 原型模式
- 结构型
- 适配器模式
- 装饰器模式
- 代理模式
- 外观模式
- 桥接模式
- 组合模式
- 享元模式
- 行为型
- 策略模式
- 模板方法模式
- 观察者模式 (Js应用较多)
- 迭代器模式 (Js应用较多)
- 职责链模式
- 命令模式
- 备忘录模式
- 状态模式 (Js应用较多)
- 访问者模式
- 中介者模式
- 解释器模式
工厂模式
介绍
- 将
new
操作单独封装 - 遇到
new
的时候就可以考虑是否使用工厂模式
示例
- 去购买汉堡的时候,直接点餐,取餐,不需要自己亲手做
- 商店封装做汉堡的工作,做好直接给买者
UML类图
代码演示
class Product {
constructor(name) {
this.name = name
}
init() {
console.log('init')
}
fun1() {
console.log('fun1')
}
fun2() {
console.log('fun2')
}
}
class Creator {
create(name) {
return new Product(name)
}
}
// Test
let creator = new Creator()
let product = creator.create('product')
product.init()
product.fun1()
product.fun2()
场景
- Jquery $('div')就是一个工厂
// Product
class JQuery {
constructor(selector) {
let slice = Array.prototype.slice
let dom = slice.call(document.querySelectorAll(selector)) // 获取每一个dom元素
let len = dom ? dom.length : 0
for (let i = 0; i < len; i++) {
this[i] = dom[i]
}
this.length = len
this.selector = selector || ''
}
// JQuery自身API
append(node) {
// TODO
}
addClass(name) {
// TODO
}
html(data) {
// TODO
}
// ...
}
// Creator
window.$ = function(selector) {
return new JQuery
}
- React.createElement
// Product
class Vnode(tag,attrs,children) {
// ...
}
// Creator
React.createElement = function(tag, attrs, children) {
return new Vnode(tag, attrs, children)
}
- vue 异步组件
Vue异步组件有这样的用法
Vue.component('Component',function(resolve,reject){
setTimeout(() => {
resolve({
template:'<div> I am Component </div>'
})
},1000)
})
设计原则验证
工厂模式中构造函数与创建者分离,通过工厂方法将构造函数与用户隔离开,让用户不能去修改构造函数,符合开放封闭原则
单例模式
介绍
- 系统中被唯一使用
- 一个类只能初始化一个实例
示例
- 登陆框(再复杂,也只能有一个)
- 购物车(一个商城app,也是只能有一个购物车)
场景
- JQuery中只有一个 $
// JQuery 只有一个 $
if(window.jQuery != null){
return window.jQuery
}else{
// 初始化...
}
这里的代码实现与我们的单例模式不一样,但思想是一样的(有则返回,无则创建)
- 模拟登录框(登陆框只有一个)
// 使用单例思想
class LoginForm {
constructor() {
this.state = 'hide'
}
show() {
if(this.state === 'show'){
alert('已经显示')
return
}
this.state = 'show'
console.log('登陆框显示成功')
}
hide() {
if(this.state = 'hide'){
alert('已经隐藏')
return
}
this.state = 'hide'
console.log('登录框隐藏成功')
}
}
LoginForm.getInstance = (() => {
let instance
return function() {
if(!instance) {
instance = new LoginForm()
}
return instance
}
})()
- 其他
- 购物车(和登录框类似)
vuex
和redux
中的store
设计原则验证
只实例化唯一的对象,符合单一职责原则
没法具体体现开放封闭原则,但是绝不违背开放封闭原则
适配器模式
介绍
- 旧接口格式与现使用者不兼容
- 中间加一个适配来转换接口
通俗来像就有点像我们的数据线转换器
UML类图
代码演示
class Adaptee {
specificRequest() {
return '标准插头'
}
}
class Target {
constructor() {
this.adaptee = new Adaptee()
}
request() {
let info = this.adaptee.specificRequest
return `${info} - 转换 → 大号插头`
}
}
场景
- 封装旧接口
// 自己封装的ajax,使用方法如下
ajax({
url:'/getData',
type:'Post',
dataType:'json',
data: {
id:'123'
}
}).done(() => {
})
// 但因为历史原因,代码都是
$.ajax({
//...
})
// 即做了一层适配器
var $ = {
ajax: function(options){
return ajax(options)
}
}
- Vue computed
// 官方computed例子
<div id='example'>
<p>Original message: "{{message}}"</p>
<p>Computed reversed message:"{{reversedMessage}}"
</div>
var vm = new Vue({
el: '#example',
data: {
message: 'Hello'
},
computed: {
// 计算属性的 getter
reversedMessage:function() {
// this 执行 vm实例
return this.message.split('').reverse().join('')
}
}
})
从模板我们可以知道 我们需要一个普通的message,还需要一个倒序的message
因此data中的message不足以满足我们的需求,我们需要再做处理(封装旧接口思想)
这里computed就相当于一个适配器
设计原则验证
将旧接口和使用者进行分离,使用者不再操作旧接口,符合开放封闭原则
装饰器模式
介绍
- 为对象添加新功能
- 不改变其原有的结构和功能
与适配器不同,适配器是旧接口已经不能用了,或者说数据,数据格式不能用,所以需要重新设计一个去用,而装饰器模式是原有的功能我们还会继续用,在其基础上再添加功能
UML类图
代码演示
// 被装饰者
class Circle {
draw() {
console.log('画一个圆形')
}
}
// 装饰器
class Decorator {
constructor(circle) {
this.circle = circle
}
setRedBorder(circle) {
console.log('设置红色边框')
}
draw() {
this.circle.draw()
this.setRedBorder(circle)
}
}
// Test
let circle = new Circle()
circle.draw()
let decorator = new Decorator()
decorator.draw()
场景
- ES7装饰器
- 第三方库 core-decorators
ES7 装饰器解析
- 配置环境
npm install babel-plugin-transform-decorators-legacy --save-dev
.babelrc
{
"presets": [
"es2015",
"latest"
],
"plugins": [
"transform-decorators-legacy"
]
}
- 装饰类
// 例1:
// 对Demo类进行修饰
@testDec
class Demo {
// ...
}
function testDec(target) {
target.isDec = true
}
console.log(Demo.isDec) // true
// 可以加参数
// 装饰器
function testDec(isDec) {
return function(target) {
target.isDec = isDec
}
}
// Demo - 被装饰者
@testDec(false)
class Demo {
}
console.log(Demo.isDec) // false
// 例2: mixin示例
function mixins(...list) {
return function(target) {
Object.assign(target.prototype, ...list)
}
}
const Foo = {
foo() {
console.log('foo')
}
}
@mixins(Foo)
class MyClass {
}
let obj = new MyClass()
obj.foo() // 'foo'
- 装饰方法
// 例1:
function readonly(target, name, descriptor) {
// descriptor 属性描述对象 ( Object.defineProperty中会用到),原来的值如下
// {
// value: specifiedFunction,
// enumerable:false,
// configurable: true,
// writable: true
// }
descriptor.writable = false
return descriptor
}
class Person {
constructor() {
this.first = 'A'
this.last = 'B'
}
// 装饰方法
@readonly // 将name属性的writable装饰为false
name() {
return `${this.first} - ${this.last}`
}
}
var p = new Person()
console.log(p.name()) // A - B
// p.name = function() {} // 这里会报错,是因为 name函数被装饰为只读属性
// 例2:
function log(target, name, descriptor) {
var oldValue = descriptor.value
descriptor.value = function() {
console.log(`Calling ${name} with`, arguments)
return oldValue.apply(this,arguments)
}
return descriptor
}
class Math {
// 装饰方法
@log // oldValue → add() 经过装饰之后,会先打印日志,然后执行oldValue,也就是add()
add(a, b) {
return a + b
}
}
const math = new Math()
const result = math.add(2, 4) // 执行add时,会自动打印日志,因为add已经被log装饰器装饰过
console.log(result)
/*
Result:
Calling add with Arguments(2) [2, 4, callee: (...), Symbol(Symbol.iterator): ƒ]
6
*/
core-decorators
- 第三方开源依赖
- 提供常用的装饰器
- 示例
// 首先安装 npm i core-decorators --save
// 开始编码
import { readonly } from 'core-decorators'
class Person {
@readonly
name() {
return 'zhang'
}
}
let p = new Person()
console.log(p.name())
// p.name = function() {} // 这里会报错,是因为 name函数被装饰为只读属性
// deprecate 执行前有警告提示
import { deprecate } from 'core-decorators'
class Person {
@deprecate
facepalm(){}
@deprecate('We stopped facepalming')
facepalmHard(){}
}
设计原则验证
装饰器模式将现有对象和装饰器进行分离,两者是独立存在的,装饰的时候不去修改原有的对象,符合开放封闭原则
代理模式
介绍
- 使用者无权访问目标对象
- 中间加代理,通过代理做授权和控制
UML类图
代码演示
class ReadImg {
constructor(fileName) {
this.fileName = fileName
this.loadFromDisk() // 初始化即从硬盘中加载
}
loadFromDisk() {
console.log('loading...',this.fileName)
}
display() {
console.log('display...',this.fileName)
}
}
// 代理
class ProxyImg {
constructor(fileName) {
this.realImg = new ReadImg(fileName)
}
display() {
this.realImg.display()
}
}
// Test
let proxyImg = new ProxyImg('1.png')
proxyImg.display()
/*
Result:
loading...1.png
display...1.png
*/
场景
- 网页事件代理
<div id='div1'>
<a href='#'>a1</a>
<a href='#'>a2</a>
<a href='#'>a3</a>
<a href='#'>a4</a>
</div>
<script>
var div1 = document.getElementById('div1')
div1.addEventListener('click',function(e) {
var target = e.target
if(target.nodeName === 'A'){
alert(target.innerHtml)
}
})
</script>
- JQuery $.proxy
$('#div1').click(function() {
// this 符合期望
$(this).addClass('red')
})
$('#div1').click(function() {
setTimeout( function() {
// this 不符合期望
$(this).addClass('red')
},1000)
})
// 可以用如下方式解决
$('#div1').click(function() {
var _this = this
setTimeout( function() {
// this 符合期望
$(_this).addClass('red')
},1000)
})
// 也可以通过代理 $.proxy 解决
$('#div1').click(function() {
setTimeout( $.proxy(function() {
// this 符合期望
$(this).addClass('red')
},this),1000)
})
- ES6 Proxy
// 明星 & 经纪人 案例
// 顾客要聘请明星,不能直接与明星接触,而是与明星经纪人接触
// 明星
let star = {
name: '张三',
age: 20,
phone: '13000000000'
}
// 经纪人
let agent = new Proxy( star, {
get: function(target, key){
if(key === 'phone'){
// 返回经纪人自己的手机号
return '10086'
}
if(key === 'price'){
// 明星自己不报价,由经纪人来报价
return 120000
}
return target[key]
},
set: function(target, key, val){
if(key === 'customPrice){
if(val < 100000) {
// 最低 10w
throw new Error('顾客报价太低')
}else {
target[key] = val
return true
}
}
}
})
// Test
console.log(agent.name) // 张三
console.log(agent.age) // 20
console.log(agent.phone) // 10086
console.log(agent.price) // 120000
agent.customPrice = 80000 // 抛错
设计原则验证
代理类和目标类分离,将目标类与使用者隔离开,符合开放封闭原则
代理模式、适配器模式、装饰器模式对比
代理模式 VS 适配器模式
- 适配器模式:提供一个不同的接口,旧的无法使用,需要进行转换(如不同版本的插头)
- 代理模式:提供一模一样的接口(使用者无权使用目标类,通过一个代理,来访问到目标类的效果)
代理模式 VS 装饰器模式
- 装饰器模式:扩展功能,原有功能不变且可直接使用
- 代理模式:显示原有功能,但是经过限制或者阉割之后的(哪些可以访问,哪些不可以访问,有一定限制)
外观模式
介绍
- 为子系统中的一组接口提供了一个高层接口
- 使用者使用这个高层接口
示例
生活中去医院看病,接待员去挂号,门诊,取药
UML类图
场景
function bindEvent(elem, type, selector, fn) {
// 解决必须传4个参数的情况
if(fn == null) {
fn = selector
selector = null
}
// ...
}
// 调用
bindEvent(elem, 'click', '#div1', fn)
bindEvent(elem, 'click', fn)
设计原则验证
- 不符合单一职责原则和开放封闭原则,因此需要谨慎使用,不可滥用
观察者模式
介绍
- 发布 & 订阅
- 一对多
示例
- 点咖啡,点好之后坐等被叫
UML类图
代码演示
// 主题,保存状态,当状态发生变化后,触发所有观察者对象
class Subject {
constructor() {
this.state = 0
this.observers = []
}
getState() {
return this.state
}
// 当state发生改变,触发所有观察者
setState(state) {
this.state = state
this.notifyAllObservers()
}
// 触发观察者函数
notifyAllObservers() {
this.observers.forEach(observer => {
observers.update()
})
}
attach(observer) {
this.observers.push(observer)
}
}
// 观察者
class Observer {
constructor(name, subject) {
this.name = name
this.subject = subject
this.subject.attach(this)
}
update() {
console.log(`${this.name} update, state is ${this.subject.getState()}`)
}
}
// Test
let subject = new Subject()
let observer1 = new Observer('observer1',subject)
let observer2 = new Observer('observer2',subject)
let observer3 = new Observer('observer3',subject)
subject.setState(1)
/*
observer1 update, state is 1
observer2 update, state is 2
observer3 update, state is 3
*/
场景
- 网页事件绑定(先订阅事件,当点击时触发所有订阅好的事件)
<button id='btn1'>btn</button>
<script>
$('#btn1').click(function(){
console.log(1)
})
$('#btn1').click(function(){
console.log(2)
})
$('#btn1').click(function(){
console.log(3)
})
</script>
- Promise(Promise中每一个then函数都可以理解为是观察者,先订阅上,但不会立刻执行,然后当Promise状态变化resolved后,才执行)
var src = '/xxx'
var result = loadImg(src) // loadImg 为一个Promise
result.then(function(img){
console.log('width',img.width)
return img
}).then(function(img) {
console.log('height',img.height)
})
- jQuery callbacks、
var callbacks = $.Callbacks() // 注意大小写
// 添加观察者
callbacks.add(function(info) {
console.log('fn1',info)
})
callbacks.add(function(info) {
console.log('fn2',info)
})
callbacks.add(function(info) {
console.log('fn3',info)
})
callbacks.fire('gogogo') // 所有观察者都会执行
/*
fn1 gogogo
fn2 gogogo
fn3 gogogo
*/
- nodeJs 自定义事件
const EventEmitter = require('events').EventEmitter
const emitter1 = new EventEmitter()
emitter1.on('some', () => {
// 监听 some 事件
console.log('some event is occured 1')
})
emitter1.on('some', () => {
// 监听 some 事件
console.log('some event is occured 2')
})
// 触发 some 事件
emitter1.emit('some')
// 继承 EvenEmitter
const EventEmitter = require('event').EventEmitter
// 任何构造函数都可以继承 EvenEmitter 的方法 on emit
class Dog extends EventEmitter {
constructor(name) {
super()
this.name = name
}
}
var simon = new Dog('simon')
simon.on('bark',function() {
console.log(this.name, 'barked')
})
setInterval( () => {
simon.emit('bark')
},1000)
其他场景
- nodeJs 处理http请求;多进程通讯
- vue / react 组件生命周期触发
- vue watch (当监听的变量发生改变时,触发watch中绑定的事件,因此watch可以看做是观察者)
var vm = new Vue({
el:'#app',
data:{
firstName: 'Foo',
lastName: 'Bar',
fullName: 'Foo Bar'
},
watch: {
firstName: function(val) {
this.fullName = val + ' ' + this.lastName
},
lastName: function(val) {
this.fullName = this.firstName + ' ' + val
}
}
})
- vue 双向绑定
设计原则验证
主题与观察者分离,不是主动触发而是被动监听,两者解耦,符合开放封闭原则
迭代器模式
介绍
- 顺序访问一个集合
- 使用者无需知道集合的内部结构(封装)
示例
// Jquery的一个例子
<p>jquery each</p>
<p>jquery each</p>
<p>jquery each</p>
<script>
var arr = [1,2,3]
var nodeList = document.getElementsByTagName('p')
var $p = $('p')
// 要对这三个变量进行遍历,需要写三个遍历方法
// 第一 遍历arr
arr.forEach(function(item){
console.log(item)
})
// 第二 遍历 nodeList
var i, length = nodeList.length
for(i = 0; i < length; i++) {
console.log(nodeList[i])
}
// 第三 遍历jquery对象
$p.each(function(key, p) {
console.log(key, p)
})
</script>
// 写一个函数 能同时兼任这3种遍历方法
function each(data){
// 既能遍历arr 又能遍历nodeList和Jquery对象
var $data = $(data) // 生成迭代器
$data.each(function(key, val) {
console.log(key, val)
})
}
each(arr)
each(nodeList)
each($p)
// 顺序遍历有序集合
// 使用者不必知道集合的内部结构
UML类图
代码演示
class Iterator {
constructor(container) {
this.list = container.list
this.index = 0
}
next() {
if(this.hasNext()) {
return this.list[this.index++]
}
}
hasNext() {
if(this.index >= this.list.length) {
return false
}
return true
}
}
class Container {
constructor(list) {
this.list = list
}
// 生成迭代器
getIterator() {
return new Iterator(this)
}
}
var arr = [1,2,3,4,5,6]
let container = new Container(arr)
let iterator = container.getIterator()
while(iterator.hasNext()){
console.log(iterator.next())
}
/*
1
2
3
4
5
6
*/
场景
- jQuery each
function each(data) {
var $data = $(data)
$data.each(function(key, p) {
console.log(key, p)
})
}
- ES6 Iterator
- ES6语法中,有序集合的数据类型已经有很多
- Array Map Set String TypedArray arguments NodeList
- 需要有一个统一的遍历接口来遍历所有数据类型
- 注意:object不是有序集合,可以用Map代替
- 以上数据类型,都有[Symbol.iterator]属性shun
- 属性值是函数,执行函数返回一个迭代器
- 这个迭代器就有next方法可顺序迭代子元素
- 可运行Array.prototype[Symbol.iterator]来测试
function each(data) {
// 生成遍历器
let iterator = data[Symbol.iterator]()
// console.log(iterator.next()) // 有数据时返回 { value:1,done: false }
// console.log(iterator.next()) // 没有数据时返回 { value:undefined, done: true }
let item = { done:false }
while(!item.done) {
item = iterator.next()
if(!item.done) {
console.log(item.value)
}
}
}
// Test
let arr = [1,2,3,4]
let nodeList = document.getElementByTagName('p')
let m = new Map()
m.set('a',100)
m.set('b',200)
each(arr)
each(nodeList)
each(m)
// 'Symbol.iterator' 并不是人人都知道
// 也不是每个人都需要封装一个each方法
// 因此有了 'for...of' 语法 有[Symbol。iterator]才能执行 for...of
function each(data) {
for(let item of data) {
console.log(item)
}
}
each(arr)
each(nodeList)
each(m)
设计原则验证
迭代器对象与目标对象分离,即迭代器将使用者与目标对象隔离开,符合了开放封闭的原则
状态模式
介绍
- 一个对象有状态变化
- 每次状态变化都会触发一个逻辑
- 不能总是用if...else来控制
示例
- 交通信号灯不同颜色的变化
代码演示
// 状态( 红灯、绿灯、黄灯 )
class State {
constructor(color) {
this.color = color
}
// 设置状态
handle(context) {
console.log(`turn to ${this.color} light`)
context.setState(this)
}
}
// 主体
class Context {
constructor() {
this.state = null
}
// 获取状态
getState() {
return this.state
}
setState() {
this.state = state
}
}
let context = new Context()
let green = new State('green')
let yellow = new State('yellow')
let red = new State('red')
// 绿灯亮了
green.handle(context)
console.log(context.getState())
yellow.handle(context)
console.log(context.getState())
red.handle(context)
console.log(context.getState())
/*
turn to green light
State{ color: "green" }
turn to yellow light
State{ color: "yellow" }
turn to red light
State{ color: "red" }
*/
场景
- 有限状态机
- 有限个状态,以及在这些状态之间的变化
- 如交通信号灯
- 例子:开源库 javascript-state-machine
// '收藏'和'取消'
// 状态机模型
var machine = new StateMachine({
init: '收藏', // 初始状态,待收藏
transitions: [
{
name: 'doStore',
from: '收藏',
to: '取消收藏'
},
{
name: 'deleteStore',
from: '取消收藏',
to: '收藏'
}
],
methods: {
// 执行收藏
onDoStore: function(){
alert('收藏成功')
updateText()
}
// 取消收藏
onDeleteStore: function(){
alert('已取消收藏')
updateText()
}
}
})
var $btn = $('#btn')
// 监听点击事件
$btn.click(function(){
if(machine.is('收藏')){
machine.doStore()
}else{
machine.deleteStore()
}
})
// 更新文案
function updateText() {
$btn.text(machine.state)
}
// 初始化文案
updateText()
- 写一个简单的Promise
- Promise就是一个有限状态机
- Promise三种状态:pending、fullfilled、rejected
- pending -> fullfilled 或者 pending -> rejected
- Promise 不能逆向变化
// 定义状态机
var machine = new StateMachine({
init: 'pending',
transitions: [
{
name: 'resolve',
from: 'pending',
to: 'fullfilled'
},
{
name: 'reject',
from: 'pending',
to: 'rejected'
}
],
methods: {
// 成功
onResolve: function(state, data) {
// 参数:state - 当前状态; data - machine.resolve(xxx)执行时参入的参数
data.successList.forEach( fn => fn() )
},
onReject: function(state, data) {
// 参数:state - 当前状态; data - machine.resolve(xxx)执行时参入的参数
data.failList.forEach( fn => fn() )
}
}
})
// 定义Promise
class MyPromise {
constructor(fn) {
this.successList = []
this.failList = []
fn( () => {
// resolve函数
machine.resolve(this)
}, () => {
// reject函数
machine.reject(this)
})
}
then(successFn,failFn) {
this.successList.push(successFn)
this.failList.push(failFn)
}
}
设计原则验证
将状态对象和主题对象分离,状态的变化逻辑单独处理,符合开放封闭原则
总结
- 设计模式一共有23种,本文介绍了前端常用的9种设计模式
- 从生活上的事物作为例子,可以更好的理解设计模式的概念
- 设计模式其实在我们的日常开发的经常出现,可以有的人在不知不觉中已经运用。
- 将设计模式融入我们的日常代码风格中,可以让我们的编码更加优雅,减少冗余也提高复用性