js - js运行机制 事件循环 宏任务和微任务
js - js运行机制 事件循环 宏任务和微任务
参考链接
举个栗子
console.log('start')
setTimeout(() => {
console.log('setTimeout')
}, 0)
new Promise((resolve) => {
console.log('promise')
resolve()
})
.then(() => {
console.log('then1')
})
.then(() => {
console.log('then2')
})
console.log('end')
// start
// promise
// end
// then1
// then2
// undefined
// setTimeout
问题点
涉及到JavaScript事件轮询
中的宏任务
和微任务
- 宏任务和微任务是什么
- 是谁发起的?
- 为什么微任务的执行要先于宏任务呢?
首先,我们需要先知道JS运行机制。
JS运行机制
JS是单线程执行
”JS是单线程的”指的是JS 引擎线程。
在浏览器环境中,有JS 引擎线程和渲染线程,且两个线程互斥。
Node环境中,只有JS 线程。
宿主(JS运行的环境)
JS运行的环境。一般为浏览器或者Node。
执行栈
是一个存储函数调用的栈结构,遵循先进后出的原则。
function foo() {
throw new Error('error')
}
function bar() {
foo()
}
bar()
当开始执行 JS 代码时,首先会执行一个 main
函数,然后执行我们的代码。根据先进后出的原则,后执行的函数会先弹出栈,在图中我们也可以发现,foo
函数后执行,当执行完毕后就从栈中弹出了。
Event Loop (事件循环)
JS到底是怎么运行的呢?
JS
引擎常驻于内存中,等待宿主将JS
代码或函数传递给它。
也就是等待宿主环境分配宏观任务,反复等待 - 执行即为事件循环。
Event Loop中,每一次循环称为tick,每一次tick的任务如下:
- 执行栈选择最先进入队列的宏任务(一般都是script),执行其同步代码直至结束;
- 检查是否存在微任务,有则会执行至微任务队列为空;
- 如果宿主为浏览器,可能会渲染页面;
- 开始下一轮tick,执行宏任务中的异步代码(setTimeout等回调)。
宏任务和微任务
ES6 规范中,microtask 称为
jobs
,macrotask 称为task
宏任务是由宿主发起的,而微任务由JavaScript自身发起。
在ES3以及以前的版本中,JavaScript本身没有发起异步请求的能力,也就没有微任务的存在。在ES5之后,JavaScript引入了Promise
,这样,不需要浏览器,JavaScript引擎自身也能够发起异步任务了。
微任务
一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前
常见的微任务有:
- Promise.then
- MutaionObserver
- Object.observe(已废弃;Proxy 对象替代)
- process.nextTick(Node.js)
宏任务
宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合
常见的宏任务有:
- script (可以理解为外层同步代码)
- setTimeout/setInterval
- UI rendering/UI事件
- postMessage、MessageChannel
- setImmediate、I/O(Node.js)
这时候,事件循环,宏任务,微任务的关系如图所示
按照这个流程,它的执行机制是:
执行一个宏任务,如果遇到微任务就将它放到微任务的事件队列中
当前宏任务执行完成后,会查看微任务的事件队列,然后将里面的所有微任务依次执行完
区别
所以,总结一下,两者区别为:
宏任务(macrotask) | 微任务(microtask) | |
---|---|---|
谁发起的 | 宿主(Node、浏览器) | JS引擎 |
具体事件 | 1. script (可以理解为外层同步代码) 2. setTimeout/setInterval 3. UI rendering/UI事件 4. postMessage,MessageChannel 5. setImmediate,I/O(Node.js) |
1. Promise.then 2. MutaionObserver 3. Object.observe(已废弃; Proxy 对象替代)4. process.nextTick(Node.js) |
谁先运行 | 后运行 | 先运行 |
会触发新一轮Tick吗 | 会 | 不会 |
拓展
async
和await
async
是异步的意思,await
则可以理解为 async wait
。所以可以理解async
就是用来声明一个异步方法,而 await
是用来等待异步方法执行
async
async
是通过Promise
包装异步任务。
async
函数返回一个promise
对象,下面两种方法是等效的
function f() {
return Promise.resolve('TEST');
}
// asyncF is equivalent to f!
async function asyncF() {
return 'TEST';
}
await
正常情况下,await
命令后面是一个 Promise
对象,返回该对象的结果。如果不是 Promise
对象,就直接返回对应的值
async function f(){
// 等同于
// return 123
return await 123
}
f().then(v => console.log(v)) // 123
不管await
后面跟着的是什么,await
都会阻塞后面的代码
async function fn1 (){
console.log(1)
await fn2()
console.log(2) // 阻塞
}
async function fn2 (){
console.log('fn2')
}
fn1()
console.log(3)
上面的例子中,await
会阻塞下面的代码(即加入微任务队列),先执行 async
外面的同步代码,同步代码执行完,再回到 async
函数中,再执行之前阻塞的代码
所以上述输出结果为:1,fn2,3,2
ex
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()
resolve()
}).then(() => {
// 执行async1()函数await之后的语句
console.log('async1 end')
})
当调用 async1
函数时,会马上输出 async2 end
,并且函数返回一个 Promise
,接下来在遇到 await
的时候会就让出线程开始执行 async1
外的代码(可以把 await
看成是让出线程的标志)。
然后当同步代码全部执行完毕以后,就会去执行所有的异步代码,那么又会回到 await
的位置,去执行 then
中的回调。
setTimeout
,setImmediate
谁先执行?
setImmediate
和process.nextTick
为Node环境下常用的方法(IE11支持setImmediate
),所以,后续的分析都基于Node宿主。
Node.js是运行在服务端的js,虽然用到也是V8引擎,但由于服务目的和环境不同,导致了它的API与原生JS有些区别,其Event Loop还要处理一些I/O,比如新的网络连接等,所以与浏览器Event Loop不太一样。
执行顺序如下:
- timers: 执行setTimeout和setInterval的回调
- pending callbacks: 执行延迟到下一个循环迭代的 I/O 回调
- idle, prepare: 仅系统内部使用
- poll: 检索新的 I/O 事件;执行与 I/O 相关的回调。事实上除了其他几个阶段处理的事情,其他几乎所有的异步都在这个阶段处理。
- check: setImmediate在这里执行
- close callbacks: 一些关闭的回调函数,如:socket.on('close', ...)
一般来说,setImmediate
会在setTimeout
之前执行,如下:
console.log('outer');
setTimeout(() => {
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
});
}, 0);
其执行顺序为:
- 外层是一个setTimeout,所以执行它的回调的时候已经在timers阶段了
- 处理里面的setTimeout,因为本次循环的timers正在执行,所以其回调其实加到了下个timers阶段
- 处理里面的setImmediate,将它的回调加入check阶段的队列
- 外层timers阶段执行完,进入pending callbacks,idle, prepare,poll,这几个队列都是空的,所以继续往下
- 到了check阶段,发现了setImmediate的回调,拿出来执行
- 然后是close callbacks,队列是空的,跳过
- 又是timers阶段,执行
console.log('setTimeout')
但是,如果当前执行环境不是timers阶段,就不一定了。。。。顺便科普一下Node里面对setTimeout
的特殊处理:setTimeout(fn, 0)
会被强制改为setTimeout(fn, 1)
。
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
});
其执行顺序为:
- 遇到
setTimeout
,虽然设置的是0毫秒触发,但是被node.js强制改为1毫秒,塞入times阶段 - 遇到
setImmediate
塞入check阶段 - 同步代码执行完毕,进入
Event Loop
- 先进入
times
阶段,检查当前时间过去了1毫秒没有,如果过了1毫秒,满足setTimeout
条件,执行回调,如果没过1毫秒,跳过 - 跳过空的阶段,进入
check
阶段,执行setImmediate
回调
可见,1毫秒是个关键点,所以在上面的例子中,setImmediate
不一定在setTimeout
之前执行了。
Promise
,process.nextTick
谁先执行?
因为process.nextTick
为Node环境下的方法,所以后续的分析依旧基于Node。
process.nextTick()
是一个特殊的异步API,其不属于任何的Event Loop
阶段。事实上Node在遇到这个API时,Event Loop
根本就不会继续进行,会马上停下来执行process.nextTick()
,这个执行完后才会继续Event Loop
。
所以,nextTick
和Promise
同时出现时,肯定是nextTick
先执行,原因是nextTick
的队列比Promise
队列优先级更高。
应用场景 - Vue
中的vm.$nextTick
vm.$nextTick
接受一个回调函数作为参数,用于将回调延迟到下次DOM更新周期之后执行。
这个API就是基于事件循环实现的。
“下次DOM更新周期”的意思就是下次微任务执行时更新DOM,而vm.$nextTick
就是将回调函数添加到微任务中(在特殊情况下会降级为宏任务)。
因为微任务优先级太高,Vue 2.4版本之后,提供了强制使用宏任务的方法。
vm.$nextTick优先使用Promise,创建微任务。
如果不支持Promise或者强制开启宏任务,那么,会按照如下顺序发起宏任务:
- 优先检测是否支持原生 setImmediate(这是一个高版本 IE 和 Edge 才支持的特性)
- 如果不支持,再去检测是否支持原生的MessageChannel
- 如果也不支持的话就会降级为 setTimeout。
流程分析
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(function () {
console.log('settimeout')
})
async1()
new Promise(function (resolve) {
console.log('promise1')
resolve()
}).then(function () {
console.log('promise2')
})
console.log('script end')
分析过程:
- 执行整段代码,遇到 console.log('script start') 直接打印结果,输出 script start
- 遇到定时器了,它是宏任务,先放着不执行
- 遇到 async1(),执行 async1 函数,先打印 async1 start,下面遇到await怎么办?先执行 async2,打印 async2,然后阻塞下面代码(即加入微任务列表),跳出去执行同步代码
- 跳到 new Promise 这里,直接执行,打印 promise1,下面遇到 .then(),它是微任务,放到微任务列表等待执行
- 最后一行直接打印 script end,现在同步代码执行完了,开始执行微任务,即 await 下面的代码,打印 async1 end
- 继续执行下一个微任务,即执行 then 的回调,打印 promise2
- 上一个宏任务所有事都做完了,开始下一个宏任务,就是定时器,打印 settimeout
所以最后的结果是:script start、async1 start、async2、promise1、script end、async1 end、promise2、settimeout
测试
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
// chrome 控制台结果
// script start
// async2 end
// Promise
// script end
// async1 end
// promise1
// promise2
// undefined // ???
// setTimeout
// edge 控制台结果
// script start
// async2 end
// Promise
// script end
// undefined // ???
// async1 end
// promise1
// promise2
// setTimeout