一、前言
本篇文章的目的是解释清楚事件循环中的宏任务与微任务,还是三个问题着手:
- 什么是宏任务与微任务?
- 为什么需要微任务?
- 微任务(promise)的应用?
二、什么是宏任务与微任务?
宏任务
主线程任务队列中的任务:
- 加载的js文件执行
- 各种交互事件
- 浏览器渲染事件
- setTimeout,setInterval定时器
- I/O线程加入的事件
微任务
主线程运行时,调用栈中维护的队列:
- promise
- MutationObserver
代码示例:
const promise = new Promise(resolve => {
console.log('promise start')
console.log('add setTimeout1')
setTimeout(_ => {
console.log('setTimeout1 start')
resolve()
}, 0)
console.log('add setTimeout2')
setTimeout(_ => console.log('setTimeout2 start'), 0)
console.log('promise end')
})
promise.then(res => {
console.log('promise.then')
})
promise start
/*
add setTimeout1
add setTimeout2
promise end
setTimeout1 start
promise.then
setTimeout2 start
*/
运行过程示意图:
由上可见,微任务的执行时机是在当前宏任务执行结束之前执行,所以并不会受到事件队列的影响。这保证了微任务的实时性优势。
三、为什么需要微任务?
ES6之前,javascript语言并不能创建真正意义上的异步任务,必须借用宿主API实现类似功能,如setTimeout。
随着前端技术的发展,对于异步回调的功能需求越来越多,导致出现了各种各样实现方式,缺乏统一标准。这对于一门语言来说,不利于长期发展。所以,ES6提供了Promise对象
传统的宏任务异步任务,性能不高。例如,创建一个宏任务的异步任务,在执行开始到被创建之间这段时间,有可能会触发一些其他事件。这将导致本应该作为下一个事件执行的异步任务,却必须要等待前面任务执行完才能执行。这将导致该任务实时性变得不可靠。
在代码执行中,经常需要监听dom节点的变化。因为这种需求有两个特征。第一:变化频率高。第二:实时性要求高。基于以上特征,引出了三种处理方式:
处理方式 | api | 优点 | 缺点 |
---|---|---|---|
暴力轮询 | setTimeout, setInterval | 简单粗暴 | 实时性低,不好把控时间间隔,性能低 |
同步回调 | Mutation Event | 实时性高,观察者模式回调,不会额外创建任务 | 每一次dom变化都会触发,增删过程很容易触发无用回调 |
异步微任务处理 | MutationObserver | 实时性高,等待当前任务中,所有节点变换完成后才执行,不会执行无用回调 | 实时性没有同步任务高,需要浏览器支持 |
四、微任务(promise)的应用
微任务的出现
微任务的出现,最初是为了处理监听dom变化的性能问题,也就是MutationObserver。而Promise的出现,其实主要是解决一些异步编码风格的问题(李冰老师)。
promise解决了什么异步编码风格的问题?
- 将处理多状态结果的函数封装成只返回成功与失败两种状态,解决编码状态处理混乱的问题。如以下代码:
// 最简单最常用的请求事件 function ajax_get(url) { return new Promise((resolve, reject) => { const request = new XMLHttpRequest() request.onreadystatechange = function() { if (request.readyState === 4) { // 请求成功状态 if (request.status === 200) resolve(request.response) else reject('错误') } } request.onerror = e => reject(e) request.ontimeout = e => reject(e) request.open('GET', url, true) request.send() }) } // 借用菜鸟教程的请求地址 const promise = ajax_get('https://www.runoob.com/try/ajax/ajax_info.txt') promise.then(res => { console.log(res) }, error => console.log(error))
以上代码中,我们关心的只有request.readyState = 4且request.status = 200时的成功返回,而其他多种状态都视为请求失败。通过Promise函数,可以很好的处理该种场景。当然,还有很多种方式可以实习,而Promise属于官方给出的处理办法,所以更具有安全跟权威性。
- 解决回调地狱,让代码更线性,增加代码的可读性以及可维护性,如下
如上所示,// 等待第一个请求完成后,调用第二个请求 const promise1 = ajax_get('https://www.runoob.com/try/ajax/ajax_info.txt') const promise2 = promise1.then(res => { console.log('第一次请求结果:', res) return ajax_get('https://www.runoob.com/try/ajax/ajax_info.txt') }, error => console.log(error)) promise2.then(res => { console.log('第二次请求结果:', res) }, error => console.log(error))
- promise将函数的结果,保存在一个对象中,具有保存状态的功能。可以等待将来的任一时刻,再对结果进行处理。这种方式有效的消除了同步回调处理的执行顺序问题。
- promise将函数的结果,从层层回调中返回出来。使得代码层次更清晰,免除了作用域限制,使得回调地狱的场景得到完美的解决。
综上所述,promise的出现,更多的是为了实现语言风格的统一,并且根据用户的实际需求,解决现有代码的不足。