Event loop 事件循环

栈 Stack

函数调用形成一个 frames 栈。 function fn() {} function f() { return fn() } f()

调用 f 时,第一个 f 被创建,包含 f 的参数和局部变量。当 f 调用 fn 时,第二个 frame 被创建,包含 fn 的参数和局部变量,并压到第一个 frame 上面,当 fn 返回时,栈顶弹出(只剩 f 的调用 frame),当 f 也返回时,栈变空。

堆 Heap

对象分配在一个堆上,一个指示一大块通常未结构化的内存区。

队列 Queue

JavaScript 运行时(runtime)使用一个消息队列(message queue)。每个消息有一个关联函数,会被顺序调用,用来处理消息。

事件循环处理时,运行时开始处理队列中信息,先处理最早的。消息从队列移除,并作为输入参数传入对应调用函数,通常调用函数时创建新的栈 frame。
不断处理函数,直到栈空,事件循环接着处理队列中下一个消息。

事件循环

事件循环来源于它实际实现,类似这样:

while (queue.waitForMessage()) { queue.processNextMessage() }

如果一个消息尚未可用,则 queue.waitForMessage() 同步等待其到达。

从运行直到完成 Run-to-completion

每个信息必须在其他信息处理前完全处理完。无论何时函数运行,都不能被 pre-empted 并且其他代码执行前完整执行完。这不同于C,一个函数在一个线程执行时,可以随时被运行时系统停止去执行另一个线程的代码。

这种模式的一个缺点是,如果一个信息处理时间过长,web 应用将不能处理像 click 或 scroll 等用户交互。浏览器通常会用"a script is taking too long to run"对话框来缓解。一个好的习惯是尽量让一个信息处理变短,可能的话把一个信息分成多个信息。

增加消息 Adding messages

在web浏览器中,任何时候,事件发生且事件有监听器,那么消息就会被加入,如果没监听器,事件会丢失。

setTimeout 执行有两个参数: 一个加入队列的消息;一个延迟时间,表示消息被实际插入队列后(最小)延迟时间。如果队列中没其它消息,则延迟时间到后,消息被立即处理。然而如果有其他消息,则 setTimeout 消息将等其他消息处理完。

const _start = Date.now() setTimeout(function() { console.log(`期望100ms,实际${Date.now() - _start}ms后`) }, 100) while(true) { if(Date.now() - _start >= 1000) { console.log('1s') break } }

Several runtimes communicating together

一个 web worker 或者一个跨域 iframe 有自己的 stack、heap 和 message queue。 2个不同运行时只能通过 postMessage 发送消息方式来通信,如果另一方监听 message 事件,这个方式就会把 message 加到另一个运行时。

通过事件或回调处理 I/O 是种典型非阻塞操作。比如当应用程序在等待一个 IndexedDB 查询返回或一个 XHR 请求返回时,可以依然处理用户输入。 例外,比如 alert 或同步 synchronous XHR,但这些可以通过良好练习避免。 exceptions to the exception do exist

每个线程(thread)有自己的事件循环,每个 web worker 也就有自己的,从而能独立执行,然而同一个域的 window 公用一个事件循环。

一个事件循环有多个任务源,这些任务源保证了该源内的执行顺序(比如 IndexedDB 规范定义了它们自己的任务),但是浏览器可以在每次循环时选择从哪个源获取任务。这允许浏览器可以优先处理性能敏感的任务,比如用户输入。

Tasks 被调度时,浏览器从内部转到 JS/DOM 并且保证这些动作依次发生。

Microtasks 被调度时,通常是为了一些在当前正执行脚本之后将要发生的事情,比如响应一批动作,或把一些事情转成异步而不用影响整个新任务。微任务队列在没有其它 JS 在 mid-execution 回调后执行,并且在每个任务最后。在微任务处理时增加的微任务会被加到队列末尾。微任务包括 mutation observer 回调和 promise 回调。 确保 promise 回调时是异步的,即使这个 promise 已经被解决 settled。这样再次执行一个解决过 promise,会立即把一个微任务插入队列 .then(yey, nay)。 Promises 来自 ECMAScript 而不是 HTML。ECMAScript 已经有“jobs”,类似微任务, vague mailing list discussions。 把 promises 当任务处理会导致性能问题,因为回调会不必要地被任务相关的事拖延,比如渲染。还会和其它任务源交互时引起 non-determinism,被其它 API 打断交互。

const outer = document.querySelector('.c') const inner = document.querySelector('.inner') // 监听 outer 元素上属性变化 new MutationObserver(function() { console.log('mutate') }).observe(outer, { attributes: true }) // 监听 click function onClick() { console.log('click') setTimeout(function() { console.log('timeout') }, 0) Promise.resolve().then(function() { console.log('promise') }) outer.setAttribute('data-random', Math.random()) } inner.addEventListener('click', onClick) outer.addEventListener('click', onClick) // 用户触发 click // chrome // click > promise > mutate > click > promise > mutate > timeout > timeout

如果 the stack of script settings objects 是空的, 则执行微任务检查。 — HTML: Cleaning up after a callback。微任务检查会转入微任务队列。类似,ECMAScript 关于 jobs: 执行 Job 仅当没有正在运行的 execution context 和 execution context 栈是空的。

.click() 导致事件同步触发(dispatch synchronously),因此.click()调用脚本始终是在 the stack between callbacks。微任务不能中断 mid-execution 的脚本,意味微任务在 listener callbacks 完成之后再处理。