作为脚本语言,JavaScript基本上是单线程事件驱动的。在特殊场景下,JavaScript可以使用某个些Worker来启动多线程。可是由于JavaScript的线程同步原语十分缺乏,基本上是靠发消息。即便是多线程情况下,也是每个线程一个事件循环,看起来跟单线程差不了太多。
JavaScript听从其宿主的驱使,但有事件发生的时候,宿主就给JavaScript的事件队列中派活,让它执行起来。一个例子是用户点击了浏览器页面的一个按钮。为了保证派活的次序,事件队列是要给先进先出队列。
像setInterval这些函数,其实不是JavaScript语言本身自带,而是浏览器提供的。调用setInterval,可以告诉浏览器,每隔一段时间往JavaScript派个指定的活。所以对于setInterval调用时所指定的任务,对于JavaScript来说是异步执行的,也就是说把当前代码执行完了之后才有可能执行。
像setInterval指定的任务,也称为MacroTask。对于MacroTask,JavaScript没有能力控制这个任务什么时候被派发。有可能宿主在派发这个任务之前插入很多其他任务,任务完成时间没法把控。为了应对这个 问题,ES6引入了一种新的异步机制,叫做promise。promise也是用来产生一个任务,但这个任务叫做MicroTask。MicroTask的特点是,当其就绪之后,必须在MacroTask之前执行。说白了,就是给JavaScript的事件队列引入了另一个优先级。
当然,为了支持promise的语义,MicroTask还增加了一些流程控制功能,比如可以在任务成功和失败的时候执行不同的代码。
那知道MicroTask和MacroTask在实际编码中有什么用处?Event loop: microtasks and macrotasks举了几个例子:
- 把特消耗CPU的任务划分成几个MacroTask,让其他MacroTask也能见机执行
- 对于不需要立马执行的任务,可以放到MacroTask里面,伺机执行
那使用promise还可以玩出新的花样,比如async function:
async function f() {
return Promise.resolve(1);
}
f().then(alert); // 1
上面的例子来自于 Async Await。async function是一个可以挂起并恢复的函数,在等待promise的时候,就挂起,在promise结果出来时就恢复。
参考
- The JavaScript Event Loop
- JavaScript Event Loop Explained
- Understanding Event Loop, Call Stack, Event & Job Queue in Javascript
- Event loop: microtasks and macrotasks
- Microtasks
- Async Await
- Understanding Asynchronous JavaScript
- Async/await vs Coroutines vs Promises vs Callbacks
- Asynchronous Programming
- https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators