事件循环及异步编程的出现和 5 种更好的 async/await 编程方式

【Promise 和 async/await 的区别是什么?】 Promise 和 async/await 是 JavaScript 中处理异步操作的两种主要方式,各有优缺点。理解它们的区别和适用场景,可以帮助开发者编写出高效、可维护的异步代码,提升应用的性能和用户体验。 阅读详情

原文请查阅这里,本文采用知识共享署名 3.0 中国大陆许可协议共享,BY Troland。

这是 JavaScript 工作原理的第四章。

现在,我们将会通过回顾单线程环境下编程的弊端及如何克服这些困难以创建令人惊叹的 JavaScript 交互界面来展开第一篇文章。老规矩,我们将会在本章末尾分享 5 条利用 async/await 编写更简洁代码的小技巧。

单线程的局限性

在第一篇文章开头,我们考虑了一个问题即当调用栈中含有需要长时间运行的函数调用的时候会发生什么。

譬如,试想下,在浏览器中运行着一个复杂的图片转化算法。

恰好此时调用栈中有函数需要执行,此时浏览器将会被阻塞,它不能够做其它任何事情。这意味着,浏览器会没有响应,不能够进行渲染和运行其它代码。这将会带来问题-你的程序界面将不再高效和令人愉悦。

程序没有响应。

在某些情况下,这或许没什么大不了的。但是,这可能会造成更加严重的问题。一旦浏览器在调用栈中同时运行太多的任务的时候,浏览器会很长时间停止响应。到了那个时候,大多数浏览器会抛出一个错误,询问你是否关闭网页。

这很丑陋且它完全摧毁了程序的用户体验。

JavaScript 程序组件

你可能会在单一的 .js 文件中书写你的 JavaScript 程序,但是你的程序是由多个代码块组成的,当前只有一个代码块在运行,其它代码块将在随后运行。最常见的块状单元是函数。

大多数 JavaScript 菜鸟有可能需要理解的问题即之后运行表示的是并不是必须严格且立即在现在之后执行。换句话说即,根据定义,现在不能够运行完毕的任务将会异步完成,这样你就不会不经意间遇到以上提及的 UI 阻塞行为。

看下如下代码:

// ajax 为一个库提供的任意 ajax 函数
var response = ajax('https://example.com/api');

console.log(response);
// `response` 将不会有数据返回

可能你已经知道标准的 ajax 请求不会完全同步执行完毕,意即在代码运行阶段,ajax(..) 函数不会返回任何值给 response 变量。

获得异步函数返回值的一个简单方法是使用回调函数。

ajax('https://example.com/api', function(response) {
    console.log(response); // `response` 现在有值
});

只是要注意一点:即使可以也永远不要发起同步 ajax 请求。如果发起同步 ajax 请求,JavaScript 程序的 UI 将会被阻塞-用户不能够点击,输入数据,跳转或者滚动。这将会冻结任何用户交互体验。这是非常糟糕。

以下示例代码,但请别这样做,这会毁掉网页:

// 假设你使用 jQuery
jQuery.ajax({
    url: 'https://api.example.com/endpoint',
    success: function(response) {
        // 成功回调.
    },
    async: false // 同步
});

我们以 Ajax 请求为例。你可以异步执行任意代码。

你可以使用 setTimeout(callback, milliseconds) 函数来异步执行代码。setTimeout 函数会在之后的某个时刻触发事件(定时器)。如下代码:

function first() {
    console.log('first');
}
function second() {
    console.log('second');
}
function third() {
    console.log('third');
}
first();
setTimeout(second, 1000); // 1 秒后调用 second 函数
third();

控制台输出如下:

first
third
second

事件循环详解

我们将会以一个有些让人费解的问题开始-尽管允许异步执行 JavaScript 代码(比如之前讨论的 setTimetout),但是直到 ES6,实际上 JavaScript 本身并没有集成任何直接的异步编程概念。JavaScript 引擎只允许在任意时刻执行单个的程序片段。

可以查看之前的文章来了解 JavaScript 引擎的工作原理。

那么, JS 引擎是如何执行程序片段的呢?实际上,JS 引擎并不是隔离运行的-它运行在一个宿主环境中,对大多数开发者来说是典型的 web 浏览器或者 Node.js。实际上,现在 JavaScript 广泛应用于从机器到电灯泡的各种设备之中。每个设备代表了 JS 引擎的不同类型的宿主环境。

所有宿主环境都含有一个被称为事件循环的内置机制,随着时间的推移,事件循环会执行程序中多个代码片段,每次都会调用 JS 引擎。

这意味着 JS 引擎只是任意 JS 代码的按需执行环境。这是一个封闭的环境,在其中进行事件的调度(运行JS 代码)。

所以,打个比方,当 JavaScript 程序发起 Ajax 请求来从服务器获得数据,你在回调函数中书写 "response" 代码,JS 引擎会告诉宿主环境:

"嘿,我现在要挂起执行了,现在当你完成网络请求的时候且返回了数据,请执行回调函数。"

