乐趣区

关于javascript:小程序中发布订阅的最优解

我的项目背景

一般的公布订阅办法在这里就不进行解释了,置信百度一下有一堆。
在咱们本人的小程序中,很早之前就应用了公布订阅模式来治理城市和登录态的切换,然而在小程序中会存在十分一些问题

  1. 页面登记后订阅事件不会销毁
  2. 应用 my.reLaunch 或 my.switchTab 跳转会清空页面栈,从新进入带有订阅事件的页面缓存列表会再 push 一次订阅事件,造成一次公布屡次订阅的 bug
  3. 想要手动销毁订阅事件必须在注册订阅事件时应用具名函数,而后在 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)
}

为了解决上述问题,对公布订阅做了革新,实现以下成果

  1. 订阅事件能够应用匿名函数
  2. 页面登记主动销毁订阅事件

开发

实现一个简略的公布订阅

// 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})
}

全副代码链接:https://github.com/chenerhong/alipayEventBus

参考文献

https://github.com/tangdaohai/vue-happy-bus
https://github.com/tangdaohai/happy-event-bus

退出移动版