Event Loop - 事件循环
javascript为什么设计成单线程的?
javascript为什么会被设计成单线程语言?
最初javascript作为浏览器的一种脚本语言使用的,既然是浏览器的脚本语言,那么它的用途就是为了与用户进行交互,以及操作Dom。所以这个需求决定了,javascript在同一时间内只能做一件事情。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完
全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
事件 回调
- "任务队列"是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程读取"任务队列",就是读取里面有哪些事件。
- "任务队列"中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。
- 所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。
- "任务队列"是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,"任务队列"上第一位的事件就自动进入主线程。但是,由于存在后文提到的"定时器"功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。
任务队列

同步任务
在主线程上排队执行的任务,当前一个任务执行完成,下一个任务才会执行。
异步任务
异步任务是指,不进入主线程,直接进入任务队列(task queue),当任务队列通知主线程时,某个一步任务就可以执行了,该任务才会进入主线程。
- 所有的同步任务都在主线程上执行,形成一个 执行栈 (execution context stack)
- 主线程之外,还存在一个‘任务队列(task queue)’,当异步任务有了执行结果,就会在‘任务队列’中添加一个事件,即回调函数。
- 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
- 主线程会不断重复的执行上面的第三步。

除了广义的同步任务和异步任务区分,我们对任务还有更加细致的区分:
微任务与宏任务
借助一个🌰:
这个就像去银行办业务一样,先要取号进行排号。
一般上边都会印着类似:“您的号码为XX,前边还有XX人。”之类的字样。
因为柜员同时职能处理一个来办理业务的客户,这时每一个来办理业务的人就可以认为是银行柜员的一个宏任务来存在的,当柜员处理完当前客户的问题以后,选择接待下一位,广播报号,也就是下一个宏任务的开始。
所以多个宏任务合在一起就可以认为说有一个任务队列在这,里边是当前银行中所有排号的客户。
任务队列中的都是已经完成的异步操作,而不是说注册一个异步任务就会被放在这个任务队列中,就像在银行中排号,如果叫到你的时候你不在,那么你当前的号牌就作废了,柜员会选择直接跳过进行下一个客户的业务处理,等你回来以后还需要重新取号
而且一个宏任务在执行的过程中,是可以添加一些微任务的,就像在柜台办理业务,你前边的一位老大爷可能在存款,在存款这个业务办理完以后,柜员会问老大爷还有没有其他需要办理的业务,这时老大爷想了一下:“最近P2P爆雷有点儿多,是不是要选择稳一些的理财呢”,然后告诉柜员说,要办一些理财的业务,这时候柜员肯定不能告诉老大爷说:“您再上后边取个号去,重新排队”。
所以本来快轮到你来办理业务,会因为老大爷临时添加的“理财业务”而往后推。
也许老大爷在办完理财以后还想 再办一个信用卡?或者 再买点儿纪念币?
无论是什么需求,只要是柜员能够帮她办理的,都会在处理你的业务之前来做这些事情,这些都可以认为是微任务。
这就说明:你大爷永远是你大爷
在当前的微任务没有执行完成时,是不会执行下一个宏任务的。
- 微任务包括:Promise,process.nextTick
- 宏任务包括:整体代码script,setTimeout,setInterval
setTimeout(function() {
console.log('setTimeout');
})
new Promise(function(resolve) {
console.log('promise');
resolve();
}).then(function() {
console.log('then');
})
console.log('console');
- 上面代码作为一个宏任务进入主线程。
- 首先是执行到的是一个
setTimeout
,将其回调函数注册后分发到宏任务Event Queue。 - 下面遇到一个
Promise
,立即执行new Promise
,then
函数放入微任务Event Queue里。 - 遇到
console
,立即执行。到此本段代码的第一个宏任务执行完成。 - 执行微任务,发现
then
函数微任务,执行。 - 第一轮事件循环(Event Loop)执行完成,执行下一个宏任务,发现
setTimeout
,执行 - 结束
promise
console
then
setTimeout

setTimeout setInterval
任务队列中还可以防止定时事件,即setTimeout``setInterval
,制定某些代码在多少时间之后执行。setTimeout``setInterval
两者内部运行机制基本上是一致的,区别在于一个是一次性执行,一个是循环执行。
- setTimeout
setTimeout(() => {
task()
},3000)
sleep(1000000)
上面的代码执行产生的结果并不会在三秒以后执行tash()
,只有当sleep
执行完成之后才会执行task()
,即当第一个宏任务sleep()
执行完成后才会执行第二个宏任务task()
setTimeout(fn,0)
并不会在0秒后立即执行,setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,也就是说,尽可能早得执行。它在"任务队列"的尾部添加一个事件,因此要等到同步任务和"任务队列"现有的事件都处理完,才会得到执行。
HTML5标准规定了setTimeout()的第二个参数的最小值(最短间隔),不得低于4毫秒,如果低于这个值,就会自动增加。
- setInterval
对于执行顺序来说,setInterval
会每隔指定的时间将注册的函数置入Event Queue,如果前面的任务耗时太久,那么同样需要等待。
唯一需要注意的一点是,对于setInterval(fn,ms)
来说,我们已经知道不是每过ms秒会执行一次fn,而是每过ms秒,会有fn进入Event Queue
。一旦setInterval的回调函数fn执行时间超过了延迟时间ms,那么就完全看不出来有时间间隔了。
process.nextTick
上面介绍微任务的时候说了,process.nextTick
属于微任务,那么process.nextTick
这个是什么呢。其实process.nextTick
可以理解为是nodejs中的setTimeout
在事件循环的下一次循环中调用 callback 回调函数。
写在最后
Event Loop
就是主线程从"任务队列"中读取事件,这个过程是循环不断的。