js执行本身并不慢,慢的是读写外部数据,比如等待 Ajax 请求返回结果。这个时候,如果对方服务器迟迟没有响应,或者网络不通畅,就会导致脚本的长时间停滞
所以js将执行模式分为两种,第一种同步,可以理解为是按顺序从上往下执行,执行顺序与任务排列顺序是一致同步的,这往往用于一些简单的、快速的、不涉及 IO 读写的操作。
另一种是异步,将代码分为两段,第一段包含对外部数据的请求,第二段代码被写成一个回调函数。第一段代码执行完,将程序的执行权交给其他任务,等到外部数据返回,再由系统通知执行第二段代码。所以程序的执行顺序与任务排列顺序是不一致的、异步的。关于js单线程可以参考另一篇js单线程详解
回调函数
有两个函数fn1和fn2,后者必须等到前者执行完成才执行,可以考虑改写fn1,把fn2写成fn1的回调函数
function fn1(callback){
... // fn1逻辑代码
callback(); // 调用callback
}
fn1(fn2);
回调函数的优点是简单、容易理解和部署,缺点是不利于代码的阅读和维护,各个部分之间高度耦合
事件监听
另一种异步思路是采用事件驱动,任务的执行不取决代码顺序,取决于某个事件是否发生,还是以fn1和fn2为例,这里采用jquery的写法
fn1.on('done', fn2)
当fn1发生done事件,就执行fn2,然后对fn1进行改写
function fn1(){
... // fn1逻辑代码
setTimeout(function(){
fn1.trigger('done')
}, 1000)
}
这个表示fn1在执行完逻辑代码之后,触发’done’事件,系统监听到fn1的done事件,然后fn2执行,这样和回调函数的效果一致。
这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以去耦合,fn1不需要接触fn2,只需要触发done,而done可以绑定fn3,fn4等,切断了fn1和fn2僵硬的联系
发布/订阅
事件完全可以理解为”信号”, 如果存在一个“信号中心”,某个任务执行完成,就向信号中心“发布”(publish)一个信号,其他任务可以向信号中心“订阅”(subscribe)这个信号。这叫做发布/订阅模式,又称“观察者模式”。
首先,fn2向信号中心订阅“done”信号,这里将信号中心设为jQuery
jQuery.subscribe('done', fn2);
然后,对fn1进行改写
function fn1(){
... // fn1的逻辑代码
setTimeout(function(){
jQuery.publish('done');
}, 1000)
}
fn2完成执行后,可以取消订阅
jQuery.unsubscribe('done', fn2);
这个比监听事件好的一点是,可以有更多的主动操作,比如fn2可以随时取消订阅,而通过信号中心这个中间层,可以了解存在多少信号,每个信号有多少订阅者
异步操作的流程控制
如果有多个异步操作,就存在一个流程控制问题:确定执行的顺序
function async(arg, callback){
setTimeout(function(){ callback(arg * 2); }, 1000)
}
上面的async函数模拟一个需要1秒完成的任务,1秒之后才执行回调函数,如果有6个异步任务,每个async都要等上一个async结束之后再执行,最后执行一个final函数
function final(value){
console.log('完成', value);
}
就会出现这样的代码
async(1, function(value){
... // 逻辑代码
async(value, function(value){
//第一个value是正在执行的函数参数
async(value, function(value){
async(value, function(value){
async(value, function(value){
//第六次的时候把final当做回调函数传入
async(value, final);
})
})
})
})
})
用6个回调函数的嵌套,不仅写起来麻烦,容易出错,而且难以维护
串行执行
我们可以编写一个流程控制函数,让它来控制异步任务,一个任务完成以后,再执行另一个
//items数组保存每一个异步任务的参数
var items = [1, 2, 3, 4, 5, 6];
//results数组保存每一个异步任务的运行结果
var results = [];
function series(item){
if(item){
async(item, function(result){
results.push(result);
return series(items.shift())
})
} else {
return final(results);
}
}
series(items.shift());
这样的流程控制预设了每一个异步函数的参数值,虽然容易维护, 但割裂了每一次回调之间的关系,适用在回调函数之间没什么联系的情况下
并行执行
在回调函数之间没什么联系的情况下,也可以让所有异步任务同时执行,等到全部完成之后再执行final函数
var items = [1, 2, 3, 4, 5, 6];
var results = [];
items.forEach(function(item){
async(item, function(result){
results.push(result);
if(results.length == items.length){
final(results);
}
})
})
forEach方法会同时发起6个异步任务,等到它们全部完成以后,才会执行final函数。并行执行的好处是效率较高,比起串行执行一次只能执行一个任务,较为节约时间。但是问题在于如果并行的任务较多,很容易耗尽系统资源,拖慢运行速度。因此有了第三种流程控制方式。
并行与串行的结合
所谓并行与串行的结合,就是设置一个门槛,每次最多只能并行执行n个异步任务。这样就避免了过分占用系统资源
var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];
var running = 0;
var limit = 2;
function launcher() {
while(running < limit && items.length > 0) {
var item = items.shift();
async(item, function(result) {
results.push(result);
running--;
if(items.length > 0) {
launcher();
} else if(running == 0) {
final();
}
});
running++;
}
}
launcher();
变量running记录当前正在运行的任务数,只要低于门槛值,就再启动一个新的任务,如果等于0,就表示所有任务都执行完了,这时就执行final函数。