如何表达和控制持续一段时间的程序行为。
这不仅仅是指从 for 循环开始到结束的过程,当然这也需要持续一段时间(几微秒或几毫秒)才能完成。它是指程序的一部分现在运行,而另一部分则在将来运行—现在和将来之间有段间隙,在一段间隙中,程序没有活跃执行。
实际上,所有重要的程序(特别是 JavaScript 程序)都需要通过这样或那样的方法来管理这段时间间隙,这时可能是在等待用户输入,从数据库或文件系统中请求数据,通过网络发送数据并等待响应,或者是以固定时间间隔执行重复任务(比如动画)在诸如此类的场景中,程序都需要管理这段时间间隙的状态。
程序中现在运行的部分和将来运行的部分之间的关系就是异步编程的核心。
分块的程序
可以把 JavaScipt 程序写在单个 .js 文件中,但是这个程序几乎一定由多个块构成的。这些块中只有一个是现在执行,其余的则会在将来执行。最常见的块单位是函数。
var data = ajax("http://some.rul.1");
console.log(data)
//data 通常不会包含 ajax结果
标准 Ajax 请求不是同步完成的,这意味着 ajax()函数还没有返回任何值可以赋给变量 data。如果 ajax()能够阻塞到响应返回,那么 data = … 赋值就会正确工作。
可能您已经听说过,可以发送同步 ajax 请求。尽管技术上说是这样,但是,在任何情况下都不应该使用这种方式,因为它会锁定浏览器 ui (按钮,菜单,滚动条等),并阻塞所有的用户交互。这是个可怕的想法,一定要避免。
任何时候,只要把一段代码包装成一个函数,并指定它在响应某个事件(定时器,鼠标点击,ajax响应等)时执行,您就是在代码中创建了一个将来执行的块,也由此在这个程序中引入了异步机制。
事件循环
JavaScript 引擎并不是独立运行的,它运行在宿主环境中,多大多数开发者来说通常就是 web浏览器。经过最近几年的发展,JavaScript 已经超出了浏览器的范围,进入了其他环境,比如通过像 Node.js 这样的工具进入服务器领域。实际上,JavaScript 现如今已经嵌入到了从机器人到电灯泡等各种各样的设备中。
setTimeout() 并没有把您的回调函数挂在事件循环队列中。它所做的是设定一个定时器。当定时器到时后,环境会把您的回调函数放在事件循环中,这样,在未来某个时刻的 tick 会摘下并执行这个回调。
如果事件循环有 20 项目会怎么样呢?您的回调就会等待。它得排在项目后面 — 通常没有抢占式的方式支持直接将其排到队首。这也解释了为什么 setTimeout() 定时器的精度可能不高。
ES6精确指定事件循环工作细节,这意味着不是只由宿主环境来管理。这个改变的一个主要原因是 ES6 中 Promise 的引入。
并行线程
异步是关于现在和将来的时间间隙,而并行是关于能够同时发生的事情。
并行计算最常见的工具就是进程和线程。进程和线程独立运行,并可能同时运行:在不同的处理器,甚至不同的计算机上,但多个线程能够共享单个进程的内存。
与之相对的是,事件循环把自身的工作分成一个个任务并顺序执行,不允许对共享内存的并行访问和修改。
多线程编程是复杂的
JavaScript 从不跨线程共享数据,但是这并不意思着 JavaScript 总是不确定的
不确定性是在函数(事件)顺序级别上,而不是多线程情况下的语句顺序级别
并发
两个或多个“进程”同时执行就出现了并发,不管组成它们的单个运算是否并行执行(在独立的处理器或处理器核心上同时运行)。可以把并发看作 “进程”级(或者任务级)的并行,与运算级的并行(不同处理器上的线程)相对。
这里的进程之所以打上引号,是因为这并不是计算机科学意义上的真正操作系统级进程。这是虚拟进程,或者任务,表示一个逻辑上相关的运算序列。之所以使用“进程” 而不是 “任务”,是因为从概念上来讲,“进程”的定义更符合这里我们使用的意义。
var a,b;
function foo(x){
a = x * 2;
baz();
}
function bar(y){
b = y * 2;
baz();
}
function baz(){
console.log(a + b)
}
ajax("http://some.url.1",foo);
ajax("http://some.url.2",bar);
baz二次调用就没有问题,因为这时候 a 和 b 都已经可用了。
var a,b;
function foo(x){
a = x * 2;
if(a && b){
baz();
}
}
function bar(y){
b = y * 2;
if(a && b){
baz()
}
}
function baz(){
console.log(a + b)
}
ajax("http://some.url.1",foo);
ajax("http://some.url.2",bar);
协作
并发协作。这里的重点不再是通过共享作用域中的值进行交互,是取到一个长期运行的 “进程” ,并将其分割成多个步骤或多批任务,使得其他并发 “进程” 有机会将自己的运算插入到事件循环队列中交替运行。
var res = [];
function response(data{
res = res.concat(
data.map(function(val){
return val * 2
})
)
})
ajax("http://some.url.1",response);
ajax("http://some.url.2",response);
如果有 1000 万条记录的话,就可能需要运行相当一段时间了,
所以要创建一个协作性更强更友好且不会霸占事件循环队列的并发系统,您可以异步地批处理这些结果。每次处理之后返回事件循环,让其他等待事件有机会运行。
var res = [];
function response(data{
//一次处理 1000 个
var chunk = data.splice(0,1000);
//添加到已有的 res 组
res = res.concat(
chunk.map(function(val){
return val * 2
});
// 还有剩下的需要处理吗?
if(data.length > 0){
//异步调度下一次批处理
setTimeout(function({
response(data);
},0)
}
)
res = res.concat(
data.map(function(val){
return val * 2
})
)
})
ajax("http://some.url.1",response);
ajax("http://some.url.2",response);
当然,我们并没有协调这些 “进程” 的顺序,所以结果的顺序是不可预测的。
这里使用 setTimeout 进行异步调度,基本它的意思就是 “把这个函数插入到当前事件循环队列的结尾处”
两个连续的 setTimeout 调用不能保证会严格按照调用顺序处理
在 Node.JS 中,类似的方法是 process.nextTick()。尽管它们使用方便(通常性能也更高),但并没有(至少到目前为止)直接的方法可以适应所有环境来确保异步事件的顺序。
任务
在 ES6 中,有一个新的概率建立在事件循环队列上,叫作任务队列。这个概念给大家带来的最大影响可能是 Promise 的异步特性
没有 公开API 。(:з」∠)
概念
它是挂在事件循环队列的每个 tick 之后的一个队列。在事件循环的每个 tick 中,
可能出现的异步动作不会导致一个完整的新事件添加到事件循环队列中,
而会在当前 tick 的任务队列添加一个项目(一个任务)。
这就像是在说:“哦,这里还有一件事将来要做,但要确保在其他任何事情发生之前就完成它”
事件循环队列类似于一个游乐园游戏,玩过了一个游戏之后,您需要重新到队尾排队才能再玩一次。而任务队列类似于玩过了游戏之后,插队接着继续玩。
尽管 JavaScript 语义让我们不会见到编译器语句重排序可能导致的噩梦,这是一种幸运,但是代码编写的方式(从上到下的模式)和编译后执行的方式之间的联系非常脆弱,理解这一点也非常重要。
实际上,javaScript 程序总是至少分为两个块:第一块现在运行;下一块将来运行,以响应某个事件。尽管程序是一块一块执行的,但是所有这些块共享对程序作用域和状态的访问,所以对状态的修改都是在之前累积的修改之上进行的。
一旦有事件需要运行,事件循环就会运行,直到队列清空。事件循环的每一轮称为一个 tick。用户交互,io,和定时器会向事件队列中加入事件。
任意时刻,一次只能从队列中处理一个事件。执行事件的时候,可能直接或间接地引入一个或多个后续事件。
并发是指两个或多个事件链随时间发展交替执行,以至于从更高的层次来看,就像是同时在运行(尽管在任意时刻只处理一个事件)