I’ll give it a go since I’ll likely have to explain it to my sister at some point!
To begin with a probably simplistic (but conceptually easier to deal with) view of a callback is that it is simply a function, say, fnC
, that is passed into another function, say, fnA
, as an argument. The purpose of doing so is often because fnA
produces results that have some other use. Consider the following example:
function furJede(array, fn) {
const len = array.length;
for (let i = 0; i < len; i++) {
const item = array[i];
fn(item);
}
}
function count(i) {
console.log(i);
}
furJede([1, 2, 3], count);
// Logs the following in the console:
// 1
// 2
// 3
Assuming that you have understood the code above, and in the context of the example given, the function count
is supplied as a callback to the function furJede
in the following line:
furJede([1, 2, 3], count);
That’s really all there is to it—it’s a function, or callback, that’s supplied to another function that gets called in the latter at some point. Let’s suppose we want to add the function furJede
to an array as a method, we will rewrite the furJede
function as follows:
function furJede(fn) {
const len = this.length;
// Notice that we now use 'this' instead of supplying
// the array that we want to process as an argument
// Since we are attaching it to an array, 'this' will become
// the array itself
for (let i = 0; i < len; i++) {
const item = this[i];
fn(item);
}
}
function count(i) {
console.log(i);
}
const arr = [1, 2, 3];
// Recall that an array is just an object, so we can attach
// a function to it like so:
arr.furJede = furJede;
// We can now call the function as follows, and we will
// supply the function, count, as a callback
arr.furJede(count);
// Logs the following in the console:
// 1
// 2
// 3
Alternatively, and without declaring the callback function (count
in our case) beforehand, the function call arr.furJede(count)
can be written with an anonymous function as:
arr.furJede(function(element) {
console.log(element);
});
// Or with an arrow function
arr.furJede((element) => {
console.log(element);
});
At this point you are probably thinking that the function furJede
looks seriously like Array.prototype.forEach
method:
var arr = [1, 2, 3];
arr.forEach((element) => {
console.log(element);
});
// Logs the following in the console:
// 1
// 2
// 3
And you would be right because furJede
was designed to do something very similar to forEach
. If you are familiar with forEach
, the function that you give to it as an argument is a callback; in fact, you have probably been using callbacks without thinking that they are callbacks:
const arr = [1, 2, 3];
// Array.prototype.map
const mapped = arr.map(function(val) {
return 'number ' + val;
});
console.log(mapped); // ['number 1', 'number 2', 'number 3'];
// The callback is:
// function(val) {
// return 'letter ' + val;
// }
const arr = [1, 2, 3];
// Array.prototype.filter
const filtered = arr.filter(function(val) {
return val < 3;
});
console.log(filtered); // [1, 2]
// The callback is:
// function(val) {
// return val < 3;
// }
That’s really (I think!) all there is to callback—it’s really is just a function you give to another function as an argument, which (the callback) gets called at some point inside the function you supply to.
If it’s clear what callbacks are at this point (hopefully so!), and if I’m not mistaken, callbacks may or may not be involved with Promise
(s) (more often than not they are)—so let’s forget about callbacks for now! It looks like you do have working code, but I assume that it’s a general question and you don’t seem to be the only one having troubles with it, so moving along, consider the following code:
// Asynchronous API call using the fetch Web API
// There are callbacks! But forget about them for now!
// Don't worry about too much about the fetch API if you are not
// familiar with it—just treat it simply as an asynchronous action for now!
let quote;
fetch('https://myjson.api.com/api')
.then(function(response) {
return response.json();
})
.then(function(data) {
quote = data;
console.log('Fetched: ', quote); // Fetched: { quote: 'Nyanpasu!' }
});
console.log(quote);
The expected output in the log would be in this specific order:
// 'undefined'
// Fetched: { quote: 'Nyanpasu!' }
The reason is because fetch
is asynchronous, so any code that comes after it would be executed immediately after it starts. Since it takes some time to get data from a remote location, the line console.log(quote)
will run before console.log('Fetched: ', quote);
, therefore you get undefined
.
This is where Promise
s come in; as you already know, a Promise
lets you do something, wait for your code to signal that some operations (most likely async!) has finished (Promise.resolve
, for example), and gives you the option to do things afterwards (Promise.then
, for example).
The promise constructor, as you have already seen in the MDN documentation and used in your code, takes the following (simplified) form:
new Promise(function(resolve) { ... } );
The basic idea is that you do something inside the function inside the constructor and use the resolve
method to indicate that things are done when, well, it’s done. Using our example from above, if we want the line console.log(quote)
to also return an object, we can rewrite it using a Promise
as follows:
let quote;
const promise = new Promise(function(resolve) {
fetch('https://myjson.api.com/api')
.then(function(response) {
return response.json();
})
.then(function(data) {
quote = data;
console.log('Fetched: ', quote);
resolve(); // New line
});
});
promise.then(function() {
console.log(quote);
});
The expected output will now ibe n this specific order:
// Fetched: { quote: 'Nyanpasu!' }
// { quote: 'Nyanpasu!' }
There are two key points to note here:
- The function,
resolve
, “returns a Promise object that is resolved with the given value” (from the documentation). In other words, it indicates that you are done waiting for whatever you wanted to wait to finish and ready to move on
- The promise object returned by the
resolve
method has a then()
method, which accepts a callback function that is, given the point above, only executed once the promise that returns the promise object is resolved
The following code should now be a lot more straightforward to understand, not least because you have done it in your code (see comments):
// We are not declaring a global variable, quote, anymore.
// Instead, we are going to pass the quote from the promise
// to the callback function inside then using resolve();
const promise = new Promise(function(resolve) {
fetch('https://myjson.api.com/api')
.then(function(response) {
return response.json();
})
.then(function(data) {
quote = data;
console.log('Fetched: ', quote);
resolve(quote); // This line changed—the promise is now resolved with quote
});
});
promise.then(function(val) { // This lines has also changed—the callback now takes an argument
console.log(val); // This line also changed—the value comes from the callback argument
});
The output of the code above is still:
// Fetched: { quote: 'Nyanpasu!' }
// { quote: 'Nyanpasu!' }
To avoid confusing you, something that I deliberately avoided pointing out earlier is that fetch
is actually returns a resolved promise—that’s why the then
method is available to it. So the code is actually a bit of a silly example since you can just do everything inside the second then
after fetch
in this particularly example; I wanted to avoid using setTimeout
because you may have also been confused by the example in the documentation (I personally had lot of trouble just because of the syntax).
By the same token, and with respect to your code, it is worth noting that jQuery.getJSON
actually returns a Promise
-like object that is thenable, so the Promise
constructors in your code is actually a bit redundant:
var myPromise = new Promise((resolve, reject)=>{
resolve($.getJSON("https://wind-bow.glitch.me/twitch-api/users/"+val+"?callback=?"))
});
// ...
myPromise.then((data)=>{
// Do things with data
});
… is functionally identical:
$.getJSON("https://wind-bow.glitch.me/twitch-api/users/"+val+"?callback=?")
.then((data) => {
// Do things with data
});
Now that you (hopefully) have a good grasp of how promises work, the last thing that I think you may find useful is Promise.all()
.
Currently in your code, you are iterating through a list of usernames and making API calls for each of them; since the responses from the API arrive at different times every time, the order of the users is actually (very likely to be) different every time you refresh your app. Promise.all lets you deal with an array of promises and only do something after all of them have been resolved. In conjunction with Array.prototype.map
, you can keep things the same order (if you wish to) like this:
const users = ['Alice', 'Bob', 'Callum'];
const promises = users.map((user) => {
return new Promise((resolve) {
$.getJSON('https://api.com/' + user)
.then((data) => {
resolve(data);
});
});
});
Promise.all(promises).then((arrayOfUserData) => {
arrayOfUserData.forEach((user) => {
// Do things!
});
});
While Promise
s is not the only way to deal with async actions (have a look at async function if you are interested), it’s probably not (at least currently not) a good idea to gloss over it and/or find structures that approximate/work around it.
I hope that helps and hopefully there isn’t anything wrong! I’m also happy to expand on it if anything is missing. Good luck! 