回调地狱
readFile("example.txt", function(err, contents) {
if (err) {
throw err;
}
writeFile("example.txt", function(err) {
if (err) {
throw err;
}
console.log("File was written!");
});
});
method1(function(err, result) {
if (err) {
throw err;
}
method2(function(err, result) {
if (err) {
throw err;
}
method3(function(err, result) {
if (err) {
throw err;
}
method4(function(err, result) {
if (err) {
throw err;
}
method5(result);
});
});
});
});
回调地狱嵌套多个方法调用会创建错综复杂的代码,会难以理解与调试。当想要实现更复杂的功能时,回调函数也会存在问题。
Promise 定义
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了 Promise 对象。
所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
Promise 特点
对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从 pending 变为 fulfilled 和从 pending 变为 rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。如果改变已经发生了,你再对 Promise 对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。
基本用法
Promise 对象是一个构造函数,用来生成 Promise 实例。
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
Promise构造函数接受一个函数作为参数,该函数的两个参数分别是 resolve 和 reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。
- resolve 函数的作用是,将 Promise 对象的状态从未完成变为成功(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去。
- reject 函数的作用是,将 Promise 对象的状态从未完成变为失败(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
Promise 实例生成以后,可以用 then 方法分别指定 resolved 状态和 rejected 状态的回调函数。
promise.then(function(value) {
// success
}, function(error) {
// failure
});
then 方法可以接受两个回调函数作为参数。第一个回调函数是 Promise 对象的状态变为 resolved 时调用,第二个回调函数是 Promise 对象的状态变为 rejected 时调用。其中,第二个函数是可选的,不一定要提供。这两个函数都接受 Promise 对象传出的值作为参数。
function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve('done'), ms);
});
}
timeout(100).then((value) => {
console.log(value);
});
如果调用 resolve 函数和 reject 函数时带有参数,那么它们的参数会被传递给回调函数。 reject 函数的参数通常是 Error 对象的实例,表示抛出的错误;resolve 函数的参数除了正常的值以外,还可能是另一个 Promise 实例。
const p1 = new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('fail')), 3000)
})
const p2 = new Promise(function (resolve, reject) {
setTimeout(() => resolve(p1), 1000)
})
p2.then(result => console.log(result)).catch(error => console.log(error))
// Error: fail
p1 是一个 Promise,3 秒之后变为 rejected。p2 的状态在 1 秒之后改变,resolve 方法返回的是 p1。由于 p2 返回的是另一个 Promise,导致 p2 自己的状态无效了,由 p1 的状态决定 p2 的状态。所以,后面的 then 语句都变成针对后者 p1。又过了 2 秒,p1 变为rejected,导致触发 catch 方法指定的回调函数。
注意,调用 resolve 或 reject 并不会终结 Promise 的参数函数的执行。
new Promise((resolve, reject) => {
resolve(1);
console.log(2);
}).then(r => {
console.log(r);
});
// 2
// 1
调用 resolve(1) 以后,后面的 console.log(2) 还是会执行,并且会首先打印出来。这是因为立即 resolved 的 Promise 是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务。
一般来说,调用 resolve 或 reject 以后,Promise 的使命就完成了,后继操作应该放到 then 方法里面,而不应该直接写在 resolve 或 reject 的后面。所以,最好在它们前面加上return语句,这样就不会有意外。
new Promise((resolve, reject) => {
return resolve(1);
// 后面的语句不会执行
console.log(2);
})
finally()
finally 方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
不管 promise 最后的状态,在执行完 then 或 catch 指定的回调函数以后,都会执行 finally 方法指定的回调函数。
已决的 Promise
const p = Promise.resolve('foo')
// 等价于
const p = new Promise(resolve => resolve('foo'))
const p = Promise.reject('出错了');
// 等价于
const p = new Promise((resolve, reject) => reject('出错了'))
返回一个状态已经决定的 Promise 实例。
执行器错误
如果在执行器内部抛出了错误,那么 Promise 的拒绝处理函数就会被调用。
let promise = new Promise(function(resolve, reject) {
throw new Error("Explosion!");
});
promise.catch(function(error) {
console.log(error.message); // "Explosion!"
});
串联
let p1 = new Promise(function(resolve, reject) {
resolve(42);
});
p1.then(function(value) {
console.log(value);
}).then(function() {
console.log("Finished");
});
串联版本的代码中对 p1.then() 的调用返回了第二个 Promise ,又在这之上调用了 then() 。仅当第一个 Promise 已被决议后,第二个 then() 的完成处理函数才会被调用。
响应多个 Promise
- Promise.all() 方法
接收单个可迭代对象(如数组)作为参数,并返回一个 Promise 。这个可迭代对象的元素都是 Promise ,只有在它们都完成后,所返回的 Promise 才会被完成。
let p1 = new Promise(function(resolve, reject) {
resolve(42);
});
let p2 = new Promise(function(resolve, reject) {
resolve(43);
});
let p3 = new Promise(function(resolve, reject) {
resolve(44);
});
let p4 = Promise.all([p1, p2, p3]);
p4.then(function(value) {
console.log(Array.isArray(value)); // true
console.log(value[0]); // 42
console.log(value[1]); // 43
console.log(value[2]); // 44
});
- Promise.race() 方法
接受一个包含需监视的 Promise 的可迭代对象,并返回一个新的 Promise ,但一旦来源 Promise 中有一个被解决,所返回的 Promise 就会立刻被解决。与等待所有 Promise 完成的 Promise.all() 方法不同,在来源 Promise 中任意一个被完成时, Promise.race() 方法所返回的 Promise 就能作出响应。
let p1 = Promise.resolve(42);
let p2 = new Promise(function(resolve, reject) {
resolve(43);
});
let p3 = new Promise(function(resolve, reject) {
resolve(44);
});
let p4 = Promise.race([p1, p2, p3]);
p4.then(function(value) {
console.log(value); // 42
});
Promise 缺点
- 无法取消Promise,一旦新建它就会立即执行,无法中途取消。
- 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
- 当处于pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
async
- async 表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
- async 函数返回一个 Promise 对象。
- async 函数内部 return 语句返回的值,会成为 then 方法回调函数的参数。
async function f() {
return 'hello world';
}
f().then(v => console.log(v))
// "hello world"
上面代码中,函数 f 内部 return 命令返回的值,会被 then 方法回调函数接收到。
- async 函数内部抛出错误,会导致返回的 Promise 对象变为 reject 状态。抛出的错误对象会被 catch 方法回调函数接收到。
async function f() {
throw new Error('出错了');
}
f().catch(
e => console.log(e)
)
// Error: 出错了
- async 函数返回的 Promise 对象,必须等到内部所有 await 命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到 return 语句或者抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。
async function asyncTest() {
let getUrl = await fetch();
console.log(getUrl);
let downloadUrl = await download();
console.log(downloadUrl);
return 'complete';
}
asyncTest().then(console.log)
function fetch() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('fetchDone');
}, 1000);
});
}
function download() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('downloadDone');
}, 2000);
});
}
上面代码中,模拟了一个抓取和一个下载,只有这两个操作全部完成,才会执行 then 方法里面的 console.log。
await
- 正常情况下,await 命令后面是一个 Promise 对象。如果不是,会被转成一个立即 resolve 的 Promise 对象。
async function f() {
return await 123;
}
f().then(v => console.log(v))
// 123
await 命令的参数是数值123,它被转成 Promise 对象,并立即 resolve。
- await 命令后面的 Promise 对象如果变为 reject 状态,则 reject 的参数会被 catch 方法的回调函数接收到。
async function f() {
await Promise.reject('出错了');
}
f().then(v => console.log(v)).catch(e => console.log(e))
// 出错了
注意,上面代码中,await 语句前面没有 return,但是 reject 方法的参数依然传入了 catch 方法的回调函数。这里如果在 await 前面加上 return,效果是一样的。
- 只要一个 await 语句后面的 Promise 变为 reject,那么整个 async 函数都会中断执行。
async function f() {
await Promise.reject('出错了');
await Promise.resolve('hello world'); // 不会执行
}
- 有时,我们希望即使前一个异步操作失败,也不要中断后面的异步操作。这时可以将第一个 await 放在 try...catch 结构里面,这样不管这个异步操作是否成功,第二个 await 都会执行。
async function f() {
try {
await Promise.reject('出错了');
} catch(e) {
}
return await Promise.resolve('hello world');
}
f().then(v => console.log(v))
// hello world
另一种方法是 await 后面的 Promise 对象再跟一个 catch 方法,处理前面可能出现的错误。
async function f() {
await Promise.reject('出错了')
.catch(e => console.log(e));
return await Promise.resolve('hello world');
}
f().then(v => console.log(v))
// 出错了
// hello world
注意点
- await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try...catch 代码块中。
async function myFunction() {
try {
await somethingThatReturnsAPromise();
} catch (err) {
console.log(err);
}
}
// 另一种写法
async function myFunction() {
await somethingThatReturnsAPromise()
.catch(function (err) {
console.log(err);
});
}
- 多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。
// 写法一
async function test() {
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
}
function getFoo() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
console.log('getFoo');
resolve()
}, 2000);
})
}
function getBar() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
console.log('getBar');
resolve()
}, 2000);
})
}
test()
// 写法二
async function test() {
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
}
- await 命令只能用在 async 函数之中,如果用在普通函数,就会报错。
async function test() {
let promises = [function1, function2, function3]
promises.forEach(function (fun) {
const t = await fun();
console.log(t)
});
}
function function1() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('function1')
}, 2000)
})
}
function function2() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('function2')
}, 2000)
})
}
function function3() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('function3')
}, 2000)
})
}
test()
- 如果将 forEach 方法的参数改成 async 函数,也有问题。
promises.forEach(async function (fun) {
const t = await fun();
console.log(t)
});
上面代码可能不会正常工作,原因是这时三个异步操作将是并发执行,也就是同时执行,而不是继发执行。正确的写法是采用 for 循环。
for (let i of promises) {
const t = await i()
console.log(t)
}
- 如果确实希望多个请求并发执行,可以使用 Promise.all 方法。当三个请求都 resolved 时,下面两种写法效果相同。
async function dbFuc(db) {
async function test() {
let promises = [function1(), function2(), function3()]
let results = await Promise.all(promises);
console.log(results)
}