1. 什么是异步
当我们在开发程序时往往需要管理现在运行的代码和将来要运行的代码之中产生的时间间隙,比如当我们等待程序用户输入、通过网络请求或发送数据等待返回或者固定时间间隙执行重复任务时。事实上,程序中现在运行的部分和将来运行的部分之间的关系就是异步编程的核心。
2. Javascript事件循环
JavaScript 语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。为了协调事件、用户交互、UI 渲染和网络处理等行为,防止主线程的不阻塞,也就是说为了实现异步机制Javascript采用了事件循环的方式。
Javascript本身并没有时间概念,它的事件循环由宿主环境实现,一般是浏览器,这些宿主环境会有一个线程来对js代码进行调度,实现事件循环。在node.js中则是node通过libuv来实现。事件循环是一个持续循环的过程,过程中的每一轮称为一个tick。对于一个tick而言,当发现循环队列中有任务待执行时,便会取出该任务执行。需要注意的是,setTimeout是在设定的时间之后将任务插入事件队列,所以当事件队列中有其它任务时,该setTimeout任务并不会立即执行。
在ES6中,JavaScript提出一个新的概念建立在事件循环之上,叫做任务队列(job queue),这个影响最大的就是Promise的异步特性。
我们来梳理一下,事件循环是Javascript依赖于宿主环境实验的一套任务调度,它处理了事件IO,setTimeout等异步任务。而任务队列是ES6提出的概念,主要服务于Promise。一般我们把事件循环队列中的任务称作宏任务,而任务队列的任务称为微任务。整个事件循环可以理解为先执行循环队列中的宏任务,然后如果任务队列中有微任务,则全部执行。接下来执行下一轮宏任务。
3. 回调函数
在异步过程中,未来执行的函数块,我们称为回调函数,因为它们在事件循环中是回头调用到程序中的函数。可以说回调函数是Javascript异步模式的基础,甚至复杂高深的Javascript程序所依赖的异步基础也是回调函数。
但是,随着Javascript越来越成熟,对于异步函数的发展,回调函数已经不够用了。因为它主要存在以下几个问题:
1. 大脑对于事情的计划方式是线性的、阻塞的、单线程的语义,但是回调表达异步流 程的方式是非线性的、非顺序的,这使得正确推导这样的代码难度很大。难于理解的代码 是坏代码,会导致坏 bug。
考虑这样一段代码:
listen( "click", function handler(evt){
setTimeout( function request(){
ajax( "http://some.url.1", function response(text){
if (text == "hello") {
handler();
} else if (text == "world") {
request();
}
});
}, 500) ;
});
这样的代码我们一般称为"回调地狱"。我们可以抽象一下这个调用
doA( function(){
doB();
doC( function(){
doD();
})
doE();
});
doF(); 我们当然可以凭借我们的经验和知识发现其正确的执行顺序是A->F->B->C->E->D 所以纯回调的写法使代码区别于大脑的思维方式,使得代码非常复杂且难以理解,从而变得无法维护。
2. 也是更重要的一点,回调会受到控制反转的影响,因为回调暗中把控制权交给第三方(通常是不受你控制的第三方工具)来调用你代码中的回调函数。这种控制转移导致一系列麻烦的信任问题,比如回调被调用的次数是否会超出预期。 这类问题我们称为控制反转。
所以我们需要一个通用的方案来解决这些信任问题。不管我们创建多少回调,这一方案都应可以复用,且没有重复代码的开销。我们需要比回调更好的机制。到目前为止,回调提供了很好的服务,但是未来的 JavaScript 需要更高级、功能更强大的异步模式。该系列的后续文章会来介绍几种新的异步技术,下一篇会介绍Promise方案以及该方案的实现方式。