众所周知,JavaScript是单线程,同一时刻只会有一段代码在运行。JavaScript又具有异步的特性,这二者是否冲突呢。JavaScript又是怎样在单线程的情况下执行异步代码的呢?
但凡单线程且具备异步特性的语言,都是event-driven(事件驱动)的,而驱动这个事件(event-handler)的平台即驱动平台。浏览器就是一个驱动平台,JavaScript虽然是单线程的,但是浏览器是多线程的,浏览器通过暴露给JavaScript一些API(WebAPI),来实现异步的功能。JavaScript里面常用到的setTimeout、Ajax、DOM Events就是WebAPI。
浏览器有两个核心部分,渲染引擎和JavaScript引擎。JavaScript主线程执行时候,产生对和栈。JavaScript异步的实现过程描述如下:
- 程序中的代码一次进入栈,等待执行,执行完毕后出栈。
- 当执行到setTimeout等WebAPI时,将setTimeout方法压栈,浏览器内核的其他线程开始执行WebAPI的callback,setTimeout出栈。
- JavaScript主线程继续执行后续代码(完成进栈出栈的过程),同时浏览器处理callback方法,当callback达到触发条件时,方法被添加到任务队列。
- 栈中代码执行完毕后,主线程去查看任务队列中,是否有callback等待执行,如果有,将callback方法压入栈中,执行完毕后出栈。
以下代码
alert(1);
setTimeout("alert(2)", 0);
for(var i = 3; i < 30; i++) {
alert(i);
}
运行,会发现,弹出1后并不会马上弹出2,而是先弹出3-30,然后在弹出2,按照以上介绍的异步实现过程可以,程序会先执行alert(1),然后执行setTimeout方法,setTimeout方法是WebAPI,所以浏览器的其他线程会处理callback,callback方法0秒后就被触发了,放入任务队列,但是此时,JavaScript后续代码还没有执行完毕,JavaScript主线程会继续执行后续代码至全部执行完毕,此时栈为空,主线程才会将堆中的callback方法压入栈中执行,所以setTimeout 0的callback不一定会0秒后执行。
以演讲中的示例进一步说明(转载地址
http://www.alloyteam.com/2015/10/turning-to-javascript-series-from-settimeout-said-the-event-loop-model/#prettyPhoto)
以图中代码为例,执行引擎开始执行上述代码时,相当于先讲一个main()方法加入执行栈。继续往下开始console.log('Hi')时,log('Hi')方法入栈,console.log方法是一个webkit内核支持的普通方法,而不是前面图中WebAPIs涉及的方法,所以这里log('Hi')方法立即出栈被引擎执行。
console.log('Hi')语句执行完成后,log()方法出栈执行,输出了Hi。引擎继续往下,将setTimeout(callback,5000)添加到执行栈。setTimeout()方法属于事件循环模型中WebAPIs中的方法,引擎在将setTimeout()方法出栈执行时,将延时执行的函数交给了相应模块,即图右方的timer模块来处理。
执行引擎将setTimeout出栈执行时,将延时处理方法交由了webkit timer模块处理,然后立即继续往下处理后面代码,于是将log('SJS')加入执行栈,接下来log('SJS')出栈执行,输出SJS。而执行引擎在执行万console.log('SJS')后,程序处理完毕,main()方法也出栈。