I've been trying to figure out how Promises work with a rather simple example: one that fetches a number of images, loads it onto the page in order, counts the number of images loaded.
const addImg = url => {
fetch(url)
.then(validateResponse)
.then(readResponseAsBlob)
.then(showImage)
.catch(Error);
}
function showImage(responseAsBlob) {
const container = document.getElementById('img-container');
const imgElem = document.createElement('img');
container.appendChild(imgElem);
const imgUrl = URL.createObjectURL(responseAsBlob);
imgElem.src = imgUrl;
return imgUrl;
}
document.getElementById("add").onclick = () => {
document.getElementById("status").innerHTML = "Fetching...";
Promise.all(urls.map(url => addImg(url)))
.then(setTimeout(() => {
document.getElementById("status").innerHTML = document.getElementsByTagName("img").length + " images";
}, 0));
}
The addImg function fetches an image from the url, processes it as a blob and showImage renders adds a new img. When I try to add images from an array of urls, I have noticed a few problems I want to fix:
The images don't necessarily show up in order
the img count is not accurate
My first thought: if I deconstruct the addImg function so that it execute each step as a separate promise( fetch all -> then validate all -> then ... so on), it might work the way I intend it to, but I'm not sure if that's the right approach to it.
It might make more sense to you if you rewrote your code using async/await. If you rewrote your AJAX call as
const addImg = url => fetch(url)
.then(validateResponse)
.then(readResponseAsBlob)
.then(showImage)
.catch(Error);
And then you could do something like:
async function loadImages(){
for(image in imageList){
await addImg(image.url);
}
console.log('Images loaded');
}
This way your code will wait for each image load to complete before the next. Note that this isn't very performant but if you want them loading specifically in order then this is one you could achieve that easily.
Related
I have written an image-loading function like this:
export function loadImage(url: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
if (image_map.has(url)) {
resolve(image_map.get(url));
} else {
let image: HTMLImageElement = new Image();
if (typeof window.static_url != 'undefined' && url[0] == '/') {
image.src = `${window.static_url}${url}`;
} else {
image.src = url;
}
image_map.set(url, image);
image.crossOrigin = 'Anonymous'; // Prevent canvas getImageData CORS issue
image.onload = function () {
resolve(image);
};
image.onerror = reject;
}
});
}
I have about 200 images to load, and prior to asking this question, I have been writing code like this:
for (let url of image_list) {
await loadImage(url);
}
And it took me ages (16s or more) to load all these images.
Today I decided to remove the await from my code, and miracle happened: the image loading process finished within 2s. How is that even possible? I thought javascript is single-threaded, and I had expected that, by removing await, the image loading should simply take place after the rest of my code, instead of taking place before, but the total time for them to load should not differ so greatly.
Your current code loads the images serially (one after the other).
You can instead execute them in parallel using promise all.
// 1. simultaneously kick off load image on each url
const promises = image_list.map(loadImage);
// 2. await them all
await Promise.all(promises);
// 3. Images have loaded.
Or more succinctly.
await Promise.all(image_list.map(loadImage));
I am sending chained Fetch requests. First, I retrieve data from database and request pictures related to every title I got.
The HTML code won't be loaded to results div before image requests are sent. So it takes long time to see articles. How can I make the text to load before image requests starting to be sent?
async function getArticles(params) {
url = 'http://127.0.0.1:8000/articles/api/?'
url2 = 'https://api.unsplash.com/search/photos?client_id=XXX&content_filter=high&orientation=landscape&per_page=1&query='
const article = await fetch(url + params).then(response => response.json());
const cards = await Promise.all(article.results.map(async result => {
try {
let image = await fetch(url2 + result.title).then(response => response.json())
let card = // Creating HTML code by using database info and Splash images
return card
} catch {
let card = // Creating HTML code by using info and fallback images from database
return card
}
}))
document.getElementById('results').innerHTML = cards.join("")
};
I have tried using them separately but I was getting Promise Object.
If you don't want to wait for all the fetches, use an ordinary for loop and await each one sequentially.
async function getArticles(params) {
url = 'http://127.0.0.1:8000/articles/api/?'
url2 = 'https://api.unsplash.com/search/photos?client_id=XXX&content_filter=high&orientation=landscape&per_page=1&query='
const article = await fetch(url + params).then(response => response.json());
for (let i = 0; i < article.results.length; i++) {
let result = article.results[i];
let card;
try {
let image = await fetch(url2 + result.title).then(response => response.json())
card = // Creating HTML code by using database info and Splash images
} catch {
card = // Creating HTML code by using info and fallback images from database
}
document.getElementById('results').innerHTML += card;
}
}
However, this will be slower because it won't start each fetch until the previous one completes.
It's hard to run all the fetches concurrently but display the results in the order that they were sent, rather than the order that the responses were received. You could do it by creating a container DIV for each response before sending, then filling in the appropriate DIV when its response is received.
For my SPFx webpart (using React/TypeScript) I am trying to replace the source of some images as the initially fetched source must be retrieved first using another Microsoft Graph API call.
I fetch all images inside the HTML which will be later rendered using temporalDivElement.querySelectorAll("img") and then checking them if the source needs to be replaced. If this is the case I then call the Graph API to fetch the new image source (or a replacement span node, if the image cannot be fetched). As I have to iterate over all images, I first collect all those requests to the Graph API in an array of Promises and then later execute them using Promise.all().
This is my current code:
public parseAndFixBodyContent(messageId: string, bodyContent: string) {
return new Promise<string>(async (resolve, reject) => {
let temporalDivElement = document.createElement("div");
temporalDivElement.innerHTML = bodyContent;
// For hosted content images: fetch hosted content with Graph API, convert to BLOB and then replace existing src attribute with BLOB value
const imgTags = temporalDivElement.querySelectorAll("img");
const hostedImagesReplacementPromises: Promise<void>[] = [];
imgTags.forEach((img) => {
if (img.src &&
img.src.indexOf(`/messages/${messageId}/hostedContents/`) > -1) {
// Hosted Content url found, try to fetch image through API
let hostedContentApiUrl: string = img.src;
hostedImagesReplacementPromises.push(this.replaceHostedContentImageSource(hostedContentApiUrl, img));
}
});
Promise.all(hostedImagesReplacementPromises)
.then(() => {
resolve(temporalDivElement.innerHTML);
});
});
}
public replaceHostedContentImageSource(hostedContentApiUrl: string, image: HTMLImageElement) {
return new Promise<void>(async (resolve, reject) => {
this.getHostedContentAsBlob(hostedContentApiUrl).then((imageBlobUrl) => {
image.src = imageBlobUrl;
resolve();
})
.catch(error => {
// We could not fetch the hosted content for the image
let missingImageInfo = document.createElement("span");
missingImageInfo.innerText = `(${strings.ImageNotAvailable})`;
image.parentNode.replaceChild(missingImageInfo, image);
resolve();
});
});
}
public getHostedContentAsBlob(hostedContentApiUrl: string) {
return new Promise<string>(async (resolve, reject) => {
this.context.msGraphClientFactory
.getClient()
.then((client: MSGraphClient): void =>{
client
.api(hostedContentApiUrl)
.version("beta")
.responseType('blob')
.get((error, response: Blob, rawResponse?: any) => {
if (rawResponse.status == 200 && response) {
const imageUrl: string = URL.createObjectURL(response);
resolve(imageUrl);
} else {
reject(new Error(strings.ErrorCouldNotGetHostedConent));
}
});
})
.catch(error => {
reject(error);
});
});
}
This code does sometimes work and sometimes not at all and sometimes it works for half of the images and not the other half. For example I use it on the same two channel replies that have hosted content images in them and sometimes I get both images, then I get only one image and the other one hasn't been replaced at all (not even the SPAN tag with the information that the replacement failed) or both haven't been processed. It is like sometimes the promises don't get executed or at least not at the right time before rendering.
I don't see what's wrong with my code but I guess there is some timing issue here?
I was approached, if I ever found a solution for this issue.
To my surprise I have unfortunately forgotten that I posted this question but on the brighter side I seem to have solved the issue a day later.
I cannot remember fully but looking at the new code I believe it was indeed a timing issue or more detailed the issue comes from trying to solve the promises inside a forEach loop.
I'm not yet very proficient in JS/React but from my understanding promises are also async, so this code I previously used is a big no-no:
// Some message body content needs to be prepared for the display (fetch hosted content, etc.)
slicedChannelTopics.forEach((t) => {
this.parseAndFixBodyContent(t.message.id, t.message.content).then((transformedContent) => {
if (t.message.contentType && t.message.contentType == 'html' && t.message.content) {
t.message.content = transformedContent;
}
})
.catch(parseError => {
console.log(parseError);
});
});
I changed this to first collect all promises and then solve them using Promise.all(...):
let promisesForParseAndFixBodyContent = topicReplies.map((reply) =>
{
return this.parseAndFixBodyContent(reply.message, MessageType.Reply);
});
Promise.all(promisesForParseAndFixBodyContent).then(() => {
resolve(topicReplies);
});
Since making this change, the issue with loading the hosted content was gone.
i am trying to make a simple app that calls an api and the renders 10 images per page.
the first page loads but does not show images but the second page does.
what am i doing wrong?
let imageData = [];
fetch({api}
).then(res => res.json())
.then((data) => {
imageData.push(...data.results)
})
fetch({api}
).then(res => res.json())
.then((data) => {
imageData.push(...data.results)
})
let currentPage = 1;
let imagesPerPage = 10;
const changePage = (page)=> {
let nextBttn = document.getElementById("nextBttn");
let prevBttn = document.getElementById("prevBttn");
let root = document.getElementById("root");
let pageCount = document.getElementById("page");
if (page < 1) page = 1;
if (page > numPages()) page = numPages();
root.innerHTML = "";
for (var i = (page - 1) * imagesPerPage; i < (page * imagesPerPage) && i < imageData.length; i++) {
const createImage = document.createElement('img')
createImage.src = imageData[i].urls.thumb
createImage.setAttribute('id',imageData[i].id)
root.appendChild(createImage)
}
pageCount.innerHTML = page + "/" + numPages();
window.onload = ()=>{
changePage(1);
};
there are two fetches because it returns 30 images and i need 60
The problem is you have two (three technically) asynchronous tasks running
that depend on one another, but without any code to synchronize them back up.
Here's the possible order of events:
You initiate a fetch of the first 30 images
You initiate a fetch of the second 30 images
No matter how fast these requests are, their callback won't fire until the rest of this code is parsed/executed.
You set a callback for Page Load
Here's where things can get wonky.
Scenario A (unlikely, you wouldn't have an error):
The server is fast as heck (or cached response) and already has a response waiting for you. In theory, I believe its possible the fetch callback fires before the page load (though I could be wrong here). In this unlikely scenario, the response data is loaded into the imageData. Then the page load event fires, and calls changePage, which displays the images from imageData.
Scenario B (most likely):
The server takes some milliseconds to respond but the page elements are all created and therefore onLoad callback fires first. It attempts to display the imageData (but there isn't any yet). The server finally responds with the 60 images. No code is executed that tells the webpage to draw this new image data.
As you can see, because your code assumes the image data is already available when it tries to display some images on page load (not data load), it fails when the image data takes awhile to return and upon returning does not notify the page to display the new image data.
Here's how you can modify it:
let response1 = fetch({api})
.then(res => res.json())
.then((data) => {
imageData.push(...data.results)
});
let response2 = fetch({api})
.then(res => res.json())
.then((data) => {
imageData.push(...data.results)
});
Promise.all([response1, response2])
.then(() => changePage(1));
// Remove onLoad callback because we don't really care when the page loads, we care when the data loads.
I am fairly new to javascript. Let's say I have the following code.
let sources =["http://localhost:3001/resources/black_bish",""http://localhost:3001/resources/white_bish""]
let loaded_images=0
sources.forEach(ele = > {
let img = new Image()
img.src = ele
img.onload = function () {
loaded_images=++
}
})
Here I had a question about Javascript and concurrency. Can't the 2 callbacks be called at the same time similar to when working with threads? Here, won't there be a race condition? If I were to perform a different action than "loaded_images=++", is there any race condition like manipulating a data structure that I should be worried about?
Thank You
One way is to return a Promise for each image that you load. This promise will resolve, or in laymen terms: continue when the right condition is met, whenever the images has been loaded. It's like a return statement but instead of ending the function you continue to the next step.
Promise.all is a method on the promise constructor which takes in an array of promises. When all of the promises in the array have been fullfilled (meaning resolve has been called) then do something with the values of all of the promises.
const loadImage = (source) => new Promise(resolve => {
let img = new Image();
img.onload = function() {
resolve(img);
};
img.src = source;
});
const whenAllImagesAreLoaded = (...sources) => Promise.all(
sources.map(source => loadImage(source));
);
whenAllImagesAreLoaded('image-1.jpg', 'image-2.jpg').then((images) => {
images.forEach(image => {
console.log(image, ' is loaded');
});
});
Another example is also with promises in combination with the async / await syntax, which halt execution until the promise you are waiting for has been fulfilled.
This opens up the possibility to, for example: load images one after another, after the previous image has been loaded.
async function loadImagesSequentially(...sources) {
for (const source of sources) {
let image = await loadImage(source);
console.log(image, ' is loaded');
}
}
Both methods give you better control over how to handle race conditions, or eliminate them completely. I'd suggest that you practice as much as you can with promises. It is a very powerful tool in your JavaScript toolbox.
If you have any questions, please let me know.