背景
一天惬意的下午。敌人给我分享了一道头条面试题,如下:
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')
这个题目次要是考查对同步工作、异步工作:setTimeout、promise、async/await的执行程序的了解水平。(倡议大家也本人先做一下o)
过后因为我对async、await理解的不是很分明,答案错的千奇百怪 :(),就不记录了,而后我就去看文章理了理思路。当初写在上面以供日后参考。
js事件轮询的一些概念
这里首先须要明确几个概念:同步工作、异步工作、工作队列、microtask、macrotask
同步工作
指的是,在主线程上排队执行的工作,只有前一个工作执行结束,能力执行后一个工作;
异步工作
指的是,不进入主线程、而进入"工作队列"(task queue)的工作,期待同步工作执行结束之后,轮询执行异步工作队列中的工作
macrotask 即宏工作,宏工作队列等同于咱们常说的工作队列,macrotask是由宿主环境散发的异步工作,事件轮询的时候总是一个一个工作队列去查看执行的,"工作队列"是一个先进先出的数据结构,排在后面的事件,优先被主线程读取。
microtask 即微工作,是由js引擎散发的工作,总是增加到当前任务队列开端执行。另外在解决microtask期间,如果有新增加的microtasks,也会被增加到队列的开端并执行。留神与setTimeout(fn,0)的区别:
setTimeOut(fn(),0)
指定某个工作在主线程最早可得的闲暇工夫执行,也就是说,尽可能早得执行。它在"工作队列"的尾部增加一个事件,因而要等到同步工作和"工作队列"现有的事件都解决完,才会失去执行。
总结一下:
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.
所以咱们能够失去js执行程序是:
开始 -> 取第一个task queue里的工作执行(能够认为同步工作队列是第一个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 上面的语句会被当成微工作增加到当前任务队列的开端异步执行。参考 前端进阶面试题具体解答
咱们来看一下答案
不记得题的!持续往下看,舒适的筹备了题目,不必往上翻 :)
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')
=node10版本是这个后果: script start -> async1 start -> async2 -> promise1 -> script end -> promise2 -> async1 end -> setTimeout<node10版本是这个后果: script start -> async1 start -> async2 -> promise1 -> script end -> async1 end -> promise2 -> setTimeout
依照下面写的js执行程序就能够失去正确后果,但最初却又存在两个答案,为什么会呈现两种后果呢?咱们能够看到两种后果中就是async1 end 和 Promise2之间的程序呈现差异,我猜测是因为不同版本的node对await的执行办法不同,导致await上面的代码进入工作队列的工夫点不同。具体参见 如何在V8中优化JavaScript异步编程? 外面的《深刻理解await》
简略了解如下:
async function f(){ await p console.log(1);}//node.js8及行将推广的规范应该会解析成上面这样function f(){ Promise.resolve(p).then(()=>{ console.log(1) })}//其余的版本应该会解析成上面的这样function f(){ new Promise(resolve=>{ resolve(p) }).then(()=>{ console.log(1) })}
正对下面的这两种差别次要是:
- 当Promise.resolve 的参数为 promise 对象时间接返回这个 Promise 对象,then 函数在这个 Promise 对象产生扭转后立即执行。
- 旧版的解析 await 时会从新生成一个Promise对象。只管该 promise 确定会 resolve 为 p,但这个过程自身是异步的,也就是当初进入队列的是新 promise 的 resolve 过程,所以该 promise 的 then 不会被立刻调用,而要等到以后队列执行到前述 resolve 过程才会被调用,而后再执行then函数。(上面的练习一下的例子会解说当resolve()参数为promise时会怎么执行)
不必放心这个题没解,假相只有一个。依据 TC39 最近决定,await将间接应用 Promise.resolve() 雷同语义。
最初咱们以最新决定来剖析这个题目的可能的执行过程(在Chrome环境下):
- 定义函数async1、async2。输入'script start'
- 将 setTimeout 外面的回调函数(宏工作)增加到下一轮工作队列。因为这段代码后面没有执行任何的异步操作且等待时间为0s。所以回调函数会被立即放到下一轮工作队列的结尾。
- 执行async1。咱们晓得async函数外面await标记之前的语句和 await 前面的语句是同步执行的。所以这里先后输入"async1 start",’async2 start‘.
- 这时暂停执行上面的语句,上面的语句被放到以后队列的最初。
- 继续执行同步工作。
- 输入 ‘Promise1’。将then外面的函数放在以后队列的最初。
- 而后输入‘script end’,留神这时只是同步工作执行完了,当前任务队列的工作还没有执行结束,还有两个微工作被增加进来了!队列是先进先出的构造,所以这里先输入 ‘async1 end’ 再输入 ‘Promise2’,这时第一轮工作队列才真算执行完了。
- 而后执行下一个工作列表的工作。执行setTimeout外面的异步函数。输入‘setTimeout’。
练习一下
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));
所以这里第一次执行到这儿的时候:
- 第一个then函数外面的
() => resolvedPromise.then(resolve, reject)
为microtask。会被放入当前任务列表的最初 - 而后是Promise1被放入工作列表的最初。
- 没有同步操作了开始执行工作列表,这时因为resolvedPromise是一个曾经resolved的Promise间接执行then函数,将then函数中的resole()函数放入以后队列的最初,而后输入Promise1。
- 将Promise2放入队列的最初。执行resole()
- 这时的resolvePromise终于变成了一个resolved状态的Promise对象了,将‘resolvePromise resolved’放入当前任务列表的最初。输入Promise2。
- 将Promise3放到当前任务队列的最初。输入resolvePromise resolved。最初输入Promise3.
完结!这外面的几段代码是比拟重要的,解释了js会依照什么样的形式来执行这些新个性。
最初如果有误,欢送斧正。