之后浏览器会监听从网络中返回的数据,当有数据返回的时候,它会通过把回调函数插入事件循环以便调度执行。

让我们看下如下图示:

你可以在之前的文章中阅读更多关于动态内存管理和调用栈的信息。

什么是网页 API ?本质上,你没有权限访问这些线程,你只能够调用它们。它们是浏览器自带的,且可以在浏览器中进行并发操作。如果你是个 Node.js 开发者,这些是 C++ APIs。

说了那么多,事件循环到底是啥?

事件循环只有一项简单的工作-监测调用栈和回调队列。如果调用栈是空的,它会从回调队列中取得第一个事件然后入栈,并有效地执行该事件。

事件循环中的这样一次遍历被称为一个 tick。每个事件就是一个回调函数。

console.log('Hi');
setTimeout(function cb1() { 
    console.log('cb1');
}, 5000);
console.log('Bye');

让我们执行这段代码,然后看看会发生什么:

1.空状态。浏览器控制台是空的,调用栈也是空的。

2.console.log('Hi') 入栈。

3.执行 console.log('Hi')。

4.console.log('Hi') 出栈

  1. setTimeout(function cb1() { ... }) 入栈。

6.执行 setTimeout(function cb1() { ... }),浏览器创建定时器作为网页 API 的一部分并将会为你处理倒计时。

7.setTimeout(function cb1() { ... }) 执行完毕并出栈。

8.console.log('Bye') 入栈。

9.执行 console.log('Bye')。

10.console.log('Bye') 出栈。

11.至少 5 秒之后,定时器结束运行并把 cb1 回调添加到回调队列。

12.事件循环从回调队列中获得 cb1 函数并且将其入栈。

13.运行 cb1 函数并将 console.log('cb1') 入栈。

14.执行 console.log('cb1')。

15.console.log('cb1') 出栈。

16.cb1 出栈

录像快速回放:

令人感兴趣的是,ES6 规定事件循环如何工作的,这意味着从技术上讲,它在 JS 引擎负责的范围之内,而 JS 引擎将不再只是扮演着宿主环境的角色。ES6 中 Promise 的出现是导致改变的主要原因之一,因为 ES6 要求有权限直接细粒度地控制事件循环队列中的调度操作(之后会深入探讨)。

setTimeout(…) 工作原理

需要注意的是 setTimeout(…) 并没有自动把你的回调添加到事件循环队列。它创建了一个定时器。当定时器过期,宿主环境会把回调函数添加至事件循环队列中,然后,在未来的某个 tick 取出并执行该事件。查看如下代码:

setTimeout(myCallback, 1000);

这并不意味着 1 秒之后会执行 myCallback 回调而是在 1 秒后将其添加到回调队列。然而,该队列有可能在之前就添加了其它的事件-所以回调就会被阻塞。

有相当一部分的文章和教程开始会建议你使用 setTimeout(callback, 0) 来书写 JavaScript 异步代码。那么,现在你明白了事件循环和 setTimeout 的原理:调用 setTimeout 把其第二个参数设置为 0 表示延迟执行回调直到调用栈被清空。

查看如下代码:

console.log('Hi');
setTimeout(function() {
    console.log('callback');
}, 0);
console.log('Bye');

虽然定时时间设定为 0, 但是控制台中的结果将会如下显示:

Hi
Bye
callback

ES6 作业概念

ES6 介绍了一个被称为『作业队列』的概念。它位于事件循环队列的顶部。你极有可能在处理 Promises(之后会介绍) 的异步行为的时候无意间接触到这一概念。

现在我们将会接触这个概念,以便当讨论 Promises 的异步行为之后,理解如何调度和处理这些行为。

像这样想象一下:作业队列是附加于事件循环队列中每个 tick 末尾的队列。事件循环的一个 tick 所产生的某些异步操作不会导致添加全新的事件到事件循环队列中,但是反而会在当前 tick 的作业队列末尾添加一个作业项。

这意味着,你可以添加延时运行其它功能并且你可以确保它会在其它任何功能之前立刻执行。

一个作业也可以在同一队列末尾添加更多的作业。理论上讲,存在着作业循环的可能性(比如作业不停地添加其它作业)。

为了无限循环,就会饥饿程序所需要的资源直到下一个事件循环 tick。从概念上讲,这类似于在代码里面书写耗时或者死循环(类似 while(true))。

作业是有些类似于 setTimeout(callback, 0) 小技巧,但是是以这样的方式实现的,它们拥有明确定义和有保证的执行顺序:之后且尽快地执行。

回调

正如你已知的那样,回调函数是 JavaScript 程序中用来表示和进行异步操作的最常见方法。的确,回调是 JavaScript 语言中最为重要的异步模式。无数的 JS 程序,甚至非常复杂的那些,都是建立在回调函数之上的。

回调并不是没有缺点。许多开发者试图找到更好的异步模式。然而,如果你不理解底层的原理而想要高效地使用任何抽象化的语法这是不可能的。

在接下来的章节中,我们将会深入探究这些抽象语法并理解更复杂的异步模式的必要性。

