核心要点
Promise.all()
方法为每个组创建一个Promise,该Promise在数组中的所有Promise都解析后解析。 本文探讨一个具体问题:如何并行预加载大量图片。 我最近遇到了这个问题,发现它比最初预期的更具挑战性,也从中学习了很多。首先,让我简要描述一下场景。假设页面上有几个“组”。广义上说,一个组就是一个图片集合。我们希望预加载每个组的图片,并能够知道何时完成某个组的图片加载。此时,我们可以自由运行任何我们想要的代码,例如向组添加一个类、运行图像序列、记录某些内容等等。起初,这听起来很简单,甚至非常简单。但是,你可能和我一样忽略了一个细节:我们希望所有组并行加载,而不是顺序加载。换句话说,我们不希望先加载组1的所有图片,然后加载组2的所有图片,再加载组3的所有图片,依此类推。事实上,这不是理想的,因为最终会有一些组需要等待前面的组完成。因此,在一个场景中,如果第一个组有几十张图片,而第二个组只有一两张图片,我们就必须等待第一个组完全加载才能准备第二个组。这不好。我们肯定可以做得更好!所以我们的想法是并行加载所有组,这样当一个组完全加载时,我们不必等待其他组。为此,大致思路是加载所有组的第一张图片,然后加载所有组的第二张图片,依此类推,直到所有图片都已预加载。好了,让我们从创建一些标记开始,这样我们就能就正在发生的事情达成一致。
顺便说一句,在本文中,我假设您熟悉Promise的概念。如果不是这样,我建议您阅读这篇文章。
标记
从标记的角度来看,一个组只不过是一个元素(例如div),带有deck类以便我们可以定位它,以及一个包含图片URL数组(作为JSON)的data-images属性。
<div class="deck" data-images='["...", "...", "..."]'>...</div> <div class="deck" data-images='["...", "..."]'>...</div> <div class="deck" data-images='["...", "...", "...", "..."]'>...</div>
准备工作
在JavaScript方面,这——不出所料——有点复杂。我们将构建两样不同的东西:一个组类(请将此放在非常大的引号之间,不要对术语吹毛求疵)和一个预加载器工具。因为预加载器必须知道所有组的所有图片才能以特定的顺序加载它们,所以它需要在所有组之间共享。一个组不能有它自己的预加载器,否则我们会遇到最初的问题:代码是顺序执行的,这不是我们想要的。所以我们需要一个传递给每个组的预加载器。后者将它的图片添加到预加载器的队列中,一旦所有组都将它们的项目添加到队列中,预加载器就可以开始预加载。执行代码片段如下:
// 实例化一个预加载器 var ip = new ImagePreloader(); // 从DOM获取所有组 var decks = document.querySelectorAll('.deck'); // 遍历它们并为每个组实例化一个新的组,将预加载器传递给每个组,以便组可以将它的图片添加到队列中 Array.prototype.slice.call(decks).forEach(function (deck) { new Deck(deck, ip); }); // 一旦所有组都将它们的项目添加到队列中,就预加载所有内容 ip.preload();
我希望到目前为止,这是有意义的!
构建组
根据您想对组做什么,这个“类”可能相当长。对于我们的场景,我们唯一要做的事情是在其图片加载完成后向节点添加一个loaded类。Deck函数没有太多工作要做:1. 加载数据(从data-images属性);2. 将数据添加到预加载器队列的末尾;3. 告诉预加载器在数据预加载完成后该做什么。
var Deck = function (node, preloader) { // 我们从`data-images`属性获取并解析数据 var data = JSON.parse(node.getAttribute('data-images')); // 我们调用预加载器的`queue`方法,将数据和回调函数传递给它 preloader.queue(data, function () { node.classList.add('loaded'); }); };
到目前为止,进展顺利,不是吗?唯一剩下的就是预加载器,尽管它也是本文中最复杂的代码部分。
构建预加载器
我们已经知道我们的预加载器需要一个queue方法来将图片集合添加到队列中,以及一个preload方法来启动预加载。它还需要一个辅助函数来预加载图片,称为preloadImage。让我们从这里开始:
var ImagePreloader = function () { ... }; ImagePreloader.prototype.queue = function () { ... } ImagePreloader.prototype.preloadImage = function () { ... } ImagePreloader.prototype.preload = function () { ... }
预加载器需要一个内部queue属性来保存它必须预加载的组,以及它们各自的回调。
var ImagePreloader = function () { this.items = []; }
items是一个对象数组,其中每个对象有两个键:- collection包含要预加载的图片URL数组;- callback包含在组完全加载后要执行的函数。
知道了这一点,我们可以编写queue方法。
<div class="deck" data-images='["...", "...", "..."]'>...</div> <div class="deck" data-images='["...", "..."]'>...</div> <div class="deck" data-images='["...", "...", "...", "..."]'>...</div>
好了。此时,每个组都可以将它的图片添加到队列中。我们现在必须构建preload方法,它将负责实际预加载图片。但在跳转到代码之前,让我们退一步来理解我们需要做什么。我们的想法不是一个接一个地预加载每个组的所有图片。我们的想法是预加载每个组的第一张图片,然后是第二张,然后是第三张,依此类推。预加载一张图片意味着使用JavaScript(使用new Image())创建一个新的图片,并为其应用一个src。这将提示浏览器异步加载源。由于这个异步过程,我们需要注册一个Promise,该Promise在浏览器下载资源后解析。基本上,我们将用一个Promise替换我们数组中的每个图片URL,该Promise在浏览器加载给定图片后解析。此时,我们将能够使用Promise.all(..) 来获得一个最终的Promise,该Promise在数组中的所有Promise都解析后解析。对于每个组都是如此。让我们从preloadImage方法开始:
// 实例化一个预加载器 var ip = new ImagePreloader(); // 从DOM获取所有组 var decks = document.querySelectorAll('.deck'); // 遍历它们并为每个组实例化一个新的组,将预加载器传递给每个组,以便组可以将它的图片添加到队列中 Array.prototype.slice.call(decks).forEach(function (deck) { new Deck(deck, ip); }); // 一旦所有组都将它们的项目添加到队列中,就预加载所有内容 ip.preload();
现在是preload方法。它做两件事(因此可能可以拆分成两个不同的函数,但这不在本文的范围内):1. 它以特定的顺序(每个组的第一张图片,然后是第二张,然后是第三张……)将所有图片URL替换为Promise;2. 对于每个组,它注册一个Promise,当组中的所有Promise都解析后(!)调用组的回调。
var Deck = function (node, preloader) { // 我们从`data-images`属性获取并解析数据 var data = JSON.parse(node.getAttribute('data-images')); // 我们调用预加载器的`queue`方法,将数据和回调函数传递给它 preloader.queue(data, function () { node.classList.add('loaded'); }); };
就是这样!毕竟没有那么复杂,你同意吗?
进一步推进
代码运行良好,尽管使用回调来告诉预加载器在组加载完成后该做什么并不是很优雅。您可能希望使用Promise而不是回调,尤其是在我们一直使用Promise的情况下!我不确定如何解决这个问题,所以我不得不承认我请我的朋友Valérian Galliat帮我解决这个问题。我们在这里使用的是延迟Promise。延迟Promise不是原生Promise API的一部分,因此我们需要为其添加polyfill;谢天谢地,这只需要几行代码。基本上,延迟Promise是一个稍后可以解析的Promise。将其应用于我们的代码,只会改变很少的东西。首先是.queue(..)
方法:
var ImagePreloader = function () { ... }; ImagePreloader.prototype.queue = function () { ... } ImagePreloader.prototype.preloadImage = function () { ... } ImagePreloader.prototype.preload = function () { ... }
.preload(..)
方法中的解析:
var ImagePreloader = function () { this.items = []; }
当然,最后是我们添加数据到队列的方式!
// 如果没有指定回调,则为空函数 function noop() {} ImagePreloader.prototype.queue = function (array, callback) { this.items.push({ collection: array, // 如果没有回调,我们推送一个no-op(空)函数 callback: callback || noop }); };
我们完成了!如果您想查看代码的实际运行情况,请查看下面的演示:(此处应插入CodePen演示链接,因为我无法直接嵌入CodePen)
结论
好了,朋友们。大约70行JavaScript代码,我们就成功地异步并行加载了不同集合中的图片,并在集合加载完成后执行了一些代码。从这里开始,我们可以做很多事情。在我的例子中,重点是在点击按钮时将这些图片作为快速循环序列(gif样式)运行。因此,我在加载期间禁用了按钮,并在组完成所有图片的预加载后重新启用它。由于浏览器已经缓存了所有图片,因此第一个循环运行非常流畅。我希望你喜欢它!您可以在GitHub上查看代码,也可以直接在CodePen上使用它。(此处应插入GitHub链接和CodePen链接)
(此处应添加FAQ部分,与输入文本中的FAQ部分内容一致,但语言表达上进行了一些调整和润色。)
以上是与承诺并行预加载图像的详细内容。更多信息请关注PHP中文网其他相关文章!