Vue中的發布訂閱模式分析
模塊:instanceEventEmiiter.ts(在下方有簡單實現和解析)
在Vue3中,已經取消了對這個模塊的引用,故而不再支持 $on、$off、$once相關的方法,不過還是可以對進行學習和借鑒,運用到工作中。
-
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'))
-
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)) } }
成功之前我們要做應該做的事情,成功之后我們才可以做喜歡做的事情