JavaScript是一门单线程的语言,所谓"单线程",就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务。
看下面一段代码在浏览器中的执行的结果:
console.log( "1" );
alert("2");
console.log( "3" );
可以看到当打开浏览器时,先输出了1,然后弹出2,点击确定才输出3(alert是同步的,会阻塞任务的执行,只有点击确定时才会继续往下执行)。
在JavaScript中,所有的任务,可以分成两类:同步任务(synchronous)和异步任务(asynchronous)。
同步任务是那些没有被引擎挂起、在主线程上排队执行的任务。只有前一个任务执行完毕,才能执行后一个任务。就像打电话一样,一个手机只能接一个电话,第二个电话想要接进来,就必须等第一个电话结束。
异步任务是那些被引擎放在一边,不进入主线程、而进入任务队列的任务。只有引擎认为某个异步任务可以执行了(比如 Ajax 操作从服务器得到了结果),该任务(采用回调函数的形式)才会进入主线程执行。排在异步任务后面的代码,不用等待异步任务结束会马上运行,也就是说,异步任务不具有“堵塞”效应。
看下面一段代码在浏览器中的执行的结果:
console.log("1");
setTimeout(function () {
console.log("2");
}, 500);
console.log("3");
定时器是异步任务,输出3时并没有等定时器执行完毕就输出了。
JavaScript 运行时,除了一个正在运行的主线程,引擎还提供一个任务队列(task queue),里面是各种需要当前程序处理的异步任务。(实际上,根据异步任务的类型,存在多个任务队列。为了方便理解,这里假设只存在一个队列。)
首先,主线程会去执行所有的同步任务。等到同步任务全部执行完,就会去看任务队列里面的异步任务。如果满足条件,那么异步任务就重新进入主线程开始执行,这时它就变成同步任务了。等到执行完,下一个异步任务再进入主线程开始执行。一旦任务队列清空,程序就结束执行。
异步任务的写法通常是回调函数。一旦异步任务重新进入主线程,就会执行对应的回调函数。如果一个异步任务没有回调函数,就不会进入任务队列,也就是说,不会重新进入主线程,因为没有用回调函数指定下一步的操作。
JavaScript 引擎怎么知道异步任务有没有结果,能不能进入主线程呢?答案就是引擎在不停地检查,一遍又一遍,只要同步任务执行完了,引擎就会去检查那些挂起来的异步任务,是不是可以进入主线程了。这种循环检查的机制,就叫做事件循环(Event Loop)。维基百科的定义是:“事件循环是一个程序结构,用于等待和发送消息和事件(a programming construct that waits for and dispatches events or messages in a program)”
看下面一段代码在浏览器中的执行的结果:
console.log("1");
setTimeout(function () {
console.log("2");
}, 0);
console.log("3");
alert("4");
setTimeout(function () {
console.log("5");
}, 0);
console.log("6");
可以看到2开始没有输,出因为它是异步任务,不在主程序而在任务队列,虽然定时器设置的是0秒后执行,但是在主程序执行完前它是不会执行的。而弹出提示框4是同步操作,没有点击确定前,会阻塞主程序里的任务执行。点击确定后继续执行主程序输出6,到此主程序执行完毕,开始执行任务队列的任务输出2和5。
实现异步编程最简单基本的方法。有两个函数fun1和fun2,后者需等待前者的执行完毕后才能执行。
function fun1(){
setTimeout(function(){
console.log("1")
},500)
}
function fun2(){
console.log("2")
}
fun1();
fun2();
// 2,1
上面代码的问题在于,如果fun1
是异步操作,fun2
会立即执行,不会等到fun1
结束再执行
function fun1(callback) {
setTimeout(function () {
console.log("1");
callback();
}, 500);
}
function fun2() {
console.log("2");
}
fun1(fun2);
回调函数的优点是简单、容易理解和实现,缺点是不利于代码的阅读和维护,各个部分之间高度耦合(coupling),使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套的情况),而且每个任务只能指定一个回调函数。
采用事件驱动模式。异步任务的执行不取决于代码的顺序,而取决于某个事件是否发生
//这里采用的是jQuery的写法
fun1.on('done', fun2);
//当fun1发生done事件,就执行fun2。然后,对fun1进行改写:
function fun1(){
setTimeout(function () {
// fun1的任务代码
fun1.trigger('done');
}, 1000);
}
上面代码中,fun1.trigger('done')
表示,执行完成后,立即触发done
事件,从而开始执行fun2
。
这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以去耦合(decoupling),有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。阅读代码的时候,很难看出主流程。
事件完全可以理解成“信号”,如果存在一个“信号中心”,某个任务执行完成,就向信号中心“发布”(publish)一个信号,其他任务可以向信号中心“订阅”(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做”发布/订阅模式”(publish-subscribe pattern),又称“观察者模式”(observer pattern)。
以下代码输出什么
for(var i = 0; i < 5 ;i++){
setTimeout(function(){
console.log(i)
},500)
}
异步任务会在任务队列等待主程序执行完毕才执行, i = 5
主程序执行完毕,执行任务队列,此刻输出i= 5,所以答案为5个5。
参考资料:《JavaScript 语言入门教程》