最近的工作學習中接觸到了釋出-訂閱模式。該思想程式設計中的應用也是很廣泛的, 例如在 Vue
中也大量使用了該設計模式,所以會結合Vue的原始碼和大家談談自己粗淺的理解.
釋出訂閱模式主要包含哪些內容呢?
- 釋出函式,釋出的時候執行相應的回撥
- 訂閱函式,新增訂閱者,傳入釋出時要執行的函式,可能會攜額外引數
- 一個快取訂閱者以及訂閱者的回撥函式的列表
- 取消訂閱(需要分情況討論)
這麼看下來,其實就像 JavaScript
中的事件模型,我們在DOM節點上繫結事件函式,觸發的時候執行就是應用了釋出-訂閱模式.
我們先按照上面的內容自己實現一個 Observer
物件如下:
//用於儲存訂閱的事件名稱以及回撥函式列表的鍵值對
function Observer() {
this.cache = {}
}
//key:訂閱訊息的型別的標識(名稱),fn收到訊息之後執行的回撥函式
Observer.prototype.on = function (key,fn) {
if(!this.cache[key]){
this.cache[key]=[]
}
this.cache[key].push(fn)
}
//arguments 是釋出訊息時候攜帶的引數陣列
Observer.prototype.emit = function (key) {
if(this.cache[key]&&this.cache[key].length>0){
var fns = this.cache[key]
}
for(let i=0;i<fns.length;i++){
Array.prototype.shift.call(arguments)
fns[i].apply(this,arguments)
}
}
// remove 的時候需要注意,如果你直接傳入一個匿名函式fn,那麼你在remove的時候是無法找到這個函式並且把它移除的,變通方式是傳入一個
//指向該函式的指標,而 訂閱的時候存入的也是這個指標
Observer.prototype.remove = function (key,fn) {
let fns = this.cache[key]
if(!fns||fns.length===0){
return
}
//如果沒有傳入fn,那麼就是取消所有該事件的訂閱
if(!fn){
fns=[]
}else {
fns.forEach((item,index)=>{
if(item===fn){
fns.splice(index,1)
}
})
}
}
//example
var obj = new Observer()
obj.on(`hello`,function (a,b) {
console.log(a,b)
})
obj.emit(`hello`,1,2)
//取消訂閱事件的回撥必須是具名函式
obj.on(`test`,fn1 =function () {
console.log(`fn1`)
})
obj.on(`test`,fn2 = function () {
console.log(`fn2`)
})
obj.remove(`test`,fn1)
obj.emit(`test`)
複製程式碼
為什麼會使用釋出訂閱模式呢? 它的優點在於:
- 實現時間上的解耦(元件,模組之間的非同步通訊)
- 物件之間的解耦,交由釋出訂閱的物件管理物件之間的耦合關係.
釋出-訂閱模式在 Vue
中的應用
Vue
的例項方法中的應用:(當前版本:2.5.16)
// vm.$on
export function eventsMixin (Vue: Class<Component>) {
const hookRE = /^hook:/
//引數型別為字串或者字串組成的陣列
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
const vm: Component = this
// 傳入型別為陣列
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
this.$on(event[i], fn)
//遞迴併傳入相應的回撥
}
} else {
//
(vm._events[event] || (vm._events[event] = [])).push(fn)
// optimize hook:event cost by using a boolean flag marked at registration
// instead of a hash lookup
if (hookRE.test(event)) {
vm._hasHookEvent = true
}
}
return vm
}
// vm.$emit
Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this
if (process.env.NODE_ENV !== `production`) {
const lowerCaseEvent = event.toLowerCase()
if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
tip(
`Event "${lowerCaseEvent}" is emitted in component ` +
`${formatComponentName(vm)} but the handler is registered for "${event}". ` +
`Note that HTML attributes are case-insensitive and you cannot use ` +
`v-on to listen to camelCase events when using in-DOM templates. ` +
`You should probably use "${hyphenate(event)}" instead of "${event}".`
)
}
}
let cbs = vm._events[event]
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs
const args = toArray(arguments, 1)
for (let i = 0, l = cbs.length; i < l; i++) {
try {
cbs[i].apply(vm, args)// 執行之前傳入的回撥
} catch (e) {
handleError(e, vm, `event handler for "${event}"`)
}
}
}
return vm
}
複製程式碼
Vue
中還實現了vm.$once
(監聽一次);以及vm.$off
(取消訂閱) ,大家可以在同一檔案中看一下是如何實現的.
Vue
資料更新機制中的應用
- observer每個物件的屬性,新增到訂閱者容器Dependency(Dep)中,當資料發生變化的時候發出notice通知。
- Watcher:某個屬性資料的監聽者/訂閱者,一旦資料有變化,它會通知指令(directive)重新編譯模板並渲染UI
- 部分原始碼如下: 原始碼傳送門-observer
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that has this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, `__ob__`, this)
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
}
/**
* Walk through each property and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
// 屬性為物件的時候,observe 物件的屬性
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
複製程式碼
- Dep物件: 訂閱者容器,負責維護watcher原始碼傳送門
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = [] //儲存訂閱者
}
// 新增watcher
addSub (sub: Watcher) {
this.subs.push(sub)
}
// 移除
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
// 變更通知
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
複製程式碼
工作中小應用舉例
- 場景: 基於wepy的小程式. 由於專案本身不是足夠的複雜到要使用提供的
redux
進行狀態管理.但是在不同的元件(不限於父子元件)之間,存在相關聯的非同步操作.所以在wepy物件上掛載了一個本文最開始實現的Observer物件.作為部分元件之間通訊的匯流排機制:
wepy.$bus = new Observer()
// 然後就可以在不同的模組和元件中訂閱和釋出訊息了
複製程式碼
要注意的點
當然,釋出-訂閱模式也是有缺點的.
- 建立訂閱者本身會消耗記憶體,訂閱訊息後,也許,永遠也不會有釋出,而訂閱者始終存在記憶體中.
- 物件之間解耦的同時,他們的關係也會被深埋在程式碼背後,這會造成一定的維護成本.
當然設計模式的存在是幫助我們解決特定場景的問題的,學會在正確的場景中使用才是最重要的.
廣而告之
本文釋出於薄荷前端週刊,歡迎Watch & Star ★,轉載請註明出處。