在JavaScript中使用“全局”等待





可以改变我们编写



JavaScript方式的一项新功能是一种高度灵活且功能强大的语言,正在塑造着现代Web的发展。JavaScript之所以在Web开发中如此占主导地位的主要原因之一是它的快速开发和持续改进。



一种改进JavaScript的建议是称为“顶级等待”(顶级等待,“全局”等待)。该提议的目标是将ES模块变成类似异步功能的东西。这将允许模块获取即用型资源,并阻止模块导入它们。导入等待资源的模块仅在接收到资源并准备使用后才能运行代码执行。



该提案目前处于3个考虑阶段,因此该功能尚不能在生产中使用。但是,您可以确定在不久的将来肯定会实现它。



不用担心 继续阅读。我将向您展示如何立即使用命名功能。



正常等待有什么问题?



如果尝试在异步函数之外使用“ await”关键字,则会收到语法错误。为避免这种情况,开发人员使用立即调用函数表达式(IIFE)。



await Promise.resolve(console.log("️")); // 

(async () => {
    await Promise.resolve(console.log("️"))
})();


指定的问题及其解决方案只是冰山一角




使用ES6模块时,您倾向于处理许多实例的导出和导入值。让我们考虑一个例子:



// library.js
export const sqrt = Math.sqrt;
export const square = (x) => x * x;
export const diagonal = (x, y) => sqrt((square(x) + square(y)));

// middleware.js
import { square, diagonal } from "./library.js";

console.log("From Middleware");

let squareOutput;
let diagonalOutput;

const delay = (ms) => new Promise((resolve) => {
    const timer = setTimeout(() => {
        resolve(console.log("️"));
        clearTimeout(timer);
    }, ms);
});

// IIFE
(async () => {
    await delay(1000);
    squareOutput = square(13);
    diagonalOutput = diagonal(12, 5);
})();

export { squareOutput, diagonalOutput };


在上面的示例中,我们正在library.js和middleware.js之间导出和导入变量。您可以根据需要命名文件。



延迟函数返回一个在延迟后解析的承诺。由于此函数是异步的,因此我们在IIFE中使用“ await”关键字来“等待”它完成。在实际的应用程序中,将有一个访存调用或一些其他异步任务,而不是“延迟”函数。解决承诺后,我们将值分配给变量。这意味着在答应解决之前,我们的变量将是不确定的。



在代码末尾,我们导出变量,以便可以在其他代码中使用它们。



让我们看一下导入和使用这些变量的代码:



// main.js
import { squareOutput, diagonalOutput } from "./middleware.js";

console.log(squareOutput); // undefined
console.log(diagonalOutput); // undefined
console.log("From Main");

const timer1 = setTimeout(() => {
    console.log(squareOutput);
    clearTimeout(timer1);
}, 2000); // 169

const timer2 = setTimeout(() => {
    console.log(diagonalOutput);
    clearTimeout(timer2);
}, 2000); // 13


如果运行此代码,则在前两种情况下将无法定义,在第三种情况和第四种情况下分别为169和13。为什么会发生?



这是因为我们试图在执行异步函数之前获取main.js中从middleware.js导出的变量的值。您还记得我们有一个有待解决的承诺吗?



为了解决这个问题,我们需要以某种方式通知导入模块变量已准备就绪。



解决方法


至少有两种方法可以解决此问题。



1.导出承诺进行初始化


首先,可以将IIFE导出。async关键字使方法异步,这样的方法总是返回promise。因此,在下面的示例中,异步IIFE返回了promise。



// middleware.js
import { square, diagonal } from "./library.js";

console.log("From Middleware");

let squareOutput;
let diagonalOutput;

const delay = (ms) => new Promise((resolve) => {
    const timer = setTimeout(() => {
        resolve(console.log("️"));
        clearTimeout(timer);
    }, ms);
});

//   ,   , 
export default (async () => {
    await delay(1000);
    squareOutput = square(13);
    diagonalOutput = diagonal(12, 5);
})();

export { squareOutput, diagonalOutput };


在main.js中访问导出的变量时,您可以等待IIFE执行。



// main.js
import promise, { squareOutput, diagonalOutput } from "./middleware.js";

promise.then(() => {
    console.log(squareOutput); // 169
    console.log(diagonalOutput); // 169
    console.log("From Main");
});

const timer1 = setTimeout(() => {
    console.log(squareOutput);
    clearTimeout(timer1);
}, 2000); // 169

const timer2 = setTimeout(() => {
    console.log(diagonalOutput);
    clearTimeout(timer2);
}, 2000); // 13


尽管该代码段已解决了问题,但它还会导致其他问题。



  • 使用指定的模板时,您必须寻找所需的承诺
  • 如果另一个模块也使用变量“ squareOutput”和“ diagonalOutput”,则必须确保IIFE被重新导出


还有另一种方式。



2.使用导出的变量解析IIFE Promise


在这种情况下,我们不是从变量中单独导出,而是从异步IIFE返回它们。这使“ main.js”文件可以简单地等待promise解析并检索其值。



// middleware.js
import { square, diagonal } from "./library.js";

console.log("From Middleware");

let squareOutput;
let diagonalOutput;

