JavaScript事件循环(Event Loop)机制
JavaScript 是单线程单并发语言
-
什么是单线程
主程序只有一个线程,即同一时间片断内其只能执行单个任务。
-
为什么选择单线程?
JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。
-
单线程意味着什么?
单线程就意味着,所有任务都需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就需要一直等着。这就会导致
IO操作(耗时但cpu闲置)
时造成性能浪费的问题。 -
如何解决单线程带来的性能问题?
答案是异步!主线程完全可以不管IO操作,暂时挂起处于等待中的任务,先运行排在后面的任务。等到IO操作返回了结果,再回过头,把挂起的任务继续执行下去。于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)
注: 当主线程阻塞时,任务队列仍然是能够被推入任务的
事件循环(Event Loop)
-
JavaScript 内存模型
讲事件循环之前,先看一张下网上看到的 JavaScript 内存模型,相信看完这个会对事件循环机制有一种豁然开朗的感觉。
- 调用栈(Call Stack):用于主线程任务的执行
- 堆(Heap): 用于存放非结构化数据,譬如程序分配的变量与对象
- 任务队列(Queue): 用于存放异步任务与定时任务。
-
JavaScript 代码执行机制:
- 所有同步任务都在主线程上的栈中执行。
- 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
- 一旦"栈"中的所有同步任务执行完毕,系统就会读取"任务队列",选择出需要首先执行的任务(由浏览器决定,并不按序)。
-
Event Loop
现在我们来聊事件循环。事件循环顾名思义它就是一个循环,主线程会不断循环执行上面的第三步,其基本的代码逻辑如下所示:
while (queue.waitForMessage()) { queue.processNextMessage(); }
-
常见异步任务进入任务队列时机
行为 时机 DOM操作 在用户点击等操作事件完成后 网络操作(Ajax等) 在网络操作响应后 定时器 在规定时间到达后 事件循环机制图解:
任务
-
MacroTask(Task)
setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering -
MicroTask(在ES2015规范中称为Job)
process.nextTick, Promise, Object.observe, MutationObserver
规范:
- 每个浏览器环境,至多有一个event loop。
- 一个event loop可以有1个或多个task queue,而仅有一个 MicroTask Queue。
- 一个task queue是一列有序的task, 每个task定义时都有一个task source,从同一个task source来的task必须放到同一个task queue,从不同源来的则被添加到不同队列。
- tasks are scheduled,所以浏览器可以从内部到JS/DOM,保证动作按序发生。
- Microtasks are scheduled,Microtask queue 在当前 task queue 的结尾执行。microtask中添加的microtask也被添加到Microtask queue的末尾并处理。
注: event loop的每个turn,是由浏览器决定先执行哪个task queue。这允许浏览器为不同的task source设置不同的优先级,比如为用户交互设置更高优先级来使用户感觉流畅。
示例
function ELoop() {
// 当前任务
let p = new Promise((resolve, reject)=>{
console.log("current Task")
resolve();
});
let nextP;
setTimeout(()=>{
console.log("MacroTask_1");
nextP.then(()=>{
// 第一次执行时,这段代码并没有执行到。
console.log("MicroTask_promise_1"); //第一个MicroTask
})
console.log("MacroTask_1 end")
}, 0) // 第一个 MacroTask
setTimeout(()=>{
console.log("MacroTask_2");
console.log("MacroTask_2 end")
}, 0)// 第二个MacroTask
nextP = p.then(()=>{
console.log("MicroTask_promise_2"); //第一个MicroTask
console.log(1)
}).then(()=>{
console.log("MicroTask_promise_3"); // 第二个MicroTask
console.log(1)
})
console.log("current Task end")
}
ELoop();
/**输出结果:
current Task
MicroTask_promise_2
MicroTask_promise_3
MacroTask_1
MicroTask_promise_1
MacroTask_2
**/
参考文献:
从Promise来看JavaScript中的Event Loop、Tasks和Microtasks