JavaScript发展到现在,异步编程已经得到了极大的改进。异步编程的进化史:callback -> promise -> generator -> async + await。现在我们来根据其发展历史一一进行分析。
callback
在早期的JavaScript中,只支持定义回调函数来表明异步操作完成,它也是异步操作中最基本的一种方法。在这里我们需要知道,回调函数就是将函数作为变量看待,即在一个函数里将变量设置为函数,它不一定是异步代码,异步代码却一定是回调函数 。以下就是一个回调函数的例子:
function test(value, callback) {
setTimeout(() => callback(value),1000)
}
test(3, (x) => console.log(`测试数据为: ${x}`))
// 测试数据为: 3 (大约在1000ms后)
上述方式可以完成了一个回调操作,但是如果异步返回值依赖于另一个异步返回值,结果又会怎样呢?让我们看看接下来的一个例子:
function test(value, success, failure) {
setTimeout(() => {
try {
if (typeof value !== 'number') {
throw 'please provide number as first argument'
}
success(3*value) // 成功回调
} catch(e) {
failure(e) // 失败回调
}
},1000)
}
const successCallback = (x) => {
test(x, (y) => console.log(`suceess: ${y}`))
}
const failureCallback = (e) => {
console.log(`failure:${e}`)
}
test(3, successCallback, failureCallback) // suceess: 27(大约100ms之后)
很显然,随着代码越复杂,回调策略就难以扩展,嵌套回调的代码也就难以维护,这就是著名的“回调地狱”。
Promise
什么是Promise?
Promise 是一个由异步函数返回的可以向我们指示当前操作所处的状态的对象。在Promise返回给调用者时,操作往往还没有完成,但 Promise 对象可以让我们操作最终完成时对其进行处理(无论成功还是失败)。
正如前面所述,Promises是一个有状态的对象,它可能处于以下三种状态:
- 待定(pending):初始状态,既没有被兑现,也没有被拒绝。这是调用
fetch()
返回Promise的状态,此时请求还在进行中。 - 兑现(fullfilled):操作成功完成,然后调用
then()
处理函数。 - 拒绝(rejected):操作失败,然后调用
catch()
处理函数。
依图所示,我们知道无论落定为哪种状态都是不可逆的,只要从pending转换为fullfilled/rejected,期约的状态就不再改变。同时,我们应该了解到期约的状态是私有的,为了避免根据读取到的期约状态以同步方式处理Promise,它不能直接通过JavaScript检测到。
因为期约状态私有,所以只能在期约的执行器函数中进行内部操作。执行器函数主要有两个职责:初始化期约的异步行为和控制状态的最终转换。而一般来说切换期约状态是通过调用它的两个函数参数实现,该两个函数参数通常命名为resolve()和reject()。
let p1 = new Promise((resolve,reject) => resolve())
setTimeout(console.log,0,p1) // Promise <resolved>
let p2 = new Promise((resolve, reject) => reject())
setTimeout(console.log,0,p2) // Promise <rejected>
好了,说了这么多,让我们通过一个例子来详细探讨下。在以下例子中,我们通过发送一个请求,获得一个JSON文件:
const fetchPromise = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');
console.log(fetchPromise); // Promise { <state>: "pending" }
fetchPromise
.then( response => {
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
} else {
console.log(`已收到响应:${response.status}`);
}
return response.json(); // 获取JSON格式的数据
})
.then( json => {
console.log(json[0].name);
});
console.log("已发送请求……");
在上述代码中,通过调用fetch()
API将返回值赋予变量fetchPromise
,然后将json()
这个处理函数传递给Promise的then()
方法进行调用。因为json()
方法也是异步,我们必须连续调用两个异步函数。我们将一个新的then()
处理程序传递response.json()
返回的Promise。此处采用链式调用,从而能够避免了回调嵌套而导致的回调地域问题。
以下为完整的输出结果:
Promise { <state>: "pending" }
已发送请求……
已收到响应:200
baked beans
可以看出,已发送请求……
的消息在我们收到响应前就已被输出,即fetch()
在请求进行时返回,这使得程序保持响应性。
在Promise中,有着几个常用的API,分别是Promise.all()
和 Promise.race()
,大家可以去详细了解下,这里就不阐述了。
async/await
在学习异步函数async/await
前,让我们先来看一个例子:
let p = new Promise((resolve, reject) => setTimeout(resolve,1000,2));
这个期约在1000ms后解决为数值3,如果我们想要访问它,则必须写一个解决处理程序:
p.then((x) => console.log(x);
这样极其不方便,任何需要访问该期约产生值的代码都需要以处理程序的形式来接收这个值。为了解决这样的问题,ECMAScript提供了async/await
关键字。
1.async
async关键字用于声明异步函数,它可以用在函数声明、函数表达式、箭头函数和方法上。当异步函数中使用return关键字返回值(没有return则返回undefind)时,这个值会被Promise.resolve()
包装成一个Promise对象,然后交由then()的处理程序进行‘解包’。同时,当在异步函数中抛出错误时,会返回一个拒绝期约,但拒绝期约的错误无法被函数捕获。
async function foo() {
console.log(1);
return 5;
}
async function qux() {
console.log(2)
throw 6;
}
async function baz() {
console.log(3);
Promise.reject(7);
}
foo().then(console.log);
qux().catch(console.log);
baz().catch(console.log);
console.log(4);
// 输出结果顺序为:1 2 3 4 5 6 Uncaught (in promise):7
相信大家看了上面的例子都了解,async关键字就是一个标识符,其执行基本上与普通函数没什么区别。所以这个时候就需要await的暂停和恢复执行的能力了。
2.await
await关键字能暂停异步函数后面的代码,记录在哪里暂停,让出JavaScript运行时的执行线程;然后尝试‘解包’对象,等到await右边的值可用,JavaScript运行时就会向消息队列中推送一个任务,这个任务就会恢复异步函数的执行。
下面来看个例子:
async function foo() {
console.log(1);
console.log(await Promise.resolve(5));
console.log(6);
}
async function qux() {
console.log(2)
await (() => {throw 7})();
}
async function baz() {
console.log(3);
await Promise.reject(8);
console.log(9); // 该行代码不会执行
}
foo().then(console.log);
qux().catch(console.log);
baz().catch(console.log);
console.log(4);
// 输出结果顺序为:1 2 3 4 5 6 7 8
值得注意一点的是,单独的Promise.reject()
不会被异步函数捕捉,而在上述的baz()
函数中,对拒绝期约使用await会释放错误值。同时,我们必须明确的一点是,await关键字必须在异步函数中使用,不能在顶级上下文(如<script>
标签)或模块中使用,它的特质也不会扩展到嵌套函数。
本文就到这里啦!谢谢观看!