const delay = (ms) => new Promise((resolve) => {
    const timer = setTimeout(() => {
        resolve(console.log("️"));
        clearTimeout(timer);
    }, ms);
});

//  
export default (async () => {
    await delay(1000);
    squareOutput = square(13);
    diagonalOutput = diagonal(12, 5);
    return { squareOutput, diagonalOutput };
})();

// main.js
import promise from "./middleware.js";

promise.then(({ squareOutput, diagonalOutput }) => {
    console.log(squareOutput); // 169
    console.log(diagonalOutput); // 169
    console.log("From Main");
});

const timer1 = setTimeout(() => {
    console.log(squareOutput);
    clearTimeout(timer1);
}, 2000); // 169

const timer2 = setTimeout(() => {
    console.log(diagonalOutput);
    clearTimeout(timer2);
}, 2000); // 13


但是,该解决方案也有一些缺点。



根据建议,“此模式有一个严重的缺点,因为它需要将相关的资源源大量重构为更多的动态模板,并将大多数模块主体放在.then()回调中以允许使用动态模块。与ES2015模块相比,这在静态分析能力,可测试性,人体工程学以及其他方面均表现出显着的下降。”



“全局”如何解决这个问题?



顶层等待使模块化系统能够处理承诺以及它们如何相互交互。



// middleware.js
import { square, diagonal } from "./library.js";

console.log("From Middleware");

let squareOutput;
let diagonalOutput;

const delay = (ms) => new Promise((resolve) => {
    const timer = setTimeout(() => {
        resolve(console.log("️"));
        clearTimeout(timer);
    }, ms);
});

// "" await
await delay(1000);
squareOutput = square(13);
diagonalOutput = diagonal(12, 5);

export { squareOutput, diagonalOutput };

// main.js
import { squareOutput, diagonalOutput } from "./middleware.js";

console.log(squareOutput); // 169
console.log(diagonalOutput); // 13
console.log("From Main");

const timer1 = setTimeout(() => {
    console.log(squareOutput);
    clearTimeout(timer1);
}, 2000); // 169

const timer2 = setTimeout(() => {
    console.log(diagonalOutput);
    clearTimeout(timer2);
}, 2000); // 13


直到Middleware.js中的承诺解决之前,main.js中的所有语句都不会执行。这是比变通办法更干净的解决方案。



那个笔记


全局等待仅适用于ES模块。必须明确指定使用的依赖项。提案存储库中的以下示例很好地说明了这一点。



// x.mjs
console.log("X1");
await new Promise(r => setTimeout(r, 1000));
console.log("X2");
// y.mjs
console.log("Y");
// z.mjs
import "./x.mjs";
import "./y.mjs";
// X1
// Y
// X2


如您所料,此代码段不会在控制台上显示X1,X2,Y,因为x和y是独立的模块,彼此无关。



我强烈建议您研究建议书的“常见问题”部分,以更好地了解所涉及的功能。



实作



V8


您可以立即测试此功能。



为此,请转到计算机上Chrome所在的目录。确保关闭所有浏览器选项卡。打开一个终端并输入以下命令:



chrome.exe --js-flags="--harmony-top-level-await"


您也可以在Node.js中尝试此功能。阅读本指南以了解更多信息。



ES模块


确保将“ type”属性添加到值为“ module”的“ script”标签中。



<script type="module" src="./index.js"></script>


请注意,与常规脚本不同,ES6模块遵循共享原点(单源)(SOP)和资源共享(CORS)策略。因此,最好在服务器上使用它们。



用例



根据建议,等待“全局”的用例如下:



动态依赖路径


const strings = await import(`/i18n/${navigator.language}`);


这允许模块使用运行时值来计算依赖关系路径,并且对于将开发/生产代码,国际化,基于运行时的代码拆分(浏览器,Node.js)等有用。



初始化资源


const connection = await dbConnector()


这有助于模块获取立即可用的资源,并在无法使用模块时引发异常。可以将这种方法用作安全网,如下所示。



后备选项


下面的示例显示了如何使用“全局”等待来通过回退实现加载依赖项。如果从CDN A的导入失败,则执行从CDN B的导入:



let jQuery;
try {
  jQuery = await import('https://cdn-a.example.com/jQuery');
} catch {
  jQuery = await import('https://cdn-b.example.com/jQuery');
}


批评



里奇·哈里斯(Rich Harris)整理了一份顶级等待的批评清单它包括以下内容:



  • 等待“全局”会阻止代码执行
  • 等待“全球”可能会阻止资源获取
  • 缺乏对CommonJS模块的支持


这些评论的答案在常见问题解答提案中给出:



  • 由于子节点(模块)具有执行能力,因此最终没有代码阻塞
  • 在模块图的执行阶段使用“全局”等待。在此阶段,所有资源都已接收并链接,因此没有阻塞资源获取的风险
  • 顶级等待仅限于ES6模块。最初并未计划对CommonJS模块(如常规脚本)的支持


同样,我强烈建议您阅读提案常见问题解答



我希望我能够以一种易于理解的方式解释所提建议的实质。您要利用这个机会吗?在评论中分享您的意见。



All Articles