本文介绍
本文主要讲解JavaScript
中的发布-订阅模式,也可以叫做观察者模式,个人觉得前者叫法更适合从代码的角度来理解。
文章最终目的是实现组件(模块)之间的通信,其中会额外实现一下组件的生命周期功能。因为目前某几大框架实在是太火了,所以最近写的文章,都围绕着这些框架所用的知识点来写。
本文代码风格: ES5
内容
发布-订阅模式
在讲组件间通信前,我们先从其实现的基础讲起,那就是发布-订阅模式。
下面我们就来实现一下发布-订阅的功能,那么实现之前,先来分析下该功能的需求:
注意:需求有很多种,下面只是其中一种,这种会比较贴近我们最终的需求(组件通信)
- 有一个发布者
- 发布者拥有一个记事本,上面记录着与订阅者间约定好事情和该事情的类型(事情就是函数)
- 发布者有一个广播的方法,该方法接受一个
类型
参数。每次广播时,就会把记事本上该类型的事情都执行一遍 - 发布者可以在记事本中添加某件事情,并且要指定该事情的类型
- 发布者也可以从记事本删除掉某件事情(需要知道该事情的类型)
注意我上面加粗的文字!
搞清楚需求后,就开始写代码:
// 1.发布者
var publisher = {};
// 2.拥有一个笔记本,这里使用对象而不使用数组
// 因为 "类型": "事情",属于键值对
publisher.notebook = {}
// 3.拥有广播方法,会将类型的事情都执行一遍
publisher.broadcast = function (type) {
//取出事情。一个类型,可以有多件事情
// funArray是数组,里面装的是函数
var funArray = publisher.notebook[type];
// 把所有事情执行一遍(事情就是函数)
for (var i = 0; i < funArray.length; i++) {
funArray[i]();
}
}
// 4.记事本可以添加事情并指定类型
publisher.addFun = function (type, fun) {
// 为了代码更容易阅读,没做容错处理,前面和后面也是
publisher.notebook[type].push(fun)
}
// 5.记事本可以删除事情
publisher.removeFun = function (type, fun) {
var funArray = publisher.notebook[type];
for (var i = 0; i < funArray.length; i++) {
if (fun === item) {
funArray.splice(i, 1)
}
}
}
功能写完了,来测试一下效果:
// 订阅者
var user = {
// 订阅者要约的事情
yue: function () {
console.log('正在执行要约的事');
}
}
// 添加到发布者的笔记本,随意给的类型
publisher.addFun('type1' , user.yue)
// 广播
publisher.broadcast('type1')
上面就是发布-订阅模式,已成功实现。其实同一种设计模式,可以有很多不同的用法,但是其核心思想不变。
组件之间的通信
上面的发布-订阅模式中,有一个对象充当发布者,有一个对象充当订阅者。但是组件间的通信好像完全不是这么回事,因为似乎每个组件(对象)即可以充当发布者,也可以充当订阅者。
没错,按照上面的例子,只要你为订阅者user
也添加发布者的功能,那么这两个对象就能实现通信了。
不过这样做的话实在太麻烦了,哪怕引用mixin
的概念来实现,也是浪费性能。
所以这里再引入一个中介者的概念,这个中介者就是刚才的发布者publisher
,它是个全局对象,在哪里都可以引用。(在框架中,一般为类的静态属性,如Vue.xxx,React.xxx)
组件通过中介者(全局发布者),来注册事件(约定事情),也可以主动通知(调用)中介者,让其执行某类型的事情。
基于上面代码,我们来实现一下两个组件之间的通信:
如果对下面代码阅读比较吃力的话(我觉得我注释写的挺好的,你应该看得懂),可以看下我另一篇关于生命周期的文章,会帮助你更好的理解组件大概是怎么一回事。
// 模拟一个Vue React那样的组件
function Component (option) {
// 把传递进来的option对象的属性和方法揽到自己身上
for (var key in option) {
this[key] = option[key];
}
// 开始执行生命周期方法
this.init();
}
// 定义一些生命周期方法,并让其按顺序进行
Component.prototype = {
constructor: Component,
init: function () {
// 组件渲染
this.render();
// 组件挂载完毕
this.componentDidMount();
},
// 默认有此方法,但render方法一定要自己写,不然报错
componentDidMount: function () {
}
}
// 现在我们的目的是:创建两个组件,让两个组件实现通信(传输数据),下面是component1给component2传递数据
// 由于涉及到传递数据,这里我们小幅度修改publisher的广播方法,让它多一个data参数
// 其他均不变,多了一个data参数
publisher.broadcast = function (type, data) {
var funArray = publisher.notebook[type];
for (var i = 0; i < funArray.length; i++) {
// 执行函数时,传递data
funArray[i](data);
}
}
// 组件1
var component1 = new Component({
// 该组件的数据
data: {
passValue: '我是component1传递过来的值'
},
// 重写render
render: function () {
},
// 自定义的一个方法,该方法可以通知中介者执行广播
_emit: function (type) {
publisher.broadcast(type, this.data.passValue);
}
})
var component2 = new Component({
data: {
},
render: function () {
},
// 组件挂载完后,注册事件
componentDidMount: function () {
// 利用中介者注册事件
publisher.addFun('log', function (data) {
console.log('我是组件2,接受到的数据是:' + data);
})
}
})
// 这里手动触发,在框架中,一般会有按钮,点击即可触发this._emit('log')
component1._emit('log');
代码建议复制份自己跑一下,会有助于你理解组件的原理。