JS的并发模型和Event Loop
单线程是JavaScript的一个基本特性,在JS引擎(例如V8)中只有一个调用堆栈来执行程序,因此绝不会同时运行两个事件处理程序,也不会在一个事件处理程序运行的同时触发其他计时器。
注:为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JS 创建多个线程,但是子线程完全受主线程控制,且运行于独立的环境,有着完全独立的全局对象,还不能访问Window和Document对象。所以,这个新标准并没有改变 JS单线程的本质。
JS怎么实现异步、不阻塞?
JS有一个基于事件循环的并发模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务。
之所以称之为事件循环,是因为它经常按照类似如下的方式来被实现:
while (queue.waitForMessage()) {
queue.processNextMessage();
}
虽然JS执行线程本身是单线程,但浏览器不是单线程的(GUI渲染线程、事件触发线程、XHR请求线程等等),V8引擎在遇到异步调用,如setTimeout、XMLHttpRequest等Web API调用后,会将其交给浏览器的其他线程执行处理,处理结果会放入任务队列,同时主线程继续执行程序,事件循环机制会不断检测执行栈是否为空,然后把任务队列中的下一个任务添加到执行栈。所以事件循环就是JS异步编程背后的秘密。事件循环是一个不断运行的进程,它协调调用堆栈和回调队列之间的任务以实现并发,给了我们多线程的错觉。
-
永不阻塞
因为有了这个事件循环模型,处理 I/O 通常通过事件和回调来执行,所以当一个应用正等待一个 IndexedDB 查询返回或者一个 XHR 请求返回时,它仍然可以处理其它事情,比如用户输入。
* 由于历史原因有一些例外,如 alert 或者同步 XHR,但应该尽量避免使用它们。
除了广义的同步任务和异步任务,JavaScript 单线程中的任务可以细分为宏任务和微任务。
macrotask
script(整体代码), setTimeout, setInterval, I/O, UI rendering
microtask
Promise
第一次事件循环中,JavaScript 引擎会把整个 script 代码当成一个宏任务执行,执行完成之后,再检测本次循环中是否存在微任务,存在的话就依次从微任务的任务队列中读取执行完所有的微任务,再读取宏任务的任务队列中的下一个宏任务执行,又再执行微任务队列中的所有微任务,如此循环。
* 与JS的另外一个运行时Node.js不同的是,Node端中microtasks是在事件循环的各个阶段之间执行。
why single-threaded ?
设计如此,就是为了简单易用,避免处理因多线程带来的各种复杂场景。
堆(heap)和栈(stack)
堆:堆是一个用来表示一大块(通常是非结构化的)内存区域,主要存储对象型数据。
栈:内存栈存储基本类型数据,执行栈(call stack)负责代码执行。