UzumakiItachi
首页
  • JavaSript
  • Vue

    • Vue2
    • Vue3
  • React

    • React_18
  • WebPack
  • 浏览器相关
  • 工程化相关
  • 工作中遇到的问题以及解决方案
  • Git
  • 面试
  • 学习
  • 心情杂货
  • 实用技巧
  • 友情链接
关于
  • 个人产出
  • 实用工具
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

UzumakiItachi

起风了,唯有努力生存。
首页
  • JavaSript
  • Vue

    • Vue2
    • Vue3
  • React

    • React_18
  • WebPack
  • 浏览器相关
  • 工程化相关
  • 工作中遇到的问题以及解决方案
  • Git
  • 面试
  • 学习
  • 心情杂货
  • 实用技巧
  • 友情链接
关于
  • 个人产出
  • 实用工具
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • WebPack

  • 浏览器相关

  • 工程化相关

  • 工作中遇到的问题以及解决方案

    • 解决浏览器返回页面不刷新的问题
    • 前端如何下载文件流
    • uniapp APP端实现更新最新安装包
    • GitHub Actions Process completed with exit code 128 的解决方案
    • 保留文字输入的空格和换行
    • 缩放适配大屏页
    • 一次性加载n多张图片的性能优化方案
    • NOT-Cool 低代码页面架构思路
    • h5下载vcard快捷保存联系人信息
    • 结合elementui实现的动态主题
    • 解决monorepo场景下子包作为依赖项在开发和生产暴露文件的问题
    • 给npm配置github令牌
    • 移动端电子印章解决方案
    • 小程序canvas绘制海报中遇到的一些坑
    • 超详细的虚拟列表实现过程
    • 小程序实现一个事件中心
      • 前言
      • 思路
      • 使用
      • 场景二:跨页面通信
      • 结语
    • 请求超时后如何优雅的重新请求
    • 超详细的大文件上传实现方案
    • 仿抖音短视频组件实现方案
    • swiper渲染大量数据的优化方案
    • 前端检测更新,自动刷新网页
  • Git

  • Vite

  • 一些小工具

  • 算法

  • 服务器

  • HTTP

  • 技术
  • 工作中遇到的问题以及解决方案
hanhanbuku
2024-09-03
目录

小程序实现一个事件中心

# 前言

在开发小程序的过程中遇到了这么一个场景,业务中会有许多操作需要用户当前是已登录的状态才可以进行,例如某些页面的跳转,亦或者下单等需要用户信息的操作。目前我们的做法是跳转到登录页完成登录后再返回到当前页面并保持静止。这也是市面上大多数产品的处理方式,为了更丝滑的体验,我想实现一个登录完成之后自动去执行用户上一个触发登录的操作,以此来免去用户登陆完成之后需要重复执行那个操作的尴尬。由此诞生了本篇文章

# 思路

既然是要自动触发上一次的操作,那就必须得知道用户上次点了什么,是什么行为触发的跳转到登录页。而我们需要做的其实就是保存下这个事件并且在返回这个页面之后自动去执行这个事件

import {getCurrentPage} from "@/plugin/nav";
import {localToken} from "@/utils/local";

class SelfExecutingEventCenter{
    constructor() {
        this.eventBus = new Map()
    }
    // 添加事件
    addEvent(event){
        const {route} = getCurrentPage()
        this.eventBus.set(route,event)
    }
    // 触发事件
    triggerEvent(){
        const {route} = getCurrentPage()
        const isLogin = !!localToken.get()
        if(!isLogin){
            this.delEvent(route)
            return
        }
        this.eventBus.has(route)&&this.eventBus.get(route)()
        this.delEvent(route)
    }
    delEvent(key){
        this.eventBus.delete(key)
    }
}
export default SelfExecutingEventCenter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

代码其实很简单,就是维护了一个事件中心,提供一个添加事件的api和一个触发事件的api,这里我们用添加事件的页面的path作为key来存储事件,以防止映射关系错乱。 在触发事件的api中取出当前的路由path,判断事件中心是否有待执行的事件 有的话就去执行这个事件,执行完毕删除即可。

# 使用

下面来看看如何使用

先初始化这个事件中心并挂载到全局


// main.js

import Vue from 'vue'
import SelfExecutingEventCenter from '@/url/SelfExecutingEventCenter'

Vue.propoty.$SelfExecutingEventCenter = new SelfExecutingEventCenter()
1
2
3
4
5
6
7

首先触发事件我们需要找到一个只要进入当前页面就会执行的钩子,在小程序中 onShow满足这个条件。

<!--page.vue-->