嵌套回调

查看以下示例:

listen('click', function (e){
    setTimeout(function(){
        ajax('https://api.example.com/endpoint', function (text){
            if (text == "hello") {
	        doSomething();
	    }
	    else if (text == "world") {
	        doSomethingElse();
            }
        });
    }, 500);
});

我们有三个链式嵌套函数,每个函数代表一个异步操作。

这类代码通常被称为『回调地狱』。但是,实际上『回调地狱』和代码嵌套及缩进没有任何关系。这是一个更加深刻的问题。

首先,我们监听点击事件,然后,等待定时器执行,最后等待 Ajax 返回数据,在 Ajax 返回数据的时候,可以重复执行这一过程。

乍一眼看上去,可以上把以上具有异步特性的代码拆分为按步骤执行的代码,如下所示:

listen('click', function (e) {
	// ..
});

之后:

setTimeout(function(){
    // ..
}, 500);

再后来:

ajax('https://api.example.com/endpoint', function (text){
    // ..
});

最后:

if (text == "hello") {
    doSomething();
}
else if (text == "world") {
    doSomethingElse();
}

因此,以这样顺序执行的方式来表示你的异步代码看起来一气呵成,应该有这样的方法吧?

Promises

查看如下代码:

var x = 1;
var y = 2;
console.log(x + y);

这个很直观:计算出 x 和 y 的值然后在控制台打印出来。但是,如果 x 或者 y 的初始值是不存在的且不确定的呢?假设,在表达式中使用 x 和 y 之前,我们需要从服务器得到 x 和 y 的值。想象下,我们拥有函数 loadX 和 loadY 分别从服务器获取 x 和 y 的值。然后,一旦获得 x 和 y 的值,就可以使用 sum 函数计算出和值。

类似如下这样:

function sum(getX, getY, callback) {
    var x, y;
    getX(function(result) {
        x = result;
        if (y !== undefined) {
            callback(x + y);
        }
    });
    getY(function(result) {
        y = result;
        if (x !== undefined) {
            callback(x + y);
        }
    });
}
// 同步或异步获取 `x` 值的函数
function fetchX() {
    // ..
}


// 同步或异步获取 `y` 值的函数
function fetchY() {
    // ..
}

sum(fetchX, fetchY, function(result) {
    console.log(result);
});

这里需要记住的一点是-在代码片段中,x 和 y 是未来值,我们用 sum(..)(从外部)来计算和值,但是并没有关注 x 和 y是否马上同时有值。

当然喽,这个粗糙的基于回调的技术还有很多需要改进的地方。这只是理解推出未来值而不用担心何时有返回值的好处的一小步。

Promise 值

让我们简略地看一下如何用 Promises 来表示 x+y :

function sum(xPromise, yPromise) {
	// `Promise.all([ .. ])` 包含一组 Promise,
	// 并返回一个新的 Promise 来等待所有 Promise 执行完毕
	return Promise.all([xPromise, yPromise])

	// 当新 Promise 解析完毕,就可以同时获得 `x` 和 `y` 的值并相加。
	.then(function(values){
		// `values` 是之前解析 promises 返回的消息数组
		return values[0] + values[1];
	} );
}

// `fetchX()` and `fetchY()` 返回 promise 来取得各自的返回值,这些值返回是无时序的。
sum(fetchX(), fetchY())

// 获得一个计算两个数和值的 promise,现在,就可以链式调用 `then(...)` 来处理返回的 promise。
.then(function(sum){
    console.log(sum);
});

以上代码片段含有两种层次的 Promise。

fetchX() 和 fetchY() 都是直接调用,它们的返回值(promises!)都被传入 sum(…) 作为参数。虽然这些 promises 的 返回值也许会在现在或之后返回,但是无论如何每个 promise 都具有相同的异步行为。我们可以的推算 x 和 y 是与时间无关的值。暂时称他们为未来值。

第二层次的 promise 是由 sum(…) (通过 Promise.all([ ... ]))所创建和返回的,然后通过调用 then(…) 来等待 promise 的返回值。当 sum(…) 运行结束,返回 sum 未来值然后就可以打印出来。我们在 sum(…) 内部隐藏了等待未来值 x 和 y 的逻辑。

**注意:**在 sum(…) 内部,Promise.all([ … ])创建了一个 promise(在等待 promiseX 和 promiseY 解析之后)。链式调用 .then(…) 创建了另一个 promise,该 promise 会由代码 values[0] + values[1] 立刻进行解析(返回相加结果)。因此,在代码片段的末尾即 sum(…) 的末尾链式调用 then(…)-实际上是在操作第二个返回的 promise 而不是第一个由 Promise.all([ ... ]) 创建返回的 promise。同样地,虽然我们没有在第二个then(…) 之后进行链式调用,但是它也创建了另一个 promise,我们可以选择观察/使用该 promise。我们将会在本章的随后内容中进行详细地探讨 promise 的链式调用相关。

在 Promises 中,实际上 then(…) 函数可以传入两个函数作为参数,第一个函数是成功函数,第二个是失败函数。

