灵魂三问:
- 为什么JS是一门单线程语言
- 为什么JS需要异步
- JS单线程如何实现异步
为什么JS是一门单线程语言
如果说在浏览器中,JS是多线程的,有如下场景:
在浏览器中,有两个进程process1和process2,因为JS是多进程的,所以他们对同一个dom同时进行操作。process1删除了该dom,而process2编辑了该dom,同时下达两个矛盾的命令,浏览器如何执行?
为什么JS需要异步
如果JS中不存在异步,只能自上而下按顺序执行,如果上一行解析需要很长时间,那么下面的代码就会被阻塞,对于用户而言阻塞就会导致页面卡死,进而影响用户体验
JS单线程如何实现异步
通过事件循环(event loop),理解了该机制,也就明白了JS的执行机制
观察如下代码1
2
3
4
5
6
7console.log(1)
setTimeout(function(){
console.log(2)
}, 0)
console.log(3)
浏览器会分别打印出:1 3 2
setTimeout里的函数并没有立即执行,而是延迟了一段时间,满足了一定条件后才去执行,这类代码叫做异步代码。所以按照上面代码可以将任务分成同步任务和异步任务,按照这种分类方式,JS的执行机制是:
- 首先判断是同步还是异步,同步就进入主进程,异步就进入event table
- 异步任务在event table中注册函数,当满足触发条件后被推入事件队列
- 同步任务在进入主进程后一直执行,直到主进程空闲,才会去事件队列中查看是否有可执行的异步任务,如果有就推入主进程中
但仅仅是上面这样就结束了吗???再看下面的代码1
2
3
4
5
6
7
8
9
10
11
12
13
14setTimeout(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('代码执行结束');
按照上面的理解去分析JS的执行机制:
- setTimeout是异步任务,进入event table里
- new Promise是同步任务,放到主进程里,控制台打印:马上执行for循环啦
- .then函数里的是异步任务,放到event table里
- console.log是同步代码,放到主进程里,打印:代码执行结束
照这样打印出的顺序是:马上执行for循环啦—代码执行结束—定时器开始啦—执行then函数啦
但是实际执行后的结果却是:马上执行for循环啦—代码执行结束—执行then函数啦—定时器开始啦
这么说按照同步任务和异步任务划分并不准确!!而准确的方式是(妈ma 咪mi 宏任务在前,微任务在后)
- 宏任务(macro-task):包括整体代码script、setTimeout、setInterval
- 微任务(micro-task):Promise、process.nextTick
按照这种方式,JS的执行机制是:
- 执行一个宏任务,过程中遇到微任务,将它放到微任务的事件队列里
- 当前宏任务执行完成后,会去查看微任务的事件队列,并将里面的微任务一次执行完,然后才会进入下一个宏任务
- 重复上面的两个步骤
下面再次分析第二段代码:
- 首先执行script中的宏任务,遇到setTimeout,将它放到宏任务的事件队列里
- 遇到new Promise立即执行(它是同步的),打印:马上执行for循环啦
- 遇到then方法,是微任务,将它放到微任务的事件队列里
- 打印:代码执行结束
- 本轮宏任务执行完,查看微任务,发现有一个then方法里的函数,打印:执行then函数啦
- 到这儿本轮的事件循环(event loop)结束
- 进入下一轮循环,先执行一个宏任务,发现宏任务队列里有一个setTimeout函数,执行并打印:定时器开始啦
于是,真正的代码执行顺序是:马上执行for循环啦—代码执行结束—执行then函数啦—定时器开始啦
setTimeout
1 | setTimeout(function(){ |
我们一般会解释为3秒之后执行setTimeout里的函数,但这种说法不严谨。准确来说,3秒后,setTimeout里的函数会被推到事件队列里(event queue),而事件队列里的任务只有在主线程空闲时才会去执行
所以要满足<1> 3秒后; <2> 主线程空闲; 同时满足时才会在3秒后执行该函数2>1>
如果主线程任务很多,执行时间超过了3秒,比方执行了5秒,那这个函数也只能在5秒后被执行