代码要写成别人看不懂的样子(九)

本篇文章参考书籍《JavaScript设计模式》–张容铭

前言

  前几节的结构型设计模式,各位学的乱不乱,我个人刚开始接触的时候,也觉得很抽象,不太好理解,尤其是实战的时候,总是不知道在什么情况下使用。记住一点,当我们单纯的创建一个类的时候,解决不了现有问题,这个时候就需要用到我们的结构型设计模式了。

  本节我们一起来学习一种新思想,行为型设计模式,与之前我们学习的侧重点不同,行为型设计模式注重的是类或者对象之间的交流,举个例子:

  我们第一阶段学习的创建型设计模式,让我们可以创建类,第二阶段学习的结构型设计模式,让我们可以创建更复杂的类。这一部分,行为型设计模式,则是让我们创建的类可以获得与其他类的交互。

模板方法模式

  首先来看第一种,模板方法。大家在平常开发当中,基本都会使用一些UI库吧, Vue 的 Element UI ,React 的 AntDesign,这些UI库使用的就是模板方法模式。以提示框为例,我们可以创建一个基本的提示框

//模板类 基础提示框 data 渲染数据
var Alert = function(data) {
	//没有数据就返回,防止后面程序执行
	if(!data) return;
	//设置内容
	this.content = data.content;
	//创建提示面板
	this.panel = document.creatElement('div');
	//创建提示内容
	this.contentNode = document.creatELement('p');
	//确定,关闭按钮
	this.confirmBtn = document.creatELement('span');
	this.closeBtn = document.creatELement('b');

	this.panel.className = 'alert';
	this.confirmBtn.className = 'a-confirm';
	this.closeBtn.className = 'a-close';
	//为确定按钮增加文案
	this.confirmBtn.innerHTML = data.confirm || '确认';
	//为提示内容添加文本
	this.contentNode.innerHTML = this.content;
	//点击确定按钮执行方法
	this.success = data.success || function() {};
	//点击关闭按钮执行方法
	this.fail = data.fail || function() {};
}

  这个提示框是需要有一些基本方法的,比如 init 方法来组装提示框,bindEvent 方法来绑定点击确定或关闭按钮事件。

//提示框原型方法
Alert.prototype = {
	//创建方法
	init: function() {
		//生成提示框
		this.panel.appendChild(this.closeBtn);
		this.panel.appendChild(this.contentNode);
		this.panel.appendChild(this.confirmBtn);
		//插入页面
		document.body.appendChild(this.panel);
		//绑定事件
		this.bandEvent();
		this.show();
	},
	bindEvent: function() {
		var me = this;
		//关闭按钮点击事件
		this.closeBtn.onclick = function() {
			me.fail();
			me.hide();
		}
		//确定按钮点击事件
		this.confirmBtn.onclick = function() {
			me.success();
			me.hide();
		}
	},
	//隐藏弹层方法
	hide: function() {
		this.panel.style.display = 'none';
	},
	//显示方法
	show: function() {
		this.panel.style.display = 'block';
	}
}

  有了上面的基类,拓展其他类型的弹层就方便的多了,比如右侧按钮提示框

//右侧按钮提示框
var RightAlert = function(data) {
	//继承基本提示框构造类
	Alert.call(this, data);
	//添加右侧按钮样式
	this.confirmBtn.calssName = this.confirmBtn.calssName + ' right';
}
//继承基本提示方法
RightAlert.prototype = new Alert();

  同理,实现标题提示框。

//标题提示框
var TitleAlert = function(data) {
	//继承基本提示框构造类
	Alert.call(this, data);
	//设置title
	this.title = data.title;
	//创建标题组件
	this.titleNode = document.creatElement('h3');
	//写入标题内容
	this.titleNode.innerHTML = this.title
}
//继承基本提示方法
TitleAlert.prototype = new Alert();
//基本提示框方法拓展
TitleAlert.prototype.init = function() {
	//插入标题
	this,panel,insertBefore(this.titleNodem this.panel.firstChild);
	//继承基本提示框方法
	Alert.prototype.init.call(this);
}

  模板方法的核心在于对方法的重用,它将核心方法封装在基类中,让子类继承基类,实现方法共享。这种模式需要子类必须遵守某些法则,基类中封装的方法要求不可变。

