JS异步编程
javascript的执行环境是单线程的,一次只能执行一个任务,多任务需要排队等待任务被执行。这种模式下有一个很大的弊端,会阻塞代码的执行,导致代码的执行效率降低。为了解决这个问题出现了异步编程的方案。所谓的异步编程一般是通过回调函数、事件发布/订阅、Promise等来组织代码。其本质都是通过回调函数的方式来实现异步代码的存放与执行。
EventLoop & 消息队列
EventLoop:
所谓的事件循环其实指的是js运行机制。js分为同步任务和异步任务。同步任务在主线程上依次执行,异步任务只要有了运行结果,就会把异步任务的回调放在消息队列中等待,执行栈把同步任务依次执行完毕之后,消息队列中的回调就会被读取到执行栈中去执行。然后接下来只要主线程空了,就会去读取消息队列,找任务放到执行栈上去执行,这个过程会一直循环下去,这就是JavaScript的运行机制,也就是所谓的事件循环。
消息队列(任务队列):
消息队列是用来存放异步任务的结果(其实本质就是异步任务的回调),等待被主线程读取到执行栈上执行。其是一个先进先出的数据结构。
宏任务 & 微任务
宏任务:
宏任务可以理解成每次执行栈执行的代码就是一次宏任务(包括每次从消息队列中获取一个回调并放到执行栈中执行)
浏览器为了让宏任务与DOM操作能够有序的执行,会在一个宏任务执行结束后,下个宏任务执行前,对页面进行重新的渲染。
宏任务的典型代表:script标签的整体代码、setTimeout、setInterval、以及通过js绑定的任何事件。
微任务:
当前JS执行栈里面的任务执行结束后需要立即执行的任务。在页面重新渲染前,会执行并清空微任务。其响应速度比宏任务更快,因为无需等待UI渲染。
微任务的典型代表:Promise.then、node中的process.nextTick、queueMicrotask以及检测DOM树发生变化的MutationObserver
由于js的所有任务又分为宏任务和微任务。所以队列有宏任务队列与微任务队列之分。
宏任务与微任务的执行流程
微任务的好处:
可以在DOM树发生变化之前更迅速的做一些事情。
案例
let messageQueue = [];
let sendMessage = message => {
messageQueue.push(message);
if(messageQueue.length === 1){
queueMicrotask(()=>{
const json = JSON.stringify(messageQueue);
messageQueue.length = 0;
console.log(json)
})
}
}
sendMessage('你好')
sendMessage('哈哈')
sendMessage('啦啦')
- 结果
输出一次 [“你好”,“哈哈”,“啦啦”] - 原因
首先sendMessage第一次被调用创建的微任务会进入微任务队列中等待,由于此时主线程上还有两行代码没有执行,所以创建的微任务并不会执行,此时messageQueue长度为1。
sendMessage第二次被调用的时候,messageQueue又被push进去一个元素,此时messageQueue的长度是2,那么不会创建微任务。同理sendMessage第三次被调用也是不会创建微任务。
当sendMessage(‘啦啦’)执行完毕之后,此时执行栈就空了,开始去微任务队列中读取第一次调用sendMessage创建的微任务,放到执行栈上去执行。
个人总结
- js分为同步任务与异步任务,宏任务包括全部的同步任务与部分的异步任务,异步任务有部分是微任务,又由于异步任务有宏任务与微任务,所以存放异步任务回调的消息队列又分为宏任务队列与微任务队列。异步任务是宏任务的回调放在宏任务队列中等待,异步任务是微任务的回调放在微任务队列中等待。
- js初始执行时只有宏任务,没有微任务,初始执行完毕之后,才可能会创建出来微任务,并且上次宏任务结束之后创建出来的微任务一定比下次宏任务先执行。(所以可以说微任务是在上次宏任务执行的时候创建出来的)