我们需要补充一些前置知识,比如单线程模型是什么?
js是一个单线程的脚本语言,之所以为什么不是多线程而是单线程,是因为历史遗留的原因,脚本语言如果使用了多线程,那么一个线程操作了dom,第二个线程也操作了dom,那么浏览器改听谁的,如果是多线程会对开发者来说是一个弊大于利的事情;
那么没有了多线程就说明了,我们的任务需要在一个线程中进行,但是js虽然是单线程,但是还有很多线程,只是同一时间执行事件的线程只有一个,这个线程叫做主线程;
但是我们会发现,如果现在是单线程,执行任务要等到上一个任务执行结束才会到下一个,这对于一些IO操作,ajax请求操作是非常坑的事情,我们需要等到这些任务执行结束才会执行下面的,等待IO/ajax返回并不需要消耗CPU,还需要花时间等它们,非常不划算,所以js引入了新的概念叫做消息队列(任务队列)
js在执行任务的时候,会把所有的同步任务优先执行,把所有的异步任务挂起到其他队列,等到我们的同步任务全部清空,再看异步任务是否满足条件再添加到主线程中的任务队列中执行,比较经典的例子就是setTimeout;
let timer = setTimeout(() => { |
|
console.log("这并不是准时的延迟噢") |
|
}, 1000) |
|
延时器的意思就是我需要等待一段时间把这样的任务放到主线程的最后面,那么这个参数传递的是1000,并不是真的1秒延迟执行,而是在主线程前面的任务执行完毕,如果前面有很多耗时的任务,那么这个1000指的就是最少时间而不是最终时间;
当我们的同步任务被主线程全部执行完毕,会去检查其他的队列中存在异步任务,如果检查出来异步任务满足条件那么就放到了主线程去执行,那么这个异步任务也就变成了同步任务,然后当主线程又一次清空了,又要去找异步任务去执行,如果一旦任务队列是空的,那么程序执行结束。每一个消息会与一个函数进行一个关联,等到执行到此任务(消息)的时候,会执行对应的函数,如果没有这个函数,那么这个消息就会遗失;
那么我们把这样的一次一次的去查询异步任务是否满足条件进入主线程执行任务的这个过程称之为事件循环机制
setTimeout(_ => console.log(4))
new Promise(resolve => {
resolve() console.log(1)
}).then(_ => {
console.log(3)
})
console.log(2)
setTimeout就是作为宏任务来存在的,而Promise.then则是具有代表性的微任务,上述代码的执行顺序就是按照序号来输出的。
所有会进入的异步都是指的事件回调中的那部分代码
也就是说new Promise在实例化的过程中所执行的代码都是同步进行的,而then中注册的回调才是异步执行的。
在同步代码执行完成后才回去检查是否有异步任务完成,并执行对应的回调,而微任务又会在宏任务之前执行。
所以就得到了上述的输出结论1、2、3、4。
宏任务
# |
浏览器 |
Node |
I/O |
✅ |
✅ |
setTimeout |
✅ |
✅ |
setInterval |
✅ |
✅ |
setImmediate |
❌ |
✅ |
requestAnimationFrame |
✅ |
❌ |
有些地方会列出来UI Rendering,说这个也是宏任务,可是在读了HTML规范文档以后,发现这很显然是和微任务平行的一个操作步骤
requestAnimationFrame姑且也算是宏任务吧,requestAnimationFrame在MDN的定义为,下次页面重绘前所执行的操作,而重绘也是作为宏任务的一个步骤来存在的,且该步骤晚于微任务的执行
微任务
# |
浏览器 |
Node |
process.nextTick |
❌ |
✅ |
MutationObserver |
✅ |
❌ |
Promise.then catch finally |
✅ |
✅ |