js事件轮询的一些概念
这里需要明白几个概念:同步任务、异步任务、task queue、microtask、macrotask
同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
task queue、microtask、macrotask
- An event loop has one or more task queues.(task queue is macrotask queue)
- Each event loop has a microtask queue.
- task queue = macrotask queue != microtask queue
- a task may be pushed into macrotask queue,or microtask queue
- when a task is pushed into a queue(micro/macro),we mean preparing work is finished,so the task can be executed now.
注意上面的 task queue = macrotask queue != microtask queue。macrotask队列等同于我们常说的任务队列是由宿主环境分发的任务,事件轮询的时候总是一个一个任务队列去查看执行的,"任务队列"是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。
microtask是由js引擎分发的任务,总是添加到当前任务队列末尾执行。另外在处理microtask期间,如果有新添加的microtasks,也会被添加到队列的末尾并执行。注意与setTimeout(fn,0)的区别:
setTimeOut(fn(),0)指定某个任务在主线程最早可得的空闲时间执行,也就是说,尽可能早得执行。它在"任务队列"的尾部添加一个事件,因此要等到同步任务和"任务队列"现有的事件都处理完,才会得到执行。
所以我们可以得到js执行顺序是:
开始 -> 取第一个task queue里的任务执行 -> 取microtask全部任务依次执行 -> 取下一个task queue里的任务执行 -> 再次取出microtask全部任务执行 -> … 这样循环往复
常见的一些宏任务和微任务:
macrotask:
- setTimeout
- setInterval
- setImmediate
- requestAnimationFrame
- I/O
- UI rendering
microtask:
- process.nextTick
- Promises
- Object.observe
- MutationObserver
Promise、Async、Await都是一种异步解决方案
Promise是一个构造函数,调用的时候会生成Promise实例。当Promise的状态改变时会调用then函数中定义的回调函数。我们都知道这个回调函数不会立刻执行,他是一个微任务会被添加到当前任务队列中的末尾,在下一轮任务开始执行之前执行。
async/await成对出现,async标记的函数会返回一个Promise对象,可以使用then方法添加回调函数。await后面的语句会同步执行。但 await 下面的语句会被当成微任务添加到当前任务队列的末尾异步执行。
实践出真知
先看两道题:
1、头条的一个面试题
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')
},0)
async1();
new Promise(function(resolve){
console.log('promise1')
resolve();
}).then(function(){
console.log('promise2')
})
console.log('script end')
node环境下---------Chrome环境下
script start-----------script start
async1 start---------async1 start
async2----------------async2
promise1-------------promise1
script end------------script end
promise2-------------async1 end
async1 end----------promise2
setTimeout-----------setTimeout
我们可以看到结果中就是async1 end 和 Promise2之间的顺序出现差别,这主要是V8最新版本与稍老版本的差异,他们对await的执行方法不同。
async function f(){
await p
console.log(1);
}
//新版V8会解析成下面这样
function f(){
Promise.resolve(p).then(()=>{
console.log(1)
})
}
//旧版的V8会解析成下面的这样
function f(){
new Promise(resolve=>{
resolve(p)
}).then(()=>{
console.log(1)
})
}
正对上面的这两种差异主要是: (1)Promise.resolve 的参数为 promise 对象时直接返回这个 Promise 对象,then 函数在这个 Promise 对象发生改变后立刻执行。(2)旧版的解析 await 时会重新生成一个Promise对象。尽管该 promise 确定会 resolve 为 p,但这个过程本身是异步的,也就是现在进入队列的是新 promise 的 resolve 过程,所以该 promise 的 then 不会被立即调用,而要等到当前队列执行到前述 resolve 过程才会被调用,然后再执行then函数。(第二个的例子会讲解当resolve()参数为promise时会怎么执行)
不用担心这个题没解,真相只有一个。根据 TC39 最近决议,await将直接使用 Promise.resolve() 相同语义。
最后我们以最新决议来分析这个题目的可能的执行过程:
定义函数async1、async2。输出’script start’。将 setTimeout 里面的回调函数是宏任务添加到下一轮任务队列。因为断代码前面没有执行任何的异步操作且等待时间为0s。所以回调函数会被立刻放到下一轮任务队列的开头。执行async1。我们知道async函数里面await标记之前的语句和 await 后面的语句是同步执行的。所以这里先后输出"async1 start",’async2 start‘.这时暂停执行下面的语句,下面的语句被放到当前队列的最后。继续执行同步任务。输出 ‘Promise1’。将then里面的函数里面的Promise2放在当前队列的最后。然后输出‘script end’,注意这时只是同步任务执行完了,当前任务队列的任务还没有执行完毕,还有两个微任务被添加进来了。队列是先进先出的结构,所以这里先输出 ‘async1 end’ 再输出 ‘Promise2’,这时第一轮任务队列才真算执行完了。然后执行下一个任务列表的任务。执行setTimeout里面的异步函数。输出‘setTimeout’。
2、stackoverflow上的一道题目
let resolvePromise = new Promise(resolve => {
let resolvedPromise = Promise.resolve()
resolve(resolvedPromise)
})
resolvePromise.then(() => {
console.log('resolvePromise resolved')
})
let resolvedPromiseThen = Promise.resolve().then(res => {
console.log('promise1')
})
resolvedPromiseThen
.then(() => {
console.log('promise2')
})
.then(() => {
console.log('promise3')
})
结果:
promise1
promise2
resolvePromise resolved
promise3
这道题真的是非常费解了。为什么’resolvePromise resolved’会在第三行才显示呢?
其实这个题目的难点就在于resolve一个Promise对象,js引擎会怎么处理。我们知道Promise.resolve()的参数为Promise对象时,会直接返回这个Promise对象。但当resolve()的参数为Promise对象时,情况会有所不同:
resolve(resolvedPromise)
//等同于:
Promise.resolve().then(() => resolvedPromise.then(resolve, reject));
所以这里第一次执行到这儿的时候() => resolvedPromise.then(resolve, reject)会被放入当前任务列表的最后,然后是Promise1被放入任务列表。没有同步操作了开始执行微任务列表了,这时resolvedPromise是一个已经resolved的Promise直接执行then函数,将resole()函数放入当前队列的最后,输出Promise1。将Promise2放入队列的最后。执行resole(),resolvePromise终于变成了一个resolved状态的Promise对象了,将‘resolvePromise resolved’放入当前任务列表的最后。输出Promise2。将Promise3放到当前任务队列的最后。输出resolvePromise resolved。输出PromiseP.结束!
参考:
通过microtasks和macrotasks看JavaScript异步任务执行顺序
JavaScript 运行机制详解:再谈Event Loop
async/await 在chrome 环境和 node 环境的 执行结果不一致,求解?
What’s the difference between resolve(thenable) and resolve(‘non-thenable-object’)?
本文深入探讨JS事件轮询的概念,包括同步任务、异步任务、macrotask和microtask。通过分析面试题和实际例子,揭示了Promise、Async/Await在事件循环中的执行顺序,以及在不同环境(如Node和Chrome)下的差异。文章强调了理解这些机制对于优化JavaScript代码的重要性。
2607

被折叠的 条评论
为什么被折叠?



