From aa3344a1a894de012cef439b234102e3c527ace3 Mon Sep 17 00:00:00 2001 From: Donovan Hutchinson Date: Fri, 8 Nov 2019 21:19:57 +0000 Subject: [PATCH 01/11] Adding control flow --- .eleventy.js | 1 + TEMPLATE.md | 2 +- buffers-and-streams/index.md | 2 +- cheatsheet/index.md | 47 ++++- child-processes/index.md | 2 +- control-flow/index.md | 366 ++++++++++++++++++++++++++++++++++- css/main.css | 19 ++ diagnostics/index.md | 2 +- events/index.md | 2 +- index.md | 2 +- js/main.js | 27 ++- styleguide/index.md | 2 +- 12 files changed, 449 insertions(+), 25 deletions(-) diff --git a/.eleventy.js b/.eleventy.js index d11142b..797debe 100644 --- a/.eleventy.js +++ b/.eleventy.js @@ -15,6 +15,7 @@ module.exports = function(eleventyConfig) { eleventyConfig.addPassthroughCopy("js"); eleventyConfig.addPassthroughCopy("CNAME"); eleventyConfig.addLayoutAlias("default", "default.njk"); + eleventyConfig.addLayoutAlias("post", "post.njk"); return { passthroughFileCopy: true }; diff --git a/TEMPLATE.md b/TEMPLATE.md index 07b5ac5..f61ed58 100644 --- a/TEMPLATE.md +++ b/TEMPLATE.md @@ -1,5 +1,5 @@ --- -layout: default.njk +layout: default title: Your post title --- diff --git a/buffers-and-streams/index.md b/buffers-and-streams/index.md index f7ddfdc..87653f3 100644 --- a/buffers-and-streams/index.md +++ b/buffers-and-streams/index.md @@ -1,5 +1,5 @@ --- -layout: default.njk +layout: default title: Buffers and Streams --- diff --git a/cheatsheet/index.md b/cheatsheet/index.md index a3abf0f..02a1e75 100644 --- a/cheatsheet/index.md +++ b/cheatsheet/index.md @@ -1,8 +1,53 @@ --- -layout: default.njk +layout: default title: Node Cheat Sheet --- +## [Control Flow](#control-flow) + +### Callback pattern + +``` +function foo(val1, val2, callback) { + ... + callback(); +} +``` + +### Promise pattern + +``` +function foo(ok) { + return new Promise(resolve, reject) { + if (ok) { + resolve('success'); + } else { + reject('boo'); + } + } +} + +foo() + .then(res => { + console.log(res); + }) + .catch(err => { + throw err; + }) +``` + +### Async/Await + +``` +async function callFoo() { + try { + const result = await foo(true); + } catch(err) { + throw err; + } +} +``` + ## [Events](#events) Class: events.EventEmitter diff --git a/child-processes/index.md b/child-processes/index.md index c3f39cd..1967516 100644 --- a/child-processes/index.md +++ b/child-processes/index.md @@ -1,5 +1,5 @@ --- -layout: default.njk +layout: default title: Child Processes --- diff --git a/control-flow/index.md b/control-flow/index.md index d872151..e3e9ddb 100644 --- a/control-flow/index.md +++ b/control-flow/index.md @@ -1,6 +1,368 @@ --- -layout: post.njk +layout: post title: Control Flow +author: donovan +date: Last Modified --- -I've seen things you people wouldn't believe. Attack ships on fire off the shoulder of Orion. I watched C-beams glitter in the dark near the Tannhäuser Gate. All those moments will be lost in time, like control flow. +As we create programs we write sequences of events. Usually, we expect them execute in order but this is not always the way it occurs. Even though JavaScript ios [single-threaded](https://medium.com/better-programming/is-node-js-really-single-threaded-7ea59bcc8d64 'Is Node.js Really Single-Threaded?'), functions that call APIs, setTimeouts and other structures can result in our code running in an unexpected order. + +Consider the following code. + +``` +console.log('Clean teeth'); +setTimeout(() => { + console.log('Use lavatory'); +}); +console.log('Wash face and hands'); +``` + +In the above code, our compiler goes through one line at a time executing each instruction. It doesn't pause to wait, so it'll simply run the `setTimeout` then carry on and run the subequent `console.log`. Then (a small moment later), it will log the `Use lavatory` output. + +This isn't what we want. We'll have to go wash our hands again. In production code, even worse things can happen. To get around this we can use _control flow_. + +Node offers 3 approaches we can use to control the order in which our code executes. _Callbacks_, _promises_ and _async/await_. Let's take a look at each. + +## Callbacks + +Earlier in Node's development, it was common to use the _callback pattern_ to control the order that code is executed. The pattern involves passing a function as a parameter (_callback_) into your functions, and then calling that _callback_ when you're ready to continue. + +### Example + +Let's say we have to get some data from an external service. To simulate this I've created a `randomDelayedResponse` function that will return a response after an unspecified amount of time. + +``` +function randomDelayedResponse(text) { + // Using a timeout here for the sake of simulating an external request + const timeOut = Math.floor(Math.random() * 10) + 1; + const output = text; + setTimeout(() => { + return output; + }, timeOut * 10); +} + +const output = randomDelayedResponse('Hello'); +console.log(output); // empty +``` + +If we wanted to get the response we need to wait until the response it ready. One way is to pass out final `console.log` in as a function like so: + +``` +function randomDelayedResponse(text, callback) { + // Using a timeout here for the sake of simulating an external request + const timeOut = Math.floor(Math.random() * 10) + 1; + setTimeout(() => { + callback(text); + }, timeOut * 10); +} + +const output = randomDelayedResponse('Hello', text => console.log(text)); // outputs "Hello" +console.log(output); // still empty +``` + +Here we are passing the function `text => console.log(text)` as the second parameter, and this function is then called after the `setTimeout`. + +This might be reasonable at this level, but it's easy for callbacks to get out of control. For example, if we have two different calls to this slow function that need to be run in order, we need to nest further. + +If we ran them beside each other, they would not be reliable: + +``` +function randomDelayedResponse(text, callback) { + // Using a timeout here for the sake of simulating an external request + const timeOut = Math.floor(Math.random() * 10) + 1; + setTimeout(() => { + callback(text); + }, timeOut * 10); +} + +randomDelayedResponse('Runner 1', text => console.log(text)); // outputs "Runner 1" +randomDelayedResponse('Runner 2', text => console.log(text)); // outputs "Runner 2" +randomDelayedResponse('Runner 3', text => console.log(text)); // outputs "Runner 3" +randomDelayedResponse('Runner 4', text => console.log(text)); // outputs "Runner 4" +// Who will win? +``` + +You can run the above code multiple times, and the result is unpredictable. To create a predictable flow using callbacks we'd need to nest them more: + +``` +function randomDelayedResponse(text, callback) { + // Using a timeout here for the sake of simulating an external request + const timeOut = Math.floor(Math.random() * 10) + 1; + setTimeout(() => { + callback(text); + }, timeOut * 10); +} + +randomDelayedResponse(1, text => { + console.log(text); + randomDelayedResponse(2, text => { + console.log(text); + randomDelayedResponse(3, text => { + console.log(text); + randomDelayedResponse(4, text => { + console.log(text); + }); + }); + }); +}); // outputs "1 2 3 4" +``` + +As we can see, structuring these callbacks does work but can quickly become difficult to read. This example is very simple so we can see how more complex code nested in this way would become difficult to follow. Let's look at another way. + +## Promises + +To avoid this callback hell, we can use a different structure called _promises_. A promise is a function that returns a _resolved_ response which we can chain using `then`, or a _rejected_ response which we can `catch`. It follows this pattern: + +``` +runOutPromise() // returns a response + .then(response => { + return foo; // this could optionally be another promise + }) + .then(foo => { + // they can be chained + }) + .catch(error => { + // catch "reject" output here + }); +``` + +We can rewrite our runner example using promises like so: + +``` +function randomDelayedResponse(text, callback) { + return new Promise((resolve, reject) => { + const timeOut = Math.floor(Math.random() * 10) + 1; + setTimeout(() => { + resolve(text); // Replacing the callback with "resolve" + }, timeOut * 10); + }); +} + +randomDelayedResponse(1) + .then(res => { + console.log(res); + return randomDelayedResponse(2); + }) + .then(res => { + console.log(res); + return randomDelayedResponse(3); + }) + .then(res => { + console.log(res); + return randomDelayedResponse(4); + }) + .then(res => { + console.log(res); + }) + .catch(error => { + // handle error + }) +// outputs "1 2 3 4" +``` + +Running the above code should output the correct order, albeit with random delays between each number. + +We aren't handling errors in this example, but if our function was to encounter an error we would `reject('some error')` and this would then be picked up by the `catch`. + +You can learn more about `promises` here (LINK) + +Promises remove a lot of nesting and give us easier to read code. However there is a third way to control the execution order of our code. + +## Async / Await + +The third approach is built on top of the existing _promises_ approach but makes it easier to reason with. With `async` and `await` we can write code that feels a lot more like the simple top-down code, by telling it to wait when we need it to. Let's rewrite our _who would win?_ example from above. + +``` +function randomDelayedResponse(text, callback) { + return new Promise((resolve, reject) => { + const timeOut = Math.floor(Math.random() * 10) + 1; + setTimeout(() => { + resolve(text); + }, timeOut * 10); + }); +} + +async function runTheRace() { // put `async` before the function call + + console.log(1) // Not async + + const result2 = await randomDelayedResponse(2); // async + console.log(result2); + + console.log(3); + + const result4 = await randomDelayedResponse(4); + console.log(result4) +} + +runTheRace(); +``` + +In the above we have replaced the series of `.then()` calls with an `async` function. It's still making use of a _promise_ but when we want to wait for the promise we simply place `await` before the asynchronous function. This way, the code execution order is preserved. + +You may be wondering how we handle errors here. One approach is to use the `try/catch` method. We'll be covering that and more in the [error handling section]({{ "/error-handling/" | url }}). + +## Exercise + +Given the following code, how would you change it so that the results always outputs `hello world`? Can you make it work using all 3 of the above approaches? + +``` +function hello() { + const timeOut = Math.floor(Math.random() * 10) + 1; + setTimeout(() => { + console.log('hello'); + }, timeOut * 10); +} + +function world() { + const timeOut = Math.floor(Math.random() * 10) + 1; + setTimeout(() => { + console.log('world'); + }, timeOut * 10); +} +``` + +## Suggested solution + + + + + +## Common Classes and Functions + +### Callback pattern + +``` +function foo(val1, val2, callback) { + ... + callback(); +} +``` + +### Promise pattern + +``` +function foo(ok) { + return new Promise(resolve, reject) { + if (ok) { + resolve('success'); + } else { + reject('boo'); + } + } +} + +foo() + .then(res => { + console.log(res); + }) + .catch(err => { + throw err; + }) +``` + +### Async/Await + +``` +async function callFoo() { + try { + const result = await foo(true); + } catch(err) { + throw err; + } +} +``` + +Be sure to check out the [cheat sheet]({{ "/cheatsheet/" | url }}) for more. + +## Further Reading + +- [Callback function](https://developer.mozilla.org/en-US/docs/Glossary/Callback_function) +- [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) +- [Making asynchronous programming easier with async and await](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Async_await) diff --git a/css/main.css b/css/main.css index 1c82e66..f09d9db 100644 --- a/css/main.css +++ b/css/main.css @@ -99,3 +99,22 @@ a:hover { border-color: #80bd01; color: #333; } + +.removed { + display: none; +} + +.solution { + height: auto; + opacity: 1; + transform: none; + transition: opacity 0.5s ease-out, transform 0.5s cubic-bezier(0, 1, 0.3, 1), + height 0.5s linear; +} + +.solution.hidden { + height: 0; + overflow: hidden; + opacity: 0; + transform: translateY(-10rem); +} diff --git a/diagnostics/index.md b/diagnostics/index.md index 4136e42..be4a101 100644 --- a/diagnostics/index.md +++ b/diagnostics/index.md @@ -1,5 +1,5 @@ --- -layout: default.njk +layout: default title: Diagnostics --- diff --git a/events/index.md b/events/index.md index b3f477d..97a0991 100644 --- a/events/index.md +++ b/events/index.md @@ -1,5 +1,5 @@ --- -layout: post.njk +layout: post title: Events url: events author: ian diff --git a/index.md b/index.md index f31e5ca..f76a811 100644 --- a/index.md +++ b/index.md @@ -1,5 +1,5 @@ --- -layout: default.njk +layout: default title: OpenJS NodeJS Application Developer Study Guide --- diff --git a/js/main.js b/js/main.js index ce3028d..8595853 100644 --- a/js/main.js +++ b/js/main.js @@ -1,21 +1,6 @@ /* Add an edit in repl link to code blocks */ function checkForReplLinks() { - // const replCode = document.querySelectorAll('pre'); - // [...replCode].forEach(pre => { - // console.log(pre); - // const text = encodeURI(pre.innerText); - // const link = document.createElement('a'); - // link.title = 'Run this code in the REPL'; - // link.innerText = 'Run this code in the REPL'; - // link.href = '/repl/?code=' + text; - // const paragraph = document.createElement('p'); - // paragraph.appendChild(link); - // const wrapper = document.createElement('div'); - // pre.parentNode.insertBefore(wrapper, pre); - // wrapper.appendChild(pre); - // wrapper.appendChild(paragraph); - // }); const replCode = document.querySelectorAll('.repl-code'); [...replCode].forEach(code => { const codeText = encodeURI(code.innerText); @@ -29,6 +14,18 @@ function checkForReplLinks() { }); } +/* Post-specific stuff */ + +function showSolution() { + const button = document.querySelector('.show-solution'); + const solution = document.querySelector('.solution'); + + button.classList.add('removed'); + solution.classList.remove('hidden'); +} + +/* Progress tracking stuff */ + window.addEventListener('DOMContentLoaded', event => { if (window.localStorage) { window.topicsCompleted = getTopicsFromLocalStorage(); diff --git a/styleguide/index.md b/styleguide/index.md index 3ff1ce8..e33d3f2 100644 --- a/styleguide/index.md +++ b/styleguide/index.md @@ -1,5 +1,5 @@ --- -layout: default.njk +layout: default title: Style guide description: Kitchen sink of elements for design purposes --- From 2e7c19f954e2667cff84dfd273de4bcd88e630b1 Mon Sep 17 00:00:00 2001 From: Donovan Hutchinson Date: Fri, 8 Nov 2019 21:28:23 +0000 Subject: [PATCH 02/11] Phrasing --- control-flow/index.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/control-flow/index.md b/control-flow/index.md index e3e9ddb..1d15d32 100644 --- a/control-flow/index.md +++ b/control-flow/index.md @@ -17,11 +17,11 @@ setTimeout(() => { console.log('Wash face and hands'); ``` -In the above code, our compiler goes through one line at a time executing each instruction. It doesn't pause to wait, so it'll simply run the `setTimeout` then carry on and run the subequent `console.log`. Then (a small moment later), it will log the `Use lavatory` output. +In the above code our compiler goes through each line executing each instruction. First it logs `Clean teeth`. Then it doesn't hang around for the `setTimeout`, it simply carries on and logs `Wash face and hands`. Then (a small moment later), it will log `Use lavatory`. -This isn't what we want. We'll have to go wash our hands again. In production code, even worse things can happen. To get around this we can use _control flow_. +This is not what we want. We'll have to go wash our hands again. In production code, even worse things can happen. -Node offers 3 approaches we can use to control the order in which our code executes. _Callbacks_, _promises_ and _async/await_. Let's take a look at each. +Thankfully Node offers 3 approaches we can use to control the order in which our code executes. _Callbacks_, _promises_ and _async/await_. Let's take a look at each. ## Callbacks From cfefeba2992a47c99b78021a90b29d3023383e48 Mon Sep 17 00:00:00 2001 From: Donovan Hutchinson Date: Fri, 8 Nov 2019 21:36:28 +0000 Subject: [PATCH 03/11] Small tweaks --- control-flow/index.md | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/control-flow/index.md b/control-flow/index.md index 1d15d32..fba8df0 100644 --- a/control-flow/index.md +++ b/control-flow/index.md @@ -27,9 +27,9 @@ Thankfully Node offers 3 approaches we can use to control the order in which our Earlier in Node's development, it was common to use the _callback pattern_ to control the order that code is executed. The pattern involves passing a function as a parameter (_callback_) into your functions, and then calling that _callback_ when you're ready to continue. -### Example +Let's say we have to get some data from an external service. We don't know how long this service might take to respond. We would need to wait until the service responds, we can't simply continue running code as we might need the response. -Let's say we have to get some data from an external service. To simulate this I've created a `randomDelayedResponse` function that will return a response after an unspecified amount of time. +To simulate this I've created a `randomDelayedResponse` function that will return a response after an unspecified amount of time. ``` function randomDelayedResponse(text) { @@ -45,7 +45,9 @@ const output = randomDelayedResponse('Hello'); console.log(output); // empty ``` -If we wanted to get the response we need to wait until the response it ready. One way is to pass out final `console.log` in as a function like so: +Notice that the final line above returns `undefined`. + +We need to wait until the response is ready. One way is to pass our `console.log` in to `randomDelayedResponse` as a function. ``` function randomDelayedResponse(text, callback) { @@ -60,11 +62,11 @@ const output = randomDelayedResponse('Hello', text => console.log(text)); // out console.log(output); // still empty ``` -Here we are passing the function `text => console.log(text)` as the second parameter, and this function is then called after the `setTimeout`. +We pass the function `text => console.log(text)` as the second parameter, and this function is then called after the `setTimeout`. -This might be reasonable at this level, but it's easy for callbacks to get out of control. For example, if we have two different calls to this slow function that need to be run in order, we need to nest further. +This might be reasonable in simple situations but it's easy for callbacks to get out of control. For example, if we have many different calls to this slow function that need to be run in order. To achieve this control we need to nest further. -If we ran them beside each other, they would not be reliable: +To show many responses, consider the following code. If we ran them beside each other, the output would not be reliable. ``` function randomDelayedResponse(text, callback) { @@ -75,14 +77,14 @@ function randomDelayedResponse(text, callback) { }, timeOut * 10); } -randomDelayedResponse('Runner 1', text => console.log(text)); // outputs "Runner 1" -randomDelayedResponse('Runner 2', text => console.log(text)); // outputs "Runner 2" -randomDelayedResponse('Runner 3', text => console.log(text)); // outputs "Runner 3" -randomDelayedResponse('Runner 4', text => console.log(text)); // outputs "Runner 4" +randomDelayedResponse(1, text => console.log(text)); // outputs 1 +randomDelayedResponse(2, text => console.log(text)); // outputs 2 +randomDelayedResponse(3, text => console.log(text)); // outputs 3 +randomDelayedResponse(4, text => console.log(text)); // outputs 4 // Who will win? ``` -You can run the above code multiple times, and the result is unpredictable. To create a predictable flow using callbacks we'd need to nest them more: +We can run the above code multiple times, and the result is unpredictable. It is _asynchronous_, in that the results do not arrive in order. To create a predictable, _synchronous_ flow using callbacks we'd need to nest them. ``` function randomDelayedResponse(text, callback) { @@ -107,7 +109,7 @@ randomDelayedResponse(1, text => { }); // outputs "1 2 3 4" ``` -As we can see, structuring these callbacks does work but can quickly become difficult to read. This example is very simple so we can see how more complex code nested in this way would become difficult to follow. Let's look at another way. +Structuring these callbacks works. However this approach can create code becomes difficult to understand and maintain. Let's look at another way. ## Promises @@ -170,7 +172,7 @@ Promises remove a lot of nesting and give us easier to read code. However there ## Async / Await -The third approach is built on top of the existing _promises_ approach but makes it easier to reason with. With `async` and `await` we can write code that feels a lot more like the simple top-down code, by telling it to wait when we need it to. Let's rewrite our _who would win?_ example from above. +The third approach is built on top of the existing _promises_ approach but makes it easier to reason with. With `async` and `await` we can write code that feels a lot more like the simple top-down code, by telling it to wait when we need it to. Let's rewrite our _who will win?_ example from above. ``` function randomDelayedResponse(text, callback) { From 6f36a0286b1baa71232ae869376d000e0ad11e85 Mon Sep 17 00:00:00 2001 From: Donovan Hutchinson Date: Sun, 10 Nov 2019 16:31:36 +0000 Subject: [PATCH 04/11] Updating text --- control-flow/index.md | 106 +++++++++++++++++++++--------------------- 1 file changed, 52 insertions(+), 54 deletions(-) diff --git a/control-flow/index.md b/control-flow/index.md index fba8df0..f983b18 100644 --- a/control-flow/index.md +++ b/control-flow/index.md @@ -5,7 +5,7 @@ author: donovan date: Last Modified --- -As we create programs we write sequences of events. Usually, we expect them execute in order but this is not always the way it occurs. Even though JavaScript ios [single-threaded](https://medium.com/better-programming/is-node-js-really-single-threaded-7ea59bcc8d64 'Is Node.js Really Single-Threaded?'), functions that call APIs, setTimeouts and other structures can result in our code running in an unexpected order. +JavaScript programs are made up of series of instructions. When our programs run, they run in sequence from the top down, one line at a time. Most lines of are _synchronous_, meaning they run in order. However not all do. Sometimes _asynchronous_ code can cause our code to execute in an unexpected order. Consider the following code. @@ -21,6 +21,8 @@ In the above code our compiler goes through each line executing each instruction This is not what we want. We'll have to go wash our hands again. In production code, even worse things can happen. +Node generally runs in one single thread and any _blocking_ code will be run in sequence, with _non-blocking_ code having the potential to run _asynchronously_. + Thankfully Node offers 3 approaches we can use to control the order in which our code executes. _Callbacks_, _promises_ and _async/await_. Let's take a look at each. ## Callbacks @@ -34,28 +36,28 @@ To simulate this I've created a `randomDelayedResponse` function that will retur ``` function randomDelayedResponse(text) { // Using a timeout here for the sake of simulating an external request - const timeOut = Math.floor(Math.random() * 10) + 1; + const timeOut = Math.floor(Math.random() * 100) + 1; const output = text; setTimeout(() => { return output; - }, timeOut * 10); + }, timeOut); } const output = randomDelayedResponse('Hello'); -console.log(output); // empty +console.log(output); // undefined ``` -Notice that the final line above returns `undefined`. +Notice that the final line above returns `undefined`. This is because the `randomDelayedResponse` line is _non_blocking_ and hasn't returned anything to `output` before we then try to apply `console.log` to it. We need to wait until the response is ready. One way is to pass our `console.log` in to `randomDelayedResponse` as a function. ``` function randomDelayedResponse(text, callback) { // Using a timeout here for the sake of simulating an external request - const timeOut = Math.floor(Math.random() * 10) + 1; + const timeOut = Math.floor(Math.random() * 100) + 1; setTimeout(() => { callback(text); - }, timeOut * 10); + }, timeOut); } const output = randomDelayedResponse('Hello', text => console.log(text)); // outputs "Hello" @@ -69,30 +71,26 @@ This might be reasonable in simple situations but it's easy for callbacks to get To show many responses, consider the following code. If we ran them beside each other, the output would not be reliable. ``` -function randomDelayedResponse(text, callback) { - // Using a timeout here for the sake of simulating an external request - const timeOut = Math.floor(Math.random() * 10) + 1; - setTimeout(() => { - callback(text); - }, timeOut * 10); -} +// ... replacing the last two lines of the previous example -randomDelayedResponse(1, text => console.log(text)); // outputs 1 -randomDelayedResponse(2, text => console.log(text)); // outputs 2 -randomDelayedResponse(3, text => console.log(text)); // outputs 3 -randomDelayedResponse(4, text => console.log(text)); // outputs 4 +randomDelayedResponse(1, console.log); // outputs 1 +randomDelayedResponse(2, console.log); // outputs 2 +randomDelayedResponse(3, console.log); // outputs 3 +randomDelayedResponse(4, console.log); // outputs 4 // Who will win? ``` -We can run the above code multiple times, and the result is unpredictable. It is _asynchronous_, in that the results do not arrive in order. To create a predictable, _synchronous_ flow using callbacks we'd need to nest them. +Here we pass in the function `console.log` as the second argument which is then run as `callback(text)` to log the output. + +While the code does run in order and run the `randomDelayedResponse` function with the right sequence of inputs, the random delays mean they won't be logged in order. We can run the above code multiple times and the result is never predictable. Each call to the function is _asynchronous_, in that the results do not arrive in order. To create a predictable, _synchronous_ flow using callbacks we'd need to nest them. ``` function randomDelayedResponse(text, callback) { // Using a timeout here for the sake of simulating an external request - const timeOut = Math.floor(Math.random() * 10) + 1; + const timeOut = Math.floor(Math.random() * 100) + 1; setTimeout(() => { callback(text); - }, timeOut * 10); + }, timeOut); } randomDelayedResponse(1, text => { @@ -109,7 +107,9 @@ randomDelayedResponse(1, text => { }); // outputs "1 2 3 4" ``` -Structuring these callbacks works. However this approach can create code becomes difficult to understand and maintain. Let's look at another way. +Structuring these callbacks outputs the numbers in the correct order. However when we use `callbacks` the code can become difficult to understand and maintain. This might be suitable in simple cases. Though if we find ourselves nesting multiple levels deep we should look for other ways to control the flow. + +Let's look at one such alternative. ## Promises @@ -133,10 +133,13 @@ We can rewrite our runner example using promises like so: ``` function randomDelayedResponse(text, callback) { return new Promise((resolve, reject) => { - const timeOut = Math.floor(Math.random() * 10) + 1; + const timeOut = Math.floor(Math.random() * 100) + 1; setTimeout(() => { - resolve(text); // Replacing the callback with "resolve" - }, timeOut * 10); + if (text) { + resolve(text); // Replacing the callback with "resolve" + } + reject('No text provided!'); // Reject when there is an error + }, timeOut); }); } @@ -157,32 +160,23 @@ randomDelayedResponse(1) console.log(res); }) .catch(error => { - // handle error + console.log(error); }) // outputs "1 2 3 4" ``` Running the above code should output the correct order, albeit with random delays between each number. -We aren't handling errors in this example, but if our function was to encounter an error we would `reject('some error')` and this would then be picked up by the `catch`. +In the above example we shouldn't see any errors - but if you want to adjust one of the `andomDelayedResponse()` calls to pass in no data, it should `reject` and the `catch` block will log the error. -You can learn more about `promises` here (LINK) - -Promises remove a lot of nesting and give us easier to read code. However there is a third way to control the execution order of our code. +Promises remove the nesting and give us easier to read code. Let's look at a third way that builds on this. ## Async / Await -The third approach is built on top of the existing _promises_ approach but makes it easier to reason with. With `async` and `await` we can write code that feels a lot more like the simple top-down code, by telling it to wait when we need it to. Let's rewrite our _who will win?_ example from above. +The third approach is built on top of the existing _promises_ approach and results in even simpler code. With `async` and `await` we can write code that feels a lot more like our usual top-down code. It works by telling our commands to wait when we need them to. Let's rewrite our _who will win?_ example from above. ``` -function randomDelayedResponse(text, callback) { - return new Promise((resolve, reject) => { - const timeOut = Math.floor(Math.random() * 10) + 1; - setTimeout(() => { - resolve(text); - }, timeOut * 10); - }); -} +// Replace the second part of the example above async function runTheRace() { // put `async` before the function call @@ -200,27 +194,31 @@ async function runTheRace() { // put `async` before the function call runTheRace(); ``` -In the above we have replaced the series of `.then()` calls with an `async` function. It's still making use of a _promise_ but when we want to wait for the promise we simply place `await` before the asynchronous function. This way, the code execution order is preserved. +In the above we have replaced the series of `.then()` calls with an `async` function. It's still making use of a _promise_ but when we want to wait for the promise we simply place `await` before the asynchronous function. The code execution order is preserved. You may be wondering how we handle errors here. One approach is to use the `try/catch` method. We'll be covering that and more in the [error handling section]({{ "/error-handling/" | url }}). +## Which is preferred? + +Generally you should try to aim for the `async/await` approach when possible. It helps promote clean code. None of the approaches are to be avoided entirely, but in general always aim for the approach that ensures the code is easy to read and understand. + ## Exercise Given the following code, how would you change it so that the results always outputs `hello world`? Can you make it work using all 3 of the above approaches? ``` function hello() { - const timeOut = Math.floor(Math.random() * 10) + 1; + const timeOut = Math.floor(Math.random() * 100) + 1; setTimeout(() => { console.log('hello'); - }, timeOut * 10); + }, timeOut); } function world() { - const timeOut = Math.floor(Math.random() * 10) + 1; + const timeOut = Math.floor(Math.random() * 100) + 1; setTimeout(() => { console.log('world'); - }, timeOut * 10); + }, timeOut); } ``` @@ -233,11 +231,11 @@ function world() { ``` # Callbacks function hello(callback) { - const timeOut = Math.floor(Math.random() * 10) + 1; + const timeOut = Math.floor(Math.random() * 100) + 1; setTimeout(() => { console.log('hello'); callback(); - }, timeOut * 10); + }, timeOut); } function world() { @@ -255,19 +253,19 @@ hello(world); function hello() { return new Promise((resolve, reject) => { - const timeOut = Math.floor(Math.random() * 10) + 1; + const timeOut = Math.floor(Math.random() * 100) + 1; setTimeout(() => { resolve('hello'); - }, timeOut * 10); + }, timeOut); }); } function world() { return new Promise((resolve, reject) => { - const timeOut = Math.floor(Math.random() * 10) + 1; + const timeOut = Math.floor(Math.random() * 100) + 1; setTimeout(() => { resolve('world'); - }, timeOut * 10); + }, timeOut); }); } @@ -287,19 +285,19 @@ hello() function hello() { return new Promise((resolve, reject) => { - const timeOut = Math.floor(Math.random() * 10) + 1; + const timeOut = Math.floor(Math.random() * 100) + 1; setTimeout(() => { resolve('hello'); - }, timeOut * 10); + }, timeOut); }); } function world() { return new Promise((resolve, reject) => { - const timeOut = Math.floor(Math.random() * 10) + 1; + const timeOut = Math.floor(Math.random() * 100) + 1; setTimeout(() => { resolve('world'); - }, timeOut * 10); + }, timeOut); }); } From 417c38aacec6540d24cc30389995783adb83f601 Mon Sep 17 00:00:00 2001 From: Donovan Hutchinson Date: Mon, 11 Nov 2019 10:48:47 +0000 Subject: [PATCH 05/11] Updates --- control-flow/index.md | 40 ++++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/control-flow/index.md b/control-flow/index.md index f983b18..1c092d6 100644 --- a/control-flow/index.md +++ b/control-flow/index.md @@ -23,7 +23,7 @@ This is not what we want. We'll have to go wash our hands again. In production c Node generally runs in one single thread and any _blocking_ code will be run in sequence, with _non-blocking_ code having the potential to run _asynchronously_. -Thankfully Node offers 3 approaches we can use to control the order in which our code executes. _Callbacks_, _promises_ and _async/await_. Let's take a look at each. +Thankfully Node offers 3 asynchronous approaches we can use to control the order in which our code executes. _Callbacks_, _promises_ and _async/await_. Let's take a look at each. ## Callbacks @@ -71,7 +71,14 @@ This might be reasonable in simple situations but it's easy for callbacks to get To show many responses, consider the following code. If we ran them beside each other, the output would not be reliable. ``` -// ... replacing the last two lines of the previous example +function randomDelayedResponse(text) { + // Using a timeout here for the sake of simulating an external request + const timeOut = Math.floor(Math.random() * 100) + 1; + const output = text; + setTimeout(() => { + return output; + }, timeOut); +} randomDelayedResponse(1, console.log); // outputs 1 randomDelayedResponse(2, console.log); // outputs 2 @@ -116,7 +123,7 @@ Let's look at one such alternative. To avoid this callback hell, we can use a different structure called _promises_. A promise is a function that returns a _resolved_ response which we can chain using `then`, or a _rejected_ response which we can `catch`. It follows this pattern: ``` -runOutPromise() // returns a response +runOurPromise() // returns a response .then(response => { return foo; // this could optionally be another promise }) @@ -135,10 +142,10 @@ function randomDelayedResponse(text, callback) { return new Promise((resolve, reject) => { const timeOut = Math.floor(Math.random() * 100) + 1; setTimeout(() => { - if (text) { - resolve(text); // Replacing the callback with "resolve" + if (!text) { + reject('No text provided!'); // Reject when there is no text provided } - reject('No text provided!'); // Reject when there is an error + resolve(text); // Replacing the callback with "resolve" }, timeOut); }); } @@ -176,7 +183,17 @@ Promises remove the nesting and give us easier to read code. Let's look at a thi The third approach is built on top of the existing _promises_ approach and results in even simpler code. With `async` and `await` we can write code that feels a lot more like our usual top-down code. It works by telling our commands to wait when we need them to. Let's rewrite our _who will win?_ example from above. ``` -// Replace the second part of the example above +function randomDelayedResponse(text, callback) { + return new Promise((resolve, reject) => { + const timeOut = Math.floor(Math.random() * 100) + 1; + setTimeout(() => { + if (!text) { + reject('No text provided!'); + } + resolve(text); + }, timeOut); + }); +} async function runTheRace() { // put `async` before the function call @@ -188,7 +205,14 @@ async function runTheRace() { // put `async` before the function call console.log(3); const result4 = await randomDelayedResponse(4); - console.log(result4) + console.log(result4); + + // Catching the error + try { + await randomDelayedResponse(); + } catch (error) { + console.error(error); + } } runTheRace(); From 16f166a0adfb8e663f014770e4b6018b749bfc22 Mon Sep 17 00:00:00 2001 From: Donovan Hutchinson Date: Mon, 11 Nov 2019 10:50:16 +0000 Subject: [PATCH 06/11] Fixing example code --- control-flow/index.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/control-flow/index.md b/control-flow/index.md index 1c092d6..c61300b 100644 --- a/control-flow/index.md +++ b/control-flow/index.md @@ -71,12 +71,10 @@ This might be reasonable in simple situations but it's easy for callbacks to get To show many responses, consider the following code. If we ran them beside each other, the output would not be reliable. ``` -function randomDelayedResponse(text) { - // Using a timeout here for the sake of simulating an external request +function randomDelayedResponse(text, callback) { const timeOut = Math.floor(Math.random() * 100) + 1; - const output = text; setTimeout(() => { - return output; + callback(text); }, timeOut); } @@ -93,7 +91,6 @@ While the code does run in order and run the `randomDelayedResponse` function wi ``` function randomDelayedResponse(text, callback) { - // Using a timeout here for the sake of simulating an external request const timeOut = Math.floor(Math.random() * 100) + 1; setTimeout(() => { callback(text); From 559841c49e2961e1110ae35108a08242144626a6 Mon Sep 17 00:00:00 2001 From: Donovan Hutchinson Date: Mon, 11 Nov 2019 10:52:12 +0000 Subject: [PATCH 07/11] Fixing example code --- control-flow/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/control-flow/index.md b/control-flow/index.md index c61300b..f6a8b8f 100644 --- a/control-flow/index.md +++ b/control-flow/index.md @@ -120,7 +120,7 @@ Let's look at one such alternative. To avoid this callback hell, we can use a different structure called _promises_. A promise is a function that returns a _resolved_ response which we can chain using `then`, or a _rejected_ response which we can `catch`. It follows this pattern: ``` -runOurPromise() // returns a response +ourPromiseFunction() // returns a promise that will resolve or reject .then(response => { return foo; // this could optionally be another promise }) From 33813f885f961fa82464571c8d81c797aff44dbe Mon Sep 17 00:00:00 2001 From: Donovan Hutchinson Date: Mon, 11 Nov 2019 14:27:33 +0000 Subject: [PATCH 08/11] Adding video to resources --- control-flow/index.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/control-flow/index.md b/control-flow/index.md index f6a8b8f..da41030 100644 --- a/control-flow/index.md +++ b/control-flow/index.md @@ -382,8 +382,9 @@ async function callFoo() { Be sure to check out the [cheat sheet]({{ "/cheatsheet/" | url }}) for more. -## Further Reading +## Further learning - [Callback function](https://developer.mozilla.org/en-US/docs/Glossary/Callback_function) - [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) - [Making asynchronous programming easier with async and await](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Async_await) +- What the heck is the event loop anyway? (Philip Roberts, JSConf EU, 2014) (https://www.youtube.com/watch?v=8aGhZQkoFbQ) From 9ba860a67887e576ddbf591d78f253155b331484 Mon Sep 17 00:00:00 2001 From: Donovan Hutchinson Date: Mon, 11 Nov 2019 14:46:23 +0000 Subject: [PATCH 09/11] Changing to Node.js and adding video link --- control-flow/index.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/control-flow/index.md b/control-flow/index.md index da41030..c724163 100644 --- a/control-flow/index.md +++ b/control-flow/index.md @@ -21,13 +21,13 @@ In the above code our compiler goes through each line executing each instruction This is not what we want. We'll have to go wash our hands again. In production code, even worse things can happen. -Node generally runs in one single thread and any _blocking_ code will be run in sequence, with _non-blocking_ code having the potential to run _asynchronously_. +Node.js generally runs in one single thread and any _blocking_ code will be run in sequence, with _non-blocking_ code having the potential to run _asynchronously_. For more info you may want to take a look at [this talk on how the Event Loop works](https://www.youtube.com/watch?v=8aGhZQkoFbQ). -Thankfully Node offers 3 asynchronous approaches we can use to control the order in which our code executes. _Callbacks_, _promises_ and _async/await_. Let's take a look at each. +Thankfully Node.js offers 3 asynchronous approaches we can use to control the order in which our code executes. _Callbacks_, _promises_ and _async/await_. Let's take a look at each. ## Callbacks -Earlier in Node's development, it was common to use the _callback pattern_ to control the order that code is executed. The pattern involves passing a function as a parameter (_callback_) into your functions, and then calling that _callback_ when you're ready to continue. +Earlier in the development of Node.js it was common to use the _callback pattern_ to control the order that code is executed. The pattern involves passing a function as a parameter (_callback_) into your functions, and then calling that _callback_ when you're ready to continue. Let's say we have to get some data from an external service. We don't know how long this service might take to respond. We would need to wait until the service responds, we can't simply continue running code as we might need the response. From c5efb45e71e1315e1a8ec808a3d005927dafabec Mon Sep 17 00:00:00 2001 From: Donovan Hutchinson Date: Wed, 13 Nov 2019 19:15:43 +0000 Subject: [PATCH 10/11] Adding date --- control-flow/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/control-flow/index.md b/control-flow/index.md index c724163..905090a 100644 --- a/control-flow/index.md +++ b/control-flow/index.md @@ -2,7 +2,7 @@ layout: post title: Control Flow author: donovan -date: Last Modified +date: 2019-11-12 --- JavaScript programs are made up of series of instructions. When our programs run, they run in sequence from the top down, one line at a time. Most lines of are _synchronous_, meaning they run in order. However not all do. Sometimes _asynchronous_ code can cause our code to execute in an unexpected order. From f0827913b64dbab8af2fcb30271103ccfac87aa8 Mon Sep 17 00:00:00 2001 From: Donovan Hutchinson Date: Wed, 13 Nov 2019 20:17:24 +0000 Subject: [PATCH 11/11] Including repl examples and fixing typos --- control-flow/index.md | 63 +++++++++++++++++++++++++++++++++++++------ js/main.js | 2 +- 2 files changed, 56 insertions(+), 9 deletions(-) diff --git a/control-flow/index.md b/control-flow/index.md index 905090a..bee6fde 100644 --- a/control-flow/index.md +++ b/control-flow/index.md @@ -9,6 +9,8 @@ JavaScript programs are made up of series of instructions. When our programs run Consider the following code. +
+ ``` console.log('Clean teeth'); setTimeout(() => { @@ -17,6 +19,8 @@ setTimeout(() => { console.log('Wash face and hands'); ``` +
+ In the above code our compiler goes through each line executing each instruction. First it logs `Clean teeth`. Then it doesn't hang around for the `setTimeout`, it simply carries on and logs `Wash face and hands`. Then (a small moment later), it will log `Use lavatory`. This is not what we want. We'll have to go wash our hands again. In production code, even worse things can happen. @@ -33,6 +37,8 @@ Let's say we have to get some data from an external service. We don't know how l To simulate this I've created a `randomDelayedResponse` function that will return a response after an unspecified amount of time. +
+ ``` function randomDelayedResponse(text) { // Using a timeout here for the sake of simulating an external request @@ -47,10 +53,14 @@ const output = randomDelayedResponse('Hello'); console.log(output); // undefined ``` +
+ Notice that the final line above returns `undefined`. This is because the `randomDelayedResponse` line is _non_blocking_ and hasn't returned anything to `output` before we then try to apply `console.log` to it. We need to wait until the response is ready. One way is to pass our `console.log` in to `randomDelayedResponse` as a function. +
+ ``` function randomDelayedResponse(text, callback) { // Using a timeout here for the sake of simulating an external request @@ -64,12 +74,16 @@ const output = randomDelayedResponse('Hello', text => console.log(text)); // out console.log(output); // still empty ``` +
+ We pass the function `text => console.log(text)` as the second parameter, and this function is then called after the `setTimeout`. This might be reasonable in simple situations but it's easy for callbacks to get out of control. For example, if we have many different calls to this slow function that need to be run in order. To achieve this control we need to nest further. To show many responses, consider the following code. If we ran them beside each other, the output would not be reliable. +
+ ``` function randomDelayedResponse(text, callback) { const timeOut = Math.floor(Math.random() * 100) + 1; @@ -85,10 +99,14 @@ randomDelayedResponse(4, console.log); // outputs 4 // Who will win? ``` +
+ Here we pass in the function `console.log` as the second argument which is then run as `callback(text)` to log the output. While the code does run in order and run the `randomDelayedResponse` function with the right sequence of inputs, the random delays mean they won't be logged in order. We can run the above code multiple times and the result is never predictable. Each call to the function is _asynchronous_, in that the results do not arrive in order. To create a predictable, _synchronous_ flow using callbacks we'd need to nest them. +
+ ``` function randomDelayedResponse(text, callback) { const timeOut = Math.floor(Math.random() * 100) + 1; @@ -111,6 +129,8 @@ randomDelayedResponse(1, text => { }); // outputs "1 2 3 4" ``` +
+ Structuring these callbacks outputs the numbers in the correct order. However when we use `callbacks` the code can become difficult to understand and maintain. This might be suitable in simple cases. Though if we find ourselves nesting multiple levels deep we should look for other ways to control the flow. Let's look at one such alternative. @@ -134,6 +154,8 @@ ourPromiseFunction() // returns a promise that will resolve or reject We can rewrite our runner example using promises like so: +
+ ``` function randomDelayedResponse(text, callback) { return new Promise((resolve, reject) => { @@ -169,6 +191,8 @@ randomDelayedResponse(1) // outputs "1 2 3 4" ``` +
+ Running the above code should output the correct order, albeit with random delays between each number. In the above example we shouldn't see any errors - but if you want to adjust one of the `andomDelayedResponse()` calls to pass in no data, it should `reject` and the `catch` block will log the error. @@ -179,6 +203,8 @@ Promises remove the nesting and give us easier to read code. Let's look at a thi The third approach is built on top of the existing _promises_ approach and results in even simpler code. With `async` and `await` we can write code that feels a lot more like our usual top-down code. It works by telling our commands to wait when we need them to. Let's rewrite our _who will win?_ example from above. +
+ ``` function randomDelayedResponse(text, callback) { return new Promise((resolve, reject) => { @@ -215,6 +241,8 @@ async function runTheRace() { // put `async` before the function call runTheRace(); ``` +
+ In the above we have replaced the series of `.then()` calls with an `async` function. It's still making use of a _promise_ but when we want to wait for the promise we simply place `await` before the asynchronous function. The code execution order is preserved. You may be wondering how we handle errors here. One approach is to use the `try/catch` method. We'll be covering that and more in the [error handling section]({{ "/error-handling/" | url }}). @@ -227,6 +255,8 @@ Generally you should try to aim for the `async/await` approach when possible. It Given the following code, how would you change it so that the results always outputs `hello world`? Can you make it work using all 3 of the above approaches? +
+ ``` function hello() { const timeOut = Math.floor(Math.random() * 100) + 1; @@ -243,14 +273,19 @@ function world() { } ``` +
+ ## Suggested solution + ## Common Classes and Functions ### Callback pattern diff --git a/js/main.js b/js/main.js index 8595853..da04074 100644 --- a/js/main.js +++ b/js/main.js @@ -3,7 +3,7 @@ function checkForReplLinks() { const replCode = document.querySelectorAll('.repl-code'); [...replCode].forEach(code => { - const codeText = encodeURI(code.innerText); + const codeText = encodeURIComponent(code.innerText); const link = document.createElement('a'); link.title = 'Run this code in the REPL'; link.innerText = 'Run this code in the REPL';