項目背景
普通的釋出訂閱方法在這裡就不進行解釋了,相信百度一下有一堆。
在我們自己的小程式中,很早之前就使用了釋出訂閱模式來管理城市和登入态的切換,但是在小程式中會存在非常一些問題
- 頁面登出後訂閱事件不會銷毀
- 使用my.reLaunch或my.switchTab跳轉會清空頁面棧,重新進入帶有訂閱事件的頁面緩存清單會再push一次訂閱事件,造成一次釋出多次訂閱的bug
- 想要手動銷毀訂閱事件必須在注冊訂閱事件時使用具名函數,然後在onUnload中銷毀
舉個最簡單的例子,我們在A頁面的切換了城市,B頁面接收到城市切換後觸發回調
// A頁面
click() {
app.broadcast.fire('cityChange', cityId)
}
複制
// B頁面
onLoad() {
app.broadcast.on('cityChange', this.cb)
},
// 訂閱回調
cb() {
// ...do something
},
// 登出
onUnload() {
app.broadcast.off(this.cb)
}
複制
為了解決上述問題,對釋出訂閱做了改造,實作以下效果
- 訂閱事件可以使用匿名函數
- 頁面登出自動銷毀訂閱事件
實作一個簡單的釋出訂閱
// broadcast.js
class Emitter{
constructor() {
// 存儲所有訂閱的事件
this.eventMap = new Map()
}
on(name, callback) {
// 初始化
if(!this.eventMap.has(name)) {
this.eventMap.set(name, [])
}
let callbackList = this.eventMap.get(name)
callbackList.push(callback)
}
fire(name, data) {
const callbackList = this.eventMap.get(name)
if(Array.isArray(callbackList)) {
callbackList.forEach(cb => {
typeof cb === 'function' && cb(data)
})
}
}
off(name, callback) {
if(this.eventMap.has(name)) {
let callbackList = this.eventMap.get(name).filter(item => item !== callback)
this.eventMap.set(name, callbackList)
}
}
}
const $event = new Emitter()
export.default = $event
複制
注意,在支付寶小程式内一定要将這個$event挂載在app上,不然在分包内使用釋出訂閱會存在問題,是以後面的demo我們都使用app.broadcast
實作訂閱時使用匿名函數
首先我們想得到的目标是可以使用匿名函數,并且能手動銷毀。
因為使用的是匿名函數,頁面銷毀時無法通過循環判斷匿名函數是否相等來銷毀,是以為了找到對應的匿名函數并且銷毀掉,我們在訂閱的時候直接return出關閉的方法,調用方式如下
onLoad() {
this.offCb = app.broadcast.on('cityChange', () => {
//...do something
})
},
onUnload() {
this.offCb()
}
複制
是以我們改造一下on的代碼,return出銷毀事件
on(name, callback) {
if(!this.eventMap.has(name)) {
this.eventMap.set(name, [])
}
let callbackList = this.eventMap.get(name)
callbackList.push(callback)
// 傳回一個關閉的函數,callback === callback
return () => this.off(name, callback)
}
複制
完成了這一步,但是我們還需要在頁面解除安裝的生命周期裡手動銷毀,這也太麻煩了吧,而且我們小程式裡多處用了這個釋出訂閱,改動量太多,而且後續開發也需要開發者們自己銷毀。是以我們接着改造,讓頁面銷毀時自動銷毀該頁面的所有訂閱事件
實作頁面解除安裝自動銷毀
想要自動銷毀頁面的訂閱事件,那麼必須知道目前頁面有多少個訂閱事件,并且頁面解除安裝時一一銷毀。
根據如上話述我們理想中擷取到的資料如下
{
'pages/index/index': [this.offCbA, this.offCbB, ...]
}
複制
根據這個資料,可以想到每次訂閱的時候,我們把頁面和訂閱事件return出的銷毀事件關聯起來,這時就可以做一層簡單的攔截,統一處理
// 重新建立一個執行個體對訂閱方法做一層攔截,得到如上資料
class Broadcast{
on(name, callback) {
const stopHandle = $event.on(name, callback)
// 存儲解除安裝方法到對應執行個體上
markListenHandle(stopHandle)
return stopHandle
}
fire(name, callback) {
return $event.fire(name, callback)
}
off(name, callback) {
return $event.off(name, callback)
}
}
export.default = new Broadcast()
複制
接下來讓我們關聯頁面與銷毀事件
第一步先擷取頁面路由
function markListenHandle(stopHandle) {
let currentPage
// 支付寶路由可能擷取失敗,是以需要做一層catch
try{
const routers = getCurrentPages() || []
currentPage = Array.isArray(routers) && routers[routers.length - 1] || ''
}catch(e) {
console.log(e)
}
如果擷取失敗了,也不去自動銷毀訂閱,不影響主流程
if(!currentPage) {
return
}
}
複制
第二步關聯頁面與銷毀事件
// 存儲執行個體對應的銷毀方法
const currentPageMap = new Map()
function markListenHandle(stopHandle) {
let currentPage
// 支付寶路由可能擷取失敗,是以需要做一層catch
try{
const routers = getCurrentPages() || []
currentPage = Array.isArray(routers) && routers[routers.length - 1] || ''
}catch(e) {
console.log(e)
}
如果擷取失敗了,也不去自動銷毀訂閱,不影響主流程
if(!currentPage) {
return
}
const list = currentPageMap.get(currentPage) || currentPageMap.set(currentPage, []).get(currentPage)
list.push(stopHandle)
}
複制
最後一步,劫持頁面解除安裝生命周期,頁面解除安裝時自動銷毀目前頁面下所有訂閱事件
// 存儲執行個體對應的解除安裝方法
const currentPageMap = new Map()
// 存儲執行個體頁面
const markOnUnmounted = new Set()
function markListenHandle(stopHandle) {
let currentPage
try{
const routers = getCurrentPages() || []
currentPage = Array.isArray(routers) && routers[routers.length - 1] || ''
}catch(e) {
console.log(e)
}
if(!currentPage) {
return
}
const list = currentPageMap.get(currentPage) || currentPageMap.set(currentPage, []).get(currentPage)
list.push(stopHandle)
if(!markOnUnmounted.has(currentPage)) {
markOnUnmounted.add(currentPage)
// 劫持頁面上的onUnload方法
const onUnload = currentPage.onUnload
// 重寫onUnload
currentPage.onUnload = function() {
onUnload.apply(this, arguments)
// 清空目前頁面所有的on
const stopHandleList = currentPageMap.get(currentPage)
stopHandleList.forEach(val => val())
markOnUnmounted.delete(currentPage)
currentPage = null
}
}
}
複制
好啦,完成了,然後我們就可以在頁面上愉快的使用匿名函數,并且不用關心他的銷毀
onLoad() {
app.broadcast.on('cityChange', () => {
// ...do something
})
}
複制