观察者模式

  这一节我们一起来学一个,出现率比较高的设计模式,这种设计模式被应用到,解决闭包数据交互,各种接口调用,数据交流的情况中。观察者模式又称发布-订阅模式,它定义了一种依赖关系,解决了主体对象与观察者对象之间功能的耦合。

  举个例子哈,各个国家发射的卫星,就是一个观察者,飞机可以通过卫星导航,那飞机就是一个主体对象,主体对象会经常发送消息,告诉卫星,我现在在某某位置。

  卫星收到消息之后,就可以明确飞机的位置了,当然只有卫星知道飞机的位置没有用,需要让地面上的中转站知道飞机的具体位置才行,现在问题来了,地面上有那么多中转站,卫星总不能每个都发送一遍,这样太耗时而且很浪费。

  为了解决到底发给哪个中转站这一问题,就需要引入一种机制,代码里称为“订阅”,只有当中转站在卫星上注册了某一飞机的信息后,才能收到该飞机的位置信息。

  当上面的“订阅”完成后, 飞机飞到某一位置 发送位置信息给卫星 卫星接收信息 转发给所有订阅过此飞机信息的中转站 。这一套流程下来后,中转站便知道了飞机的位置,可以根据需要来做相应的处理,避免飞机发生事故。

  上面流程还可以优化,比如当飞机飞到石家庄的时候,已经不属于北京了,那么北京的中转站也就没必要再接收消息了,这时候北京的 中转站就会注销掉飞机之前注册的消息

  最后,针对飞机发送的消息,我们需要有一个 保存消息的容器

  上面彩色字体的内容,就构成了一个完整的观察者模式。

实现观察者

//将观察者放在闭包中,页面一加载就立即执行
var Observer = (function() {
	//防止消息队列暴漏被篡改故将消息容器作为静态私有变量保存
	var __messages = {};
	return {
		//注册信息接口
		regist: function() {},
		//发布信息接口
		fire: function() {},
		//移除信息接口
		remove: function() {}
	}
}) ();

  观察者的模型出来了,下一步就是挨个实现。

  首先,注册方法,目的是将订阅者注册的消息推入到消息队列中,所以我们需要两个参数,消息类型和相应的处理动作。在推入到消息队列中的时候,如果推入到消息队列的时候,此消息不存在,则应该先创建一个该消息类型,然后再将该消息放入消息队列,如果此消息存在,则应该将消息执行方法推入该消息对应的执行方法队列中,这么做的目的也是多个模块注册同一则消息时能顺利执行。

regist: function(type, fn) {
	//如果此消息不存在则创建一个该消息类型
	if(typeof __messages[type] === 'undefined') {
		//将动作推入到该消息对应的动作执行队列中
		messages[type] = [fn];
	//如果此消息存在
	} else {
		//将动作方法推入该消息对应的动作执行序列中
		__messages[type].push(fn);
	}
}

  对于发布消息的方法,它的功能是当观察者发布一个消息时,将所有订阅者订阅的消息一次执行,故应接收两个参数,消息类型以及动作执行时需要传递的参数,当然再这里消息类型是必须的。在执行消息动作队列之前校验消息的存在是很有必要的。然后遍历消息执行方法队列,并依次执行。然后将消息类别以及传递的参数打包后依次传入消息执行方法中。

fire: function(type, args) {
	//如果该消息没有被注册,则返回
	if(!__messages[type]) return;
	//定义消息信息
	var events = {
		type: type,      //消息类型
		args: args || [] //消息携带数据
	},
	i = 0,               //消息动作循环变量
	len = __messages[type].length; //消息动作长度
	//遍历消息动作
	for(; i < len; i++) {
		//一次执行注册的消息对应的动作序列
		__messages[type][i].call(this, events);
	}
}

  最后消息注销方法,功能是将订阅者注销的消息从消息队列中清除,因此我们也需要两个参数,即消息类型以及执行的某一动作,当然为了避免删除消息动作时消息不存在情况的出现,对消息队列中的消息存在性校验也是很有必要的。

remove: function(type, fn) {
	//如果消息队列动作存在
	if(__messages[type] instanceof Array) {
		//从最后一个消息动作遍历
		var i = __messages[type].length - 1;
		for(; i >= 0; i--) {
			//如果存在该动作,则在消息动作序列中移除相应动作
			__messages[type][i] === fn && __messages[type].splice(i, 1);
		}
	}
}

  接下来订阅一个消息试试

//订阅消息
Observer.regist('test', function(e) {
	console.log(e.type, e.args.msg);
});
//发布消息
Observer.fire('test', {msg: '传递参数'});  //test 传递参数

观察者模式原理

  上面我们完整的书写了一边观察者模式,代码虽然弄完了,但是我估计第一次接触的同学还处在懵逼的状态,不知道这东西能干啥,以及这个是怎么实现解耦的。我i们以一开始抛出的那几个使用方法举例。

  比如,现在我们想实现一个评论功能,当我写完评论点击提交后,刚才的评论会出现在评论列表的最上方,然后评论数量会更新,当我点击删除按钮时,对应的评论数量会减少。正常来讲要实现这个功能并不难,但是实际情况往往是,留言功能是 A工程师 开发的,评论数量是 B工程师 开发的,提交按钮又是 C工程师 开发的,为了保证开发之的独立性,所有人的模块都是通过闭包完成的。

  这就导致不同模块之间直接传参行不通了,那我们该如何实现功能呢?

  没错,到这里就该观察者模式出场了,首先很重要一点,我们得明确, 哪些模块是注册消息的,哪些模块是发布消息的

  上面例子中,提交留言和删除留言是用户主动触发的,所以这个属于观察者发布消息,评论追加还有数量改变是被动触发的,属于订阅者注册消息。

  分析道这一层我们就可以开始编写消息注册的两个模块了,追加评论相关实现如下:

