Optimal solution of publish and subscribe in applet

Time:2021-9-5

Project background

Ordinary publish and subscribe methods are not explained here. I believe Baidu has a pile of them.
In our own applet, we used publish subscribe mode to manage the switching between city and login status long ago, but there will be some problems in the applet

  1. The subscription event will not be destroyed after the page is logged out
  2. Using my.relaunch or my.switchtab to empty the page stack and re-enter the page cache list with subscription events will push the subscription events again, resulting in the bug of publishing multiple subscriptions at one time
  3. To manually destroy a subscription event, you must use a named function when registering the subscription event, and then destroy it in onunload

For the simplest example, we switch cities on page a, and trigger a callback after receiving the city switch on page B

//Page a
click() {
    app.broadcast.fire('cityChange', cityId)
}
//Page B
onLoad() {
  app.broadcast.on('cityChange', this.cb)
},
//Subscription callback
cb() {
    // ...do something
},
//Write off
onUnload() {
    app.broadcast.off(this.cb)
}

In order to solve the above problems, publish and subscribe are transformed to achieve the following effects

  1. Anonymous functions can be used to subscribe to events
  2. Page logout auto destroy subscription event

development

Implement a simple publish and subscribe

// broadcast.js
class Emitter{
  constructor() {
    //Store events for all subscriptions
    this.eventMap = new Map()
  }
  on(name, callback) {
    //Initialization
    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

Note that in the Alipay applet, you must mount the $event on app, otherwise there will be problems in using the publish and subscribe in the subcontract, so we will use app.broadcast after demo.

Use anonymous functions when implementing subscriptions

First, we want to get the goal that anonymous functions can be used and can be destroyed manually.
Because the anonymous function is used, it is impossible to destroy the page by loop judging whether the anonymous functions are equal. Therefore, in order to find the corresponding anonymous function and destroy it, we directly return the closed method when subscribing. The call method is as follows

onLoad() {
  this.offCb = app.broadcast.on('cityChange', () => {
      //...do something
  })
},
onUnload() {
    this.offCb()
}

So let’s modify the on code and return the destruction event

on(name, callback) {
  if(!this.eventMap.has(name)) {
    this.eventMap.set(name, [])
  }
  let callbackList = this.eventMap.get(name)
  callbackList.push(callback)
  //Returns a closed function, callback = = = callback
  return () => this.off(name, callback)
}

This step has been completed, but we still need to manually destroy it in the life cycle of page unloading, which is too troublesome. Moreover, this publish subscription is used in many places in our applet, and there are too many changes, and the subsequent development also needs to be destroyed by the developers themselves. Therefore, we continue to transform to automatically destroy all subscription events of the page when the page is destroyed

Realize page unloading and automatic destruction

If you want to automatically destroy the subscription events of the page, you must know how many subscription events there are on the current page, and destroy them one by one when the page is unloaded.
According to the above, the data obtained in our ideal are as follows

{
    'pages/index/index': [this.offCbA, this.offCbB, ...]
}

According to this data, we can imagine that every time we subscribe, we associate the page with the destruction event returned by the subscription event. At this time, we can do a simple layer of interception and unified processing

//Re create an instance to intercept the subscription method and get the above data
class Broadcast{
  on(name, callback) {
    const stopHandle = $event.on(name, callback)
    //Store the uninstall method on the corresponding instance
    markListenHandle(stopHandle)
    return stopHandle
  }
  fire(name, callback) {
    return $event.fire(name, callback)
  }
  off(name, callback) {
    return $event.off(name, callback)
  }
}
export.default = new Broadcast()

Next, let’s associate the page with the destruction event
The first step is to obtain the page route

function markListenHandle(stopHandle) {
  let currentPage
  // Alipay routing may fail, so a layer of catch is needed.
  try{
    const routers = getCurrentPages() || []
    currentPage = Array.isArray(routers) && routers[routers.length - 1] || ''
  }catch(e) {
    console.log(e)
  }
  If the acquisition fails, the subscription will not be automatically destroyed, which will not affect the main process
  if(!currentPage) {
    return 
  }
}

Step 2: associate the page with the destruction event

//Destruction method corresponding to storage instance
const currentPageMap = new Map()
function markListenHandle(stopHandle) {
  let currentPage
  // Alipay routing may fail, so a layer of catch is needed.
  try{
    const routers = getCurrentPages() || []
    currentPage = Array.isArray(routers) && routers[routers.length - 1] || ''
  }catch(e) {
    console.log(e)
  }
  If the acquisition fails, the subscription will not be automatically destroyed, which will not affect the main process
  if(!currentPage) {
    return 
  }
  const list = currentPageMap.get(currentPage) || currentPageMap.set(currentPage, []).get(currentPage)
  list.push(stopHandle)
}

The last step is to hijack the page unloading life cycle, and automatically destroy all subscription events under the current page when the page is unloaded

//Unloading method corresponding to storage instance
const currentPageMap = new Map()
//Store instance page
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)
    //Hijack the onunload method on the page
    const onUnload = currentPage.onUnload
    //Override onunload
    currentPage.onUnload = function() {
      onUnload.apply(this, arguments)
      //Clear all on of the current page
      const stopHandleList = currentPageMap.get(currentPage)
      stopHandleList.forEach(val => val())
      markOnUnmounted.delete(currentPage)
      currentPage = null
    }
  }
}

Well, it’s done, and then we can happily use anonymous functions on the page without worrying about their destruction

onLoad() {
    app.broadcast.on('cityChange', () => {
      // ...do something
  })
}

All code links:https://github.com/chenerhong/alipayEventBus

reference

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