onShow(){
    this.$SelfExecutingEventCenter.triggerEvent()
}

1
2
3
4
5
6

这样每次触发页面的onshow我们都会去实践中心检索是否有待执行的函数,有的话就执行这个函数。

接下来就是收集待执行的函数

    handleGoPay() {
      if (!localToken.get()) {
        redirectToLogin()
        return
      }
      uni.navigateTo({
        url: `/pagesA/payment/index`,
      })
    }
1
2
3
4
5
6
7
8
9

上述函数中 需要判断登录态才能跳转到payment页面,所以我们可以在这里进行事件的收集

   handleGoPay() {
      if (!localToken.get()) {
        redirectToLogin()
          // 收集待执行的事件
        this.$SelfExecutingEventCenter.addEvent(()=>this.handleGoPay())
        return
      }
      uni.navigateTo({
        url: `/pagesA/payment/index`,
      })
    }
1
2
3
4
5
6
7
8
9
10
11

这里传入箭头函数是因为有些事件会需要传入参数,通过箭头函数的形式我们可以在箭头函数内闭包引用待执行函数所需的参数,这样省去了我们对参数的透传处理。

以上就是一个完整的使用案例。

# 场景二:跨页面通信

在开发过程中不仅需要掌握组件之间的通信,跨页面的通信通常也是一个很重要的点。一般来说跨页面的通信我们会采用全局的缓存,或者采用vuex等全局状态管理。但有些场景下我们可能只是想通知下一个页面或者上一个页面干某些事情,这个时候如果要通过vuex或者缓存去实现我们通常需要创建一个变量,在通知方存下 ➡ 执行方取出 ➡ 判断是否执行 ➡ 然后删除这个变量。

这种方式用起来属实不太方便,要是忘记删除这个开关变量了还会导致一些难以排查的问题,受vue eventBus的启发,这里我也自行实现了一个发布订阅模式的全局事件总线。

根据上面一个案例我们可以写出一个精简版

