前言

各种技术框架,比如 vue、react 和小程序,实现父子组件和兄弟组件通信的方案有很多,大多方案都强依赖于框架本身。这里介绍一种通过发布和订阅的方式来实现组件通信的方案,纯 JavaScript 实现,可以适用于各种框架。

发布订阅模式

发布订阅模式包含三部分内容,发布者、订阅者和数据处理中心。订阅者把自己想监听的事件和回调函数信息写入到数据处理中心去;当事件触发时,发布者发布该事件到数据处理中心,由数据处理中心统一执行订阅者写入到数据处理中心对应事件的回调函数。

比如A组件需要向B组件传递数据,通过发布订阅模式来实现的思路是:

  • 在B组件里添加(on,事件监听)一个事件订阅,监听的事件名称和回调函数;
  • 当A组件需要给B组件传递数据时,可发布(emit)对应的事件名称并携带相应的数据,数据处理中心根据相应的事件执行回调函数并传递数据给B组件

定义数据处理中心

用来存储事件和回调函数信息

1
2
3
4
5
6
class Emitter {
constructor() {
// 数据处理中心,用来存储事件和回调函数信息
this.handlers = {}
}
}

实现订阅功能

需要传入两个参数,订阅的事件名称和事件触发时的回调函数

1
2
3
4
5
6
7
8
// 订阅
on(eventName, fn) {
if (typeof fn !== "function") { console.error('fn must be a function') }
if (!this.handlers[eventName]) {
this.handlers[eventName] = []
}
this.handlers[eventName].push(fn)
}

实现发布功能

遍历数据处理中心的数据,找到并执行对应事件名称的回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
// 发布
emit(eventName) {
const fns = this.handlers[eventName]
if (fns && fns.length) {
// arguments,携带一些数据信息;将arguments函数参数列表(类数组对象)转为数组
const args = [].slice.call(arguments)
args.shift() // 参数去掉事件名称
fns.forEach((fn) => {
// 给回调方法传参
fn.apply(null, args)
})
}
}

取消订阅

删除数据处理中心数组中对应事件的回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 注销订阅
off(eventName, fn) {
// 若是没有传参,注销所有的订阅
if (!arguments.length) {
this.handlers = {};
return this;
}
const fns = this.handlers[eventName]
if (!fns) return
// 若是只传eventName,不传fn,删除对应事件名称下的所有回调函数
if (arguments.length === 1) {
delete this.handlers[eventName]
return
}
if(fns && fns.includes(fn)){
fns.splice(fns.indexOf(fn), 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
class Emitter {
constructor() {
// 数据处理中心,用来存储事件和回调函数信息
this.handlers = {}
}
// 订阅
on(eventName, fn) {
if (typeof fn !== "function") { console.error('fn must be a function') }
if (!this.handlers[eventName]) {
this.handlers[eventName] = []
}
this.handlers[eventName].push(fn)
}
// 发布
emit(eventName) {
const fns = this.handlers[eventName]
if (fns && fns.length) {
// arguments,携带一些数据信息;将arguments函数参数列表(类数组对象)转为数组
const args = [].slice.call(arguments)
args.shift() // 参数去掉事件名称
fns.forEach((fn) => {
// 给回调方法传参
fn.apply(null, args)
})
}
}
// 注销订阅
off(eventName, fn) {
// 若是没有传参,注销所有的订阅
if (!arguments.length) {
this.handlers = {};
return this;
}
const fns = this.handlers[eventName]
if (!fns) return
// 若是只传eventName,不传fn,删除对应事件名称下的所有回调函数
if (arguments.length === 1) {
delete this.handlers[eventName]
return
}
if(fns && fns.includes(fn)){
fns.splice(fns.indexOf(fn), 1)
}
}
}

module.exports = new Emitter()

代码示例

A组件需要向B组件传递数据

  • B组件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // B组件
    import emitter from 'emitter'
    export default {
    data() {
    return {
    pageIndex: 1
    }
    },
    created() {
    // 添加'getAdata'事件订阅
    emitter.on('getAData' + this.pageIndex, this.getAData.bind(this))
    },
    // 路由页面销毁
    destroyed() {
    emitter.on('off' + this.pageIndex)
    },
    methods: {
    getAData(data) {
    console.log(data.info, '获取到A组件传给B组件的数据')
    }
    }
    }
  • A组件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // A组件
    import emitter from 'emitter'
    export default {
    data() {
    return {
    pageIndex: 1
    }
    },
    created() {
    setTimeout(() => {
    // 发布'getAdata'事件并携带数据
    emitter.eimit('getAData' + this.pageIndex, {
    info: 'A组件发送给B组件的数据'
    })
    }, 2000)
    }
    }