在 Discuz! 论坛中使用JS最简洁的方式是ES导入,美中不足的是它无法与评分刷新机制完美适配,因此可以退而求其次采用回调函数加载JS资源,它解决了前者的痛点问题。
回调函数理论上可以加载多个JS资源,问题是容易陷入“厄运金字塔”困境,俗称“回调地狱”。加载一两个JS文档,回调函数是完全胜任的,多了令人头疼。仅从结构上看,考虑如下代码:
// 假设已经有了一个 loadJs() 函数
loadJs(f1) {
loadJs(f2) {
loadJs(f3) {
loadJs(f4) {
// ... 这里是业务核心代码
}
}
}
}
四个JS文件都需要全部加载完毕,业务核心代码才能出场,它处在躺平金字塔的最右侧抑或地狱的最底层。结构层面的代码无限缩进并非问题所在,致命的应该是业务逻辑问题:每一个被成功加载后可能都有各自的业务逻辑,业务代码需要分散穿插其中,分散且凌乱;其中的一个资源加载失败,局面就更不可控。
因此,Promise 对象应运而生,它会使得代码结构“扁平化”、代码逻辑清晰化。试看:
// 假设已经有了一个返回 promise 的 loadJs() 函数
loadJs(f1)
.then(()=> loadJs(f2))
.then(() => loadJs(f3))
.then(loadJs(f4))
.then(()=> {
// ... 这里是业务逻辑
});
每一个成功加载的JS如果有对应于它的业务逻辑,均可在 .then 后面加入代码块,如果没有就像上述代码那样顺延下去,直至最后一个资源加载完毕,业务核心代码最后展开。向下扩展的代码结构使得代码的结构自身“扁平化”、更易于阅读,代码逻辑也因此更明晰。
那么,如何使用 Promise 对象编写加载JS资源的函数呢?请看:
function loadJs(src) {
return new Promise(function(resolve, reject) {
let script = document.createElement('script');
script.src = src;
script.onload = () => resolve(script);
script.onerror = () => reject(new Error(`Script load error for ${src}`));
document.head.append(script);
});
}
Promise 是一个 ECMAScript 6 提供的类,目的是更加优雅地书写复杂的异步任务。
Promise 是 JavaScript 中用于处理异步操作的对象,它代表一个异步操作的最终完成(或失败)及其结果值。
Promise 有三种状态:
💠 pending:初始状态,既不是成功,也不是失败状态
💠 fulfilled:意味着操作成功完成
💠 rejected:意味着操作失败
简单来说,Promise 是一个“承诺”,表示将来某个时间点会返回一个结果:可能是成功的结果,也可能是失败的原因。必须会有返回,所以是“真诚承诺”。
可以像前面那样直接调用上述函数,也可以这样:
var promise = loadJs('./yourfile.js');
promise.then(
script => tzInit(), // JS文件加载后执行业务函数
err => console.log(`Error: ${err.message}`) // 加载失败返回错误信息
);
promise 是 Promise 对象的一个实例,来自于 loadJs 函数。Promise对象还允许使用 try/catch/finally 结构来实现更加复杂、严谨的业务逻辑。
【题外话】
Promise 其实是一个古老的概念,于1976年提出,它与未来有关(承诺都是针对未来的时间点,虽然未来已来)。JS于2015年在其推出的 ES6 中集成了此对象,它解决了回调可能出现的平躺厄运,也能以无限的链式调用制造新的then地狱,因此次年,它妈妈又生了一窝代号为 ES7 的孩纸,里面的小老弟 async/await 才是异步终极的解决方案。