Core points
Promise.all()
method to create a Promise for each group, which is parsed after all the Promises in the array. This article discusses a specific problem: how to preload a large number of images in parallel. I've had this problem recently and found it more challenging than initially expected and learned a lot from it. First, let me briefly describe the scene. Suppose there are several "groups" on the page. Broadly speaking, a group is a collection of pictures. We want to preload images for each group and be able to know when images for a group are loaded. At this point, we can freely run any code we want, such as adding a class to the group, running an image sequence, recording something, etc. At first, this sounds simple, even very simple. However, you probably overlooked one detail like me: we want all groups to load in parallel, not sequentially. In other words, we don't want to load all pictures of group 1 first, then all pictures of group 2, then all pictures of group 3, and so on. In fact, this is not ideal, because there will be some groups that will eventually need to wait for the previous group to complete. So, in a scenario, if the first group has dozens of pictures and the second group has only one or two pictures, we have to wait for the first group to fully load before we can prepare the second group. This is not good. We can definitely do better! So our idea is to load all groups in parallel so that when one group is fully loaded we don't have to wait for other groups. To do this, the general idea is to load the first image of all groups, then load the second image of all groups, and so on until all pictures are preloaded. OK, let's start by creating some markup so we can agree on what is going on.
By the way, in this article, I assume you are familiar with the concept of Promise. If that is not the case, I suggest you read this article.
Tags
From a tag point of view, a group is nothing more than an element (e.g. a div), with a deck class so that we can locate it, and a data-images property containing an array of image URLs (as JSON).
<div class="deck" data-images='["...", "...", "..."]'>...</div> <div class="deck" data-images='["...", "..."]'>...</div> <div class="deck" data-images='["...", "...", "...", "..."]'>...</div>
Preparation
In JavaScript, this - as expected - is a bit complicated. We will build two different things: a group class (please put this between very large quotes and don't pick on the term) and a preloader tool. Because the preloader must know all the pictures of all groups in order to load them in a specific order, it needs to be shared among all groups. A group cannot have its own preloader, otherwise we will have the initial problem: the code is executed sequentially, which is not what we want. So we need a preloader passed to each group. The latter adds its image to the preloader's queue, and once all groups add their items to the queue, the preloader can start preloading. The code execution snippet is as follows:
// 实例化一个预加载器 var ip = new ImagePreloader(); // 从DOM获取所有组 var decks = document.querySelectorAll('.deck'); // 遍历它们并为每个组实例化一个新的组,将预加载器传递给每个组,以便组可以将它的图片添加到队列中 Array.prototype.slice.call(decks).forEach(function (deck) { new Deck(deck, ip); }); // 一旦所有组都将它们的项目添加到队列中,就预加载所有内容 ip.preload();
I hope so far, this makes sense!
Build group
Depending on what you want to do with the group, this "class" may be quite long. For our scenario, the only thing we have to do is add a loaded class to the node after its image is loaded. There is not much work to do in the Deck function: 1. Load the data (from the data-images property); 2. Add the data to the end of the preloader queue; 3. Tell the preloader what to do after the data is preloaded.
var Deck = function (node, preloader) { // 我们从`data-images`属性获取并解析数据 var data = JSON.parse(node.getAttribute('data-images')); // 我们调用预加载器的`queue`方法,将数据和回调函数传递给它 preloader.queue(data, function () { node.classList.add('loaded'); }); };
So far, it's going well, isn't it? The only thing left is the preloader, although it is also the most complex part of the code in this article.
Build a preloader
We already know that our preloader requires a queue method to add the image collection to the queue, and a preload method to start the preload. It also requires a helper function to preload the image, called preloadImage. Let's start here:
var ImagePreloader = function () { ... }; ImagePreloader.prototype.queue = function () { ... } ImagePreloader.prototype.preloadImage = function () { ... } ImagePreloader.prototype.preload = function () { ... }
The preloader requires an internal queue property to hold the groups it must preload, and their respective callbacks.
var ImagePreloader = function () { this.items = []; }
items is an array of objects, where each object has two keys: - collection contains the array of image URLs to be preloaded; - callback contains the function to be executed after the group is fully loaded.
With this, we can write the queue method.
<div class="deck" data-images='["...", "...", "..."]'>...</div> <div class="deck" data-images='["...", "..."]'>...</div> <div class="deck" data-images='["...", "...", "...", "..."]'>...</div>
Okay. At this point, each group can add its image to the queue. We now have to build the preload method, which will be responsible for the actual preloading of the image. But before jumping to the code, let's step back to understand what we need to do. Our idea is not to preload all pictures of each group one by one. Our idea is to preload the first image of each group, then the second image, then the third image, and so on. PreloadA image means creating a new image using JavaScript (using new Image()) and applying an src to it. This will prompt the browser to load the source asynchronously. Due to this asynchronous process, we need to register a Promise, which is parsed after the browser downloads the resource. Basically, we will replace each image URL in our array with a Promise that parses after the browser loads the given image. At this point, we will be able to use Promise.all(..) to get a final Promise that is parsed after all the Promises in the array. This is true for every group. Let's start with the preloadImage method:
// 实例化一个预加载器 var ip = new ImagePreloader(); // 从DOM获取所有组 var decks = document.querySelectorAll('.deck'); // 遍历它们并为每个组实例化一个新的组,将预加载器传递给每个组,以便组可以将它的图片添加到队列中 Array.prototype.slice.call(decks).forEach(function (deck) { new Deck(deck, ip); }); // 一旦所有组都将它们的项目添加到队列中,就预加载所有内容 ip.preload();
Now is the preload method. It does two things (so it might be possible to split into two different functions, but that's not within the scope of this article): 1. It's in a specific order (the first image of each group, then the second one, Then there is the third one...) Replace all image URLs with Promise; 2. For each group, it registers a Promise, and calls the group's callback after all the Promises in the group are parsed (!).
var Deck = function (node, preloader) { // 我们从`data-images`属性获取并解析数据 var data = JSON.parse(node.getAttribute('data-images')); // 我们调用预加载器的`queue`方法,将数据和回调函数传递给它 preloader.queue(data, function () { node.classList.add('loaded'); }); };
That's it! After all, it's not that complicated, do you agree?
More promotion
The code works well, although using a callback to tell the preloader what to do after the group is loaded is not very elegant. You may want to use Promise instead of callbacks, especially if we have been using Promise! I'm not sure how to solve this problem, so I have to admit that I asked my friend Valérian Galliat to help me with this problem. What we are using here is Delay Promise. Delayed Promise is not part of the native Promise API, so we need to add polyfills to it; thankfully, this only takes a few lines of code. Basically, the delayed Promise is a Promise that can be parsed later. Applying it to our code will only change very little. First is the .queue(..)
method:
var ImagePreloader = function () { ... }; ImagePreloader.prototype.queue = function () { ... } ImagePreloader.prototype.preloadImage = function () { ... } ImagePreloader.prototype.preload = function () { ... }
.preload(..)
Method:
var ImagePreloader = function () { this.items = []; }
Of course, in the end, it's how we add data to the queue!
// 如果没有指定回调,则为空函数 function noop() {} ImagePreloader.prototype.queue = function (array, callback) { this.items.push({ collection: array, // 如果没有回调,我们推送一个no-op(空)函数 callback: callback || noop }); };
We're done! If you want to see how the code is actually running, check out the demo below: (The CodePen demo link should be inserted here, because I can't embed CodePen directly)
Conclusion
Okay, friends. With about 70 lines of JavaScript code, we successfully loaded pictures in different collections in parallel asynchronously and executed some code after the collection is loaded. From here, there are a lot of things we can do. In my example, the focus is to run these images as a quick loop sequence (gif style) when the button is clicked. So I disabled the button during loading and re-enabled it after the group has finished preloading all the pictures. Since the browser already caches all images, the first loop runs very smoothly. I hope you like it! You can view the code on GitHub or use it directly on CodePen. (GitHub link and CodePen link should be inserted here)
(The FAQ part should be added here, which is consistent with the FAQ part in the input text, but some adjustments and polishes have been made in the language expression.)
The above is the detailed content of Preloading Images in Parallel with Promises. For more information, please follow other related articles on the PHP Chinese website!