APRIL 20, 2015
Events, Concurrency and JavaScript
Modern web apps are inherently event-driven yet much of the browser internals for triggering, executing, and handling events can seem as black box. Browsers model asynchronous I/O thru events and callbacks, enabling users to press keys and click mouses while XHR requests and timers trigger in code. Understanding how events work is critical for crafting high performance JavaScript. In this post we’ll focus on the browser’s built-in Web APIs, callback queues, event loops and JavaScript’s run-time.
Code in action. A button and event handler.
Do Stuff document.getElementById('doStuff') .addEventListener('click',function() { console.log('Do Stuff'); } );
Let’s trace a Do Stuff click event thru browser and describe the components along the way.
From Philip Robert’s diagram
Browser Runtime
User Interface - User clicks the Do Stuff button. Simple enough.
Web APIs - The click event propagates thru the DOM’s Web API triggering click handlers during the capture and bubble phases on parent and child elements. Web APIs are a multi-threaded area of the browser that allows many events to trigger at once. They become accessible to JavaScript code thru the familiar window object on page load. Examples beyond the DOM’s document are AJAX’sXMLHttpRequest, and timers setTimeout() function[1].
Event Queue - Next the event’s callback is pushed into one of many event queues (also called task queues). Just as there are multiple Web APIs, browsers have event queues for things like network requests, DOM events, rendering, and more[2].
Event loop - Then a single event loop chooses which callback to push onto the JavaScript call stack[3]. Here’s C++ pseudo code for Firefox’s event loop.
while(queue.waitForMessage()){ queue.processNextMessage(); }
[4]
Finally the event callback enters the JavaScript’s runtime within the browser.
JavaScript Runtime
The JavaScript engine has many components such as a parser for script loading, heap for object memory allocation, garbage collection system, interpreter, and more. Like other code event handlers execute on it’s call stack.
5. Call Stack - Every function invocation including event callbacks creates a new stack frame (also called execution object). These stack frames are pushed and popped from the top of the call stack, the top being the currently executing code[5]. When the function is returned it’s stack frame is popped from the stack.
Chrome’s V8 C++ source code of single stack frame:
/** * v8.h line 1372 -- A single JavaScript stack frame. */classV8_EXPORTStackFrame{public:intGetLineNumber()const;intGetColumn()const;intGetScriptId()const; LocalGetScriptName()const; LocalGetScriptNameOrSourceURL()const; LocalGetFunctionName()const;boolIsEval()const;boolIsConstructor()const; };
[6]
Three characteristics of JavaScript’s call stack.
Single threaded - Threads are basic units of CPU utilization. As lower level OS constructs they consist of a thread ID, program counter, register set, and stack[7]. While the JavaScript engine itself is multi-threaded it’s call stack is single threaded allowing only one piece of code to execute at a time8.
Synchronous - JavaScript call stack carries out tasks to completion instead of task switching and the same holds for events. This isn’t a requirement by the ECMAScript or WC3 specs. But there are some exceptions like window.alert() interrupts the current executing task.
Non-blocking - Blocking occurs when the application state is suspended as a thread runs[7]. Browsers are non-blocking, still accepting events like mouse clicks even though they may not execute immediately.
CPU Intensive Tasks
CPU intensive tasks can be difficult because the single-threaded and synchronous run-time queues up other callbacks and threads into a wait state, e.g. the UI thread.
Let’s add a CPU intensive task.
Big Loop Do Stuff document.getElementById('bigLoop') .addEventListener('click',function() {// big loopfor(vararray = [], i = 0; i
Click Big Loop then Do Stuff. When Big Loop handler runs the browser appears frozen. We know JavaScript’s call stack is synchronous soBig Loopexecutes on the call stack until completion. It’s also non-blocking where Do Stuff clicks are still received even if they didn’t execute immediately.
Checkout this CodePen to see.
Solutions
1) Break the big loop into smaller loops and use setTimeout() on each loop.
... document.getElementById('bigLoop') .addEventListener('click',function() {vararray= []// smaller loopsetTimeout(function() {for(i = 0; i < 5000000; i++) {array.push(i); } }, 0);// smaller loopsetTimeout(function() {for(i = 0; i < 5000000; i++) {array.push(i); } }, 0); });
setTimeout() executes in the WebAPI, then sends the callback to an event queue and allows the event loop to repaint before pushing it’s callback into the JavaScript call stack.
2) Use Web Workers, designed for CPU intensive tasks.
Summary
Events trigger in a multi-threaded area of the browser called Web APIs. After an event (e.g. XHR request) completes, the Web API passes it’s callback to the event queues. Next an event loop synchronously selects and pushes the event callback from the callback queues onto JavaScript’s single-threaded call stack to be executed. In short, events trigger asynchronously but their handlers execute on the call stack synchronously.
In this post we covered browser’s concurrency model for events including Web APIs, event queues, event loop, and JavaScript’s runtime. I’d enjoy questions or comments. Feel free to reach out via Linkedin.
References
[1] W3C Web APIs
[2] W3C Event Queue (Task Queue)
[3] W3C Event Loop
[4] Concurrency modal and Event Loop, MDN
[5] ECMAScript 10.3 Call Stack
[6] V8 source code include/v8.h line 1372.
[7] Silberschatz, Galvin, Gagne, Operating System Concepts 8th Ed. page 153, 570
[8] V8 source code src/x64/cpu-x64.cc
转载地址:https://danmartensen.svbtle.com/events-concurrency-and-javascript
事件、并发和javascript
现代web应用程序本质上是事件驱动的,大多数浏览器内部对于触发、执行和处理事件都在一个黑盒子中。浏览器模型的异步I/O通过事件(events)和回调函数(callback)使用户在按键和点击鼠标的同时也可以在代码中触发定时器和XHR(简单讲就是ajax请求)请求。了解事件的工作机制对于写出高性能的javascript代码至关重要。在这篇文章中,我们将重点介绍浏览器内置Web API、队列、事件轮询和javascript运行时。
例子如下:
Do Stuff document.getElementById('doStuff') .addEventListener('click',function() { console.log('Do Stuff'); } );
让我们通过浏览器跟踪一个Do Stuff点击事件,并描述一路上的组件。
From Philip Robert’s diagram
Philip Robert的讲解原文:https://vimeo.com/96425312
Browser Runtime
1.用户界面:用户点击‘Do Stuff’按钮。
2.Web APIs:点击行为将DOM的Web API触发的click handler(事件句柄)通过事件捕获和事件冒泡传递给该元素的父元素和子元素,Web APIs这一模块在浏览器中是多线程的,所以Web APIs允许多个事件在同一时间触发。在页面加载时,他们通过window对象访问javascript代码,这样的例子除了DOM的documen对象,还有AJAX的XMLHttpRequest对象和timers的setTimeout()函数等。
3.事件队列
接下来,事件的回调函数被推到events queues(也叫作called task queues)中的一个里面。正如有多个Web API一样,浏览器的事件队列也包括networks事件队列、DOM事件队列和渲染事件队列等。
4.事件轮询(event loop)
然后单一的事件轮询机制就会将事件队列中处于队列最前方的事件取出来推到javascript调用栈中执行。
JavaScript Runtime
Javascript引擎包含许多组件例如用于脚本加载的解析器、为对象分配内存的堆、垃圾回收系统、解释器等。
5.调用栈
每个函数包括事件回调函数在内在调用时(推入调用栈的顶端)都会创建一个新的栈框架(也叫作执行对象)。这些栈框架从调用栈的栈顶被推入和弹出,栈顶是当前所执行的代码。函数调用完毕该函数弹出调用栈。
Javascript调用栈的三个特性
1.单线程
线程是CPU利用率的基本单位,作为低级OS构造他们由线程ID、程序计数器、寄存器和堆栈组成。虽然JavaScript引擎本身是多线程的,它的调用堆栈是单线程的,所以在同一时间内只能执行一段代码(代码只有被推入javascript调用栈才能被执行,而调用栈是单线程的)。
2.同步
Javascript 调用栈执行一个任务直到完成才会执行下一个任务,而不是在未完成的任务之间切换,事件也是一样。 这不是ECMAScript或WC3规范的要求。 但有一些例外,如window.alert()中断当前正在执行的任务。
3.非阻塞
当应用状态是暂停时,运行的线程会被阻塞。浏览器是非阻塞的,它依旧会接收像鼠标点击这样的事件即便他们不会立马被执行。
Summary
事件在称为Web API的浏览器的多线程区域中触发。 事件(例如XHR请求)完成后,Web API将其回调传递给事件队列。 接下来,事件循环同步地选择并将事件回调从回调队列推送到JavaScript的单线程调用堆栈以被执行。 简而言之,事件触发是异步的,但它们的处理程序在调用堆栈上同步执行。
在这篇文章中,我们介绍了浏览器的并发模型,包括Web API,事件队列,事件循环和JavaScript的运行时。 我会喜欢问题或意见。 欢迎通过Linkedin与我们联系。
References
[1] W3C Web APIs
[2] W3C Event Queue (Task Queue)
[3] W3C Event Loop
[4] Concurrency modal and Event Loop, MDN
[5] ECMAScript 10.3 Call Stack
[6] V8 source code include/v8.h line 1372.
[7] Silberschatz, Galvin, Gagne, Operating System Concepts 8th Ed. page 153, 570
[8] V8 source code src/x64/cpu-x64.cc