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

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

前言

  本节我们处理一个老生常谈的问题,异步。为什么会出现异步这个问题呢?这要从 JS 出生之前说起。

  很久很久以前,天和地还没有分开,宇宙混沌一片。有个叫盘古的巨人,在这混沌之中,一直睡了一万八千年。
在这里插入图片描述

  有一天,盘古突然醒了。他见周围一片漆黑,就抡起大斧头,朝眼前的黑暗猛劈过去。只听一声巨响,混沌一片的东西渐渐分开了。轻而清的东西,缓缓上升,变成了天;重而浊的东西,慢慢下降,变成了地…

  扯远了哈,异步问题是因为 JS 设计之初就是单线程的,当我们碰到一些比较耗时的事情的时候,不能就卡着不动了,用户可能会以为电脑坏了。那么就需要把耗时的事情暂时挂起,等 JS 手头的工作完成了,再去处理那个耗时的东西。

等待者模式

  通过对多个异步进程监听,来触发未来发生的动作

  工作当中我们会经常遇到请求不同接口的数据,让后把这两个接口的数据一块渲染出来。那么请求时快时慢,一个完成了,另一个还没返回结果。

  解决这个问题图省事的方法就是在一个请求的回调函数中,发送另一个请求,但是这个样整个请求的事件就变成了 T1 + T2 ,作为程序员,一定锱铢必较,快一点也是块。我们希望请求事件的总时间可以是 T1, T2 中较大的那个,就可以用到等待着模式进行优化。

  我们需要创建一个等待对象,这个对象不需要实时监听异步逻辑的完成,它只需要对注册监听的异步逻辑发生状态改变时(请求成功或失败)对所有异步逻辑的状态做一次确认迭代。

//等待对象
var Waiter = function() {
    
    
	//注册了的等待对象容器
	var dfd = [],
		//成功回调方法容器
		doneArr = [],
		//失败回调容器方法
		failArr = [],
		//缓存 Array 方法 slice
		slice = Array.prototype.slice,
		//保存当前等待对象
		that = this;
	
	//监控对象类
	var Primise = function() {
    
    
		//监控对象是否解决成功状态
		this.resolved = false;
		//监控对象是否解决失败状态
		this.rejected= false;
	}
	//监控对象类原型方法
	Primise.prototype = {
    
    
		//解决成功
		resolve: function() {
    
    },
		//解决失败
		reject: function() {
    
    }
	}

	//创建监控对象
	that.Deferred = function() {
    
    
		return new Promise();
	}
	
	//回调执行方法
	function _exec(arr) {
    
    }

	//监控异步方法 参数:监控对象
	that.when = function() {
    
    };

	//解决成功回调函数添加方法
	that.done = function() {
    
    };

	//解决失败回调函数添加方法
	that.fail = function() {
    
    };
}

  对于等待者对象可以看出,其结构如下:

  • 内部定义了三个数组,分别是等待对象容器,以及成功与失败回调函数容器;
  • 一个类-监控对象,该对象有两个属性,即监控解决成功状态,监控解决失败状态,两个方法,解决成功方法与解决失败方法;
  • 一个私有方法 _exec 来处理成功失败回调函数的方法;
  • 3个公有方法接口: when 方法监控异步逻辑, done 方法添加成功回调函数, fail 方法添加失败回调函数;

  首先我们先实现监控对象类原型方法 resolve reject ,他们都是因异步逻辑状态改变而执行的相应操作,不同的是 resolve 方法是要执行成功回调,所以要对所有被监控的异步逻辑进行状态校验。而 reject 方法是要执行失败回调函数,所以只要有一个被监控的异步逻辑状态变成失败状态,就要立即执行回调函数。

//监控对象原型方法
Primise.prototype = {
    
    
	//解决成功
	resolve: function() {
    
    
		//设置当前监控对象解决成功
		this.resolved = true;
		//如果没有监控对象则取消执行
		if(!dfd.length) return;
		//遍历所有注册了的监控对象
		for(var i = dfd.length - 1; i >= 0; i--) {
    
    
			//如果有任意一个监控对象没有被解决或者解决失败则返回
			if(dfd[i] && !dfd[i].resolved || dfd[i].rejected) return;
			//清除监控对象
			dfd.splice(i, 1);
		}
		//执行解决成功回调方法
		_exec(doneArr);
	},
	//解决失败
	reject: function() {
    
    
		//设置当前监控对象解决失败
		this.rejected = true;
		//如果没有监控对象则取消执行
		if(!dfd.length) return;
		//清除所有监控对象
		dfd.splice(0);
		//执行解决成功回调方法
		_exec(failArr);
	}
}

  对于回调执行方法要做的事情很简单,就i是遍历成功或者失败回调函数容器,然后依次执行内部方法。

//回调执行方法
function _exec(arr) {
    
    
	var i = 0,
		len = arr.length;
	//遍历回调数组执行回调
	for(; i < len; i++) {
    
    
		try {
    
    
			//执行回调函数
			arr[i] && arr[i]();
		} catch(e) {
    
    }
	}
}

  等待着对象还定义了三个共有接口方法 when、done fail ,对于 when 方法是要检测已注册过的监控对象的异步逻辑(请求: ajax 请求等;方法: setTimeout 方法等),所以 when 方法就要将检测对象放入检测对象容器中,当然还要判断检测对象是否存在、是否解决、是否是检测对象类的实例。最后还要返回该等待对象便于链式调用。

