Promise、 Async和Await详解(二)
真正理解Async/Await
你知道它是干什么的,但你知道它是怎么做到的吗?
大多数开发人员对JavaScript有一种爱恨交织的关系,其中一个原因是它的最佳品质之一:易学,难掌握。这种品质值得注意的一个方面是,有多少开发人员倾向于假设语言以某种方式工作,但实际上在幕后发生了一些非常不同的事情。这种差异体现在细节上,并导致挫折感。
例如,我毫不怀疑标准的最新变更导致许多人对“类”行为产生了误解:JavaScript实际上并没有类,它使用的是原型,即其他对象继承自的单例对象。事实上,JavaScript中的所有对象都有一个原型用于继承。这意味着JavaScript的“类”并不完全等同于传统意义上的类。类是创建对象实例的蓝图,而原型是其他对象实例委托任务的对象实例,原型并非蓝图,它真实存在,就摆在那里。
这就是为什么你实际上可以向Array添加一个新方法,突然之间所有数组都可以使用它。这可以在运行时完成,影响已经实例化的对象。
var someArray = [1, 2, 3];
Array.prototype.newMethod = function() {
console.log('I am a new method!');
};
someArray.newMethod(); // I am a new method!
//上述代码不适用于真实类,因为修改蓝图不会修改用它构建的任何东西。
简而言之,Javascript中的类是原型继承的语法糖。
我的主要观点是,如果你想充分了解一门语言的功能和局限性,你必须学习它的语法之外的真正工作原理。
Async/Await规范
异步函数是Ecmascript最新草案(第4阶段)中已经包含的语言的补充。您今天可以使用Babel转译器来使用它们。
async/await试图解决该语言自诞生以来最大的难题之一:异步。如果你不理解异步代码的概念,我建议你在继续阅读本文之前先阅读一下。
多年来,我们有多种方法来处理这种令人发疯的问题。在JavaScript的大部分时间里,我们都依赖于回调:
setTimeout(function() {
console.log('This runs after 5 seconds');
}, 5000);
console.log('This runs first');
回调很好,但如果我们必须按顺序做事呢?
doThingOne(function() {
doThingTwo(function() {
doThingThree(function() {
doThingFour(function() {
// Oh no
});
});
});
});
你上面看到的有时被称为末日金字塔或回调地狱,也有一些网站以他们的名字命名。不好的。
Behold: promises
Promise是处理异步代码的一种非常聪明的好方法。
Promise是一个对象,它表示一个最终将完成的异步任务。使用时它们看起来像这样:
function buyCoffee() {
return new Promise((resolve, reject) => {
asyncronouslyGetCoffee(function(coffee) {
resolve(coffee);
});
});
}
buyCoffee返回一个Promise,表示购买咖啡的过程。resolve函数向Promise实例发出已完成的信号。它接收一个值作为参数,稍后将通过promise提供。
Promise实例有两个主要方法:
then:这将运行一个回调函数,当promise完成时传递给它。
catch:当出现问题时,它会运行一个回调函数,并将错误信息传递给它,对于错误信息的promise处理,要用reject而不是resolve。reject要么手动调用(例如,我们正在进行AJAX调用并收到服务器错误),要么在Promise代码中抛出未捕获的异常时自动调用。
catch,你就会陷入另一种“地狱”——代码静默失败。这种情况会让人非常抓狂,所以请务必不惜一切代价避免它。buyCoffee()
.then(function() {
return drinkCoffee();
})
.then(function() {
return doWork();
})
.then(function() {
return getTired();
})
.then(function() {
return goToSleep();
})
.then(function() {
return wakeUp();
});
这里如果使用回调函数,会对代码的可维护性非常不利,甚至可能让我们精神崩溃。
如果你还不熟悉 Promise,上面的代码可能看起来违反直觉。这是因为:如果在 then 方法里返回一个新的 Promise,那么整个链式调用会返回一个新 Promise,它只会在这个内部返回的 Promise 完成后才会完成,并且会使用这个内部 Promise 的结果作为值。
const firstPromise = new Promise(function(resolve) {
return resolve("first");
});
const secondPromise = new Promise(function(resolve) {
resolve("second");
});
const doAllThings = firstPromise.then(function() {
return secondPromise;
});
doAllThings.then(function(result) {
console.log(result); // 输出结果: "second"
});
异步函数(async functions)是会返回 Promise 的函数
没错。这就是我花时间简要讲解 Promise 的原因 —— 因为要真正理解 Async/Await,你必须先明白 Promise 的工作原理,这有点像在 JavaScript 里要真正理解类(class),就得先理解原型(prototype)。
如何工作?
🤡这就是异步函数(Async Functions)。它们通过在声明时加上关键字 async 来定义:async function doAsyncStuff() { ...code }
🤠你的代码可以暂停下来,等待一个异步函数(通过 await)执行完毕。
🤓await 会返回异步函数执行完成后的结果。
🧐await 只能在 async 函数内部使用。
😝如果 async 函数抛出了异常,这个异常会像普通 JavaScript 代码一样冒泡到父级函数,并且可以通过 try/catch 来捕获。
async 函数调用链的起始位置使用 try/catch。try/catch,这是一个很好的编程习惯。这样做不仅能为异步操作提供一个统一的错误处理入口,还能强制你正确地串联你的 async 函数调用。// 几个用于处理数值的随机异步函数
async function thingOne() { ... }
async function thingTwo(value) { ... }
async function thingThree(value) { ... }
async function doManyThings() {
var result = await thingOne();
var resultTwo = await thingTwo(result);
var finalResult = await thingThree(resultTwo);
return finalResult;
}
// Call doManyThings()
这就是使用了 async/await 的代码长什么样,它看起来非常接近同步代码,而同步代码显然是更容易理解的。
那么,既然 doManyThings() 本身也是个异步函数,我们该怎么 await 它呢?
答案是:单靠这套新语法做不到。我们有三种选择:
- 让它“自生自灭”:让代码继续往下执行,不去等待它完成(其实在很多场景下,这正是我们想要的)。
- 包一层:把它放在另一个
async函数里调用,并且外面包上try/catch代码块。 - 或者……把它当成 Promise 来用。
// Option 1:
doManyThings();
// Option 2:
(async function() {
try {
await doManyThings();
} catch (err) {
console.error(err);
}
})();
// Option 3:
doManyThings().then((result) => {
// Do the things that need to wait for our function
}).catch((err) => {
throw err;
});
再说一遍,这些就是返回 Promise 的函数。
async/await 大致是如何转换成 Promise 写法的。async 函数其实仅仅是一种“语法糖”,它的本质就是创建那些返回 Promise 并等待 Promise 结果的函数。// Async/Await version
async function helloAsync() {
return "hello";
}
// Promises version
function helloAsync() {
return new Promise(function (resolve) {
resolve("hello");
});
}
// 两者是等效的
一个正在等待另一个异步函数返回结果的异步函数。
Async/Await版本,如下
async function multiply(a, b) {
return a * b;
}
async function foo() {
var result = await multiply(2, 5);
return result;
}
// Errors will be swallowed here
(async function () {
var result = await foo();
console.log(result); // Logs 10
})();
输出结果如下:
promise版本如下:
function multiply(a, b) {
return new Promise(function (resolve) {
resolve(a * b);
});
}
function foo() {
return new Promise(function(resolve) {
multiply(2, 5).then(function (result) {
resolve(result);
});
});
}
// Errors will be swallowed here
new Promise(function() {
foo().then(function(result) {
console.log(result); // Logs 10
});
});
运行结果如下:
async和promise版本汇总如下:

