This repo was created using create-react-app
Clone this repo and the promises-api repo down.
For both:
npm install
npm start
Understanding of:
- Why promises over callbacks
- How to implement a promise
- How to use Promise.all()
Brief Understanding of:
- single-threaded JS
- asynchronous JS
- non-blocking
- call stack
- task queue
- event loop
- heap
Javascript, a single-threaded non-blocking asynchronous concurrent language
. Whoa well that's a mouth full. Okay, that also seems a little confusing. Lets go ahead and break this thing off into little sections.
As we know javascript has a single-threaded call stack
that has stack frames. As our code is run, each stack frame is pushed and popped from the top of the call stack, the top being the current code being executed.
Quick plug to Philip Roberts for having an amazing talk, which you can find here
So consider the javascript:
function foo(b) {
var a = 10;
return a + b + 11;
}
function bar(x) {
var y = 3;
return foo(x * y);
}
console.log(bar(7));
So what we have is a function foo(b)
that returns 10 + b + 11
.
Then there is bar(x)
that returns foo(x * y)
.
Our console.log ends up calling bar(7)
Lets watch the call stack:
As we see the stack starts by pushing console.log(bar(7))
because it was the first executed code we have. Which then calls bar(7)
to execute foo(x * y)
to execute the inner operation x * y
. So far we have just been pushing things to the stack. Until after x * y
has finished executing then it gets popped off!
Now we continue on with foo(x * y)
which pushes a + b + 11
to the top of the stack to immediately get popped off leaving a + b
to be pushed to the top. Then finishes off by popping all the rest of the stack. This will finish by returning 42
to the console.
Go try it out here. Did anything happen you didn't expect? Talk to a neighbor about what you've learned after playing around with the stack.
So! From the image above we can start to understand these these concepts:
Single threaded:
Threads are basic units of CPU utilization.Asynchronous:
JavaScript call stack carries out tasks to completion instead of task switching and the same holds for events.Non-blocking:
Blocking occurs when the application state is suspended as a thread runs.
'A very interesting property of the event loop model is that JavaScript, unlike a lot of other languages, never blocks. Handling I/O is typically performed via events and callbacks, so when the application is waiting for an IndexedDB query to return or an XHR request to return, it can still process other things like user input.'*
Heres a great example:
Example --> When Big Loop handler runs the browser appears frozen. We know JavaScript’s call stack is synchronous soBig Loop executes 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.
Still confused? Yup; you, me and everyone else. It's hard but it's a good thing to keep in the back of your head. So lets continue on with asynchronous. Clearly with non-blocking we can have a user click a button and continue on with I/O without making them wait.
So why is that? Well for a long time the web has used callbacks
to help solve this issue. Allowing code to be executed once its finished. For example the browser
has web api's that take callbacks as an argument, like: XHR
, SetTimeout
, DOM
events. These things have their own callback queues
/task queues
which run an event loop
.
Here's an example:
try it yourself
When your code first executes, it loads events and saves them in heap
, but for now lets reference it by the web api box. So the call stack starts running through our code and setTimeout(function timeout{...})
(line 9) gets put on the call stack. Which sends it over to the web api box for storage until it's ready to execute the callback after 5 seconds! Once the wait is done, it will send the callback to the callback queue / task queue
while the event loop
waits for a good time to throw it on the stack / till the stack is cleared. ( event loop is the orange loop in this example )
Now we also saw that the DOM event listener
, when put on the call stack(line 1) is pushed to the web apis storage and waits/listens for its time to be called. You'll notice that once I trigger the event by clicking the 'Click me!' button, that callback is pushed to the callback queue / task queue
. Once the stack has cleared the event loop
will trigger the next callback.
So what does this mean? So yes, your JS code is running on a single call stack (single-threaded) but under your JS, the browser code is running multiple threads to capture events and trigger handlers, when they capture any new event, they push it on an event queue and then that event loop, in which your code is running gets triggered and it handles the request.
Talk about a quick review! How about we do some codez?
Hokay. So. lets build a Front-end Turing staff website. What we have so far is an api that serves up a collection of members here. We also have our client side code located on this repo.
What we want to do first is make a request
which we now know is writing asynchronous code. Earlier we talked about the request call being put into the heap
which stores that information until it is ready to run it's callback. Then it's transferred into the task queue
till the event loop
says it's ready to be put on the call stack
So the API given to us doesn't give us all the info needed to display the staff members.
The endpoints given to us are:
- http://localhost:3001/api/frontend-staff - this returns an array of objects that contain the name of each staff memeber and another endpoint to grab their bio.
- http://localhost:3001//api/bio/:id - this is the endpoint given from each obj inside the array from the endpoint
frontend-staff
So once we make our call we will need to iterate over the array and make more requests for additional info.
What we could do is setState() after each request is returned which will end up looking like this: :
$.get('http://localhost:3001/api/frontend-staff', (info) => {
info.bio.forEach((i) => {
$.get(i.info, (bio) => {
Object.assign(i, bio)
this.setState({ staff: info.bio })
})
})
})
It will actually load the page seamlessly. Yet if you look at the console log you will see each one come in individually(I didn't log the bio info just because of the length).
You can see that each one response comes in separately, which could be a bad UX if sizing and images came in at all different times. Especially if there was heaver data coming in. I could put a setTimeout() let it wait 2 seconds and then setState but then we would be bogging down the entire task queue. Even then we don't actually know that all of them have come in! So lets move on to uses promises!
Promises are a lot easier to work with because we can pass them around and you don't get in 'callback hell'. They are still a web api that gets stored in the heap
and once resolved are placed inside the task queue
.
So you should have learned a little bit about promises by now. Like:
- then()
- catch()
So for practice lets play around and create one. Take or comment any requests you have in your componentDidMount() and replace it with: Do yourself a favor and actually type this out
componentDidMount() {
const promise = new Promise((resolve, reject) => {
if (this.state.staff.length === 0) { reject('Where did everyone go?') }
resolve(this.state.staff)
})
promise.then((foo) => console.log(foo))
.catch((err)=> console.log('mmm', err))
}
When the array is empty what do we console.log()? When we throw an empty object in there, what does it log?
So now that you've got to play around a bit with then() & catch(), lets talk about using them with fetch
.
So if you don't know how to use fetch
yet I suggest taking a five minutes to review the docs here.
Fetch returns a promise, which will either resolve
or reject
depending on the status code. You might want to take a look on when fetch actually catches errors here. The api can actually be set up in a way that can help fix this, but this is a major reason why some people dislike fetch
.
So if fetch
returns a promise, right? Then it makes sense that you can chain .then()
or .catch()
So when we make a request and have a response come back in json, the typical move for fetch looks like:
fetch('someapi.com')
.then((res) => res.json())
.then((body) => 'the response coming in as an object.')
// we could also write this like:
fetch('someapi.com')
.then((res) => res.json().then((body) => 'the response coming in as an object.'))
// notice that we are chaining to the res.json()
This is not the preferred way of doing things, but why can we do this? Take a look here
So Promise.all() takes an array of promises, and will resolve
only if all the promises resolve
otherwise it will reject
.
How can we use this to our advantage? So when we make our request to 'api/frontend-staff' we receive an array
of staff members containing info to make more fetch calls
.
fetch('http://localhost:3001/api/frontend-staff')
.then((res) => res.json())
// this returns =>
{ bio: [
{ info: "http://localhost:3001/api/bio/1",
name: "Romeeka Gayhart"
},
{...},
{...}
]
}
So we're probably going to have to iterate through this array to make a fetch call for all the bios. If promise.all() expects an array of promises and fetch returns an promise, how can we use this to our advantage?
EDIT:
The code example should go here but because I want you to learn this stuff I took it out.
- yung-jhun
Feel free to look through resources:
- I used a lot of MDN docs which I tried to link to throughout the walkthrough MDN docs
- Loupe, by Philip Roberts, was used for examples.
- DAN MARTENSEN wrote this article that I referenced his code pen from.