You Dont Know JS Async Performance (Kyle Simpson) (Z-Library)

Author: Kyle Simpson

JavaScript

No Description

📄 File Format: PDF
💾 File Size: 1.3 MB
12
Views
0
Downloads
0.00
Total Donations

📄 Text Preview (First 20 pages)

ℹ️

Registered users can read the full content for free

Register as a Gaohf Library member to read the complete e-book online for free and enjoy a better reading experience.

📄 Page 1
(This page has no text content)
📄 Page 2
1. Introduction 2. Preface 3. Foreword 4. Table of Content 5. Chapter 01: Asynchrony: Now & Later 6. Chapter 02: Callbacks 7. Chapter 03: Promises 8. Chapter 04: Generators 9. Chapter 05: Program Performance 10. Chapter 06: Benchmarking & Tuning 11. Appendix A: `asynquence` Library 12. Appendix B: Advanced Async Patterns 13. Appendix C: Acknowledgments Table of Contents
📄 Page 3
Purchase digital/print copy from O'Reilly Table of Contents Foreword (by Jake Archibald) Preface Chapter 1: Asynchrony: Now & Later Chapter 2: Callbacks Chapter 3: Promises Chapter 4: Generators Chapter 5: Program Performance Chapter 6: Benchmarking & Tuning Appendix A: Library: asynquence Appendix B: Advanced Async Patterns Appendix C: Thank You's! You Don't Know JS: Async & Performance
📄 Page 4
I'm sure you noticed, but "JS" in the book series title is not an abbreviation for words used to curse about JavaScript, though cursing at the language's quirks is something we can probably all identify with! From the earliest days of the web, JavaScript has been a foundational technology that drives interactive experience around the content we consume. While flickering mouse trails and annoying pop-up prompts may be where JavaScript started, nearly 2 decades later, the technology and capability of JavaScript has grown many orders of magnitude, and few doubt its importance at the heart of the world's most widely available software platform: the web. But as a language, it has perpetually been a target for a great deal of criticism, owing partly to its heritage but even more to its design philosophy. Even the name evokes, as Brendan Eich once put it, "dumb kid brother" status next to its more mature older brother "Java". But the name is merely an accident of politics and marketing. The two languages are vastly different in many important ways. "JavaScript" is as related to "Java" as "Carnival" is to "Car". Because JavaScript borrows concepts and syntax idioms from several languages, including proud C-style procedural roots as well as subtle, less obvious Scheme/Lisp-style functional roots, it is exceedingly approachable to a broad audience of developers, even those with just little to no programming experience. The "Hello World" of JavaScript is so simple that the language is inviting and easy to get comfortable with in early exposure. While JavaScript is perhaps one of the easiest languages to get up and running with, its eccentricities make solid mastery of the language a vastly less common occurrence than in many other languages. Where it takes a pretty in-depth knowledge of a language like C or C++ to write a full-scale program, full-scale production JavaScript can, and often does, barely scratch the surface of what the language can do. Sophisticated concepts which are deeply rooted into the language tend instead to surface themselves in seemingly simplistic ways, such as passing around functions as callbacks, which encourages the JavaScript developer to just use the language as-is and not worry too much about what's going on under the hood. It is simultaneously a simple, easy-to-use language that has broad appeal, and a complex and nuanced collection of language mechanics which without careful study will elude true understanding even for the most seasoned of JavaScript developers. Therein lies the paradox of JavaScript, the Achilles' Heel of the language, the challenge we are presently addressing. Because JavaScript can be used without understanding, the understanding of the language is often never attained. If at every point that you encounter a surprise or frustration in JavaScript, your response is to add it to the blacklist, as some are accustomed to doing, you soon will be relegated to a hollow shell of the richness of JavaScript. While this subset has been famously dubbed "The Good Parts", I would implore you, dear reader, to instead consider it the "The Easy Parts", "The Safe Parts", or even "The Incomplete Parts". This You Don't Know JavaScript book series offers a contrary challenge: learn and deeply understand all of JavaScript, even and especially "The Tough Parts". Here, we address head on the tendency of JS developers to learn "just enough" to get by, without ever forcing themselves to learn exactly how and why the language behaves the way it does. Furthermore, we eschew the common advice to retreat when the road gets rough. I am not content, nor should you be, at stopping once something just works, and not really knowing why. I gently challenge You Don't Know JS Preface Mission
📄 Page 5
you to journey down that bumpy "road less traveled" and embrace all that JavaScript is and can do. With that knowledge, no technique, no framework, no popular buzzword acronym of the week, will be beyond your understanding. These books each take on specific core parts of the language which are most commonly misunderstood or under- understood, and dive very deep and exhaustively into them. You should come away from reading with a firm confidence in your understanding, not just of the theoretical, but the practical "what you need to know" bits. The JavaScript you know right now is probably parts handed down to you by others who've been burned by incomplete understanding. That JavaScript is but a shadow of the true language. You don't really know JavaScript, yet, but if you dig into this series, you will. Read on, my friends. JavaScript awaits you. JavaScript is awesome. It's easy to learn partially, and much harder to learn completely (or even sufficiently). When developers encounter confusion, they usually blame the language instead of their lack of understanding. These books aim to fix that, inspiring a strong appreciation for the language you can now, and should, deeply know. Note: Many of the examples in this book assume modern (and future-reaching) JavaScript engine environments, such as ES6. Some code may not work as described if run in older (pre-ES6) engines. Summary
📄 Page 6
Over the years, my employer has trusted me enough to conduct interviews. If we're looking for someone with skills in JavaScript, my first line of questioning… actually that's not true, I first check if the candidate needs the bathroom and/or a drink, because comfort is important, but once I'm past the bit about the candidate's fluid in/out-take, I set about determining if the candidate knows JavaScript, or just jQuery. Not that there's anything wrong with jQuery. It lets you do a lot without really knowing JavaScript, and that's a feature not a bug. But if the job calls for advanced skills in JavaScript performance and maintainability, you need someone who knows how libraries such as jQuery are put together. You need to be able to harness the core of JavaScript the same way they do. If I want to get a picture of someone's core JavaScript skill, I'm most interested in what they make of closures (you've read that book of this series already, right?) and how to get the most out of asynchronicity, which brings us to this book. For starters, you'll be taken through callbacks, the bread and butter of asynchronous programming. Of course, bread and butter does not make for a particularly satisfying meal, but the next course is full of tasty tasty promises! If you don't know promises, now is the time to learn. Promises are now the official way to provide async return values in both JavaScript and the DOM. All future async DOM APIs will use them, many already do, so be prepared! At the time of writing, Promises have shipped in most major browsers, with IE shipping soon. Once you've finished that, I hope you left room for the next course, Generators. Generators snuck their way into stable versions of Chrome and Firefox without too much pomp and ceremony, because, frankly, they're more complicated than they are interesting. Or, that's what I thought until I saw them combined with promises. There, they become an important tool in readability and maintenance. For dessert, well, I won't spoil the surprise, but prepare to gaze into the future of JavaScript! Features that give you more and more control over concurrency and asynchronicity. Well, I won't block your enjoyment of the book any longer, on with the show! If you've already read part of the book before reading this Foreword, give yourself 10 asynchronous points! You deserve them! Jake Archibald jakearchibald.com, @jaffathecake Developer Advocate at Google Chrome
📄 Page 7
Foreword Preface Chapter 1: Asynchrony: Now & Later A Program In Chunks Event Loop Parallel Threading Concurrency Jobs Statement Ordering Chapter 2: Callbacks Continuations Sequential Brain Trust Issues Trying To Save Callbacks Chapter 3: Promises What is a Promise? Thenable Duck-Typing Promise Trust Chain Flow Error Handling Promise Patterns Promise API Recap Promise Limitations Chapter 4: Generators Breaking Run-to-completion Generator'ing Values Iterating Generators Asynchronously Generators + Promises Generator Delegation Generator Concurrency Thunks Pre-ES6 Generators Chapter 5: Program Performance Web Workers Parallel JS SIMD asm.js Chapter 6: Benchmarking & Tuning Benchmarking Context Is King jsPerf.com Writing Good Tests Microperformance Tail Call Optimization (TCO) Appendix A: asynquence Library Appendix B: Advanced Async Patterns Appendix C: Acknowledgments You Don't Know JS: Async & Performance Table of Contents
📄 Page 8
One of the most important and yet often misunderstood parts of programming in a language like JavaScript is how to express and manipulate program behavior spread out over a period of time. This is not just about what happens from the beginning of a for loop to the end of a for loop, which of course takes some time (microseconds to milliseconds) to complete. It's about what happens when part of your program runs now, and another part of your program runs later -- there's a gap between now and later where your program isn't actively executing. Practically all nontrivial programs ever written (especially in JS) have in some way or another had to manage this gap, whether that be in waiting for user input, requesting data from a database or file system, sending data across the network and waiting for a response, or performing a repeated task at a fixed interval of time (like animation). In all these various ways, your program has to manage state across the gap in time. As they famously say in London (of the chasm between the subway door and the platform): "mind the gap." In fact, the relationship between the now and later parts of your program is at the heart of asynchronous programming. Asynchronous programming has been around since the beginning of JS, for sure. But most JS developers have never really carefully considered exactly how and why it crops up in their programs, or explored various other ways to handle it. The good enough approach has always been the humble callback function. Many to this day will insist that callbacks are more than sufficient. But as JS continues to grow in both scope and complexity, to meet the ever-widening demands of a first-class programming language that runs in browsers and servers and every conceivable device in between, the pains by which we manage asynchrony are becoming increasingly crippling, and they cry out for approaches that are both more capable and more reason-able. While this all may seem rather abstract right now, I assure you we'll tackle it more completely and concretely as we go on through this book. We'll explore a variety of emerging techniques for async JavaScript programming over the next several chapters. But before we can get there, we're going to have to understand much more deeply what asynchrony is and how it operates in JS. You may write your JS program in one .js file, but your program is almost certainly comprised of several chunks, only one of which is going to execute now, and the rest of which will execute later. The most common unit of chunk is the function . The problem most developers new to JS seem to have is that later doesn't happen strictly and immediately after now. In other words, tasks that cannot complete now are, by definition, going to complete asynchronously, and thus we will not have blocking behavior as you might intuitively expect or want. Consider: // ajax(..) is some arbitrary Ajax function given by a library var data = ajax( "http://some.url.1" ); console.log( data ); // Oops! `data` generally won't have the Ajax results You're probably aware that standard Ajax requests don't complete synchronously, which means the ajax(..) function does You Don't Know JS: Async & Performance Chapter 1: Asynchrony: Now & Later A Program in Chunks
📄 Page 9
not yet have any value to return back to be assigned to data variable. If ajax(..) could block until the response came back, then the data = .. assignment would work fine. But that's not how we do Ajax. We make an asynchronous Ajax request now, and we won't get the results back until later. The simplest (but definitely not only, or necessarily even best!) way of "waiting" from now until later is to use a function, commonly called a callback function: // ajax(..) is some arbitrary Ajax function given by a library ajax( "http://some.url.1", function myCallbackFunction(data){ console.log( data ); // Yay, I gots me some `data`! } ); Warning: You may have heard that it's possible to make synchronous Ajax requests. While that's technically true, you should never, ever do it, under any circumstances, because it locks the browser UI (buttons, menus, scrolling, etc.) and prevents any user interaction whatsoever. This is a terrible idea, and should always be avoided. Before you protest in disagreement, no, your desire to avoid the mess of callbacks is not justification for blocking, synchronous Ajax. For example, consider this code: function now() { return 21; } function later() { answer = answer * 2; console.log( "Meaning of life:", answer ); } var answer = now(); setTimeout( later, 1000 ); // Meaning of life: 42 There are two chunks to this program: the stuff that will run now, and the stuff that will run later. It should be fairly obvious what those two chunks are, but let's be super explicit: Now: function now() { return 21; } function later() { .. } var answer = now(); setTimeout( later, 1000 ); Later: answer = answer * 2; console.log( "Meaning of life:", answer ); The now chunk runs right away, as soon as you execute your program. But setTimeout(..) also sets up an event (a timeout) to happen later, so the contents of the later() function will be executed at a later time (1,000 milliseconds from now).
📄 Page 10
Any time you wrap a portion of code into a function and specify that it should be executed in response to some event (timer, mouse click, Ajax response, etc.), you are creating a later chunk of your code, and thus introducing asynchrony to your program. There is no specification or set of requirements around how the console.* methods work -- they are not officially part of JavaScript, but are instead added to JS by the hosting environment (see the Types & Grammar title of this book series). So, different browsers and JS environments do as they please, which can sometimes lead to confusing behavior. In particular, there are some browsers and some conditions that console.log(..) does not actually immediately output what it's given. The main reason this may happen is because I/O is a very slow and blocking part of many programs (not just JS). So, it may perform better (from the page/UI perspective) for a browser to handle console I/O asynchronously in the background, without you perhaps even knowing that occurred. A not terribly common, but possible, scenario where this could be observable (not from code itself but from the outside): var a = { index: 1 }; // later console.log( a ); // ?? // even later a.index++; We'd normally expect to see the a object be snapshotted at the exact moment of the console.log(..) statement, printing something like { index: 1 } , such that in the next statment when a.index++ happens, it's modifying something different than, or just strictly after, the output of a . Most of the time, the preceding code will probably produce an object representation in your developer tools' console that's what you'd expect. But it's possible this same code could run in a situation where the browser felt it needed to defer the console I/O to the background, in which case it's possible that by the time the object is represented in the browser console, the a.index++ has already happened, and it shows { index: 2 } . It's a moving target under what conditions exactly console I/O will be deferred, or even whether it will be observable. Just be aware of this possible asynchronicity in I/O in case you ever run into issues in debugging where objects have been modified after a console.log(..) statement and yet you see the unexpected modifications show up. Note: If you run into this rare scenario, the best option is to use breakpoints in your JS debugger instead of relying on console output. The next best option would be to force a "snapshot" of the object in question by serializing it to a string , like with JSON.stringify(..) . Let's make a (perhaps shocking) claim: despite your clearly being able to write asynchronous JS code (like the timeout we just looked at), up until recently (ES6), JavaScript itself has actually never had any direct notion of asynchrony built into it. What!? That seems like a crazy claim, right? In fact, it's quite true. The JS engine itself has never done anything more than execute a single chunk of your program at any given moment, when asked to. "Asked to." By whom? That's the important part! The JS engine doesn't run in isolation. It runs inside a hosting environment, which is for most developers the typical web browser. Over the last several years (but by no means exlusively), JS has expanded beyond the browser into other environments, such as servers, via things like Node.js. In fact, JavaScript gets embedded into all kinds of devices these Async Console Event Loop
📄 Page 11
days, from robots to lightbulbs. But the one common "thread" (that's a not-so-subtle asynchronous joke, for what it's worth) of all these environments is that they have a mechanism in them that handles executing multiple chunks of your program over time, at each moment invoking the JS engine, called the "event loop." In other words, the JS engine has had no innate sense of time, but has instead been an on-demand execution environment for any arbitrary snippet of JS. It's the surrounding environment that has always scheduled "events" (JS code executions). So, for example, when your JS program makes an Ajax request to fetch some data from a server, you set up the "response" code in a function (commonly called a "callback"), and the JS engine tells the hosting environment, "Hey, I'm going to suspend execution for now, but whenever you finish with that network request, and you have some data, please call this function back." The browser is then set up to listen for the response from the network, and when it has something to give you, it schedules the callback function to be executed by inserting it into the event loop. So what is the event loop? Let's conceptualize it first through some fake-ish code: // `eventLoop` is an array that acts as a queue (first-in, first-out) var eventLoop = [ ]; var event; // keep going "forever" while (true) { // perform a "tick" if (eventLoop.length > 0) { // get the next event in the queue event = eventLoop.shift(); // now, execute the next event try { event(); } catch (err) { reportError(err); } } } This is, of course, vastly simplified pseudocode to illustrate the concepts. But it should be enough to help get a better understanding. As you can see, there's a continuously running loop represented by the while loop, and each iteration of this loop is called a "tick." For each tick, if an event is waiting on the queue, it's taken off and executed. These events are your function callbacks. It's important to note that setTimeout(..) doesn't put your callback on the event loop queue. What it does is set up a timer; when the timer expires, the environment places your callback into the event loop, such that some future tick will pick it up and execute it. What if there are already 20 items in the event loop at that moment? Your callback waits. It gets in line behind the others -- there's not normally a path for preempting the queue and skipping ahead in line. This explains why setTimeout(..) timers may not fire with perfect temporal accuracy. You're guaranteed (roughly speaking) that your callback won't fire before the time interval you specify, but it can happen at or after that time, depending on the state of the event queue. So, in other words, your program is generally broken up into lots of small chunks, which happen one after the other in the event loop queue. And technically, other events not related directly to your program can be interleaved within the queue as well. Note: We mentioned "up until recently" in relation to ES6 changing the nature of where the event loop queue is managed.
📄 Page 12
It's mostly a formal technicality, but ES6 now specifies how the event loop works, which means technically it's within the purview of the JS engine, rather than just the hosting environment. One main reason for this change is the introduction of ES6 Promises, which we'll discuss in Chapter 3, because they require the ability to have direct, fine-grained control over scheduling operations on the event loop queue (see the discussion of setTimeout(..0) in the "Cooperation" section). It's very common to conflate the terms "async" and "parallel," but they are actually quite different. Remember, async is about the gap between now and later. But parallel is about things being able to occur simultaneously. The most common tools for parallel computing are processes and threads. Processes and threads execute independently and may execute simultaneously: on separate processors, or even separate computers, but multiple threads can share the memory of a single process. An event loop, by contrast, breaks its work into tasks and executes them in serial, disallowing parallel access and changes to shared memory. Parallelism and "serialism" can coexist in the form of cooperating event loops in separate threads. The interleaving of parallel threads of execution and the interleaving of asynchronous events occur at very different levels of granularity. For example: function later() { answer = answer * 2; console.log( "Meaning of life:", answer ); } While the entire contents of later() would be regarded as a single event loop queue entry, when thinking about a thread this code would run on, there's actually perhaps a dozen different low-level operations. For example, answer = answer * 2 requires first loading the current value of answer , then putting 2 somewhere, then performing the multiplication, then taking the result and storing it back into answer . In a single-threaded environment, it really doesn't matter that the items in the thread queue are low-level operations, because nothing can interrupt the thread. But if you have a parallel system, where two different threads are operating in the same program, you could very likely have unpredictable behavior. Consider: var a = 20; function foo() { a = a + 1; } function bar() { a = a * 2; } // ajax(..) is some arbitrary Ajax function given by a library ajax( "http://some.url.1", foo ); ajax( "http://some.url.2", bar ); In JavaScript's single-threaded behavior, if foo() runs before bar() , the result is that a has 42 , but if bar() runs before foo() the result in a will be 41 . If JS events sharing the same data executed in parallel, though, the problems would be much more subtle. Consider these two lists of pseudocode tasks as the threads that could respectively run the code in foo() and bar() , and consider what happens if they are running at exactly the same time: Parallel Threading
📄 Page 13
Thread 1 ( X and Y are temporary memory locations): foo(): a. load value of `a` in `X` b. store `1` in `Y` c. add `X` and `Y`, store result in `X` d. store value of `X` in `a` Thread 2 ( X and Y are temporary memory locations): bar(): a. load value of `a` in `X` b. store `2` in `Y` c. multiply `X` and `Y`, store result in `X` d. store value of `X` in `a` Now, let's say that the two threads are running truly in parallel. You can probably spot the problem, right? They use shared memory locations X and Y for their temporary steps. What's the end result in a if the steps happen like this? 1a (load value of `a` in `X` ==> `20`) 2a (load value of `a` in `X` ==> `20`) 1b (store `1` in `Y` ==> `1`) 2b (store `2` in `Y` ==> `2`) 1c (add `X` and `Y`, store result in `X` ==> `22`) 1d (store value of `X` in `a` ==> `22`) 2c (multiply `X` and `Y`, store result in `X` ==> `44`) 2d (store value of `X` in `a` ==> `44`) The result in a will be 44 . But what about this ordering? 1a (load value of `a` in `X` ==> `20`) 2a (load value of `a` in `X` ==> `20`) 2b (store `2` in `Y` ==> `2`) 1b (store `1` in `Y` ==> `1`) 2c (multiply `X` and `Y`, store result in `X` ==> `20`) 1c (add `X` and `Y`, store result in `X` ==> `21`) 1d (store value of `X` in `a` ==> `21`) 2d (store value of `X` in `a` ==> `21`) The result in a will be 21 . So, threaded programming is very tricky, because if you don't take special steps to prevent this kind of interruption/interleaving from happening, you can get very surprising, nondeterministic behavior that frequently leads to headaches. JavaScript never shares data accross threads, which means that level of nondeterminism isn't a concern. But that doesn't mean JS is always deterministic. Remember earlier, where the relative ordering of foo() and bar() produces two different results ( 41 or 42 )? Note: It may not be obvious yet, but not all nondeterminism is bad. Sometimes it's irrelevant, and sometimes it's intentional. We'll see more examples of that throughout this and the next few chapters. Because of JavaScript's single-threading, the code inside of foo() (and bar() ) is atomic, which means that once foo() starts running, the entirety of its code will finish before any of the code in bar() can run, or vice versa. This is called "run- to-completion" behavior. Run-to-Completion
📄 Page 14
In fact, the run-to-completion semantics are more obvious when foo() and bar() have more code in them, such as: var a = 1; var b = 2; function foo() { a++; b = b * a; a = b + 3; } function bar() { b--; a = 8 + b; b = a * 2; } // ajax(..) is some arbitrary Ajax function given by a library ajax( "http://some.url.1", foo ); ajax( "http://some.url.2", bar ); Because foo() can't be interrupted by bar() , and bar() can't be interrupted by foo() , this program only has two possible outcomes depending on which starts running first -- if threading were present, and the individual statements in foo() and bar() could be interleaved, the number of possible outcomes would be greatly increased! Chunk 1 is synchronous (happens now), but chunks 2 and 3 are asynchronous (happen later), which means their execution will be separated by a gap of time. Chunk 1: var a = 1; var b = 2; Chunk 2 ( foo() ): a++; b = b * a; a = b + 3; Chunk 3 ( bar() ): b--; a = 8 + b; b = a * 2; Chunks 2 and 3 may happen in either-first order, so there are two possible outcomes for this program, as illustrated here: Outcome 1: var a = 1; var b = 2; // foo() a++; b = b * a; a = b + 3; // bar() b--; a = 8 + b; b = a * 2; a; // 11
📄 Page 15
b; // 22 Outcome 2: var a = 1; var b = 2; // bar() b--; a = 8 + b; b = a * 2; // foo() a++; b = b * a; a = b + 3; a; // 183 b; // 180 Two outcomes from the same code means we still have nondeterminism! But it's at the function (event) ordering level, rather than at the statement ordering level (or, in fact, the expression operation ordering level) as it is with threads. In other words, it's more deterministic than threads would have been. As applied to JavaScript's behavior, this function-ordering nondeterminism is the common term "race condition," as foo() and bar() are racing against each other to see which runs first. Specifically, it's a "race condition" because you cannot predict reliably how a and b will turn out. Note: If there was a function in JS that somehow did not have run-to-completion behavior, we could have many more possible outcomes, right? It turns out ES6 introduces just such a thing (see Chapter 4 "Generators"), but don't worry right now, we'll come back to that! Let's imagine a site that displays a list of status updates (like a social network news feed) that progressively loads as the user scrolls down the list. To make such a feature work correctly, (at least) two separate "processes" will need to be executing simultaneously (i.e., during the same window of time, but not necessarily at the same instant). Note: We're using "process" in quotes here because they aren't true operating system–level processes in the computer science sense. They're virtual processes, or tasks, that represent a logically connected, sequential series of operations. We'll simply prefer "process" over "task" because terminology-wise, it will match the definitions of the concepts we're exploring. The first "process" will respond to onscroll events (making Ajax requests for new content) as they fire when the user has scrolled the page further down. The second "process" will receive Ajax responses back (to render content onto the page). Obviously, if a user scrolls fast enough, you may see two or more onscroll events fired during the time it takes to get the first response back and process, and thus you're going to have onscroll events and Ajax response events firing rapidly, interleaved with each other. Concurrency is when two or more "processes" are executing simultaneously over the same period, regardless of whether their individual constituent operations happen in parallel (at the same instant on separate processors or cores) or not. You can think of concurrency then as "process"-level (or task-level) parallelism, as opposed to operation-level parallelism (separate-processor threads). Note: Concurrency also introduces an optional notion of these "processes" interacting with each other. We'll come back to that later. For a given window of time (a few seconds worth of a user scrolling), let's visualize each independent "process" as a series Concurrency
📄 Page 16
of events/operations: "Process" 1 ( onscroll events): onscroll, request 1 onscroll, request 2 onscroll, request 3 onscroll, request 4 onscroll, request 5 onscroll, request 6 onscroll, request 7 "Process" 2 (Ajax response events): response 1 response 2 response 3 response 4 response 5 response 6 response 7 It's quite possible that an onscroll event and an Ajax response event could be ready to be processed at exactly the same moment. For example let's visualize these events in a timeline: onscroll, request 1 onscroll, request 2 response 1 onscroll, request 3 response 2 response 3 onscroll, request 4 onscroll, request 5 onscroll, request 6 response 4 onscroll, request 7 response 6 response 5 response 7 But, going back to our notion of the event loop from eariler in the chapter, JS is only going to be able to handle one event at a time, so either onscroll, request 2 is going to happen first or response 1 is going to happen first, but they cannot happen at literally the same moment. Just like kids at a school cafeteria, no matter what crowd they form outside the doors, they'll have to merge into a single line to get their lunch! Let's visualize the interleaving of all these events onto the event loop queue. Event Loop Queue: onscroll, request 1 <--- Process 1 starts onscroll, request 2 response 1 <--- Process 2 starts onscroll, request 3 response 2 response 3 onscroll, request 4 onscroll, request 5 onscroll, request 6 response 4 onscroll, request 7 <--- Process 1 finishes response 6 response 5 response 7 <--- Process 2 finishes "Process 1" and "Process 2" run concurrently (task-level parallel), but their individual events run sequentially on the event loop queue.
📄 Page 17
By the way, notice how response 6 and response 5 came back out of expected order? The single-threaded event loop is one expression of concurrency (there are certainly others, which we'll come back to later). As two or more "processes" are interleaving their steps/events concurrently within the same program, they don't necessarily need to interact with each other if the tasks are unrelated. If they don't interact, nondeterminism is perfectly acceptable. For example: var res = {}; function foo(results) { res.foo = results; } function bar(results) { res.bar = results; } // ajax(..) is some arbitrary Ajax function given by a library ajax( "http://some.url.1", foo ); ajax( "http://some.url.2", bar ); foo() and bar() are two concurrent "processes," and it's nondeterminate which order they will be fired in. But we've constructed the program so it doesn't matter what order they fire in, because they act independently and as such don't need to interact. This is not a "race condition" bug, as the code will always work correctly, regardless of the ordering. More commonly, concurrent "processes" will by necessity interact, indirectly through scope and/or the DOM. When such interaction will occur, you need to coordinate these interactions to prevent "race conditions," as described earlier. Here's a simple example of two concurrent "processes" that interact because of implied ordering, which is only sometimes broken: var res = []; function response(data) { res.push( data ); } // ajax(..) is some arbitrary Ajax function given by a library ajax( "http://some.url.1", response ); ajax( "http://some.url.2", response ); The concurrent "processes" are the two response() calls that will be made to handle the Ajax responses. They can happen in either-first order. Let's assume the expected behavior is that res[0] has the results of the "http://some.url.1" call, and res[1] has the results of the "http://some.url.2" call. Sometimes that will be the case, but sometimes they'll be flipped, depending on which call finishes first. There's a pretty good likelihood that this nondeterminism is a "race condition" bug. Note: Be extremely wary of assumptions you might tend to make in these situations. For example, it's not uncommon for a developer to observe that "http://some.url.2" is "always" much slower to respond than "http://some.url.1" , perhaps by virtue of what tasks they're doing (e.g., one performing a database task and the other just fetching a static file), so the Noninteracting Interaction
📄 Page 18
observed ordering seems to always be as expected. Even if both requests go to the same server, and it intentionally responds in a certain order, there's no real guarantee of what order the responses will arrive back in the browser. So, to address such a race condition, you can coordinate ordering interaction: var res = []; function response(data) { if (data.url == "http://some.url.1") { res[0] = data; } else if (data.url == "http://some.url.2") { res[1] = data; } } // ajax(..) is some arbitrary Ajax function given by a library ajax( "http://some.url.1", response ); ajax( "http://some.url.2", response ); Regardless of which Ajax response comes back first, we inspect the data.url (assuming one is returned from the server, of course!) to figure out which position the response data should occupy in the res array. res[0] will always hold the "http://some.url.1" results and res[1] will always hold the "http://some.url.2" results. Through simple coordination, we eliminated the "race condition" nondeterminism. The same reasoning from this scenario would apply if multiple concurrent function calls were interacting with each other through the shared DOM, like one updating the contents of a <div> and the other updating the style or attributes of the <div> (e.g., to make the DOM element visible once it has content). You probably wouldn't want to show the DOM element before it had content, so the coordination must ensure proper ordering interaction. Some concurrency scenarios are always broken (not just sometimes) without coordinated interaction. Consider: var a, b; function foo(x) { a = x * 2; baz(); } function bar(y) { b = y * 2; baz(); } function baz() { console.log(a + b); } // ajax(..) is some arbitrary Ajax function given by a library ajax( "http://some.url.1", foo ); ajax( "http://some.url.2", bar ); In this example, whether foo() or bar() fires first, it will always cause baz() to run too early (either a or b will still be undefined ), but the second invocation of baz() will work, as both a and b will be available. There are different ways to address such a condition. Here's one simple way: var a, b; function foo(x) { a = x * 2; if (a && b) { baz(); } } function bar(y) {
📄 Page 19
b = y * 2; if (a && b) { baz(); } } function baz() { console.log( a + b ); } // ajax(..) is some arbitrary Ajax function given by a library ajax( "http://some.url.1", foo ); ajax( "http://some.url.2", bar ); The if (a && b) conditional around the baz() call is traditionally called a "gate," because we're not sure what order a and b will arrive, but we wait for both of them to get there before we proceed to open the gate (call baz() ). Another concurrency interaction condition you may run into is sometimes called a "race," but more correctly called a "latch." It's characterized by "only the first one wins" behavior. Here, nondeterminism is acceptable, in that you are explicitly saying it's OK for the "race" to the finish line to have only one winner. Consider this broken code: var a; function foo(x) { a = x * 2; baz(); } function bar(x) { a = x / 2; baz(); } function baz() { console.log( a ); } // ajax(..) is some arbitrary Ajax function given by a library ajax( "http://some.url.1", foo ); ajax( "http://some.url.2", bar ); Whichever one ( foo() or bar() ) fires last will not only overwrite the assigned a value from the other, but it will also duplicate the call to baz() (likely undesired). So, we can coordinate the interaction with a simple latch, to let only the first one through: var a; function foo(x) { if (!a) { a = x * 2; baz(); } } function bar(x) { if (!a) { a = x / 2; baz(); } } function baz() { console.log( a ); } // ajax(..) is some arbitrary Ajax function given by a library ajax( "http://some.url.1", foo ); ajax( "http://some.url.2", bar );
📄 Page 20
The if (!a) conditional allows only the first of foo() or bar() through, and the second (and indeed any subsequent) calls would just be ignored. There's just no virtue in coming in second place! Note: In all these scenarios, we've been using global variables for simplistic illustration purposes, but there's nothing about our reasoning here that requires it. As long as the functions in question can access the variables (via scope), they'll work as intended. Relying on lexically scoped variables (see the Scope & Closures title of this book series), and in fact global variables as in these examples, is one obvious downside to these forms of concurrency coordination. As we go through the next few chapters, we'll see other ways of coordination that are much cleaner in that respect. Another expression of concurrency coordination is called "cooperative concurrency." Here, the focus isn't so much on interacting via value sharing in scopes (though that's obviously still allowed!). The goal is to take a long-running "process" and break it up into steps or batches so that other concurrent "processes" have a chance to interleave their operations into the event loop queue. For example, consider an Ajax response handler that needs to run through a long list of results to transform the values. We'll use Array#map(..) to keep the code shorter: var res = []; // `response(..)` receives array of results from the Ajax call function response(data) { // add onto existing `res` array res = res.concat( // make a new transformed array with all `data` values doubled data.map( function(val){ return val * 2; } ) ); } // ajax(..) is some arbitrary Ajax function given by a library ajax( "http://some.url.1", response ); ajax( "http://some.url.2", response ); If "http://some.url.1" gets its results back first, the entire list will be mapped into res all at once. If it's a few thousand or less records, this is not generally a big deal. But if it's say 10 million records, that can take a while to run (several seconds on a powerful laptop, much longer on a mobile device, etc.). While such a "process" is running, nothing else in the page can happen, including no other response(..) calls, no UI updates, not even user events like scrolling, typing, button clicking, and the like. That's pretty painful. So, to make a more cooperatively concurrent system, one that's friendlier and doesn't hog the event loop queue, you can process these results in asynchronous batches, after each one "yielding" back to the event loop to let other waiting events happen. Here's a very simple approach: var res = []; // `response(..)` receives array of results from the Ajax call function response(data) { // let's just do 1000 at a time var chunk = data.splice( 0, 1000 ); // add onto existing `res` array res = res.concat( // make a new transformed array with all `chunk` values doubled chunk.map( function(val){ return val * 2; } ) ); Cooperation
The above is a preview of the first 20 pages. Register to read the complete e-book.

💝 Support Author

0.00
Total Amount (¥)
0
Donation Count

Login to support the author

Login Now
Back to List