在我的JavaScript学习系列第一篇文章里面说过,调用堆栈一次可以执行一个函数,如果一个函数堵塞,整个浏览器都会直接冻结。而异步就是解决问题的方案。
首先上代码:
setTimeout(callback, 1000);
function callback(){
console.log('异步');
}
setTimeout是浏览器API的一部分,是一种浏览器提供的工具,10s后,浏览器接收我们传入的回调函数并将其移动到回调队列(Callback Queue)中去。
首先,setTimeout在全局上下文中运行,10s后计时器被触发,回调函数准备运行,但是它必须要先经过回调队列。
回调队列是什么?
回调队列就是一个队列数据结构,它是一个有序的函数队列,遵守先进先出(FIFO的原则。
每个异步函数在被放入调用堆栈之前必须要通过回调队列,这时我们的第三个主角登场,事件循环(Event Loop)。
事件循环的任务只有一个:检查调用堆栈是否为空,如果调用堆栈里是空闲的,而且回调队列里有某个函数,那么就将其放入调用堆栈中。
这就是所谓的基本JS异步。
再来深入的看一看,上代码:
console.log('1')
console.log('2')
setTimeout(() => {
console.log('?')
}, 1000)
console.log('3')
这段代码在输出之后会发现,"?"在3之后才被打印出来,也就是说,计时器在这没有堵塞住下面的代码,这个用上面的原理很容易解释:
异步函数若想进入调用堆栈,必须先要通过回调队列,再经过事件循环的确定审查后再进入调用堆栈开始运行,真的挺惨一函数。
再讨论一下我在之前文章里面说过的JavaScript线程的问题,JavaScript单线程是指浏览器中负责解释和执行JavaScript代码的只有一个线程,即JS引擎线程,但是浏览器的渲染进程是提供多个线程的,如下:
- JS引擎线程
- 事件触发线程
- 定时触发器线程
- 异步http请求线程
- GUI渲染线程
当JS引擎收到不同的请求时,它会用不同的线程去处理不同的请求,例如:DOM监听事件或者是网络请求,还有就像定时器等等吧,而JS引擎线程则继续后面的其他任务,这样就实现了异步非堵塞。
但是像上面的定时触发线程,它是来处理定时器的,但是它的作用也仅仅是来定时而已,设置的时间一到,回调函数就会进入回调队列,等待Event Loop的采摘。
类似的JS引擎线程遇到异步(DOM事件监听,网络请求,setTimeout计时器等...),会将这些异步任务交给相应的线程去维护,等待时机(用户点击DOM,网络请求成功,计时器结束),再之后就不必赘述了。
对了,异步任务一般只有:网络请求,计时器和DOM事件监听。
其实我反复在讲的,其实都是一个事情,希望能看懂。
接下来,我们更深入讨论。
上代码:
console.log('script start')
setTimeout(function() {
console.log('timer over')
}, 0)
Promise.resolve().then(function() {
console.log('promise1')
}).then(function() {
console.log('promise2')
})
console.log('script end')
// script start
// script end
// promise1
// promise2
// timer over
这里我们会发现,"promise1"和"promise2"在"time over"之前就打印出来了,我们暂时不谈Promise,在这里我们不过多解释,等明天写一期关于Promise的。
这里引入两个新的概念:宏任务(macro-task)和微任务(micro-task)。
所有的任务都可以这么分类:
- macro-task:主代码块、setTimeout、setInterval等(可以看到,回调队列中的每一个事件都是一个 macro-task,现在称之为宏任务队列)
- micro-task:Promise、process.nextTick(暂时用不上)等
JS引擎线程首先执行主代码块,在执行栈中每次执行的代码都是一个宏任务,回调堆栈的也是这样,但是在执行宏任务过程中遇到了Promise,就会创建微任务,加入到微任务队列队尾。
微任务肯定是在宏任务执行的时候创建的,在这个宏任务的下一个宏任务执行开始之前,浏览器会对页面重新渲染。在上一个宏任务执行完成后,且在渲染页面之前,会执行当前微任务队列中的所有微任务。
简单来说,在一个宏任务执行完之后,在重新渲染和开始下一个宏任务之前,将会把它执行期间内产生的所有微任务都执行完。
这就可以解释上面的打印结果了,两个微任务是在"script end"这个宏任务执行时创建的,所以先执行两个微任务(两个微任务都是.then()创建的微任务,.then()会被分发到微任务中去)。
注意:如果在微任务里面包含了微任务,那么先不管内层微任务,在处理掉外层后继续执行微任务队列,不给宏任务喘息的机会。
而如果在微任务里面添加了宏任务呢,比如set1先加入了回调队列,而微任务里面的宏任务set2后加入回调队列,如果set2的最小延迟毫秒数小于set1,那么set2仍然先被执行,只比较这个第二参数毫秒。
说到事件循环,第一轮事件循环是对整体代码的运行,整体可以看作是一个宏任务,宏任务放到回调队列,微任务放到微任务队列,一轮执行完后,检查一下是否有微任务,有的话就执行,然后重新渲染,再开始下一轮事件循环,对于不断嵌套的类型要一层一层解决,延迟毫秒数要重点注意。
最后就是一定要牢记:js是单线程执行,要有完整的事件循环思想。
又看了几篇博客,看着内容都差不多,基本都概括到了,写麻了。