代码要写成别人看不懂的样子(二十二)
作者:互联网
本篇文章参考书籍《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 中遍历并操作一张大图片中的每一个像素点,定时器操作,异步请求等。等待者模式提供了一个抽象的非阻塞解决方案。
标签:二十二,function,看不懂,异步,对象,监控,写成,回调,方法 来源: https://blog.csdn.net/EcbJS/article/details/111615662