JavaScript 宏任务、微任务
在了解宏任务与微任务之前,我们需要知道基础的几个 JS 运行机制的概念
JS 运行机制
- JS 是单线程执行
描述: 执行 JS 代码的线程只有一个 是浏览器提供的 JS 引擎线程 (主线程)
function person() {
play()
}
function play() {
swim()
}
function swim() {
throw new Error('error')
}
person()
有一个人打算去玩,然后去游泳,遇到危险
报错的时候,遵循 先进后出
的原则,这不就是栈的特性嘛——这也就是执行栈
2. 那么 JS 是如何异步执行的呢?
【答案】: 浏览器是多线程的,当 JS 需要执行异步任务的时候,浏览器帮助我们另外启动一个线程
【举例】浏览器包含 HTTP 请求线程,当主线程请求数据的时候,将任务给另一个浏览器线程,也就是说浏览器才是真正执行发送请求任务的角色,JS 只是执行最后的回调处理
【总结】浏览器渲染进程提供 的多个线程为 JS的异步执行提供了基础
3. 浏览器异步任务的执行原理是事件驱动
- 事件可以是人为触发的也可以是程序自动触发(浏览器定时器线程)
- 事件驱动与 状态驱动或数据驱动的区别
- 事件驱动举例: 如图,点击方向,通过事件驱动人像移动
- 状态驱动活或数据驱动: 点击方向,修改人像的位置,通过定时器判断人像是否发生变化,然后进行人像移动,即根据数据的而变化来判断是否需要重新渲染
- 浏览器的点击事件会暂时进入到队列中,等待 JS 的同步任务执行完成后,就会从队列中取出要执行的队列,要执行的顺序由事件循环机制决定
浏览器事件循环机制
Event Loop中,每一次循环称为tick,每一次tick的任务如下:
- 执行栈选择最先进入队列的宏任务(一般都是script),执行其同步代码直至结束;
- 检查是否存在微任务,有则会执行至微任务队列为空;
- 如果宿主为浏览器,可能会渲染页面;
- 开始下一轮tick,执行宏任务中的异步代码(setTimeout等回调)。
扩展小知识 Vue 中的 $nextTick
vm.$nextTick
接受一个回调函数作为参数,用于将回调延迟到下次DOM更新周期之后执行。
这个API就是基于事件循环实现的。
“下次DOM更新周期”的意思就是下次微任务执行时更新DOM,而vm.$nextTick
就是将回调函数添加到微任务中(在特殊情况下会降级为宏任务)。
若想知道更多Node.js事件循环,请查看:Node.js 事件循环,定时器和 process.nextTick()
什么是宏任务与微任务呢
宏任务 微任务
基础概念
- ES6 规范中,microtask 称为 jobs,macrotask 称为 task
- 宏任务是由宿主发起的,而微任务由JavaScript自身发起。
- 宏 (Macro)是一种批量处理的称谓
宏任务(Macro) | 微任务(micro) | |
---|---|---|
谁发起的 | 宿主 | JavaScript |
具体事件 | script、setTimeout、setInterval、I/O等 | .then、 catch、 finally process.nextTick(Node.js)new MutaionObserver()等…… (new Promise 不是一个微任务) |
会触发新一轮Tick吗 | 会 | 不会 |
数量 | 可能有多个 | 只有一个 |
特征 | 有明确的异步任务需要执行和回调,需要其他异步线程支持 | 没有明确异步任务执行,只有回调,不需要其他异步线程支持 |
执行程序:
- 每个宏任务,都单独关联了一个微任务队列
- 执行顺序为: 主线程> 微任务>宏任务 【通俗的记忆方法:先从小事做起】
- 区分宏任务与微任务:
- 【举例】
- 定时器等待任务,需要定时器线程执行 =>(宏任务)
- Ajax 发送请求任务,需要 HTTP 线程执行 =>(宏任务)
- promise.then 没有任何异步任务需要其他线程执行,只有回调 =>(微任务)
- 【举例】
微任务的产生
-
在ES3以及以前的版本中,JavaScript本身没有发起异步请求的能力,也就没有微任务的存在。
-
在ES5之后,JavaScript引入了Promise,这样,不需要浏览器,JavaScript引擎自身也能够发起异步任务了。
【问题】宏任务、微任务都可以操作异步,那微任务要解决的问题?
【答案】为了解决主线程任务过多的时候,异步回调等待时间过长的问题,可以在实时性和效率之间做一个有效的权衡
示例
console.log(11111)
setTimeout(() => {
console.log(22222)
})
new Promise(resolve => {
resolve()
console.log(3333)
}).then(() => {
console.log(44444)
}).finally(() => {
console.log(55555)
})
console.log(66666)
输出为: 136452
解析:
- 顺序执行,输出1
- 执行
setTimeout
宏应用,进入宏任务队列 - 执行
Promise
输出 3 .then
.finally
微应用进入微应用队列(这里就和事件的循环机制有关)- 顺序执行 ,输出 6
- 微应用执行比宏应用执行更快,因此输出 451
说起 Promise
,不禁想起 Vue 中的 async await
将异步转为同步
浅谈 async await
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
你预想的答案是什么呢?正确答案如下:
script start
async2 end
Promise
script end
async1 end
promise1
promise2
setTimeout
最让你意想不到的位置,应该是 async1 end
的位置了,那为什么他又在这个位置呢?
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
转为ES5的写法:
new Promise((resolve, reject) => {
// console.log('async2 end')
async2()
...
}).then(() => {
// 执行async1()函数await之后的语句
console.log('async1 end')
})
不禁看到 输出在 微任务 .then
里面
扩展小知识 setTimeout
读上述的文章可知,浏览器执行顺序
是
- 同步任务,
- 再去取出异步回调执行
- 执行 setTimeout时,浏览器启动新的线程去计时,
- 计时结束后触发定时器事件,将回调存入宏任务队列
如果同步任务和微任务执行的时间过长,此时的宏任务就只能被挂起了,这就造成了计时器不准确的问题。
【现象】使用setInterval 设置时间的时候,会发现跳秒现象
【进一步解释】微任务执行完毕之后,也就是队列执行完毕后,浏览器会执行视图渲染,这里会有浏览器的优化,他可能会合并多次事件循环的结果
【得出结果】视图更新,会先执行 requestAnimationFrame 回调,不是在每一次操作 Dom 之后,而是队列执行完毕后,执行一次视图
Node 事件循环
JS 不实现事件循环,都是由他的宿主实现的,Node 与浏览器的实现大致相同。如有不同,请查看JS事件循环
推荐文章
宏任务和微任务到底是什么?
Node.js 事件循环,定时器和 process.nextTick()
JS事件循环
requestAnimationFrame