設計模式(Design pattern)是一套被反覆使用、多數人知曉的、通過分類編目的、代碼設計經驗的總結。
使用設計模式是爲了可重用代碼、讓代碼更容易被他人理解、保證代碼可靠性。
毫無疑問,設計模式於己於他人於系統都是多贏的;設計模式使代碼編寫真正工程化;設計模式是軟件工程的基石脈絡,如同大廈的結構同樣。javascript
Observer模式也叫觀察者模式、訂閱/發佈模式,是由GoF提出的23種軟件設計模式的一種。
Observer模式是行爲模式之一,它的做用是當一個對象的狀態發生變化時,可以自動通知其餘關聯對象,自動刷新對象狀態,或者說執行對應對象的方法。
這種設計模式能夠大大下降程序模塊之間的耦合度,便於更加靈活的擴展和維護。vue
觀察者模式包含兩種角色:java
核心思想:觀察者只要訂閱了被觀察者的事件,那麼當被觀察者的狀態改變時,被觀察者會主動去通知觀察者,而無需關心觀察者獲得事件後要去作什麼,實際程序中多是執行訂閱者的回調函數。es6
在各類框架中:vue中的$emit
,Angular1.x.x中的$on
、$emit
、$broadcast
,Angular2中的emit
...都是最典型的例子。json
簡單的例子:
假設你是一個班長,要去通知班裏的某些人一些事情,與其一個一個的手動調用觸發的方法(私下裏一個一個通知),不如維護一個列表(建一個羣),這個列表存有你想要調用的對象方法(想要通知的人);
以後每次通知事件的時候只要循環執行這個列表就行了(羣發),而不用關心這個列表裏有誰。設計模式
Javascript中實現一個例子:閉包
// 咱們向某dom文檔訂閱了點擊事件,當點擊發生時,他會執行咱們傳入的callback
element.addEventListener(‘click’, callback2, false)
element.addEventListener(‘click’, callback2, false)複製代碼
咱們用Javascript實現一個簡單的播放器:app
// 一個播放器類
class Player {
constructor() {
// 初始化觀察者列表
this.watchers = {}
// 模擬2秒後發佈一個'play'事件
setTimeout(() => {
this._publish('play', true)
}, 2000)
// 模擬4秒後發佈一個'pause'事件
setTimeout(() => {
this._publish('pause', true)
}, 4000)
}
// 發佈事件
_publish(event, data) {
if (this.watchers[event] && this.watchers[event].length) {
this.watchers[event].forEach(callback => callback.bind(this)(data))
}
}
// 訂閱事件
subscribe(event, callback) {
this.watchers[event] = this.watchers[event] || []
this.watchers[event].push(callback)
}
// 退訂事件
unsubscribe(event = null, callback = null) {
// 若是傳入指定事件函數,則僅退訂此事件函數
if (callback) {
if (this.watchers[event] && this.watchers[event].length) {
this.watchers[event].splice(this.watchers[event].findIndex(cb => Object.is(cb, callback)), 1)
}
// 若是僅傳入事件名稱,則退訂此事件對應的全部的事件函數
} else if (event) {
this.watchers[event] = []
// 若是未傳入任何參數,則退訂全部事件
} else {
this.watchers = {}
}
}
}
// 實例化播放器
const player = new Player()
console.log(player)
// 播放事件回調函數1
const onPlayerPlay1 = function(data) {
console.log('1: Player is play, the `this` context is current player', this, data)
}
// 播放事件回調函數2
const onPlayerPlay2 = data => {
console.log('2: Player is play', data)
}
// 暫停事件回調函數
const onPlayerPause = data => {
console.log('Player is pause', data)
}
// 加載事件回調函數
const onPlayerLoaded = data => {
console.log('Player is loaded', data)
}
// 可訂閱多個不一樣事件
player.subscribe('play', onPlayerPlay1)
player.subscribe('play', onPlayerPlay2)
player.subscribe('pause', onPlayerPause)
player.subscribe('loaded', onPlayerLoaded)
// 能夠退訂指定訂閱事件
player.unsubscribe('play', onPlayerPlay2)
// 退訂指定事件名稱下的全部訂閱事件
player.unsubscribe('play')
// 退訂全部訂閱事件
player.unsubscribe()
// 能夠在外部手動發出事件(真實生產場景中,發佈特性通常爲類內部私有方法)
player._publish('loaded', true)複製代碼
舉個Vue中的例子吧:框架
// 事件發佈者使用'vm.$emit、vm.$dispatch(vue1.0)、vm.$broadcast(vue1.0)發佈事件
// 接受方使用$on方法或組件監聽器訂閱事件,傳遞一個回調函數
vm.$emit(event, […args]) // publish
vm.$on(event, callback) // subscribe
vm.$off([event, callback]) // unsubscribe
// 或者組件中監聽事件
<component @event="callback" />
// 在Vue中不管是$on方法仍是組件監聽事件最終都會轉化爲實例中的監聽器複製代碼
各框架中觀察者模式的實現:
Angularjs(AngularJS 1.x.x)中的實現;
一樣,Vue中使用Object.defineProperty()
實現對數據的雙向綁定,在數據變動時,使用notify
廣播事件,最終一樣執行對應屬性所維護的Watchers
列表進行回調。dom
中介者在程序設計中很是常見,和觀察者模式實現的功能很是類似。
形式上:不像觀察者模式那樣經過調用pub/sub
的形式來實現,而是經過一箇中介者統一來管理。
實質上:觀察者模式經過維護一堆列表來管理對象間的多對多關係,中介者模式經過統一接口來維護一對多關係,且通訊者之間不須要知道彼此之間的關係,只須要約定好API便可。
簡單說:就像一輛汽車的行駛系統,觀察者模式中,你須要知道車內坐了幾我的(維護觀察者列表),當汽車發生到站、停車、開車...這些事件(被訂閱者事件)時,你須要給這個列表中訂閱對應事件的的每一個人進行通知;
在中介者模式中,你只須要在車內發出廣播(到站啦、停車啦、上車啦...請文明乘車尊老愛幼啦...),而不用關心誰在車上,誰要上車誰要下車,他們本身根據廣播作本身要作的事,哪怕他不聽廣播,聽了也不作本身要作的事都無所謂。
中介者模式包含兩種角色:
Javascript中實現一個例子:
// 汽車
class Bus {
constructor() {
// 初始化全部乘客
this.passengers = {}
}
// 發佈廣播
broadcast(passenger, message = passenger) {
// 若是車上有乘客
if (Object.keys(this.passengers).length) {
// 若是是針對某個乘客發的,就單獨給他聽
if (passenger.id && passenger.listen) {
// 乘客他愛聽不聽
if (this.passengers[passenger.id]) {
this.passengers[passenger.id].listen(message)
}
// 否則就廣播給全部乘客
} else {
Object.keys(this.passengers).forEach(passenger => {
if (this.passengers[passenger].listen) {
this.passengers[passenger].listen(message)
}
})
}
}
}
// 乘客上車
aboard(passenger) {
this.passengers[passenger.id] = passenger
}
// 乘客下車
debus(passenger) {
this.passengers[passenger.id] = null
delete this.passengers[passenger.id]
console.log(`乘客${passenger.id}下車`)
}
// 開車
start() {
this.broadcast({ type: 1, content: '前方無障礙,開車!Over'})
}
// 停車
end() {
this.broadcast({ type: 2, content: '老司機翻車,停車!Over'})
}
}
// 乘客
class Passenger {
constructor(id) {
this.id = id
}
// 聽廣播
listen(message) {
console.log(`乘客${this.id}收到消息`, message)
// 乘客發現停車了,因而本身下車
if (Object.is(message.type, 2)) {
this.debus()
}
}
// 下車
debus() {
console.log(`我是乘客${this.id},我如今要下車`, bus)
bus.debus(this)
}
}
// 建立一輛汽車
const bus = new Bus()
// 建立兩個乘客
const passenger1 = new Passenger(1)
const passenger2 = new Passenger(2)
// 倆乘客分別上車
bus.aboard(passenger1)
bus.aboard(passenger2)
// 2秒後開車
setTimeout(bus.start.bind(bus), 2000)
// 3秒時司機發現2號乘客沒買票,2號乘客被驅逐下車
setTimeout(() => {
bus.broadcast(passenger2, { type: 3, content: '同志你好,你沒買票,請下車!' })
bus.debus(passenger2)
}, 3000)
// 4秒後到站停車
setTimeout(bus.end.bind(bus), 3600)
// 6秒後再開車,車上已經沒乘客了
setTimeout(bus.start.bind(bus), 6666)複製代碼
上面例子中(固然,稍微擴展了點哈),Bus即爲中介者對象,乘客爲通訊者,乘客具備一些統一的方法API,Bus只管開車停車發廣播,執行本身的事物,乘客在不斷地接受廣播,根據廣播信息的類型和內容做出本身的判斷,執行事務。
簡單說就是:爲對象提供一種代理以控制對這個對象的訪問。
代理模式使得代理對象控制具體對象的引用。代理幾乎能夠是任何對象:文件,資源,內存中的對象,或者是一些難以複製的東西。
舉個例子: 一個工廠製造商品(目標對象),你能夠給這個工廠設置一個業務代理(代理對象),提供流水線管理,訂單,運貨,淘寶網店等多種行爲能力(擴展屬性)。
固然,裏面還有最關鍵的一點就是,這個代理能把一些騙紙和忽悠都過濾掉,將最真實最直接的訂單給工廠,讓工廠可以專一於生產(控制訪問)。
上面工廠的例子:
// 真實工廠
class Factory {
constructor(count) {
// 工廠默認有1000件產品
this.productions = count || 1000
}
// 生產商品
produce(count) {
// 原則上低於5個工廠是不接單的
this.productions += count
}
// 向外批發
wholesale(count) {
// 原則上低於10個工廠是不批發的
this.productions -= count
}
}
// 代理工廠
class ProxyFactory extends Factory {
// 代理工廠默認第一次合做就從工廠拿100件庫存
constructor(count = 100) {
super(count)
}
// 代理工廠向真實工廠下訂單以前會作一些過濾
produce(count) {
if (count > 5) {
super.produce(count)
} else {
console.log('低於5件不接單')
}
}
wholesale(count) {
if (count > 10) {
super.wholesale(count)
} else {
console.log('低於10件不批發')
}
}
taobao(count) {
// ...
}
logistics() {
// ...
}
}
// 建立一個代理工廠
const proxyFactory = new ProxyFactory()
// 經過代理工廠生產4件商品,被拒絕
proxyFactory.produce(4)
// 經過代理工廠批發20件商品
proxyFactory.wholesale(20)
// 代理工廠的剩餘商品 80
console.log(proxyFactory.productions)複製代碼
Proxy
對象:ES6中Proxy
對象能夠理解爲:在目標對象以前架設一層「攔截」,外界對該對象的訪問,都必須先經過這層攔截,所以提供了一種機制,能夠對外界的訪問進行過濾和改寫。Proxy 這個詞的原意是代理,用在這裏表示由它來「代理」某些操做,能夠譯爲"代理器"。
基本形式:
// 參數分別爲目標對象和代理解析器
var proxy = new Proxy(target, handler)複製代碼
無操做轉發代理:
const target = {}
const p = new Proxy(target, {})
p.a = 3 // 被轉發到代理的操做
console.log(target.a) // 3 操做已經被正確地轉發至目標對象複製代碼
使用錯誤攔截屬性讀取操做:
const handler = {
get(target, property) {
if (property in target) {
return target[property]
} else {
throw new ReferenceError("Property \"" + property + "\" does not exist.")
}
}
}
const p = new Proxy({}, handler)
p.a = 1
p.b = undefined
console.log(p.a, p.b) // 1, undefined
console.log('c' in p, p.c) // Uncaught ReferenceError: Property "c" does not exist.複製代碼
實現一個service客戶端:
function createWebService(baseUrl) {
return new Proxy({}, {
get(target, propKey, receiver) {
return () => httpGet(baseUrl+'/' + propKey)
}
})
}
const serviceA = createWebService('http://example.com/data-a')
const serviceB = createWebService('http://example.com/data-b')
const serviceC = createWebService('http://example.com/data-c')
serviceA.employees().then(json => {
const employees = JSON.parse(json)
// ···
})
serviceB...複製代碼
簡單說:保證一個類只有一個實例,並提供一個訪問它的全局訪問點(調用一個類,任什麼時候候返回的都是同一個實例)。
實現方法:使用一個變量來標誌當前是否已經爲某個類建立過對象,若是建立了,則在下一次獲取該類的實例時,直接返回以前建立的對象,不然就建立一個對象。
類/構造函數實例:
class Singleton {
constructor(name) {
this.name = name
this.instance = null
}
getName() {
alert(this.name)
}
static getInstance(name) {
if (!this.instance) {
this.instance = new Singleton(name)
}
return this.instance
}
}
const instanceA = Singleton.getInstance('seven1')
const instanceB = Singleton.getInstance('seven2')
console.log(instanceA, instanceB)複製代碼
閉包包裝實例:
const SingletonP = (function() {
let instance
return class Singleton {
constructor(name) {
if (instance) {
return instance
} else {
this.init(name)
instance = this
return this
}
}
init(name) {
this.name = name
console.log('已初始化')
}
}
})()
const instanceA = new SingletonP('seven1')
const instanceB = new SingletonP('seven2')
console.log(instanceA, instanceB)複製代碼
惰性包裝實例:
const getSingle = function (fn) {
let result
return function() {
return result || (result = fn.apply(this, arguments))
}
}複製代碼
與建立型模式相似,工廠模式建立對象(視爲工廠裏的產品)時無需指定建立對象的具體類。
工廠模式定義一個用於建立對象的接口,這個接口由子類決定實例化哪個類。該模式使一個類的實例化延遲到了子類。而子類能夠重寫接口方法以便建立的時候指定本身的對象類型。
簡單說:假如咱們想在網頁面裏插入一些元素,而這些元素類型不固定,多是圖片、連接、文本,根據工廠模式的定義,在工廠模式下,工廠函數只需接受咱們要建立的元素的類型,其餘的工廠函數幫咱們處理。
上代碼:
// 文本工廠
class Text {
constructor(text) {
this.text = text
}
insert(where) {
const txt = document.createTextNode(this.text)
where.appendChild(txt)
}
}
// 連接工廠
class Link {
constructor(url) {
this.url = url
}
insert(where) {
const link = document.createElement('a')
link.href = this.url
link.appendChild(document.createTextNode(this.url))
where.appendChild(link)
}
}
// 圖片工廠
class Image {
constructor(url) {
this.url = url
}
insert(where) {
const img = document.createElement('img')
img.src = this.url
where.appendChild(img)
}
}
// DOM工廠
class DomFactory {
constructor(type) {
return new (this[type]())
}
// 各流水線
link() { return Link }
text() { return Text }
image() { return Image }
}
// 建立工廠
const linkFactory = new DomFactory('link')
const textFactory = new DomFactory('text')
linkFactory.url = 'https://surmon.me'
linkFactory.insert(document.body)
textFactory.text = 'HI! I am surmon.'
textFactory.insert(document.body)複製代碼
裝飾者(decorator)模式可以在不改變對象自身的基礎上,在程序運行期間給對像動態的添加職責(方法或屬性)。與繼承相比,裝飾者是一種更輕便靈活的作法。
簡單說:能夠動態的給某個對象添加額外的職責,而不會影響從這個類中派生的其它對象。
實例:假設同事A在window.onload
中指定了一些任務,這個函數由同事A維護,如何在對window.onload
函數不進行任何修改的基礎上,在window.onload
函數執行最後執行本身的任務?
Show me the code:
// 同事A的任務
window.onload = () => {
console.log('window loaded!')
}
// 裝飾者
let _onload= window.onload || function () {}
window.onload = () => {
_onload()
console.log('本身的處理函數')
};複製代碼
如何在全部函數執行先後分別執行指定函數:
// 新添加的函數在舊函數以前執行
Function.prototype.before = function (beforefn) {
let _this = this
return function () {
beforefn.apply(this, arguments)
return _this.apply(this, arguments)
}
}
// 新添加的函數在舊函數以後執行
Function.prototype.after = function(afterfn) {
let _this = this
return function () {
let ret = _this.apply(this, arguments)
afterfn.apply(this, arguments)
return ret
}
}
// 使用
var func = function(param) {
console.log(param)
}
func = func.before(function(param) {
param.name = 'beforename'
})
func({ name: 'func' }) // { name: 'beforename' }複製代碼
不污染Function原型的作法:
// 裝飾器
const before = function(fn, before) {
return function() {
before.apply(this, arguments)
return fn.apply(this, arguments)
}
}
// 普通函數
function a() { console.log('a') }
function b() { console.log('b') }
// 使用裝飾器執行函數
const c = before(a, b)
c() // b a複製代碼
模擬傳統語言的裝飾者:
// 飛機
class Plane {
constructor(name) {
this.name = name
}
// 發射子彈
fire() {
console.log('發射普通子彈')
}
}
// 武器增強版(裝飾類)
class MissileDecorator {
constructor(plane) {
this.plane = plane
this.plane.name = '高級飛機'
}
fire() {
this.plane.fire()
console.log('發射導彈')
}
}
let plane = new Plane('普通飛機')
plane = new MissileDecorator(plane)
plane.fire()
// 發射普通子彈
// 發射導彈複製代碼
使用ES7中的裝飾器:
首先須要搞清楚ES6中Class
語法糖的背後工做原理:
class Cat {
say() {
console.log("meow ~")
}
}
// 實際上當咱們給一個類添加一個屬性的時候,會調用到 Object.defineProperty 這個方法,它會接受三個參數:target 、name 和 descriptor ,上面的Class本質等同於:
function Cat() {}
Object.defineProperty(Cat.prototype, 'say', {
value: function() { console.log("meow ~"); },
enumerable: false,
configurable: true,
writable: true
})複製代碼
ES7裝飾器基本示例:
function isAnimal(target) {
target.isAnimal = true
return target
}
// 裝飾器
@isAnimal
class Cat {
// ...
}
console.log(Cat.isAnimal) // true
// 上面裝飾器代碼基本等同於
Cat = isAnimal(function Cat() { ... })複製代碼
做用於類屬性的裝飾器:
function readonly(target, name, descriptor) {
discriptor.writable = false
return discriptor
}
class Cat {
@readonly
say() {
console.log("meow ~")
}
}
var kitty = new Cat()
kitty.say = function() {
console.log("woof !")
}
kitty.say() // meow ~複製代碼
在類的屬性中定義裝飾器的時候,參數有三個:target
、name
、descriptor
,上面說了,由於裝飾器在做用於屬性的時候,其實是經過Object.defineProperty
來進行擴展和封裝的。
因此在上面的這段代碼中,裝飾器實際的做用形式是這樣的:
let descriptor = {
value: function() {
console.log("meow ~")
},
enumerable: false,
configurable: true,
writable: true
}
descriptor = readonly(Cat.prototype, 'say', descriptor) || descriptor
Object.defineProperty(Cat.prototype, 'say', descriptor)複製代碼
這裏也是 JS 裏裝飾器做用於類和做用於類的屬性的不一樣的地方。
當裝飾器做用於類自己的時候,咱們操做的對象也是這個類自己,而當裝飾器做用於類的某個具體的屬性的時候,咱們操做的對象既不是類自己,也不是類的屬性,而是它的描述符(descriptor),
而描述符裏記錄着咱們對這個屬性的所有信息,因此,咱們能夠對它自由的進行擴展和封裝,最後達到的目的呢,就和以前說過的裝飾器的做用是同樣的。
也能夠直接在 target 上進行擴展和封裝,好比:
function fast(target, name, descriptor) {
target.speed = 20
let run = descriptor.value
descriptor.value = function() {
run()
console.log(`speed ${this.speed}`)
}
return descriptor;
}
class Rabbit {
@fast
run() {
console.log("running~")
}
}
var bunny = new Rabbit()
bunny.run()
// running~
// speed 20
console.log(bunny.speed) // 20複製代碼
總結:裝飾器容許你在類和方法定義的時候去註釋或者修改它。裝飾器是一個做用於函數的表達式,它接收三個參數target
、name
和descriptor
,而後可選性的返回被裝飾以後的descriptor
對象。
裝飾者模式和代理模式的區別:
完
內容如有誤差,期待指正修改。
原文地址:surmon.me/article/40