結合 Vue 原始碼談談釋出-訂閱模式

薄荷前端發表於2019-03-03

最近的工作學習中接觸到了釋出-訂閱模式。該思想程式設計中的應用也是很廣泛的, 例如在 Vue中也大量使用了該設計模式,所以會結合Vue的原始碼和大家談談自己粗淺的理解.

釋出訂閱模式主要包含哪些內容呢?

  1. 釋出函式,釋出的時候執行相應的回撥
  2. 訂閱函式,新增訂閱者,傳入釋出時要執行的函式,可能會攜額外引數
  3. 一個快取訂閱者以及訂閱者的回撥函式的列表
  4. 取消訂閱(需要分情況討論)

這麼看下來,其實就像 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`)

複製程式碼

為什麼會使用釋出訂閱模式呢? 它的優點在於:

  1. 實現時間上的解耦(元件,模組之間的非同步通訊)
  2. 物件之間的解耦,交由釋出訂閱的物件管理物件之間的耦合關係.

釋出-訂閱模式在 Vue中的應用

  1. 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 (取消訂閱) ,大家可以在同一檔案中看一下是如何實現的.

  1. 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])
    }
  }
}
複製程式碼
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()
    }
  }
}
複製程式碼

工作中小應用舉例

  1. 場景: 基於wepy的小程式. 由於專案本身不是足夠的複雜到要使用提供的 redux進行狀態管理.但是在不同的元件(不限於父子元件)之間,存在相關聯的非同步操作.所以在wepy物件上掛載了一個本文最開始實現的Observer物件.作為部分元件之間通訊的匯流排機制:
wepy.$bus = new Observer()
// 然後就可以在不同的模組和元件中訂閱和釋出訊息了
複製程式碼

要注意的點

當然,釋出-訂閱模式也是有缺點的.

  1. 建立訂閱者本身會消耗記憶體,訂閱訊息後,也許,永遠也不會有釋出,而訂閱者始終存在記憶體中.
  2. 物件之間解耦的同時,他們的關係也會被深埋在程式碼背後,這會造成一定的維護成本.

當然設計模式的存在是幫助我們解決特定場景的問題的,學會在正確的場景中使用才是最重要的.

廣而告之

本文釋出於薄荷前端週刊,歡迎Watch & Star ★,轉載請註明出處。

歡迎討論,點個贊再走吧 。◕‿◕。 ~

相關文章