Reputation: 1515
I'm building up a hierarchy of scripts, where all the scripts at the same level can be run in parallel, but each level needs to complete before the next level runs.
I had this working when I had a fixed, known number of levels, by querying the document for the scripts at level 1, having a function that returns a promise when the arg passed loads, pushing that function onto an array, repeating for each level, and then creating a Promise.all chain for the resulting arrays:
function loadScript (script) {
return new Promise(function(resolve, reject) {
script.setAttribute("src", script.getAttribute("data-src"));
script.onload = function() {
resolve(script.src);
};
script.onerror = function() {
reject(script.src);
}
});
}
function loadScripts() {
const firstScripts = document.querySelectorAll( "script[data-type='lazy']"
+ "[data-order='1']" );
const secondScripts = document.querySelectorAll( "script[data-type='lazy']"
+ "[data-order='2']" );
let first = [];
let second = [];
firstScripts.forEach(function(element) {
first.push(loadScript(element));
});
secondScripts.forEach(function(element) {
second.push(loadScript(element));
});
Promise.all(first)
.then(Promise.all(second))
.then(function() {
console.log('all scripts loaded');
})
.catch(function(script) {
console.log(script + ' failed to load.');
});
}
I'm now trying to abstract it a bit, so it will work with an arbitrary, unknown depth. This seems like a fine place to use Array.map
; however, the problem is, it's actually executing the Promise.
So, what I'm looking for is a way to map the calls to loadScript
such that they can be called later:
function loadScripts() {
let maxDepth = 0;
let hierarchy = [];
while ( Boolean(document.querySelector( "script[data-type='lazy']"
+ "[data-order='" + maxDepth + "']" )) ) {
hierarchy[ maxDepth ] = [ ...document.querySelectorAll(
"script[data-type='lazy'][data-order='" + maxDepth + "']" ) ];
maxDepth++;
}
hierarchy.forEach((scripts, level) => {
// these loadScript calls should ... not be calls. They should be queued to
// run later when walkHierarchy is called
hierarchy[ level ] = scripts.map((element) => loadScript( element ) );
});
walkHierarchy = async function (arr) {
for (const level of arr) await Promise.all(level);
}
walkHierarchy( hierarchy )
.then(function() {
console.log('all scripts loaded');
})
.catch(function(script) {
console.log(script + ' failed to load.');
});
}
Alternatively, if there's a way to take the walkHierarchy
logic and include it in the hierarchy.forEach
, such that each level completed before moving on to the next, that would be fine, too.
Upvotes: 1
Views: 86
Reputation: 135217
dynamic import
Maybe you are looking for dynamic import which already supports on-demand loading.
DIY level-order script loader
Maybe you don't want to use import
or it's not supported in your environment. Let's take a concrete example so we can verify the behaviour. We start with a bunch of unordered <scripts>
-
<script data-type="lazy" data-src="a.js" data-order="1"></script>
<script data-type="lazy" data-src="b.js" data-order="1"></script>
<script data-type="lazy" data-src="bbbb.js" data-order="4"></script>
<script data-type="lazy" data-src="aa.js" data-order="2"></script>
<script data-type="lazy" data-src="bb.js" data-order="2"></script>
<script data-type="lazy" data-src="aaa.js" data-order="3"></script>
<script data-type="lazy" data-src="c.js" data-order="1"></script>
<script data-type="lazy" data-src="cc.js" data-order="2"></script>
<script data-type="lazy" data-src="d.js" data-order="1"></script>
<script data-type="lazy" data-src="bbb.js" data-order="3"></script>
<script data-type="lazy" data-src="aaaa.js" data-order="4"></script>
graph
The first step is to make a graph
of (order, script array) map
-
// script : html_script_element
// order : int
// graph : (order, script array) map
// makeGraph : script iterable -> graph
function makeGraph(scripts) {
const g = new Map
for (const s of scripts) {
const order = Number(s.getAttribute("data-order"))
if (!g.has(order)) g.set(order, [])
g.get(order).push(s)
}
return g
}
makeGraph(document.querySelectorAll("script[data-type=lazy]"))
Map {
1 => [<script>, <script>, <script>, <script>],
4 => [<script>, <script>],
2 => [<script>, <script>, <script>],
3 => [<script>, <script>]
}
loadScripts
It doesn't matter that the graph appears unordered. loadScripts
will recursively traverse the graph, incrementing order
one-by-one. When a particular order is not found in the graph, we know it is time to stop loading and exit -
// loadScripts : (graph, int) -> (string array) promise
async function loadScripts(g, order = 1) {
if (!g.has(order))
return []
else
return [
...await Promise.all(g.get(order).map(loadScript)),
...await loadScripts(g, order + 1)
]
}
loadScript
We can demo this here by modifying loadScript
to resolve
whether the script loads successfully or not. We also added a console.log
so we can see which order loadScript
is called in -
// loadScript : script -> string promise
function loadScript(script) {
return new Promise(function(resolve, reject) {
console.log("loading", script.getAttribute("data-src")) // ⚠️ for demo
script.setAttribute("src", script.getAttribute("data-src"))
script.onload = _ => resolve(script.src)
script.onerror = _ => resolve(script.src) // ⚠️ resolve for demo only
})
}
all together now
loadScripts(makeGraph(document.querySelectorAll("script[data-type=lazy]")))
.then(console.log)
.catch(console.error)
loading a.js
loading b.js
loading c.js
loading d.js
loading aa.js
loading bb.js
loading cc.js
loading aaa.js
loading bbb.js
loading bbbb.js
loading aaaa.js
[
"https://stacksnippets.net/a.js",
"https://stacksnippets.net/b.js",
"https://stacksnippets.net/c.js",
"https://stacksnippets.net/d.js",
"https://stacksnippets.net/aa.js",
"https://stacksnippets.net/bb.js",
"https://stacksnippets.net/cc.js",
"https://stacksnippets.net/aaa.js",
"https://stacksnippets.net/bbb.js",
"https://stacksnippets.net/bbbb.js",
"https://stacksnippets.net/aaaa.js"
]
demo
Verify the results in your own browser -
function loadScript(script) {
return new Promise(function(resolve, reject) {
console.log("loading", script.getAttribute("data-src")) // ⚠️ demo only
script.setAttribute("src", script.getAttribute("data-src"))
script.onload = _ => resolve(script.src)
script.onerror = _ => resolve(script.src) // ⚠️ demo only
})
}
async function loadScripts(g, order = 1) {
if (!g.has(order))
return []
else
return [
...await Promise.all(g.get(order).map(loadScript)),
...await loadScripts(g, order + 1)
]
}
function makeGraph(scripts) {
const g = new Map
for (const s of scripts) {
const order = Number(s.getAttribute("data-order"))
if (!g.has(order)) g.set(order, [])
g.get(order).push(s)
}
return g
}
loadScripts(makeGraph(document.querySelectorAll("script[data-type=lazy]")))
.then(console.log)
.catch(console.error)
.as-console-wrapper { min-height: 100%; top: 0; }
<script data-type="lazy" data-src="a.js" data-order="1"></script>
<script data-type="lazy" data-src="b.js" data-order="1"></script>
<script data-type="lazy" data-src="bbbb.js" data-order="4"></script>
<script data-type="lazy" data-src="aa.js" data-order="2"></script>
<script data-type="lazy" data-src="bb.js" data-order="2"></script>
<script data-type="lazy" data-src="aaa.js" data-order="3"></script>
<script data-type="lazy" data-src="c.js" data-order="1"></script>
<script data-type="lazy" data-src="cc.js" data-order="2"></script>
<script data-type="lazy" data-src="d.js" data-order="1"></script>
<script data-type="lazy" data-src="bbb.js" data-order="3"></script>
<script data-type="lazy" data-src="aaaa.js" data-order="4"></script>
Upvotes: 2