sum(fetchX(), fetchY())
.then(
    // 成功句柄
    function(sum) {
        console.log( sum );
    },
    // 拒绝句柄
    function(err) {
    	console.error( err ); // bummer!
    }
);

当获取 x 或者 y 出现错误或者计算和值的时候出现错误,sum(…) 返回的 promise 将会失败,传入 then(…) 作为第二个参数的回调错误处理程序将会接收 promise 的返回值。

因为 Promise 封装了时间相关的状态-等待外部的成功或者失败的返回值,Promise 本身是与时间无关的,这样就能够以可预测的方式组成(合并) Promise 而不用关心时序或者返回结果。

除此之外,一旦 Promise 解析完成,它就会一直保持不可变的状态且可以被随意观察。

链式调用 promise 真的很管用:

function delay(time) {
    return new Promise(function(resolve, reject){
        setTimeout(resolve, time);
    });
}

delay(1000)
.then(function(){
    console.log("after 1000ms");
    return delay(2000);
})
.then(function(){
    console.log("after another 2000ms");
})
.then(function(){
    console.log("step 4 (next Job)");
    return delay(5000);
})
// ...

调用 delay(2000) 创建一个将在 2 秒后返回成功的 promise,然后,从第一个 then(…) 的成功回调函数中返回该 promise,这会导致第二个 then(…) 返回的 promise 等待 2 秒后返回成功的 promise。

**Note:**因为一个 promise 一旦解析其状态就不可以从外部改变,由于它的状态不可以被随意修改,所以可以安全地把状态值随意分发给任意第三方。当涉及多方观察 Promise 的返回结果时候更是如此。一方影响另一方观察 Promise 返回结果的能力是不可能。不可变性听起来像是个晦涩的科学课题,但是,实际上这是 Promise 最根本和重要的方面,你得好好研究研究。

Promise 使用时机

Promise 的一个重要细节即确定某些值是否是真正的 Promise。换句话说,这个值是否具有 Promise 的行为。

我们知道可以利用 new Promise(…) 语法来创建 Promise,然后,你会认为使用 p instanceof Promise 来检测某个对象是否是 Promise 类的实例。然而,并不全然如此。

主要的原因在于你可以从另一个浏览器窗口(比如 iframe)获得 Promise 实例,iframe 中的 Promise 不同于当前浏览器窗口或框架中的 Promise,因此,会导致检测 Promise 实例失败。

除此之外,库或框架或许会选择使用自身自带的 Promise 而不是原生的 ES6 实现的 Promise。实际工作中,你可以使用库自带的 Promise 来兼容不支持 Promise 的老版本浏览器。

异常捕获

如果在创建 Promise 或者是在观察解析 Promise 返回结果的任意时刻,遇到了诸如 TypeError 或者 ReferenceError 的 JavaScript 错误异常,这个异常会被捕获进而强制 Promise 为失败状态。

比如:

var p = new Promise(function(resolve, reject){
    foo.bar();	  // `foo` 未定义,产生错误!
    resolve(374); // 永不执行 :(
});

p.then(
    function fulfilled(){
        // 永不执行 :(
    },
    function rejected(err){
        // `err` 会是一个 `TypeError` 异常对象
	     // 由于 `foo.bar()` 代码行.
    }
);

但是,如果 Promise 成功解析了而在成功解析的监听函数(then(…) 注册回调)中抛出 JS 运行错误会怎么样?仍然可以捕捉到该异常,但或许你会发现处理这些异常的方式有些让人奇怪。直到深入理解其中原理:

var p = new Promise( function(resolve,reject){
	resolve(374);
});

p.then(function fulfilled(message){
    foo.bar();
    console.log(message);   // 永不执行
},
    function rejected(err){
        // 永不执行
    }
);

看起来 foo.bar() 抛出的错误异常真的被捕获到了。然而,事实上并没有。然而,深入理解你会发现我们没有监测到其中一些错误。p.then(…) 调用本身返回另一个 promise,该 promise 会返回 TypeError 类型的异常失败信息。

拓展一下以上的说明,这是原文没有的。

var p = new Promise( function(resolve,reject){
	resolve(374);
});

p.then(function fulfilled(message){
    foo.bar();
    console.log(message);   // 永不执行
},
    function rejected(err){
        // 永不执行
    }
).then(
function() {},
function(err) { console.log('err', err);}
);

如上代码所示就可以真正捕获到 promise 成功解析回调函数里面的代码错误。

处理未捕获的异常

有其它许多据说更好的处理异常的技巧。

普遍的做法是为 Promises 添加 done(..) 回调,本质上这会标记 promise 链的状态为 "done."。done(…) 并不会创建和返回 promise,因此,当不存在链式 promise 的时候,传入 done(..) 的回调显然并不会抛出错误。

和未捕获的错误状况一样:任何在 done(..) 失败处理函数中的异常都将会被抛出为全局错误(基本上是在开发者控制台)。

var p = Promise.resolve(374);