//监控异步方法 参数:监控对象
that.when = function() {
    
    
	//设置监控对象
	dfd = slice.call(arguments);
	//获取监控对象数组长度
	var i = dfd.length;
	//向前遍历监控对象, 最后一个监控对象的索引值为 length - 1
	for(--i; i >= 0; i--) {
    
    
		//如果不存在监控对象,或者监控对象已经解决,或者不是监控对象
		if(!dfd[i] || dfd[i].resolved || dfd[i].rejected || !dfd[i] instanceof Primise) {
    
    
			//清理内存 清除当前监控对象
			dfd.splice(i, 1);
		}
	}
	//返回等待者对象
	return that;
};

  对于 done 方法与 fail 方法的功能则简单得多,只需要向对应的回调函数容器中添加相应回调函数即可,并最终将等待者对象返回便于链式调用。

//解决成功回调函数添加方法
that.done = function() {
    
    
	//向成功回调函数容器中添加回调方法
	doneArr = doneArr.concat(slice.call(arguments));
	//返回等待者对象
	return that;
};
//解决失败回调函数添加方法
that.fail = function() {
    
    
	//向失败回调函数容器中添加回调方法
	failArr = failArr.cancat(slice.call(arguments));
	//返回等待者对象
	return that;
}

  好了,有了这个等待对象,我们就可以对异步逻辑进行监控了。我们先写一个异步方法的例子。

setTimeout(function() {
    
    
	console.log('first');
}, 30)
console.log('second');
//控制台输出结果
//second
//first

  简单说一下同步原理,类似单行道马路,汽车只能一辆接着一辆过,后面的哪怕有个火箭,也只能老实排队挨个通过。

  那不能总让火箭等待吧,这就出现了异步,类似开辟多车道,让那些跑的慢的车在一边行驶,把马路让出来,让火箭快速通过。这就是异步的产生,所以我们可以看到上面控制台是先输出 second 然后再输出 first

  解释完异步的产生,我们设置一个场景来验证等待者对象。

  比如现在有一个页面,里面有几个活动彩蛋,让所有彩蛋运动结束后,出现欢迎页面。要检测几个运动彩蛋全部结束是一件很头疼的事,这时候就需要我们的等待者对象。

var waiter = new Waiter();

  这里我们简化彩蛋,将其抽象成一个方法,而且在某一时刻会结束,我们要在彩蛋方法执行内部创建监听对象。

//第一个彩蛋,5 秒后停止
var first = function() {
    
    
	//创建监听对象
	var dtd = waiter.Deferred();
	setTimeout(function() {
    
    
		console.log('first finish');
		//发布解决成功消息
		dtd.resolve();
	}, 5000);
	//返回监听对象
	return dtd;
}();
//第二个彩蛋,10 秒后停止
var second = function() {
    
    
	//创建监听对象
	var dtd = waiter.Deferred();
	setTimeout(function() {
    
    
		console.log('second finish');
		//发布解决成功消息
		dtd.resolve();
	}, 10000)
	//返回监听对象
	return dtd;
}();

  最后我们要用等待者对象监听两个彩蛋的工作状态,并执行相应的成功回调函数与失败回调函数。

waiter
	//监听两个彩蛋
	.when(first, sceond)
	//添加成功回调函数
	.done(function() {
    
    
		console.log('success');
	}, function() {
    
    
		console.log('success again')
	})
	//添加失败回调函数
	.fail(function() {
    
    
		console.log('fail');
	});
//输出结果
// first
// second
// success
// success again

等待者模式应用

  等待者模式被很多框架运用,比如 jQuery 中的 Deferred 对象,并且它还用等待者思想对 ajax 方法进行了封装。

$.ajax("text.php")
	.done(function() {
    
     console.log("成功"); })
	.fail(function() {
    
     console.log("失败"); });

  当然我们自己也可以将原生的 ajax 方法封装成等待者模式,将 resolve 方法放在请求成功回调函数内调用,将 reject 方法放在请求失败回调函数中调用。

//封装 get 请求
var ajaxGet = function(url, success, fail) {
    
    
	var xhr = new XMLHttpRequest();
	//创建检测对象
	var dtd = waiter.Deferred();
	xhr.onload = function(event) {
    
    
		//请求成功
		if(xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) {
    
    
			success && success();
			dtd.resolved();
		//请求失败
		} else {
    
    
			dtd.reject();
			fail && fail();
		}
	};
	xhr.open("get", url, true);
	xhr.send(null);
};

  有了等待者对象,以后就不用愁后端的部署了,我们只需要修改一下接口就可以了。省去了很多工作。

  除此之外,我们的轮询(定期向后端发送请求)实现的机制也有些类似等待者模式,只不过是在其自身内部对未来动作进行了监听。

//长轮询
(function getAjaxData() {
    
    
	//保存当前函数
	var fn = arguments.callee;
	setTimeout(function() {
    
    
		$.get('./test.php', function() {
    
    
			console.log('轮询一次');
			//在执行一次轮询
			fn();
		})
	}, 5000)
})();

  观察者模式意在处理耗时比较长的操作,比如 canvas 中遍历并操作一张大图片中的每一个像素点,定时器操作,异步请求等。等待者模式提供了一个抽象的非阻塞解决方案。




猜你喜欢

转载自blog.csdn.net/EcbJS/article/details/111615662