事件循环机制(Event Loop)
一、介绍
JavaScript是一门单线程语言,指主线程只有一个。
为什么Javascript号称是单线程,但是又能支持异步非阻塞执行呢?
在整个JavaScript执行线程中只有一个调用栈,也就是只有一个时间线索,即每调用一个函数就只能往这个栈进行压栈,最后调用的函数在栈顶。执行完成后从栈顶逐个返回。
JavaScript没有提供创建新线程的方法,让我们可以像上面多线程一样创建多一个线程出来,让线程A执行函数FA,线程B执行函数FB。
JavaScript执行线程本身是单线程,但是整个浏览器不是单线程的。V8引擎在检测到异步调用,如setTimeout等WebAPIs调用后,会将其交给浏览器的其他线程进行执行处理,完了后再通过事件循环机制返回执行线程执行回调。
浏览器是多进程的,浏览器每一个打开一个Tab页面(网页)都代表着创建一个独立的进程(至少需要四个,若页面有插件运行,则五个)。渲染进程(浏览器内核)是多线程的,也是浏览器的重点,因为页面的渲染,JS执行等都在这个进程内进行。
浏览器的进程主要包括:
GUI渲染线程JS引擎线程定时触发器线程事件触发线程异步http请求线程
浏览器的事件循环分为同步任务和异步任务:所有同步任务都在主线程上执行,形成一个函数调用栈(执行栈),而异步则先放到任务队列(task queue)里,任务队列又分为宏任务(macro-task)与微任务(micro-task)。下面的整个执行过程就是事件循环
宏任务大概包括::script(整块代码)、setTimeout、setInterval、I/O、UI交互事件、setImmediate(node环境)
微任务大概包括::new promise().then(回调)、MutationObserver(html5新特新)、Object.observe(已废弃)、process.nextTick(node环境)
ps:若同时存在promise和nextTick,则先执行nextTick。
二、执行过程
1.先从script(整块代码,即一次宏任务)开始第一次循环执行;
2.接着对同步任务进行执行,直到调用栈被清空;
3.一次宏任务(script)执行结束后,需要去执行该次宏任务中生成的所有的微任务,直到所有微任务执行完毕,微任务队列清空;
4.此时微任务队列已经清空,再次从宏任务队列按照先入先出的规则取出一个宏任务任务执行;
5.每个宏任务完成后需要执行该次宏任务中生成的微任务队列;
6.如果在执行微队列任务的过程中,又产生了微任务,那么会加入微任务队列的队尾,也会在当前的周期中执行,直到两个任务队列全部执行完毕,代码结束。
以上过程相当于任务在不断的宏任务和微任务之间循环,所以称为事件循环。
三、async/await
async/await也经常会用于处理异步。
async没什么好说的,一个修饰符,可以单独出现,使函数的返回值变成一个Promise对象。
await修饰符只能放在async函数内部,await关键字的作用就是获取Promise中返回的内容,获取的是Promise函数中resolve或者reject的值,如果await后面是 promise对象会造成异步函数停止执行并且等待 promise 的解决,如果await 后面并不是一个Promise的返回值,则会按照同步程序返回值处理。
1.后面跟Promise对象
function sleep(second) {
return new Promise((resolve, reject) => {
console.log('test')
setTimeout(() => {
resolve(' enough sleep~');
}, second);
})
}
async function awaitDemo() {
let result = await sleep(2000);
console.log("123")
console.log(result);
}
awaitDemo();
console.log('321');// 立刻打印test和321,两秒之后会被打印出来 '123'和' enough sleep~'
2.后面跟正常的表达式
function sleep(second) {
setTimeout(() => {
console.log(' enough sleep~');
}, second);
}
async function awaitDemo() {
let result = await sleep(2000);
console.log("123");
}
awaitDemo();
console.log('321');//立即按顺序输出321和123,两秒后输出' enough sleep~'
另外,async修饰的函数会返回一个Promise对象,当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
以下的例子中1比2先打印就用到了这个知识点,有兴趣可以研究以便于加深理解
async function test1() {
let a = await 1
console.log('2');
let c = await new Promise(resolve => {
setTimeout(() => {
resolve('setTimeout')
console.log('test2')
}, 3000);
resolve('3')
})
console.log('c:', c);
}
test1()
console.log('1');
setTimeout(() => {
console.log('test1');
})
// 1、2、c:3、test1、(三秒后)test2
四、参考
已经了解了上述的基础知识,就可以尝试做以下例题了。
async function async1() {
console.log('async1 start');
await async2();
console.log('asnyc1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(() => {
console.log('setTimeOut');
}, 0);
async1();
new Promise(function (reslove) {
console.log('promise1');
reslove();
}).then(function () {
console.log('promise2');
})
console.log('script end');
答案:script start => async1 => start => async2 => promise1 => script end => asnyc1 end => promise2 => setTimeOut
解析:
-
整个代码片段(script)作为一个宏任务执行,其内部按照事件循环机制的顺序执行代码。
-
同步代码
script start最先被打印 -
setTimeout作为异步中的宏任务被加入宏任务队列进行等待 -
执行
async1(),根据async/await的用法,如果await后面是 promise对象会造成异步函数停止执行并且等待 promise 的解决,如果await后面是正常的表达式则立即执行,await后面跟promise对象的这种情况可以将该函数剩余的代码看作是Promise.resolve().then()的内容加入到微任务队列中。等到整个代码片段(script)完成再接着执行函数后面的内容,所以先打印
async1 start,再执行async2(),打印async2,此时函数返回,后面的内容阻塞。 -
执行
new Promise,(已知promise本身是同步,但是promise的回调then和catch是异步的),先输出promise1,然后将resolve()放入微任务异步队列 -
执行
console.log('script end'),输出script end -
此时整个代码片段(script),即一次宏任务已经结束,当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件,所以将之前微任务队列中的队列按照先入先出的规则运行,先打印
asnyc1 end,再打印promise2。 -
最后执行
setTimeout,输出了settimeout。