p.then(function fulfilled(msg){
    // 数字没有字符类的函数,所以会报错
    console.log(msg.toLowerCase());
})
.done(null, function() {
    // 若发生错误,将会抛出全局错误
});

ES8 中的 Async/await

JavaScript ES8 中介绍了 async/await,这使得处理 Promises 更加地容易。我们将会简要介绍 async/await 的所有可能姿势并利用其来书写异步代码。

那么,让我们瞧瞧 async/await 工作原理。

使用 async 函数定义一个异步函数。该函数会返回异步函数对象。AsyncFunction 对象表示在异步函数中运行其内部代码。

当调用异步函数的时候,它会返回一个 Promise。异步函数返回值并非 Promise,在函数过程中会自动创建一个 Promise 并使用函数的返回值来解析该 Promise。当 async 函数抛出异常,Promise 失败回调会获取抛出的异常值。

async 函数可以包含一个 await 表达式,这样就可以暂停函数的执行来等待传入的 Promise 的返回结果,之后重启异步函数的执行并返回解析值。

你可以把 JavaScript 中的 Promise 看作 Java 中的 Future 或 C# 中的 Task。

async/await 本意是用来简化 promises 的使用。

看下如下代码:

// 标准 JavaScript 函数
function getNumber1() {
    return Promise.resolve('374');
}
// 和 getNumber1 一样
async function getNumber2() {
    return 374;
}

类似地,抛出异常的函数等价于返回失败的 promises。

function f1() {
    return Promise.reject('Some error');
}
async function f2() {
    throw 'Some error';
}

await 关键字只能在 async 函数中使用并且允许你同步等待 Promise。如果在 async 函数外使用 promises,我们仍然必须使用 then 回调。

async function loadData() {
    // `rp` 是个发起 promise 的函数。
    var promise1 = rp('https://api.example.com/endpoint1');
    var promise2 = rp('https://api.example.com/endpoint2');
   
    // 现在,并发请求两个 promise,现在我们必须等待它们结束运行。
    var response1 = await promise1;
    var response2 = await promise2;
    return response1 + ' ' + response2;
}
// 因为不再使用 `async function`,所以必须使用 `then`。
loadData().then(() => console.log('Done'));

你也可以使用异步函数表达式来定义异步函数。异步函数表达式拥有和异步函数语句相近的语法。异步函数表达式和异步函数语句的主要区别在于函数名,异步函数表达式可以忽略函数名来创建匿名函数。异步函数表达式可以被用作 IIFE(立即执行函数表达式),可以在定义的时候立即运行。

像这样:

var loadData = async function() {
    // `rp` 是个发起 promise 的函数。
    var promise1 = rp('https://api.example.com/endpoint1');
    var promise2 = rp('https://api.example.com/endpoint2');
   
    // 现在,并发请求两个 promise,现在我们必须等待它们结束运行。
    var response1 = await promise1;
    var response2 = await promise2;
    return response1 + ' ' + response2;
}

更为重要的是,所有的主流浏览器都支持 async/await。

如果该兼容性不符合你的需求,你可以使用诸如 Babel 和 TypeScript 的 JS 转译器来转换为自己需要的兼容程度。

最后要说的是,不要盲目地使用最新的技术来写异步代码。理解 JavaScript 中 async 的内部原理是非常重要的,学习为什么深入理解所选择的方法是很重要的。正如编程中的其它东西一样,每种技术都有其优缺点。

书写高可用,强壮的异步代码的 5 条小技巧

1.简洁:使用 async/await 可以让你写更少的代码。每次书写 async/await 代码,你都可以跳过书写一些不必要的步骤: 比如不用写 .then 回调,创建匿名函数来处理返回值,命名回调返回值。

// `rp` 是个发起 promise 的工具函数。
rp(‘https://api.example.com/endpoint1').then(function(data) {
 // …
});

对比:

