Vue中的發布訂閱分析(Vue2/3中的 emit 實現)


Vue中的發布訂閱模式分析

模塊:instanceEventEmiiter.ts(在下方有簡單實現和解析)

在Vue3中,已經取消了對這個模塊的引用,故而不再支持 $on、$off、$once相關的方法,不過還是可以對進行學習和借鑒,運用到工作中。

  1. Vue3中的簡單實現

    Vue3中 emit 的實現相對 Vue2 來說更加簡單一些了,他是通過 h函數 的第二個參數來實現的

    實現 Child 組件

    const { createApp, h } = Vue
    // 創建一個子組件
    const Child = {
      setup(props, ctx) {
        return {
          buttonClick() {
            // 源碼中掛載 emit 的函數:createComponentInstance
            // 掛載代碼:instance.emit = emit.bind(null, instance)
            // emit函數接受3個參數: emit(instance, event, ...rawArgs)
            // 其中 instance 在掛載到 instance 的時候默認傳入了
            // ctx.emit('test') 相當於  ctx.emit('當前Vue實例', 'test', 123)
            // emit函數中會對 on開頭、 Once 結尾等關鍵字符串進行解析
            // 然后從 instance.vnode.props 中去獲取對應的回調函數
            // 然后執行 回調函數,此次發布訂閱流程也就完成了
              
            // 執行 emit 方法發布一個 test 事件
            ctx.emit('test', 123)
          }
        }
      },
      render(e) {
        return h('button', {
          onClick: this.buttonClick
        }, '派發Emit')
      }
    }
    

    實現App組件

    const App = {
      render(e) {
        // h函數返回一個 vnode
        // 當 h 函數的第二個參數是一個 Object,並且不是一個 VNode 時
        // vnode.props 就是 h 函數的第二個參數值
        return h(
          'div', {}, [
            // 引用 Child 組件,並且傳入 onTest 方法
            h(Child, {
              // 監聽 test 事件 vnode.props = { onText: Funtion }
              onTest(e) {
                console.log('emit test event!', e)
              }
            })
          ]
        ) 
      }
    }
    
    createApp(App).mount(document.getElementById('emitApp'))
    
  2. instanceEventEmiiter 實現代碼(簡易 JS 版本)

    · 其中像 WeakMap對象、getRegistry方法的運用,及對與 eventName 為數組的處理邏輯還是值得學習的

    · 在工作中可以在 Vue3 的項目中實現一個 Emiiter 模塊,代替 Vue2中的 bus 實現,用於全局事件通訊

    // 發布訂閱倉庫
    // 數據結構類似: { Instance, Object<[event: string]: Function[] | undefined> }
    const eventRegistryMap = new WeakMap()
    
    /**
     * 根據 instance 進行事件注冊
     * @param {*} instance 當前 Vue 實例對象
     * @returns 當前事件對象s
     */
    function getRegistry(instance) {
      // 從 map 中獲取事件對象
      let events = eventRegistryMap.get(instance)
    
      if(!events) {
        // 若沒有注冊過,則進行新注冊
        eventRegistryMap.set(instance, (events = Object.create(null)))
      }
    
      // 返回 實例的事件對象
      return events
    }
    
    /**
     * 添加訂閱
     * @param {*} instance 實例
     * @param {*} eventName 事件名稱  string | string[]
     * @param {*} fn 回調函數 function
     */
    function on(instance, eventName, fn) {
      if(Array.isArray(eventName)) {
        // 處理 eventName 為 數組的情況
        //(如:this.$on(['tab:click', 'tab:change'], () => {}))
        eventName.forEach(c => on(instance, c, fn))
      } else{
        // 獲取 instance實例 的事件對象的
        let events = getRegistry(instance)
        // 將回調函數添加到事件對象的 回調函數 列表中
        (events[eventName] || (events[eventName] = [])).push(fn)
      }
    }
    
    /**
     * 發布訂閱
     * @param {*} instance 實例
     * @param {*} eventName 事件名稱  string | string[]
     * @param {*} args 回調函數 function
     */
    function emit(instance, eventName, args) {
      // 獲取 instance實例 中事件名稱為 eventName 的回調函數列表
      const cbs = getRegistry(instance)[eventName]
      
      if(cbs) {
        // 在 Vue 源碼中,這里是通過 callWithAsyncErrorHandling 函數統一進行執行的
        // 目的是為了統一收集回調函數中的異常
    
        // 執行回調函數(這里我們直接調用了)
        cbs.forEach(fn => {
          fn.apply(instance.proxy, args)
        })
      }
    }
    
    /**
     * 添加訂閱(只觸發一次)
     * @param {*} instance 實例
     * @param {*} eventName 事件名稱  string | string[] 
     * @param {*} fn  回調函數 function
     * @returns 
     */
    function once(instance, eventName, fn) {
      // 由於只需要執行一次,這里對 回調 函數進行了包裝
      const wrapped = (...args) => {
        // 執行回調是我們需要吧他刪除掉
        off(instance, eventName, fn)
        // 執行回調函數
        fn.call(instance.proxy, ...args)
      }
    
      // 在包裝函數上增加一個 fn屬性,用於刪除這個回調函數時做匹配
      wrapped.fn = fn
      // 添加到 訂閱 列表中
      on(instance, eventName, wrapped)
      return instance.proxy
    }
    
    /**
     * 移除訂閱事件
     * @param {*} instance 實例
     * @param {*} eventName 事件名稱  string | string[]
     * @param {*} fn 回調函數 function
     * @returns 
     */
    function off(instance, eventName, fn) {
      if(Array.isArray(eventName)) {
        // 處理 eventName 為 數組的情況
        eventName.forEach(c => off(instance, c, fn))
      } else {
        const vm = instance.proxy
        const events = getRegistry(instance)
        const cbs = events[eventName]
        // 事件 訂閱列表 為空時提前處理
        if(!cbs) {
          return vm
        }
        
        // 沒有傳入回調函數,直接將 事件訂閱列表 清空即可
        if(!fn) {
          events[eventName] = undefined
          return vm
        }
        
        // 過濾掉需要清除的回調,重新賦值給這個事件的 訂閱列表
        events[eventName] = cbs.filter(c => !(c === fn))
      }
    }
    

成功之前我們要做應該做的事情,成功之后我們才可以做喜歡做的事情


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM