单线程
JavaScript
是一种单线程的编程语言,也就是说 JavaScript
只有一个调用栈,任何时候只能有一个函数被调用,其他函数必须等待当前执行函数执行完毕之后才能得到执行。这意味着 JavaScript
不支持并发执行,只能按顺序来执行同步代码。为了支持并发和异步操作,JavaScript
引入了事件循环机制和任务队列。那么读到此处,我们就有中疑问————为什么 JavaScript
不能有多个线程呢?
答:JavaScript
作为浏览器脚本语言,其主要用途是与用户互动,以及操作 DOM
。这就决定了它只能是单线程,否则会带来很复杂的同步问题,比如,假定 JavaScript
同时有两个线程,一个线程在某个 DOM
节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准。而为了避免这种复杂性,JavaScript
只能是单线程。
事件循环机制(Event Loop)
目前JavaScript的主要运行环境有两个,即 浏览器 和 Node.js 。那事件循环也就有两种,即 浏览器事件循环 和 Node.js事件循环。JavaScript
是一门单线程语言,即主线程只有一个。事件循环(Event Loop) 就是JS引擎管理事件执行的一个流程,具体由运行环境决定。
事件循环机制告诉我们JS代码的执行顺序,是指 浏览器或 Node 的一种解决JS单线程运行时不会阻塞的一种机制。
浏览器的事件循环中将任务分为 同步任务 和 异步任务 。
1. 同步任务和异步任务
同步任务
- 指在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行下一个任务。
- 所有同步任务都在主线程上执行,形成一个函数调用栈(执行栈)。
- 例如:事件处理程序中的同步任务代码。
- (同步代码:逐行执行,原地等待结果后,才继续向下执行)
异步任务
- 异步任务不进入主线程,它会进入 “任务队列”(task queue) 里等待,当 调用栈(执行栈) 为空时,再将 “任务队列”(task queue) 里的 回调函数 依次压入调用栈中来执行。
- 异步任务不会阻塞主线程,可以与其它任务并发执行。
- 例如:setTimeout、HTTP请求等操作完成后会进入“任务队列”等待。
- (异步代码:调用后耗时,不阻塞代码执行,将来完成后触发回调函数)。
- 异步任务在 “任务队列”(task queue) 里又分为 宏任务(macro-task) 和 微任务(micro-task)。
宏任务
- 浏览器执行的异步代码
- 宏任务包括:script(整体代码)、setTimeout、setInterval、AJAX请求完成事件、用户交互事件等
微任务
- JS引擎执行的异步代码
- 微任务包括:new Promise().then( )、MutationObserve( )、process.nextTick( )、Object.observe( )
PS:若同时存在new Promise().then()和process.nextTick(),先执行process.nextTick()
console.log('start');
Promise.resolve().then(() => {
console.log('promise');
});
process.nextTick(() => {
console.log('nextTick');
});
console.log('end');
// 执行结果为:
start
end
nextTick
promise
2. 执行过程
- 所有同步任务都在主线程上执行,形成一个执行栈(调用栈)
- 主线程之外,还存在一个“任务队列”(task queue),浏览器中的各种 Web API(如DOM事件、AJAX等) 为JavaScript提供了一个可执行异步代码的单独运行空间,当异步代码运行完毕后,会将代码中的回调加入到“任务队列”(task queue)中
- 一旦主线程的栈中的所有同步任务执行完毕后,调用栈为空时系统就会将“任务队列”(task queue)中的回调函数依次压入调用栈中执行,当调用栈为空时,仍然会不断循环检测任务队列中是否有代码需要执行
3. 执行顺序
- 先执行同步代码
- 遇到异步宏任务则将异步宏任务放入宏任务队列中
- 遇到异步微任务则将异步微任务放入微任务队列中
- 当所有同步代码执行完毕后,再将异步微任务从队列中调入主线程执行
- 微任务执行完毕后再将异步宏任务从队列中调入主线程执行
- 一直循环直至所有任务执行完毕
PS:当宏任务和微任务都处于“任务队列”(task queue)中时,微任务的优先级大于宏任务,即先将微任务执行完,再执行宏任务
4. 举例
示例1
setTimeout(() => {
console.log("4")
setTimeout(() => {
console.log("8")
}, 0)
new Promise(r => {
console.log("5") // 构造函数是同步的
r()
}).then(() => {
console.log("7") // then()是异步的,这入队
})
console.log("6")
}, 0)
new Promise(r => {
r()
console.log("1") // 构造函数是同步的
}).then(() => {
console.log("3") // then()是异步的,这里队
})
console.log("2")
//输出顺序:1 2 3 4 5 6 7 8
解释:
- 遇到
setTimeout
,为异步宏任务,放入Web API
提供的运行空间里运行(于此同时代码往下执行),运行完后将回调放入宏任务队列中 - 遇到
new Promise,Promise
在实例化的过程中所执行代码都是同步代码(即同步任务),所以输出1 - 而**
Promise().then()
中注册的回调才是异步执行**,将其放入微任务队列中 - 遇到同步任务console.log(“2”),输出2,此时主线程中的同步任务执行完
- 从微任务队列中取出任务(即 Promise().then()的回调)到主线程中,输出3,微任务队列为空
- 从宏任务队列中取出任务(即 setTimeout的回调)到主线程中
- 遇到同步任务(console.log(“4”)),输出4
- 遇到setTimeout,为异步宏任务,放入宏任务队列中
- 遇到new Promise实例化,输出5
- 遇到Promise().then(),放入微任务队列中
- 遇到console.log(“6”),输出6
- 从微任务队列中取出任务(即6.4的回调),输出7,微任务队列为空
- 从宏任务队列中取出任务(即6.2的回调),输出8,宏任务队列为空,结束~~~
示例2
setTimeout(function(){
console.log('1')
})
new Promise(function(resolve){
console.log('2')
resolve()
}).then(function(){
console.log('3')
});
console.log('4');
//输出顺序:2 4 3 1
解释:
- 遇到setTimout,异步宏任务,放入宏任务队列中;
- 遇到new Promise,Promise在实例化的过程中所执行的代码都是同步进行的,所以输出2;
- 而Promise.then中注册的回调才是异步执行的,将其放入微任务队列中
- 遇到同步任务console.log(‘4’);输出4;主线程中同步任务执行完
- 从微任务队列中取出任务到主线程中,输出3,微任务队列为空
- 从宏任务队列中取出任务到主线程中,输出1,宏任务队列为空,结束~~~
示例3
setTimeout(()=>{
new Promise(resolve =>{
resolve()
}).then(()=>{
console.log('test')
})
console.log(4)
})
new Promise(resolve => {
resolve()
console.log(1)
}).then( () => {
console.log(3)
Promise.resolve().then(() => {
console.log('before timeout');
}).then(() => {
Promise.resolve().then(() => {
console.log('also before timeout')
})
})
})
console.log(2);
//输出:1 2 3 before timeout also before timeout 4 test
解释:
- 遇到setTimeout,异步宏任务,将() => {console.log(4)}放入宏任务队列中;
- 遇到new Promise,Promise在实例化的过程中所执行的代码都是同步进行的,所以输出1;
- 而Promise.then中注册的回调才是异步执行的,将其放入微任务队列中
- 遇到同步任务console.log(2),输出2;主线程中同步任务执行完
- 从微任务队列中取出任务到主线程中,输出3,此微任务中又有微任务,Promise.resolve().then(微任务a).then(微任务b),将其依次放入微任务队列中;
- 从微任务队列中取出任务a到主线程中,输出 before timeout;
- 从微任务队列中取出任务b到主线程中,任务b又注册了一个微任务c,放入微任务队列中;
- 从微任务队列中取出任务c到主线程中,输出 also before timeout;微任务队列为空
- 从宏任务队列中取出任务到主线程,此任务中注册了一个微任务d,将其放入微任务队列中,接下来遇到输出4,宏任务队列为空
- 从微任务队列中取出任务d到主线程 ,输出test,微任务队列为空,结束~~~
总的来说:事件循环就是执行代码和收集异步任务,在调用栈空闲时,反复调用任务队列里的回调函数执行机制。