引言
先出道题,如果大家能答对,那可以关掉页面了。
以下这段代码的执行结果是什么?
setTimeout(function() {
console.log('我是定时器')
},2000);
new Promise(function(resolve) {
console.log('开始循环');
for (var i = 0; i < 1000; i++) {
i == 999 && resolve();
}
}).then(function() {
console.log('循环结束了')
});
console.log('代码执行结束');
参考答案
开始循环
代码执行结束
循环结束了
我是定时器
一、单线程的JavaScript
单线程就是同一个时间只能做一件事。多线程就是同一个时间可以做很多事情。
JavaScript是单线程的。举个很简单的例子你就明白了,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,那浏览器要怎么显示,是不是乱套了。所以JavaScript只能是单线程的。
也许会有人说再HTML5中可以用new Worker(xxx.js)在JavaScript中创建多个线程。但是子线程完全受主线程控制,且不得操作DOM。所以JavaScript还是单线程的。
二、JavaScript中的同步任务和异步任务
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。这就是JavaScript中的同步任务。
但是同步任务有个很大的缺点,如果前一个任务执行了很长时间还没结束,那下一个任务就不能执行,举个简单的例子,页面某个区域渲染过程中需要用Ajax去请求数据,如这个请求很长时间都请求不到数据,那下个任务就不能执行,也就说页面其他区域不能渲染。于是就有了JavaScript异步任务来解决这个缺点。
异步任务可以单独执行,不要等前一个任务结束后再执行。但是异步任务执行结束后就会在那边等待,直到线程里面没有任务了。才会喊异步任务的回调函数过来执行。
好了,直接开车上图。来讲一下JavaScript中的同步任务和异步任务是怎么执行的。
- 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入事件列表(Event Table)并注册回调函数。
- 当异步任务执行结束后,会将这个回调函数添加事件队列(Event Queue)。
- 主线程内的任务执行完毕后,会去事件队列(Event Queue)中询问有没有要执行的任务,如果有,那就按先添加先执行的顺序进入任务执行栈,然后按之前步骤继续执行。
- 上述过程会不断重复,也就是常说的事件循环(Event Loop)。
下面来个例子测试一下你是否懂了
setTimeout(function() {
console.log(1);
}, 2000)
console.log(2);
setTimeout(function() {
console.log(3);
}, 1000)
console.log(4);
参考答案
2,4,3,1
上面例子如果你回答正确,回到最初的那道题:
setTimeout(function() {
console.log('我是定时器')
},1000);
new Promise(function(resolve) {
console.log('开始循环');
for (var i = 0; i < 1000; i++) {
i == 999 && resolve();
}
}).then(function() {
console.log('循环结束了')
});
console.log('代码执行结束');
按上面的内容分析一下。你可能会得到下面的答案
开始循环
代码执行结束
我是定时器
循环结束了
当然这个答案是错误,这时你是不是开始认为上面的内容是错的。
别着急,其实JavaScript异步任务还有宏任务和微任务的区分,下面就来介绍。
三、JavaScript中的宏任务和微任务
- 宏任务(macro-task):整体代码script、setTimeout、setInterval、setImmediate
- 微任务(micro-task):Promise、process.nextTick
宏任务和微任务是对JavaScript异步任务再次细分。异步事件队列分为宏任务事件队列和微任务事件队列。
直接开车上图。来说明JavaScript中的宏任务和微任务是怎么执行的。
- 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入事件列表并注册回调函数。
- 当异步任务执行结束后,判断该异步任务是宏任务还是微任务,将宏任务的回调函数添加宏任务事件队列,将微任务的回调函数添加到微任务事件队列。
- 主线程内的任务执行完毕后。
- 先去微任务事件队列中询问有没有要执行的任务,如果有,那就按先添加先执行的顺序进入任务执行栈。
- 如果没有,再去宏任务事件队列中询问有没有要执行的任务。如果有,那就按先添加先执行的顺序进入任务执行栈。
- 如果没有,那任务都执行完毕。
- 上述过程会不断重复,也就是常说的事件循环(Event Loop)。
我们按上面的流程来分析一下最初的那道题:
setTimeout(function() {
console.log('我是定时器')
},1000);
new Promise(function(resolve) {
console.log('开始循环');
for (var i = 0; i < 1000; i++) {
i == 999 && resolve();
}
}).then(function() {
console.log('循环结束了')
});
console.log('代码执行结束');
-
整体script作为第一个宏任务进入主线程。
-
遇到
setTimeout为异步任务,任务执行结束后,判断其为宏任务,故将回调函数添加到宏任务事件队列,我们将它记为setTimeout。 -
遇到
new Promise为异步任务,new Promise直接执行,输出开始循环,任务执行结束后,判断其为微任务,故将then 方法绑 定的回调函数添加到微任务事件队列,我们将它记为then。 -
遇到
console.log('代码执行结束'),为同步任务,直接执行,输出代码执行结束。 -
任务都执行完毕,询问微任务事件队列有没有要执行的任务,有,为
then,执行,输出循环结束了。 -
微任务队列都执行完毕了,询问宏任务事件队列有没有要执行的任务,有,为
setTimeout,执行,输出我是定时器。宏任务Event Queue 微任务Event Queue setTimeout then
最终输出结果为
开始循环
代码执行结束
循环结束了
我是定时器
和正确答案一对没错。看来上面的JavaScript执行机制是没错的。下面我们再看一个复杂的例子,一起来彻底掌握JavaScript执行机制。
四、来个例子小结一下
console.log('1');
setTimeout(function() {
console.log('2');
new Promise(function(resolve) {
console.log('3');
resolve();
}).then(function() {
console.log('4')
})
setTimeout(function(){
console.log('5')
})
})
new Promise(function(resolve) {
console.log('6');
resolve();
}).then(function() {
console.log('7')
})
setTimeout(function() {
console.log('8');
new Promise(function(resolve) {
console.log('9');
resolve();
}).then(function() {
console.log('10')
})
setTimeout(function(){
console.log('11')
})
})
- 整体script作为第一个宏任务进入主线程。
- 遇到
console.log(1),为同步任务,直接执行,输出1。 - 遇到
setTimeout为异步任务,任务执行结束后,判断其为宏任务,故将回调函数添加到宏任务事件队列,我们将它记为setTimeout1。 - 遇到
new Promise为异步任务,new Promise直接执行,输出6,任务执行结束后,判断其为微任务,故将then 方法绑 定的回调函数添加到微任务事件队列,我们将它记为then1。 - 遇到
setTimeout为异步任务,任务执行结束后,判断其为宏任务,故将回调函数添加到宏任务事件队列,我们将它记为setTimeout2。
| 宏任务Event Queue | 微任务Event Queue |
|---|---|
| setTimeout1 | then1 |
| setTimeout2 |
- 上表是第一轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了
1和6。 - 询问微任务事件队列有没有要执行的任务,有,为
then1,执行,输出7。 - 微任务队列都执行完毕了,询问宏任务事件队列有没有要执行的任务,有,为
setTimeout1和setTimeout2。 - 执行
setTimeout1,首先遇见console.log(2),直接执行,输出2。 - 遇到
new Promise为异步任务,new Promise直接执行,输出3,任务执行结束后,判断其为微任务,故将then 方法绑 定的回调函数添加到微任务事件队列,我们将它记为then2。 - 遇到
setTimeout为异步任务,任务执行结束后,判断其为宏任务,故将回调函数添加到宏任务事件队列,我们将它记为setTimeout3。
| 宏任务Event Queue | 微任务Event Queue |
|---|---|
| setTimeout2 | then2 |
| setTimeout3 |
- 上表是第二轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了
7、2、3。 - 询问微任务事件队列有没有要执行的任务,有,为
then2,执行,输出4。 - 微任务队列都执行完毕了,询问宏任务事件队列有没有要执行的任务,有,为
setTimeout2和setTimeout3。 - 执行
setTimeout2,首先遇见console.log(8),直接执行,输出8。 - 遇到
new Promise为异步任务,new Promise直接执行,输出9,任务执行结束后,判断其为微任务,故将then 方法绑 定的回调函数添加到微任务事件队列,我们将它记为then3。 - 遇到
setTimeout为异步任务,任务执行结束后,判断其为宏任务,故将回调函数添加到宏任务事件队列,我们将它记为setTimeout4。
| 宏任务Event Queue | 微任务Event Queue |
|---|---|
| setTimeout3 | then3 |
| setTimeout4 |
- 上表是第三轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了
4、8、9。 - 询问微任务事件队列有没有要执行的任务,有,为
then3,执行,输出10。 - 微任务队列都执行完毕了,询问宏任务事件队列有没有要执行的任务,有,为
setTimeout3和setTimeout4。 - 执行
setTimeout3,首先遇见console.log(5),直接执行,输出5。
| 宏任务Event Queue | 微任务Event Queue |
|---|---|
| setTimeout4 |
- 上表是第四三轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了
10、5。 - 询问微任务事件队列有没有要执行的任务,没有。
- 微任务队列都执行完毕了,询问宏任务事件队列有没有要执行的任务,有,为
setTimeout4。 - 执行
setTimeout2,首先遇见console.log(11),直接执行,输出11
| 宏任务Event Queue | 微任务Event Queue |
|---|---|
- 上表是第五轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了
11。 - 询问微任务事件队列有没有要执行的任务,没有。
- 微任务队列都执行完毕了,询问宏任务事件队列有没有要执行的任务,没有。
- 任务都执行完毕。
- 最终输出
1、6、7、2、3、4、8、9、10、5、11
在chrome上打印输出,对比没错。
如果把
setTimeout(function(){
console.log('5')
})
改成
setTimeout(function(){
console.log('5')
},2000)
结果又会怎么样,大家按上面推理一下。给大家提个醒,异步任务是执行完成后在添加到事件队列中。