什么是事件循环机制?
我们都知道JavaScript是一门单线程语言,主线程只有一个,那么在这里我们就有疑问了,既然js是单线程的,那么它在处理操作的时候的效率是不是很慢,但是为什么我没有感受到卡顿呢?这是因为js本身存在的事件循环机制(EventLoop)。Event Loop事件循环,其实就是JS引擎管理事件执行的一个流程,具体由运行环境确定。目前JS的主要运行环境有两个,浏览器和Node.js。
浏览器是多进程的,浏览器每一个打开一个Tab页面(网页)都代表着创建一个独立的进程(至少需要四个,若页面有插件运行,则五个)。渲染进程(浏览器内核)是多线程的,也是浏览器的重点,因为页面的渲染,JS执行等都在这个进程内进行
EventLoop(事件循环)
浏览器的事件循环分为同步任务和异步任务;所有同步任务都在主线程上执行,形成一个函数调用栈(执行栈),而异步则先放到任务队列(task queue)里,任务队列又分为宏任务(macro-task)与微
任务(micro-task)。下面的整个执行过程就是事件循环
宏任务大概包括::script(整块代码)、setTimeout、setInterval、I/O、UI交互事件、setImmediate(node环境)
微任务大概包括::promise().then(回调)、MutationObserver、process.nextTick(node环境),若同时存在promise和nextTick,则先执行nextTick
执行过程
先从script(整块代码)开始第一次循环执行,接着对同步任务进行执行,在执行同步任务的过程中,遇到了微任务则会将微任务添加到当前的同步任务的任务队列里面,注意是当前的同步任务,直到调用栈被清空,然后去执行所有的微任务,当所有微任务执行完毕之后。再次从宏任务开始循环执行,直到执行完毕,然后再执行所有的微任务,就这样一直循环下去。如果在执行微队列任务的过程中,又产生了微任务,那么会加入整个队列的队尾,也会在当前的周期中执行。
setTimeout(function() {
console.log('5');
})
new Promise(function(resolve) {
console.log('1');
for(var i = 0; i < 1000; i++) {
i == 99 && resolve();
}
console.log('2');
}).then(function() {
console.log('4');
})
console.log('3');
执行结果:1 -> 2 -> 3 ->4 -> 5
执行过程:
1、首先当代码执行到,steTimeout时,由于setTimeout属于宏任务,所以会将setTimeout放入宏任务队列
2、执行到new Promise的时候,里面的代码属于同步任务,则会直接执行输出1和2,由于Promise的状态变成了成功状态,所以会继续往下走,遇到了then,由于then是一个微任务,所以会将其放入微任务队列中
3、然后去执行输出3
4、执行完成3之后,去执行当前的任务队列中的微任务,输出4,执行完成之后,一次事件循环就算是结束了
5、最后在去执行宏任务队列中的代码输出5
async、await
对于在执行的过程中遇到了async
、await
的话,会有些与不一样。
使用 async
函数声明来定义一个异步函数。这样的函数返回一个 AsyncFunction 对象。AsyncFunction 对象表示执行包含在这个函数中的代码的异步函数。当一个 async 函数被调用,它返回一个 Promise。当 async 函数返回一个值,它不是一个 Promise,Promise 将会被自动创建,然后它使用函数的返回值来决定状态。当 async 抛出一个异常,Promise 使用抛出的值进入已失败状态。
我们需要知道的是,如果 await 后面跟的不是一个 Promise,那 await 后面表达式的运算结果就是它等到的东西;如果 await 后面跟的是一个 Promise 对象,await 它会“阻塞”后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值作为 await 表达式的运算结果。但是此“阻塞”非彼“阻塞”这就是 await 必须用在 async 函数中的原因。async 函数调用不会造成“阻塞”,它内部所有的“阻塞”都被封装在一个 Promise 对象中异步执行。(这里的阻塞理解成异步等待更合理)
我们来看在任务队列中async/await的运行机制:
1、async定义的是一个Promise函数和普通函数一样只要不调用就不会进入事件队列。
2、async内部如果没有主动return Promise,那么async会把函数的返回值用Promise包装。
3、await关键字必须出现在async函数中,await后面不是必须要跟一个异步操作,也可以是一个普通表达式。
4、遇到await关键字,await右边的语句会被立即执行然后await下面的代码进入等待状态,等待await得到结果。
5、await后面如果不是 promise 对象, await会阻塞后面的代码,先执行async外面的同步代码,同步代码执行完,再回到async内部,把这个非promise的东西,作为 await表达式的结果。
6、await后面如果是 promise 对象,await 也会暂停async后面的代码,先执行async外面的同步代码,等着 Promise 对象 fulfilled,然后把 resolve 的参数作为 await 表达式的运算结果。
async function test() {
console.log('test start');
await undefined;
console.log('await 1');
await new Promise(r => {
console.log('promise in async');
r();
});
console.log('await 2');
}
test();
new Promise((r) => {
console.log('promise');
r();
}).then(() => {
console.log(1)
}).then(() => {
console.log(2)
}).then(() => {
console.log(3)
}).then(() => {
console.log(4)
});
我们先看一下执行结果: test start -> promise -> await 1 -> promise in async -> 1-> await 2 -> 2 -> 3 -> 4
为什么会出现这样的结果呢:
1、从test()开始,先输出 ‘test start’,await 后面并不是Promise,不需要等then,这时候后面的 console.log(‘await 1’) await new Promise console.log(‘await 2’) 会立即进入微任务队列。
2、然后 console.log(‘promise’) 运行,console.log(1) 进入微任务队列。
3、现在开始运行微任务,依据先入先出,console.log(‘await 1’)、console.log(‘promise in async’) 被运行。
4、console.log(‘promise in async’) 所在的Promise前面加了await,所以 console.log(‘await 2’) 作为微任务进入微任务队列。
5、刚才第二位进入微任务队列的 console.log(1) 运行,console.log(2) 进入微任务队列,在 console.log(2) 运行之前要先运行 console.log(‘await 2’) 因为await2入队列早,后面的依次 2、3、4。