Javascript是一个单线程、非阻塞、异步、解释性脚本语言。js的并发模型基于事件循环,Event Loop是由js宿主环境,如浏览器实现的。v8是Chrome里的javascript运行环境,在V8的源码中并不存在setTimeout/DOM/HTTP请求等 ,这些异步请求在浏览器中由webAPI处理,它是由C++实现的浏览器创建的线程。
以下是浏览器中事件循环机制的流程图,只要执行栈中没有代码在执行,微任务会在回调后立即执行。
关于宏任务、微任务的说法有争议,ecma-262 中称之为
Jobs
和Job Queues
,这里对宏任务和微任务的理解是参考 Tasks, microtasks, queues and schedules 和 事件循环处理模型,并且不同浏览器对一些异步事件的执行机制有所不同,这里只考虑Chrome情况下的执行顺序。此外这个视频 到底什么是Event Loop 比较基础地介绍了事件循环机制。
以下各个例子有助于加深理解事件循环机制,可自己思考得出结果后再对比运行结果 。
e.g.1
Promise.resolve().then(function promise1 () {
console.log('promise1');
})
setTimeout(function setTimeout1 (){
console.log('setTimeout1')
Promise.resolve().then(function promise2 () {
console.log('promise2');
})
}, 0)
setTimeout(function setTimeout2 (){
console.log('setTimeout2')
}, 0)
e.g.2
new Promise(resolve => {
resolve(1)
Promise.resolve().then(() => console.log(42))
console.log(4)
}).then(t => console.log(t))
console.log(3)
e.g.3
async函数是promise的语法糖,await中的语句相当于在promise.resolve中,相当于同步任务,会被立即加入执行栈;await后的语句相当于在promise.then中,如果微任务中嵌套有宏任务,会在执行到该微任务后再将宏任务进入宏任务队列,从而进入下一轮事件循环。
console.log('script start')
async function async1() {
await async2();
console.log('async1 end');
setTimeout(function() {
console.log('async1 setTimeout')
}, 0);
}
async function async2() {
console.log('async2 end');
setTimeout(function() {
console.log('async2 setTimeout')
}, 0);
}
async1();
setTimeout(function() {
Promise.resolve().then(function() {
console.log('setTimeout promise');
})
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')
e.g.4
执行下面 两个例子时会发现与预期有出入,是因为:
- promise.nextTick是放到当前执行栈的尾部,一定比异步的任务队列早,并不是因为优先级高于其他异步任务。
- 根据 mdn
setTimeout()
/setInterval()
的每调用一次定时器的最小间隔是4ms,参考 底层源码,第一个例子中,传入1ms和0ms最后执行的都是1ms,第二个例子中的两个setTimeout延时相同,所以被合入了一个宏任务一起执行。
setTimeout(() => {
console.log(2)
}, 2)
setTimeout(() => {
console.log(1)
}, 1)
setTimeout(() => {
console.log(0)
}, 0)
console.log(1)
setTimeout(() => {
console.log(2)
new Promise(resolve => {
console.log(4)
resolve()
}).then(() => {
console.log(5)
})
process.nextTick(() => {
console.log(3)
})
})
new Promise(resolve => {
console.log(7)
resolve()
}).then(() => {
console.log(8)
})
process.nextTick(() => {
console.log(6)
})
setTimeout(() => {
console.log(9)
process.nextTick(() => {
console.log(10)
})
new Promise(resolve => {
console.log(11)
resolve()
}).then(() => {
console.log(12)
})
})
浏览器与node事件循环的根本区别就是:浏览器是执行完一个宏任务后就会清空微任务队列,node是将同源的宏任务执行完毕之后再去清空微任务队列,简单理解就是宏观任务可以进行合并。可以用下面这个例子在浏览器和node上自行运行试验
console.log(1);
setTimeout(() => {
console.log(2)
new Promise((resolve) => {
console.log(6);
resolve(7);
}).then((num) => {
console.log(num);
})
});
setTimeout(() => {
console.log(3);
new Promise((resolve) => {
console.log(9);
resolve(10);
}).then((num) => {
console.log(num);
})
setTimeout(()=>{
console.log(8);
})
})
new Promise((resolve) => {
console.log(4);
resolve(5)
}).then((num) => {
console.log(num);
new Promise((resolve)=>{
console.log(11);
resolve(12);
}).then((num)=>{
console.log(num);
})
})
参考
浏览器中的事件循环
Event Loop的规范和实现
浏览器中的事件循环
https://github.com/yangxy6/FE_Learning/issues/2
https://learnku.com/articles/38802
https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/26