请注意,上面那种使用 Promise 的方式其实并不推荐。我之所以那样写,纯粹是为了让它跟 async/await 的例子对比起来更直观、更容易理解罢了。
我们关心的示例
这里有一个例子来说明为什么了解basec/await的工作原理是有用的。
async function foo() {
someArray.forEach(function (value) {
doSomethingAsync(value);
});
}
到目前为止一切顺利,因为我们没有使用 await,所以 doSomethingAsync 是多次并行执行的。但是,如果我们想(按顺序)做这件事,该怎么做呢?
不像这样:
async function foo() {
someArray.forEach(function (value) {
await doSomethingAsync(value);
});
}
forEach 的是一个同步函数。async function foo() {
someArray.forEach(async function (value) {
await doSomethingAsync(value);
});
}
这里出了什么问题?好吧,让我们看看这实际上会转换成什么代码。为了不过于啰嗦,我不会在这里展开完整的 Promise 写法,而是直接翻译成我们真正不得不这样做时会写的样子:
function foo() {
someArray.forEach(function () {
// this is returning a promise
return doSomethingAsync(value);
});
}
forEach 不会等待你的异步函数执行完毕;如果用 Promise 的思维来理解,就是它不会等待当前迭代返回的 Promise 解决(resolve)后,再调用下一次迭代。forEach 调用本身使用 await(尽管这并不能解决根本问题)。forEach。事实上,任何同步迭代器都无法胜任。我们需要的是能够处理 Promise 的迭代器。for 循环 —— for...of,它能够自动等待 Promise。它像如下这样:for (item of someArray) {
await foo();
}
理解Promises,你就理解了promises/await。

浙公网安备 33010602011771号