class MessageChannel {
    constructor() {
        this.subscribers = {}
    }
    /**
     * 订阅函数
     * @param topic
     * @param subscriber
     */
    on(topic,subscriber){
        if (this.subscribers[topic]){
            this.subscribers[topic].push(subscriber)
        }else{
            this.subscribers[topic] = [subscriber]
        }
    }
    /**
     * 移除订阅事件
     * @param 一个参数都不传入则移除所有的订阅事件
     * @param topic 只传入事件名则移除这个事件名下的所有事件
     * @param subscriber 同时传入事件名和对应事件 则移除这个对应事件,这个事件必须和订阅时传入的事件相同
     */
    off(topic,subscriber){
        if(!topic&&!subscriber){
            this.subscribers = {}
        }else if(topic&&!subscriber){
            this.subscribers[topic] = []
        }else if (topic&&subscriber){
            const events = this.subscribers[topic]||[]
            this.subscribers[topic] = events.filter(sub => sub !== subscriber);
        }
    }
    emit(topic,data){
        const events = this.subscribers[topic]||[]
        // 发布所有事件
        events.forEach((event,index)=>{
            if(event&&typeof event === "function"){
                event(data)
            }
        })
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

简易版的我们实现了发布事件 订阅事件以及移除事件 三个功能,为了使他的功能更加完善,我们可以再加入一个只订阅一次的方法,执行完之后自动销毁。代码改造如下

/**
 * 订阅一次
 * @param topic
 * @param subscriber
 */
once(topic,subscriber){
    if (this.subscribers[topic]){
        this.subscribers[topic].push({
            e:subscriber,
            once:true
        })
    }else{
        this.subscribers[topic] = [{
                e:subscriber,
                once:true
        }]
    }
}
emit(topic,data){
    const events = this.subscribers[topic]||[]
    let onceIdx = null
    // 发布所有事件
    events.forEach((event,index)=>{
        if(event&&typeof event === "function"){
            event(data)
        }else if(event&&event.once){
            event.e(data)
            onceIdx = index
        }
    })
    // 如果是只订阅一次的 在出发完后就删除这个事件
    if(onceIdx!==null){
        events.splice(onceIdx,1)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

上述代码中我们新增了一个只订阅一次的api,并且在发布函数里对这个api进行了兼容,如果是只订阅一次的 发布完消息就把他删除

现在这个类看似就已经完善了,但是在使用过程中遇到了一个问题。

  • 问题:
    • 如果是A页面跳转到B页面,再从B页面回到A页面的时候需要A页面执行某些操作。那我们就应该在B页面发布消息,A页面订阅消息。但是这和我们之前写的逻辑就相悖了,按我们之前的设计:应该是先订阅消息 然后发布的时候回去触发这些订阅消息的回调函数。但此处我们的场景是先发布消息再订阅消息。这里就引入了一个新概念:已过期的消息

提示

什么是已过期的消息呢?就是事件中心先发布了这条消息,但是订阅者没有及时订阅,但他又希望再他订阅的时候能获取到这条旧消息。这也就对应了我们上面的使用场景,B页面发布了消息,A页面后来才订阅,但是也需要拿到B页面发布的这条消息。

那怎么去实现这个功能呢?

其实也很简单,此处我们需要改造一下事件池的数据结构: 原先我们的数据结构如下

this.subscribers = {
    'key':[event,event,event...],
    ...
}
1
2
3
4

由于需要能拿到过期的消息,所以我们需要在每次发布完消息后把这个消息的值保存下来

this.subscribers = {
    'key':{
        value:'',
        events:[event,event,event...]
    },
    ...
}
1
2
3
4
5
6
7

经改造之后的结构我们就可以缓存以过期的值啦,接下来再去改造一下之前的发布函数和订阅函数

class MessageChannel {
    constructor() {
        this.subscribers = {}
    }
    /**
     * 订阅函数
     * @param topic
     * @param subscriber
     * @param isOld 是否接受之前发布的消息
     */
    on(topic,subscriber,isOld = true){
        if (this.subscribers[topic]){
            this.subscribers[topic].events.push(subscriber)
            // 如果存在过期的消息,则自动触发一次回调
            if(this.subscribers[topic].value){
                subscriber(this.subscribers[topic].value)
            }
        }else{
            this.subscribers[topic] = {
                value:'',
                events:[subscriber]
            }
        }
    }
    /**
     * 订阅一次
     * @param topic
     * @param subscriber
     */
    once(topic,subscriber){
        if (this.subscribers[topic]){
            // 如果存在过期的消息,则自动触发一次回调,因为是订阅一次 所以就不用存入事件池了
            if(this.subscribers[topic].value){
                subscriber(this.subscribers[topic].value)
            }else{
                this.subscribers[topic].events.push({
                    e:subscriber,
                    once:true
                })
            }
        }else{
            this.subscribers[topic] = {
                value:'',
                events:[{
                    e:subscriber,
                    once:true
                }]
            }
        }
    }
    /**
     * 移除订阅事件
     * @param 一个参数都不传入则移除所有的订阅事件
     * @param topic 只传入事件名则移除这个事件名下的所有事件
     * @param subscriber 同时传入事件名和对应事件 则移除这个对应事件,这个事件必须和订阅时传入的事件相同
     */
    off(topic,subscriber){
        if(!topic&&!subscriber){
            this.subscribers = {}
        }else if(topic&&!subscriber){
            this.subscribers[topic].events = []
        }else if (topic&&subscriber){
            const events = this.subscribers[topic].events||[]
            this.subscribers[topic].events = events.filter(sub => sub !== subscriber);
        }
    }
    emit(topic,data){
        // 存储本次事件的值
        this.subscribers[topic].value = data
        const events = this.subscribers[topic].events||[]
        let onceIdx = null
        // 发布所有事件
        events.forEach((event,index)=>{
            if(event&&typeof event === "function"){
                event(data)
            }else if(event&&event.once){
                event.e(data)
                onceIdx = index
            }
        })
        // 如果是只订阅一次的 在出发完后就删除这个事件
        if(onceIdx!==null){
            events.splice(onceIdx,1)
        }
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87

这次改动主要是两个地方: 1.发布的时候将这个值保存在事件池对应的key的value上 2.订阅的时候先判断当前订阅的key是否有已发布的值,如果有的话就自执行一次订阅函数,并且将之前缓存的值传递出去。

# 结语

设计模式在日常开发中无处不在,掌握他们可以让我们在面对一些问题的时候处理的更得心应手,本文就是通过发布订阅模式解决了日常开发中跨页面通信的问题,第一个问题的解决方案其实也是发布订阅模式的思想。

编辑 (opens new window)
上次更新: 2024/09/05, 14:34:55
超详细的虚拟列表实现过程
请求超时后如何优雅的重新请求

← 超详细的虚拟列表实现过程 请求超时后如何优雅的重新请求→

最近更新
01
前端检测更新,自动刷新网页
06-09
02
swiper渲染大量数据的优化方案
06-06
03
仿抖音短视频组件实现方案
02-28
更多文章>
Theme by Vdoing | Copyright © 2023-2025 UzumakiItachi | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式