原文:http://www.ruanyifeng.com/blog/2014/10/event-loop.html
为什么JavaScript是单线程
JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程再某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,运行JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
任务队列
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。多线程可以将任务放到不同的线程中去处理。CPU的调度单位是线程,它会在不同的线程之间切换,任务是隶属于线程的。
如果一个线程中,任务排队是因为计算量大,CPU忙不过来,倒也算了。但是很多时候CPU处理一个线程时是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等结果出来,任务才能往下执行。
JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回头,把挂起的任务继续执行下去。
于是,所有任务可以分成两种,一种是同步任务,另一种是异步任务。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入“任务队列”的任务,只有“任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
注:在理解上面的内容时,不要将CPU处理其他进程(比如QQ等)考虑进来,上面内容是针对CPU处理一个线程来说的。在一个单线程中(比如一个页面加载过程中的js处理),会包含许多任务。在任务很多的情况下,想要不被阻塞,采用的方案是异步任务+事件轮询。多线程是将任务分到不同的线程中去,各个线程内部采用的是同步任务。
事件和回调函数
“任务队列”是事件队列(也可以理解成消息队列),表示相关的异步任务可以进入“执行栈”了。主线程读取“任务队列”,这个过程是循环不断的。
“任务队列”中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入“任务队列”,等待主线程读取。
异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。
Event Loop
主线程从“任务队列”中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件轮询)。
上图中,主线程运行的时候,产生堆(heap)和栈(stack)。只要栈中的代码执行完毕,主线程就会去读取“任务队列”,依次执行那些事件所对应的回调函数。
任务队列中的任务,是“回调函数”指定的,然后通过事件触发添加进去的,是一种异步任务。而在页面初始化时,主线程执行的任务是同步任务。
需要注意的是,setTimeout()只是将事件插入了"任务队列",必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行。
Node.js的Event Loop
Node.js也是单线程的Event Loop,但是它的运行机制不同于浏览器环境。
根据上图,Node.js的运行机制如下。
- V8引擎解析JavaScript脚本
- 解析后的代码,调用Node API
- libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop,以异步的方式将任务的执行结果返回给V8引擎。
- V8引擎再将结果返回给用户
除了setTimeout和setInterval这两个方法,Node.js还提供了另外两个与“任务队列”有关的方法:process.nextTick和setImmediate。它们可以帮助我们加深对“任务队列”的理解。
process.nextTick方法可以在当前“执行栈”的尾部,下一次Event Loop(主线程读取“任务队列”)之前,触发回调函数。也就是说,它指定的任务总是发生在所有一般任务之前。
setImmediate方法则是在当前“任务队列”的尾部添加事件,也就是说,它指定的任务总是在下一次Event Loop时执行,这与setTimeout很像。