JS单线程
JS是一门单线程的非阻塞的脚本语言,只有一个主线程来处理所有任务。当JS执行一系列任务时,由于JS是单线程的,同一时间只能处理一个任务,于是这些任务就在执行栈中排队,JS会依次执行这些任务。当JS执行一项异步任务(如I/O)时,主线程不会一直等待其返回结果,而是挂起(pending)这个任务,继续执行执行栈中的其他任务,等异步任务返回结果时再根据一定规则去执行相应的回调。当异步任务返回结果时,JS会将这个异步任务加入与当前执行栈不同的另一个队列--事件队列。被加入事件队列的异步任务的回调不会立即被执行,而是等待当前执行栈中的任务执行完毕,主线程闲置时,JS从事件队列中,取出排在第一位的任务,将对应回调放入执行栈,并执行其中的同步代码,如此反复,形成一个无限循环,称为事件循环(Event Loop)。
浏览器的事件循环
上图所示,js中的基本数据与对象都会储存在栈内存中,其中复杂类型数据对象会在堆内存储存其数据结构,栈内存储存的是对这个数据结构的引用。
执行栈
javaScript是单线程,也就是说只有一个主线程,主线程有一个栈。当JS代码执行时,代码会被推入执行栈中进行运行,运行代码的过程中,同步事件会立即执行,其中Dom、Ajax以及SetTimeout等异步事件会注册回调函数,放入事件回调队列中,等同步代码执行完之后执行。这样一个循环便是浏览器的Event Loop。
Macro Task 和 Micro Task
- 宏任务(MacroTask):
包括整体代码script,setTimeout、setInterval、setImmediate、I/O、UI渲染 - 微任务(MicroTask):
Promise、process.nextTick、Object.observe、MutationObserver
在栈内存中代码执行完后,浏览器空闲,立即处理回调队列,将回调队列中的宏任务队列中的事件推入执行栈中执行。
- 首先会执行宏任务,如果宏任务中存在宏任务,则会把该任务放到宏任务队列中。如果该任务里存在微任务,则把微任务放在微任务队列。
- 在这个宏任务执行完后,首先去看微任务队列中是否有任务,然后把微任务推到执行栈中执行。
- 执行完微任务队列,这一次循环就结束了,然后再进行在宏任务队列中进行下一个宏任务,微任务,直至回调队列清空。
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
let promise = new Promise((resolve, reject)=>{
console.log(1)
resolve()
})
promise.then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
执行结果:
循环1:
- 【MacroTask队列:script ;microtask队列:空】
- 首先整个代码被推到执行栈中执行,这是一个宏任务(整个script代码)
- 运行中,同步代码立即执行,new Promise中的fn是立即执行的。setTimeout被放在宏任务队列中,promise1、promise2被放在微任务队列中。
- 【MacroTask队列:setTimeout ;microtask队列:promise1、promise2】
- 宏任务script执行完后,执行微任务队列,取出microtask队列,推入执行栈执行,第一次循环到此结束。
循环2:
- 【MacroTas队列:setTimeout ;microtask队列:空】
- 取出宏任务中的setTimeout推入执行栈执行,如果有微任务则,则被放在微任务队列(这里没有)。
- 宏任务执行完,去微任务队列执行(微任务队列为空)。
- 【MacroTas队列:空 ;microtask队列:空】
- 宏任务队列为空,循环至此结束。
Node.js 的 Event Loop
当 Node.js 启动时,会做这几件事
- 初始化 event loop
- 开始执行脚本(或者进入 REPL,本文不涉及 REPL)。这些脚本有可能会调用一些异步 API、设定计时器或者调用 process.nextTick()
-
开始处理 event loop
nodejs的Event Loop 一共有6个阶段:
各阶段详解
- timers 阶段:这个阶段执行 setTimeout 和 setInterval 的回调函数。
- I/O callbacks 阶段:不在 timers 阶段、close callbacks 阶段和 check 阶段这三个阶段执行的回调,都由此阶段负责,这几乎包含了所有回调函数。
- idle, prepare 阶段(译注:看起来是两个阶段,不过这不重要):event loop 内部使用的阶段(译注:我们不用关心这个阶段)
- poll 阶段:获取新的 I/O 事件。在某些场景下 Node.js 会阻塞在这个阶段。
- check 阶段:执行 setImmediate() 的回调函数。
- close callbacks 阶段:执行关闭事件的回调函数,如 socket.on('close', fn) 里的 fn。
1. timers 此阶段执行由setTimeout()和setInterval()的回调。
- 依次看回调的时间到没到,到了就执行回调函数,没到就往下走到poll阶段停下来。
2. poll 轮训阶段执行 文件、网络请求等 除了timers 以外的事情
- 重复检查定时器有没有到时间
- 时间到了就经过 poll --->check --->timers 这样一个阶段去处理定时器
- 假如在等待 setTimeout()的过程中,进来一个文件读完了的消息,就处理这个回调函数;在处理这个回调函数的时间过长,则将错过 settimeout()的执行时间。
栗子:
const fs = require('fs');
function someAsyncOperation(callback) {
// 假设需要95ms才能完成
fs.readFile('/path/to/file', callback);
}
const timeoutScheduled = Date.now();
setTimeout(function() {
const delay = Date.now() - timeoutScheduled;
console.log(delay + 'ms have passed since I was scheduled');
}, 100);
// 100ms执行
// 执行一个耗时95ms的异步操作
someAsyncOperation(function() {
const startCallback = Date.now();
// 执行一个耗时 10ms 的同步操作
while (Date.now() - startCallback < 10) {
// 什么也不做
}
});
// 在 95ms 的时候执行同步操作 需要 10ms
// 在 100ms 的时候正在执行 同步操作 ,执行不了 settimeout 这个定时器
// 等同步操作执行完了 在执行 settimeout 这个定时器,需要 105ms 才能执行
当 event loop 进入 poll 阶段,发现 poll 队列为空(因为文件还没读完),event loop 检查了一下最近的计时器,大概还有 100 毫秒时间,于是 event loop 决定这段时间就停在 poll 阶段。在 poll 阶段停了 95 毫秒之后,fs.readFile 操作完成,一个耗时 10 毫秒的回调函数被系统放入 poll 队列,于是 event loop 执行了这个回调函数。执行完毕后,poll 队列为空,于是 event loop 去看了一眼最近的计时器(译注:event loop 发现卧槽,已经超时 95 + 10 - 100 = 5 毫秒了),于是经由 check 阶段、close callbacks 阶段绕回到 timers 阶段,执行 timers 队列里的那个回调函数。这个例子中,100 毫秒的计时器实际上是在 105 毫秒后才执行的。
check 处理setImmediate()的回调。
- setTimeout() 最小间隔时间是 4ms 。(chrome可能不一样,不绝对)
- 原则上 setImmediate()会比 setTimeout()先执行
- 由于setTimeout()有一个最小间隔时间4ms,如果进入的时间大于4ms,Event Loop则会发现定时器时间过了就会马上执行 setTimeout();如果进入的时间小于4ms,Event Loop则会发现定时器时间还没到就会去往下一个阶段poll,等时间到了,就要去执行 setTimeout(), 再去执行 setTimeout()的过程中会经过check阶段,就会先执行setImmediate()。这就是为什么有时候 setTimeout()会先执行。
栗子:
// timeout_vs_immediate.js
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
//$ node timeout_vs_immediate.js
//timeout
//immediate
//$ node timeout_vs_immediate.js
//immediate
//timeout
nextTick() 不属于Event Loop的一部分,nextTick()会先于所有的回调执行。
setTimeout(()=>{
console.log('setTiomeout')
},0)
setInmediate(()=>{
console.log('setInmediate')
})
proces.nextTick(()=>{
console.log('nextTick')
})
上述代码中nextTick先于其它两个执行,Vue中有Vue.nextTick()方法就是类似的思想。
参考:
Event Loop、计时器、nextTick
网友评论