1. JS的单线程概念
-
JavaScript语言特点就是单线程,其主要用途是与用户互动和DOM操作。因此它只能是单线程,否则会带来复杂的同步问题。
-
为了利用多核CPU的计算能力,HTML5提出
Web Worker
标准,允许JavaScript
脚本创建多个线程,但这并不是真正意义上的多线程,其子线程由主线程完全控制,且子线程不能对DOM进行操作。 -
js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。这个循环机制又称为Event Loop(事件循环)
-
同步任务都在主线程上执行,形成一个执行栈(execution context stack);如果在微任务执行期间微任务队列加入了新的微任务,会将新的微任务加入队列尾部,之后也会被执行;
2. 任务队列
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
2.1 同步任务&异步任务
任务可以分成两种:
- 同步任务 (synchronous):在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
- 异步任务 (asynchronous):不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
此时,js运行机制如下:(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)
- (1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
- (2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
- (3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
- (4)主线程不断重复上面的第三步。
只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。
哪些是异步任务
- setTimeout 和 setInterval
- DOM 事件
- Promise
- 网络请求
- I/O
2.2 宏任务 & 微任务
但按照异步和同步的划分方式,并不准确,更准确的任务划分方式: 宏任务 & 微任务
宏任务(Macrotask) | 微任务(Microtask) |
---|---|
setTimeout | requestAnimationFrame(有争议) |
setInterval | MutationObserver(浏览器环境) |
MessageChannel | Promise.[ then/catch/finally ] |
I/O,事件队列 | process.nextTick(Node环境) |
setImmediate(Node环境) | queueMicrotask |
script(整体代码块) |
按照这种分类方式,JS的执行机制是
- 执行一个宏任务,过程中如果遇到微任务,就将其放到微任务的【事件队列】里
- 当前宏任务执行完成后,会查看微任务的【事件队列】,并将里面全部的微任务依次执行完
示例:
setTimeout(function(){
console.log('定时器开始啦')
});
new Promise(function(resolve){
console.log('马上执行for循环啦');
for(var i = 0; i < 10000; i++){
i == 99 && resolve();
}
}).then(function(){
console.log('执行then函数啦')
});
console.log('代码执行结束');
执行结果:【马上执行for循环啦 — 代码执行结束 — 执行then函数啦 — 定时器开始啦】
首先执行script下的宏任务,遇到setTimeout,将其放到宏任务的【队列】里
遇到 new Promise直接执行,打印"马上执行for循环啦"
遇到then方法,是微任务,将其放到微任务的【队列里】
打印 "代码执行结束"
本轮宏任务执行完毕,查看本轮的微任务,发现有一个then方法里的函数, 打印"执行then函数啦"
到此,本轮的event loop 全部完成。
下一轮的循环里,先执行一个宏任务,发现宏任务的【队列】里有一个 setTimeout里的函数,执行打印"定时器开始啦"
3. EventLoop 事件循环
主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。
- 整体的script(作为第一个宏任务)开始执行的时候,会把所有代码分为两部分:“同步任务”、“异步任务”;
- 同步任务会直接进入主线程依次执行;
- 异步任务会再分为宏任务和微任务;
- 宏任务进入到Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中;
- 微任务也会进入到另一个Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中;
- 当主线程内的任务执行完毕,主线程为空时,会检查微任务的Event Queue,如果有任务,就全部执行,如果没有就执行下一个宏任务;
- 上述过程会不断重复,这就是Event Loop事件循环;
4. 定时器
除了异步任务的事件,定时器事件也能放入任务队列,根据定时器函数setTimeout()
和setInterval()
定时或者周期执行定时器事件。
当定时器函数设置事件为0时:setTimeout(fn,0)
,该定时器事件将在主线程空闲时立即执行,即在同步任务、任务队列的事件均处理完成后,立即执行。因此,定时器函数是把定时器事件加入"任务队列",但必须等到当前任务栈执行结束,主线程才执行其指定的回调函数。
5 示例
function add(x, y) {
console.log(1)
setTimeout(function() { // timer1
console.log(2)
}, 1000)
}
add();
setTimeout(function() { // timer2
console.log(3)
})
new Promise(function(resolve) {
console.log(4)
setTimeout(function() { // timer3
console.log(5)
}, 100)
for(var i = 0; i < 100; i++) {
i == 99 && resolve()
}
}).then(function() {
setTimeout(function() { // timer4
console.log(6)
}, 0)
console.log(7)
})
console.log(8)
执行结果: 1,4,8,7,3,6,5,2