了解这个,就得了解JavaScript异步编程,了解任务队列才能知其根本。
一个事件循环(event loop)+多个任务队列(task queue)
事件循环(event loop)
Task Queue
Macrotask Queue 宏任务队列:
- setTimeout
- setInterval
- setImmediate
- requestAnimationFrame
- UI rendeing
- NodeJS中的`I/O (fs.readFile)等
Microtask Queue 微任务队列:
主要包括两类:
- 独立回调microTask:如Promise,其成功/失败回调函数相互独立;
- 复合回调microTask:如 Object.observe, MutationObserver 和NodeJs中的 process.nextTick ,不同状态回调在同一函数体;
MacroTask MicroTask 两者关系
入栈过程:
- 开始执行JavaScript脚本,将任务JavaScript Run入栈macroTask队列;
- 同步resolvePromise后;
- 入栈
第一个
setTimeout任务进入macroTask队列- 入栈Proimse.then任务进入microTask队列;
- 入栈第二个setTimeout任务进入macroTask队列;
出栈执行过程:- 同步执行代码,退出第一个macroTask,即JavaScript Run;
- 按顺序执行microTask queue 所有microTask;
- 执行下一个macroTask;
可以参考这个流程图:
Show me the code
例题1-5 来自ES6 Book :
例题1
Promise 新建后就会立即执行。
1 | let promise = new Promise(function(resolve, reject) { |
上面代码中,Promise 新建后立即执行,所以首先输出的是Promise。然后,then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以resolved最后输出。
例题2
1 | new Promise((resolve, reject) => { |
上面代码中,调用resolve(1)以后,后面的console.log(2)还是会执行,并且会首先打印出来。这是因为立即 resolved 的 Promise 是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务。
一般来说,调用resolve或reject以后,Promise 的使命就完成了,后继操作应该放到then方法里面,而不应该直接写在resolve或reject的后面。所以,最好在它们前面加上return语句,这样就不会有意外。
1 | new Promise((resolve, reject) => { |
例题3
需要注意的是,立即resolve()的 Promise 对象,是在本轮“事件循环”(event loop)的结束时执行,而不是在下一轮“事件循环”的开始时。
1 | setTimeout(function () { |
上面代码中,setTimeout(fn, 0)在下一轮“事件循环”开始时执行,Promise.resolve()在本轮“事件循环”结束时执行,console.log(‘one’)则是立即执行,因此最先输出。
例题4 Promise.try
1 | const f = () => console.log('now'); |
上面代码中,函数f是同步的,但是用 Promise 包装了以后,就变成异步执行了。
那么有没有一种方法,让同步函数同步执行,异步函数异步执行,并且让它们具有统一的 API 呢?回答是可以的,并且还有两种写法。第一种写法是用async函数来写。
1 | const f = () => console.log('now'); |
上面代码中,第二行是一个立即执行的匿名函数,会立即执行里面的async函数,因此如果f是同步的,就会得到同步的结果;如果f是异步的,就可以用then指定下一步,就像下面的写法。
1 | (async () => f())() |
需要注意的是,async () => f()会吃掉f()抛出的错误。所以,如果想捕获错误,要使用promise.catch方法。
1 | (async () => f())() |
第二种写法是使用new Promise()。
1 | const f = () => console.log('now'); |
上面代码也是使用立即执行的匿名函数,执行new Promise()。这种情况下,同步函数也是同步执行的。
鉴于这是一个很常见的需求,所以现在有一个提案,提供Promise.try方法替代上面的写法。
1 | const f = () => console.log('now'); |
例题5, 6 来自浅析setTimeout与Promise:
例题5
1 | var p1 = new Promise(function(resolve, reject){ |
1 | Before resolve |
- 开始执行JavaScript脚本,将任务JavaScript Run入栈macroTask队列;
- 同步resolvePromise后;
- 入栈第一个setTimeout任务进入macroTask队列
- 入栈Proimse.then任务进入microTask队列;
- 入栈第二个setTimeout任务进入macroTask队列;
- 同步执行代码完毕,退出第一个macroTask,即JavaScript Run; 输出 Before resolve
- 执行清空microTask;输出 p1 fulfilled
- 执行下一个macroTask;输出 will be executed at the top of the next Event Loop
will be executed at the bottom of the next Event Loop
例题6
1 | setTimeout(function(){ |
例题7
来源: Promise的队列与setTimeout的队列有何关联?
1 | setTimeout(function(){console.log(4)},0); |
1 |
1 |
- 首先同步执行完所有代码,其间注册了三个setTimeout异步任务,100个Promise异步任务;
- 然后检查MacroTask队列,取第一个到期的MacroTask,执行输出will be executed at the top of the next Event Loop;
- 然后检查MicroTask队列,发现没有到期的MicroTask,进入第4步;
- 再次检查MacroTask,执行第二个setTimeout处理函数,resolve Promise;
- 然后检查MicroTask队列,发现Promise已解决,其异步处理函数均可执行,依次执行,输出promise then - 0 至promise then - 99;
- 最后再次检查MacroTask队列,执行输出will be executed at the bottom of the next Event Loop
- 交替往复检查两个异步任务队列,直至执行完毕;
Reference Links
- 浅析setTimeout与Promise
- What is the relationship between event loop and Promise [stackoverflow]
- ES6 Book
- Promise的队列与setTimeout的队列有何关联?[知乎]
- Writing a JavaScript framework - Execution timing, beyond setTimeout
- 6-Concurrency model and event loop -part B
- Microtasks vs Events and how to define what as which?
- JavaScript: How is callback execution strategy for promises different than DOM events callback?