如何高效优雅地实现JavaScript脚本按顺序执行?
按顺序加载并执行脚本的高效优雅实现方式
问题背景
我需要按顺序执行多个脚本(比如3个,生产环境可能多达20个),每个后续脚本必须等前一个完全执行完毕才能启动。另外要注意:给脚本添加defer属性无法保证严格的执行顺序。
现有可行实现示例
下面是一个通过事件触发实现顺序加载的示例:
index.html
<html> <head> <title>按顺序加载脚本</title> <script> const scripts = ["script1.js", "script2.js", "script3.js"] for (let i = 1; i < scripts.length; i++) { document.head.addEventListener(scripts[i - 1], () => { console.log(`完成处理 ${scripts[i - 1]}`) const nextScript = document.createElement("script") nextScript.src = scripts[i] document.head.appendChild(nextScript) }) } </script> <!-- 加载第一个脚本,启动顺序加载流程 --> <script src="script1.js"></script> </head> <body> <h1>Hello world</h1> </body> </html>
script1.js
const foobar = async () => { return new Promise((resolve, reject) => { setTimeout(() => { resolve() }, 3000) setTimeout(() => { reject(new Error("未在规定时间内完成。")) }, 10000); }) } foobar().then(() => { console.log("script1.js 加载执行完成") document.head.dispatchEvent(new Event("script1.js")) })
script2.js
const foobar2 = async () => { return new Promise((resolve, reject) => { setTimeout(() => { resolve() }, 3000) setTimeout(() => { reject(new Error("未在规定时间内完成。")) }, 10000); }) } foobar2().then(() => { console.log("script2.js 加载执行完成") document.head.dispatchEvent(new Event("script2.js")) })
script3.js
console.log("hello world")
执行日志
script1.js 加载执行完成 完成处理 script1.js script2.js 加载执行完成 完成处理 script2.js hello world
更高效优雅的实现方案
上面的事件监听方式虽然可行,但当脚本数量达到20个时,每个脚本都要手动触发事件,维护成本会很高。下面推荐两种更优的实现方式:
1. Promise + async-await 循环
封装一个异步加载脚本的函数,通过async-await按顺序遍历脚本列表,代码简洁易读,无需每个脚本手动触发事件。
实现代码:
<html> <head> <title>用async-await顺序加载脚本</title> <script> // 封装加载并执行脚本的异步函数 async function loadScript(src) { return new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = src; // 脚本加载并执行完成后resolve script.onload = () => { console.log(`脚本 ${src} 加载执行完成`); resolve(); }; // 加载失败时reject script.onerror = () => reject(new Error(`加载脚本 ${src} 失败`)); document.head.appendChild(script); }); } // 按顺序加载所有脚本 async function loadScriptsSequentially(scripts) { for (const script of scripts) { await loadScript(script); } console.log("所有脚本已按顺序加载完成"); } // 脚本列表 const scripts = ["script1.js", "script2.js", "script3.js", /* ... 其他脚本 */]; // 启动加载流程 loadScriptsSequentially(scripts).catch(err => console.error(err)); </script> </head> <body> <h1>Hello world</h1> </body> </html>
脚本改造(以script1.js为例):
无需手动触发事件,只需保证脚本核心逻辑执行完成即可(若有异步逻辑,需等待异步操作结束):
const foobar = async () => { return new Promise((resolve, reject) => { setTimeout(() => { resolve() }, 3000) setTimeout(() => { reject(new Error("未在规定时间内完成。")) }, 10000); }) } // 等待异步逻辑完成 await foobar(); console.log("script1.js 加载执行完成");
注意:如果脚本只有同步逻辑,直接保留原有代码即可,
script.onload会在同步代码执行完毕后触发;如果有需要等待的异步逻辑,需确保脚本内的异步操作完成后,才算真正执行完毕。
2. 动态import(现代浏览器支持)
如果项目环境支持ES模块,可以使用import()函数动态加载脚本——它本身返回Promise,结合async-await能轻松实现顺序加载,代码最简洁。
实现代码:
<html> <head> <title>用动态import顺序加载脚本</title> <script type="module"> async function loadScriptsSequentially(scripts) { for (const script of scripts) { try { await import(`./${script}`); console.log(`脚本 ${script} 加载执行完成`); } catch (err) { console.error(`加载脚本 ${script} 失败`, err); } } console.log("所有脚本已按顺序加载完成"); } const scripts = ["script1.js", "script2.js", "script3.js", /* ... 其他脚本 */]; loadScriptsSequentially(scripts); </script> </head> <body> <h1>Hello world</h1> </body> </html>
脚本改造(以script1.js为例):
改为ES模块,确保异步逻辑完成后脚本才算执行完毕:
export const init = async () => { return new Promise((resolve, reject) => { setTimeout(() => { resolve() }, 3000) setTimeout(() => { reject(new Error("未在规定时间内完成。")) }, 10000); }) } await init(); console.log("script1.js 加载执行完成");
方案对比
- 事件监听方式:兼容性好,但维护成本高,每个脚本需手动触发事件,适合老项目兼容场景。
- Promise+async-await:代码简洁易读,无需大量修改原有脚本逻辑,兼容性较好(支持ES6的浏览器均可运行)。
- 动态import:现代浏览器支持,代码最简洁,适合使用ES模块的新项目,天然支持异步加载与顺序执行。
内容的提问来源于stack exchange,提问作者Alexa Jenasoba
相关产品推荐
相关产品推荐