//外观模式,简化元素获取(活学活用哈,前面我们学习的外观模式,这里可以用来练练手)
function $(id) {
	return document.getElementById(id);
}
//工程师 A
(function() {
	//追加一则消息
	function addMsgItem(e) {
		var text = e.args.text,                    //获取消息中用户添加的文本内容
			ul = $('msg'),                         //留言容器元素
			li = document.createElement('li'),     //创建内容容器元素
			span = document.createElement('span'); //删除按钮
		li.innerHTML = text;                       //写入评论
		span.onclick = function() {
			ul.removeChild(li);                    //移除留言
			//发布删除留言消息
			Observer.fire('removeCommentMessage', {
				num: -1
			});
		}
		//添加删除按钮
		li.appendChild(span);
		//添加留言节点
		ul.appednChild(li);
	}
	//注册添加评论信息
	Observer.regist('addCommentMessage', addMsgItem);
}) ();

  实现评论数量递增,只需要在原信息数目基础上增加一条即可。

//工程师 B
(function() {
	//更改用户消息数目
	function changeMsgNum(e) {
		//获取需要增加的用户消息数目
		var num = e.args.num;
		//增加用户消息数目并显示在页面中
		$('msg_num').innerHTML = parseInt($('msg_num').innerHTML) + num;
	}
	//注册添加评论信息
	Observer
		.regist('addCommentMseeage', changeMsgNum)
		.regist('removeCommentMessage', changeMsgNum);
}) ();

  最后对于一个用户来说,当他提交信息时,就要触发消息发布功能。

//工程师 C
(function() {
	//用户点击提交按钮
	$('user_submit').onclick = function() {
		//获取用户输入框中的输入信息
		var text = $('user_input');
		//如果消息为空则提交失败
		if(text.value === '') return;
		//发布一则评论消息
		Observer.fire('addCommentMessage', {
			text: text.value,    //消息评论内容
			num: 1               //消息评论数量
		})
		text.value = '';         //将输入框置为空
	}
}) ();

  OK了,我们完整的实现了整个观察者模式。这种模式是不是很神奇,它可以完美的解决各个模块之间的耦合问题。属于各个模块的方法都可以写在各自对应的原模块中,不用其他模块是如何实现。唯一要做的就是收发消息。

对象间解耦

  观察者模式的强大功能还可以解决类或者对象间的耦合。举个例子,课堂上老师提问,我们就可以抽下出一些类。

//学生类
var Student = function(result) {
	var that = this;
	//学生回答结果
	that.result = result;
	//学生回答问题动作
	that.say = function() {
		console.log(that.result);
	}
};

  学生在课堂上是可以回答问题的,所以要添加 answer 方法。

//回答问题方法
Student.prototype.answer = function(question) {
	//注册参数问题
	Observer.regist(question, this.say);
}

  当然课堂上会有一类学生睡着了,不能回答问题,所以要取消订阅

//学生睡觉,不能回答问题
Student.prototype.sleep = function(question) {
	console.log(this.result + ' ' + question + '已被注销');
	//取消对老师问题的监听
	Observer.remove(question, this.say);
}

  学生类创建完成了,接下来是教师类,对于教师,他会提问学生,所以他是一个观察者,需要有一个提问题的方法。

//教师类
var Teacher = function() {}
//教师提问问题的方法
Teacher.prototype.ask = function(question) {
	console.log('问题是: ' + question);
	//发布消息
	Observer.fire(question);
}

  类有了,接下来就是创建实例了

var student1 = new Student('学生1回答问题'),
	student2 = new Student('学生2回答问题'),
	student3 = new Student('学生3回答问题');

  然后这三位同学订阅了老师的问题

student1.answer('什么是设计模式');
student1.answer('简述观察者模式');
student2.answer('什么是设计模式');
student3.answer('什么是设计模式');
student3.answer('简述观察者模式');

  后来第三位同学睡着了,对于订阅的问题"简述观察者模式",消息就注销了。

student3.sleep('什么是设计模式');
student3.sleep('简述观察者模式');

  最后我们创建一个教师实例

var teacher = new Teacher();

  提问两个问题

teacher.ask('什么是设计模式');
teacher.ask('简述观察者模式');

  输出结果如下:

//学生3回答问题 简述观察者模式 已被注销
//问题是: 什么是设计模式
//学生1回答问题
//学生2回答问题
//学生3回答问题
//问题是: 简述观察者模式
//学生1回答问题

  有没有感觉观察者模式很强大,如果大家熟悉前端开发框架,那么肯定接触过一个词,那就是数据绑定,很多数据绑定都是通过观察者模式实现的。

  本章节文字有点多,大家一定要坚持看到最后,一边看不懂就收藏起来,多看几遍,观察者模式很重要,而且原理需要多琢磨才能理解。一开始记不住没关系,把 Observe 这个方法存下来,没事就用一下,自然而然就学会了。




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值