// `rp` 是个发起 promise 的工具函数
var response = await rp(‘https://api.example.com/endpoint1');

2.错误处理:Async/await 允许使用日常的 try/catch 代码结构体来处理同步和异步错误。看下和 Promise 是如何写的:

function loadData() {
    try { // 捕获同步错误.
        getJSON().then(function(response) {
            var parsed = JSON.parse(response);
            console.log(parsed);
        }).catch(function(e) { // 捕获异步错误.
            console.log(e); 
        });
    } catch(e) {
        console.log(e);
    }
}

对比:

async function loadData() {
    try {
        var data = JSON.parse(await getJSON());
        console.log(data);
    } catch(e) {
        console.log(e);
    }
}

3.条件语句:使用 async/await 来书写条件语句会更加直观。

function loadData() {
  return getJSON()
    .then(function(response) {
      if (response.needsAnotherRequest) {
        return makeAnotherRequest(response)
          .then(function(anotherResponse) {
            console.log(anotherResponse)
            return anotherResponse
          })
      } else {
        console.log(response)
        return response
      }
    })
}

对比:

async function loadData() {
  var response = await getJSON();
  if (response.needsAnotherRequest) {
    var anotherResponse = await makeAnotherRequest(response);
    console.log(anotherResponse)
    return anotherResponse
  } else {
    console.log(response);
    return response;    
  }
}

4.堆栈桢:和 async/await 不同的是,从链式 promise 返回的错误堆栈中无法得知发生错误的地方。看如下代码:

function loadData() {
  return callAPromise()
    .then(callback1)
    .then(callback2)
    .then(callback3)
    .then(() => {
      throw new Error("boom");
    })
}
loadData()
  .catch(function(e) {
    console.log(err);

// Error: boom at callAPromise.then.then.then.then (index.js:8:13)
});

对比:

async function loadData() {
  await callAPromise1()
  await callAPromise2()
  await callAPromise3()
  await callAPromise4()
  await callAPromise5()
  throw new Error("boom");
}
loadData()
  .catch(function(e) {
    console.log(err);
    // output
    // Error: boom at loadData (index.js:7:9)
});

5.调试:如果使用 promise,你就会明白调试它们是一场噩梦。例如,如果你在 .then 代码块中设置一个断点并且使用诸如 "stop-over" 的调试快捷键,调试器不会移动到下一个 .then 代码块,因为调试器只会步进同步代码。

使用 async/await 你可以就像同步代码那样步进到下一个 await 调用。

不仅是程序本身还有库,书写异步 JavaScript 代码都是相当重要的。

参考资源:

英飞凌功率MOSFET SPICE 瞬态仿真不收敛问题解决(基于TINA) 英飞凌功率MOSFET SPICE仿真容易出现不收敛问题,这个对于IGBT和SIC的管子应该是一样的,在瞬态仿真分析中,出现如下图所示的报错,Convergence problem。spice是为线性电路设计的,例如运算放大器、线性电源等,不是专门仿真开关电路的。在瞬态仿真时,仿真器是要不断迭代求解的,开关快速的跳变,容易在迭代式出现不收敛的问题。,由于时间步长减少,会消耗更多的计算机内存,如果运行多次内存消耗过大,会报“Out of memory”.解决方法是:报错工程,重启软件,内存重新计算。 阅读详情

相关推荐

mysql数据目录迁移后表不存在_mysql 直接从date 文件夹备份表,还原数据库之后提示 table doesn`t exist的原因和解决方法...

补充:正常情况下,建议数据库备份最好用工具进行备份,通过拷贝数据库表进行数据迁移,不同的环境会出现各种不同的意外问题。背景:今天在整理一个网站的时候,操作系统由于系统自动更新导致一直出现系统蓝屏死机,唉,悲剧了,于是重新安装了系统 windows server 2008 enterprise 32bit。详情:系统安装完成后,重新配置之前那个网站,appache服务配好之后,再配置数据的时候是这样...

weixin_39628271的博客 2673

解决Cannot read property ‘tapAsync‘ of undefined 问题过程

前两天从公司git 上clone了一个前端项目采用Nodejs + React 写的,想本地跑起来试试看,自己倒腾倒腾,结果在尝试的过程中遇到各种问题,真是一路上过关斩将终于跑起来了,其中有个典型的问题,因为是初次接触这个问题困扰了我几天,具体错误如下: Module build failed (from ./node_modules/_mini-css-extract-plugin@0.7.0@mini-css-extract-plugin/dist/loader.js): TypeEr...

u014520006的博客 1万+

告别Mac自带终端:iTerm2 + Oh My Zsh 保姆级配置指南(含国内镜像源)

本文提供了一份详细的Mac终端优化指南,涵盖iTerm2、Oh My Zsh和Homebrew的配置流程,特别针对国内用户提供镜像源解决方案。通过安装现代化工具链和优化配置,帮助开发者打造高效、美观的终端工作环境,提升日常开发效率。

weixin_42638178的博客 397

AJAX 04 回调函数地狱和 Promise 链式调用、async 和 await、事件循环

Ajax 同步代码和异步代码、回调函数地狱和 Promise 链式调用、async 和 await、事件循环-EventLoop、Promise.all 静态方法

yavlgloss的博客 1746

(十一)随机性与独立性回顾

随机性与独立性回顾

旺旺的碎冰冰~的博客 1017

STM32入门教程:图像处理

然后,我们使用 STM32 的 DSP 模块中的 arm_mean_u8 函数计算原始图像的平均值,并将结果存储在 meanValue 变量中。然后,我们需要编写代码配置 STM32 的外部中断输入引脚,并在中断处理函数中读取 OV7670 摄像头模块输出的图像数据。然后,我们需要编写代码配置 STM32 的 USB OTG 接口,并在回调函数中读取 USB 摄像头输出的图像数据。一、图像采集 图像采集是图像处理的第一步,我们需要将外部图像源的数据导入到 STM32 中进行处理。

2401_85625565的博客 1616

第一TypeError: Cannot read property ‘tapAsync‘ of undefined第二TypeError: compilation. templatesPlugin

出现场景:从远程仓库拉取后出现报错首先是compilation. templatesPlugin is not a function,其次是出现Cannot read property ‘tapAsync‘ of undefined;接下来会出现 TypeError: Cannot read property ‘tapAsync‘ of undefined。出现报错具体原因没有搞明白,但是这两条命令可以解决你的报错。

m0_67021283的博客 1292

Parasoft C++Test软件单元测试_操作指南

本文主要为使用C++TEST进行单元测试的流程性操作讲解,后续文章将以专题形式进行详细讲解,以及用一些被测函数作为示例进行实例讲解。

文章主要涉及嵌入式软件开发、测试、项目管理等,欢迎交流,共同进步。 1574

await-async-事件循环

一文带你了解js异步代码执行运行机制。学习事件循环,宏任务、微任务等知识。

weixin_51629637的博客 879

【GEOS-Chem模拟教程第二期上】生物质燃烧排放 GFED4 数据配置

生物质燃烧排放 GFED4 数据配置

WW、forever的博客 1301

3D深度饼图的QT实现

首先,我们需要创建一个QT项目并创建一个窗口,用于显示我们的饼图。在QT Creator中,选择创建一个新的QT Widgets应用程序项目,并创建一个新的窗口类。在本文中,我将介绍如何使用QT实现一个3D深度饼图(Pie 3D)的可视化效果。在构造函数中,我们将创建一个饼图(QChartView)并设置饼图的数据。这是一个简单的示例,您可以根据需要自定义和扩展代码来创建更复杂的饼图。现在,我们已经完成了3D深度饼图的QT实现。您将看到一个具有相应数据的3D深度饼图显示在窗口中。3D深度饼图的QT实现。

CodeJolt的博客 277

TypeError: Cannot read property ‘tapAsync‘ of undefined

TypeError: Cannot read property 'tapAsync' of undefined

LTE777的博客 3709

灰色预测GM(1,n)模型_python

灰色系统理论及其应用系列博文: 一、灰色关联度分析法(GRA)_python 二、灰色预测模型GM(1,1) 文章目录一、GM(1,n)算法二、示例2.1 数据:2.2 代码_使用GM(1,n)城市用水总量2.3 结果分析 一、GM(1,n)算法 二、示例 注:数据均来自论文《基于灰色系统理论的贵阳市城市用水总量预测》 2.1 数据: data.csv 年份,城区人口,建成区面积,供水总量,用水人口,人口综合用水指标,城市用水总量 2002, 191.85, 98.00, 26396.00,

qq_34229228的博客 1万+

【译】JavaScript的工作原理:事件循环及异步编程的出现和 5 种更好的 async/await 编程方式...

“此篇是JavaScript的工作原理的第四篇,其它三篇可以看这里:【译】JavaScript的工作原理:引擎,运行时和调用堆栈的概述【译】JavaScript的工作原理:V8引擎内部...

玄魂工作室 369

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

await原理 js_JavaScript 工作原理之四-事件循环及异步编程的出现和 5 种更好的 async/await 编程方式(译)...

原文请查阅这里,略有改动。本系列持续更新中,Github 地址请查阅这里。这是 JavaScript 工作原理的第四章。现在,我们将会通过回顾单线程环境下编程的弊端及如何克服这些困难以创建令人惊叹的 JavaScript 交互界面来展开第一篇文章。老规矩,我们将会在本章末尾分享 5 条利用 async/await 编写更简洁代码的小技巧。单线程的局限性在第一篇文章开头,我们考虑了一个问题即当调用...

weixin_32955259的博客 248

IDEA的使用指南

IDEA基于java的使用手册,可以最快速的上手,在使用中不断的丰富自己的使用技巧.

How Javascript works (Javascript工作原理) (四) 事件循环及异步编程的出现和 5 种更好的 async/await 编程方式...

个人总结: 1.讲解了JS引擎,webAPI与event loop合作的机制。 2.setTimeout是把事件推送给Web API去处理,当时间到了之后才把setTimeout中的事件推入调用栈。 3.Promise与Async/Await 这是 JavaScript 工作原理的第四章。 现在,我们将会通过回顾单线程环境下编程的弊端及如何克服这些困难以创建令人惊叹...

dglf54292的博客 175

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

JavaScript 工作原理之四-事件循环及异步编程的出现和 5 种更好的 async/await 编程方式(译)...

原文请查阅这里,略有改动。 本系列持续更新中,Github 地址请查阅这里。 这是 JavaScript 工作原理的第四章。 现在,我们将会通过回顾单线程环境下编程的弊端及如何克服这些困难以创建令人惊叹的 JavaScript 交互界面来展开第一篇文章。老规矩,我们将会在本章末尾分享 5 条利用 async/await 编写更简洁代码的小技巧。 单线程的局限性 在第一篇文章开头,我们考虑了一...

weixin_34101784的博客 324

【Rust异步】async和await异步编程实战:高并发任务处理全解析

async 和 await 是 Rust 中用于异步编程的关键字。 async 用于定义异步函数,表示函数体中包含异步代码。 await 用于等待异步操作完成,并返回异步操作的结果。 • 异步函数使用 async 关键字定义,并返回实现了 Future Trait 的类型。 异步函数可以在其他异步函数中使用 await 关键字等待异步操作完成。 调用异步函数时,会返回一个实现了 Future Trait 的对象,可以通过调用.await 方法等待结果。

景天科技苑 2万+

C#中异步编程(async/await)详解

异步这个概念在不同语境下有不同的解释,比如在一个单核CPU里开启两个线程执行两个函数,通常认为这种调用是异步的,但对于CPU来说它是单核不可能同时运行两个函数,不过是由系统调度在不同的时间分片中执行。一般来说,如果两个工作能同时进行,就认为是异步的。在编程中,它通常代表函数的调用可以在不执行完的情况下返回,必要时在完成时回调。有一个概念常常被混淆,多线程和异步。

u011555996的博客 3655

JavaScript 异步编程:从回调到 Promise 和 Async/Await

在现代 Web 开发中,异步编程是 JavaScript 的核心概念之一,变得愈发重要。由于 JavaScript 是单线程的,异步编程使得我们能够在不阻塞主线程的情况下执行耗时操作(如网络请求、文件读写等),从而提高了性能和用户体验

vvilkin的学习备忘 1369

JavaScript 中的 Promise、async/await 和异步编程

在 JavaScript 中,代码默认是同步执行的,即每一行代码都会按顺序执行,前一行代码执行完毕后才会执行下一行。然而,某些操作(如网络请求、文件读写等)可能需要花费较长时间,如果使用同步方式,程序会一直等待这些操作完成,导致页面卡顿或无响应。让耗时的操作在后台执行,同时不阻塞主线程。当操作完成后,再通过回调函数、Promise 或 async/await 来处理结果。Promise是 JavaScript 中处理异步操作的基础,它提供了.then()和.catch()方法来处理成功和失败的情况。

m0_73659339的博客 1433

异步编程:Promise 和 Async/Await

本文深入探讨TypeScript中的异步编程,重点解析Promise和Async/Await机制。文章首先介绍异步编程在TypeScript中的定位,强调其对非阻塞操作处理的重要性。接着详细讲解Promise的基础概念、状态机制和链式调用,并通过代码示例展示其基本用法。文章还深入分析Promise的并发处理方法(all、race等)以及类型定义机制,说明如何通过泛型Promise<T>为异步操作添加静态类型安全。最后,简要提及async/await语法如何简化异步代码的编写。全文通过渐进式

zimin的专栏 1097

Python 中的 async 和 await 入门

Python中的async/await用于编写异步代码,使程序能同时处理多个任务。同步代码会阻塞程序执行,而异步代码能在等待I/O操作时执行其他任务,提高效率。通过async定义协程函数,用await等待异步任务完成,配合asyncio.run()运行主函数和asyncio.gather()并发执行多个任务。异步编程适用于网络请求等I/O密集型场景,但不适合CPU密集型计算。常见框架如FastAPI利用异步处理高并发请求。注意直接调用async函数只会返回协程对象,需通过await或事件循环驱动执行。

尽日寻春不见春,芒鞋踏遍陇头云。归来笑拈梅花嗅,春在枝头已十分。 1211

web3.swift异步编程:async/await与闭包使用对比

web3.swift是一个功能强大的Ethereum Swift API,支持智能合约、ENS和ERC20等功能。在Swift开发中,异步编程是处理网络请求和耗时操作的关键技术。本文将深入探讨web3.swift中两种主要的异步编程模式:现代的async/await语法和传统的闭包(completionHandler)回调方式,帮助开发者选择最适合的编程风格。 ## 🌐 异步编程在web3.s

gitblog_00722的博客 726

探索C#异步编程从Task到async/await的全面解析

Task作为.NET 4.0引入的核心异步编程模型,代表了异步操作的抽象概念。通过Task.Run方法,可以轻松将同步代码转换为异步执行,同时Task.Status属性提供了对任务状态的实时监控能力,包括Created、WaitingForActivation、Running和Completed等状态值。异常处理方面,应当使用try-catch块包装await表达式,因为异步方法的异常会存储在返回的Task对象中。async/await关键字的引入是C# 5.0的重大革新,它极大简化了异步编程的复杂性。

技术派做新零售创业 744

JavaScript是如何工作的:事件循环和异步编程的崛起 + 5种使用 async/await 更好地编码方式!

摘要: 深度理解JS事件循环!!! 原文:JavaScript是如何工作的:事件循环和异步编程的崛起+ 5种使用 async/await 更好地编码方式! 作者:前端小智 Fundebug经授权转载,版权归原作者所有。 此篇是 JavaScript是如何工作的第四篇,其它三篇可以看这里: JavaScript是如何工作的:引擎,运行时和调用堆栈的概述! JavaScript是如何工作的:...

u010227042的博客 256
上一篇: 一秒看懂的 async/await
下一篇: 界面之下:还原真实的MV*模式 -( MVC , MVP , MVVM )
! 不再维护
博客等级 码龄11年 63粉丝 16原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值