前言
在Javascript这样类型的语言中编程最重要但最常被人误解的部分之一,就是如何控制在一段时间内程序的行为次序.同时,JavaScript中的异步,也经常被人和并行搞混.今天,我们来谈一下JavaScript中的异步.
自
JS开始以来,异步编程一直存在.然而,但是大多数JS开发人员从未真正仔细考虑过在程序中如何以及为何出现问题,也没有去探索各种其他处理方法。 比较好的方法一直是稀里糊涂的使用回调函数.到今天为止,许多人会坚持认为回调使用起来就已经绰绰有余了.
什么是异步?
首先,一段JavaScript程序是由多个块(chunk)组成的,最常见的块就是function–函数.
我们把一段时间内,程序要执行的任务分为两部分:
1.执行部分(现在执行的),2.等待部分(剩下的将来要执行的).而我们面临的问题是,当现在执行部分执行完后,程序并不是严格地立马去完成等待部分.换句话就是,这些块是异步执行的.我们不会像预期的那样阻塞地完成一个接一个的任务.
例如:
//ajax是某些JavaScript框架(如:jQurey)中实现Ajax的函数
let data =ajax( "http://some.url.1" );
//控制台输出data内容
console.log(data)
如果运行这段JavaScript代码会发现,打印出来的data通常没有我们想要的ajax请求结果.
这是因为
,Ajax请求并不是同步(synchronously,相对于异步asynchronously)完成的,当执行console.log()的时候,我们想要的data还没有返回.我们想要的其实是ajax(...)函数能够阻塞,一直到请求结果返回,最简单的解决方法就是回调(callback).
//回调方式的一个示例,具体回调方式根据具体来定.
ajax( "http://some.url.1",functionmyCallbackFunction(data){
console.log( data );
} );
这时我们会发现,data就是我们想要的了.
注意:我们是可以同步地请求
Ajax的,比如:jQurey中的ajax()将async: false加入设置.但是这样做的后果就是浏览器的UI操作(按钮,滚动等)以及用户交互等都会被阻塞等待锁死.我们应该避免这种情况,一团乱麻的回调函数也不应成为使用同步Ajax的理由.
记下来我们再考虑另一个例子帮助理解:
functionnow() {
return21;
}
function later() {
answer=answer* 2;
console.log("answer:",answer);
}
varanswer= now();
setTimeout(later, 1000 ); // answer: 42
我们再用刚才的思路去理解这个程序:分为两个部分:执行部分,等待部分.
执行部分是:
//回调方式的一个示例,具体回调方式根据具体来定.
ajax( "http://some.url.1",functionmyCallbackFunction(data){
console.log( data );
} );
等待部分就是later()中的内容:
answer=answer* 2;
console.log( "answer:",answer);
执行部分会立刻执行,而setTimeout(...)会设定一个事件(timeout事件),在1000ms后执行later().就像这样,每当我们在function中写一段代码,并让它在事件(timer,鼠标事件,Ajax响应等)响应后执行,我们就创造了一个等待部分,也就是在程序中使用了异步.
Event Loop
虽然我们在这里谈异步,但是,直到ES6*,JavaScript本身并没有内置异步的概念.听起来很震惊,但事实确实是这样的.我们会问:那我们讨论的异步是怎么实现的呢??
我们都知道的是
JavaScript引擎从来不是独立执行,总要依赖于一个环境,比如,我们最熟悉的web浏览器.以及服务器上的Node.js.这些环境会用一个机制来随时间使用JavaScript引擎处理我们的多个程序块,这个机制我们管它叫Event Loop.
换句话说,
JavaScript引擎并不知道什么时候执行,而是被执行环境的线程来安排处理哪些程序块,执行环境根据事件来调度JavaScript引擎处理.
那么什么是
Event Loop呢?
我们通过一段伪代码来了解它的概念:
//eventLoop是事件排成的先进先出的队列(queue)
vareventLoop= [ ];
var event;
while(true) {
// 处理完一个事件
if (eventLoop.length > 0) {
// 获取队列中的下个动作
event=eventLoop.shift();
// 处理刚才取出的动作
try {
event();
}
catch (err) {
reportError(err);
}
}
}
我们通过这段伪代码大体了解它的机制.我们有一个循环,循环的每一个迭代中,如果在等待队列中存在事件,就会被取出并处理,event()就是各种回调函数.
因此,到这儿我们就可以明白了,
setTimeout(..)不是把设定好的回调函数安排到event loop中,而是将一个计时器(timer)安排在event loop中,当计时器到期,执行环境将回调推入event loop,这样,在将来某个时间会被取出并执行.
假如,现在
event loop中已经存在20个等待的成员,那么这个回调就应该等待,通常没有方法能将他移动到队列头部,让他立马执行.这样就产生了,哪怕用了setTimeout(..),指定的回调并不会在指定时间后立即执行的现象,当然也不会提前,至于是否要等待,等待多久,要根据具体情况来说.
注意:之所以说是”直到ES6”,是因为ES6引入了Promise机制,ES6通过Promise将event loop的工作机制纳入到了JavaScript引擎的工作范围,而不只是执行环境的工作.关于Promise以后有机会再谈.
并行
有一个常见的现象就是,人们经常把”异步”和”并行”混为一谈,其实他们大不相同.”异步”,指的是执行部分和等待部分中间有时间差,并不是立即执行.而并行则是指一起执行.
并行计算中最常见的单位是
进程(process)和线程(thread),进程和线程之间可以是独立执行,也可以在一个处理器中,或者一台电脑中同时执行.通常,多个线程可以共享单个进程的内存.
相比之下
,event loop是将一个工作分解成多个任务,并组成队列串行执行,不能并行访问和更改共享的内存.它的并行性和”串行性”可以在不同线程下的event loop上体现(一个线程可以创立一个event loop,不同线程下的event loop具有并行性,单个event loop具有串行性).
并行地
执行线程和异步地交错处理事件在粒度级别上有着很大的不同.线程是表达式操作级别,而异步是函数级别。