Promises 与异步编程
JavaScript
引擎是基于单线程事件循环的概念构建的,同一时刻只允许一个代码块在执行,与之相反的像 Java
和 C++
一样的语言,它们允许许多不同的代码块同时执行。
JavaScript
引擎同一时刻只能执行一个代码块,所以需要跟踪即将运行的代码,那些代码被放在一个队列中,每当一段代码准备执行时,都会被添加到任务队列。每当 JavaScript
引擎中的一段代码结束执行,事件循环会执行队列中的下一个任务,它是 JavaScript
引擎中的一段程序,负责监控代码执行并管理任务队列。队列中的任务会从第一个一直执行到最后一个。
事件模型
用户点击按钮或按下键盘上的按键会触发类似 onclick 这样的事件,它会想任务队列添加一个新任务来响应用户的操作,这是 JavaScript
中最基础的异步编程形式,直到事件触发才执行事件处理程序,且执行上下文与定义是的相同。列如:
let button = document.getElementById('my-btn');
button.onclick = function(event) {
console.log('clicked');
};
回调模式
Node.js
通过普及回调函数来改进异步编程模型,回调模式与事件模型类似,异步代码都会在未来的某个时间点执行,二者的区别是回调模式中被调用的函数是作为参数传入,如下所示:
readFile('example.txt', function(err, contents) {
if (err) {
throw err;
}
console.log(contents);
});
console.log('Hi!');
由于使用来回调模式,readFile()
函数立即开始执行,当读取磁盘上的文件时会暂停执行。那也就是说,调用 readFile()
函数后,console.log('Hi!')
语句立即执行并输出 "Hi!"
;当 readFile()
结束执行时,会向队列的末尾添加一个新任务,该任务包含回调函数及相应的参数,当队列前面所有的任务完成后才执行该任务,并最终执行 console.log(contents)
输出所有的内容。
回调模式比事件模式更灵活,因此相比之下,通过回调模式链接多个调用更容易。请看这个示例:
readFile('example.txt', function(err, contents) {
if (err) {
throw err;
}
writeFile('example.txt', function(err, contents) {
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
相当于异步操作结果的占位符,它不会去订阅一个事件,也不会传递一个回调函数给目标函数,而是让函数返回一个 Promise
,就想这样:
// readFile 承诺将在未来的某个时刻完成
let promsie = readFile('example.txt');
在这段代码中,readFile()
不会立即开始读文件,函数会先返回一个表示异步读取操作的 Promise
对象,未来对这个对象的操作完全取决于 Promise
的生命周期。
Promise 的生命周期
每一个 Promise
都会经历一个短暂的生命周期:先是处于进行中(pending)的状态,此时操作尚未完成,所以它也是未处理(unsettled)的;一旦异步操作执行结束,Promise 则变为已处理的(settled)的状态。在之前的示例忠,当 readFile()
函数返回 Promise 时它变为 pending 状态,操作结束后,Promise 可能会进入到一下两个状态中的其中一个:
- Fulfilled Promise 异步操作成功完成。
- Rejected 由于程序错误或其他一些其他原因,Promise 异步操作未能成功完成。
由于内部属性[[PromiseState]]被用来表示 Promise 的 3 种状态:"pending"
、"fulfilled"
、"rejected"
。这个属性不暴露在 Promise 对象上,所以不能以编程的方式检测 Promise 的状态,只有当 Promise 的状态改变时,通过 then()
方法来采取特定的行动。
所有的 Promise 都有 then() 方法,他接受两个参数:第一个杀当 Promise 的状态变为 fulfilled 时要调用的函数,与异步操作相关的附加数据都会传递给这个完成函数;第二个杀当 Promise 的状态变为 rejected 时要调用的函数,其与完成时调用的函数类似,所有与失败状态相关的附加数据都会传递给这个拒绝函数。
then() 的两个参数都说可选的,所以可以按照任意组合的方式来监听 Promise,执行完成或拒绝都会被响应。例如:
let promise = readFile('example.txt');
promise.then(function(contents) {
// 完成
console.log(contents);
}, function(err) {
// 拒绝
console.log(err.message);
});
promise.then(function(contents) {
// 完成
console.log(contents);
});
promise.then(null, function(err) {
// 拒绝
console.log(err.message);
});
Promise 还有一个 catch()
方法,相当于只给传入拒绝处理程序的 then() 方法。列如,下面这个 catch() 方法和 then() 方法实现的功能是等价的:
promise.catch(function(err) {
// 拒绝
console.log(err.message);
});
// 与以下调用相同
promise.then(null, function(err) {
// 拒绝
console.log(err.message);
});
如果使用事件,在遇到错误时不主动触发;如果使用回调函数,则必须要记得每次都检查错误参数。你要知道,如果不给 Promise 添加拒绝处理程序,那所有失败就自动忽略来,所以一定要添加拒绝处理程序
,即使只有函数内部记录失败的结果也行。
如果一个 Promise 处于已处理状态,在这个之后添加到任务队列中的处理程序仍将执行
。所以无论何时你都可以添加新的完成处理程序或拒绝程序,同时也可以保证这些处理程序能被调用。举个例子:
let promise = readFile('example.txt');
// 最初的完成处理程序
promise.then(function(contents) {
console.log(contents);
// 现在又添加一个
promise.then(function(contents) {
console.log(contents);
});
});
在这段代码中,一个完成处理程序被调用时向同一个 Promise 添加了另一个完成处理程序,此时这个 Promise 已经完成,所以新的处理程序会被添加到任务队列中,当前面的任务完成后其才回被调用。这对拒绝处理程序也同样适用。
创建未完成的 Promise
用 Promise 构造函数可以创建新的 Promise,构造函数只接受一个参数:包含初始化 Promise 代码的执行器函数。执行器函数接受两个参数,分别是 resolve()
函数和 reject()
函数。执行器成功完成时调用 resolve() 函数,反之,失败时则调用reject() 函数。举个例子:
// Node.js 示例
let fs = require('fs');
function readFile(fileName) {
return new Promise(function(resolve, reject) {
// 触发异步操作
fs.readFile(fileName, { encoding: 'utf8' }, function(err, contents) {
// 检查是否有错误
if (err) {
reject(err);
return;
}
resolve(contents);
})
});
}
let promise = readFile('example.txt');
// 同时监听执行完成和执行拒绝
promise.then(function(contents) {
// 完成
console.log(contents);
}, function(err) {
// 拒绝
console.log(err.message);
})
Promise 具有 setTimeout() 和 setInterval() 函数类似的工作原理,Promise 的执行器会立即执行,然后才执行后续流程中的代码。例如:
let promise = new Promise(function(resolve, reject) {
console.log('Promise');
resolve();
});
console.log('Hi!');
这段代码的输出内容是:
Promise
Hi!
调用 resolve()
后会触发一个异步操作,传入 then()
和 catch()
方法的函数会被添加到任务队列中并异步执行。请看这个示例:
let promise = new Promise(function(resolve, reject) {
console.log('Promise');
resolve();
});
promise.then(function( ) {
console.log('Resolved');
});
console.log('Hi!');
这段代码的输出内容是:
Promise
Hi!
Resolved
创建一处理的 Promise
let promise = Promise.resolve(42);
promise.then(function(value) {
console.log(value); // 42
});
let promise = Promise.rejcect(42);
promise.catch(function(value) {
console.log(value); // 42
});
非 Promise 的 Thenable 对象
Promise.resolve() 方法和 Promise.reject() 方法都可以接受非 Promise 的 Thenable
对象作为参数。如果传入一个非 Promise 的 Thenable 对象,这这些方法会创建一个新的 Promise,并在 then() 函数中调用。
拥有 then() 方法并且接受 resolve
和 reject
这两个参数的普通对象就是非 Promise 的 Thenable
对象。例如:
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
let p1 = Promise.resolve(thenable);
p1.then(function(value) {
console.log(value); // 42
});
执行器错误
如果执行器内部抛出一个错误,则 Promise 的拒绝处理程序就会被调用,例如:
let promise = new Promise(function(resolve, reject) {
try {
throw new Error('Explosion!');
} catch (ex) {
reject(ex);
}
});
promise.catch(function(error) {
console.log(error.message);
});
串联 Promise
每次调用 then() 方法或 catch() 方法时实际上创建并返回来另一个 Promise,只有当第一个 Promise 完成或被拒绝后,第二个才会解决。请看以下这个示例:
let p1 = new Promise(function(resolve, reject) {
resolve(42);
});
p1.then(function(value) {
console.log(value);
}).then(function(value) {
console.log('Finished');
});
这段代码的输出内容是:
42
Finished
如果将这个示例拆解开,看起来就像这样的:
let p1 = new Promise(function(resolve, reject) {
resolve(42);
});
let p2 = p1.then(function(value) {
console.log(value);
});
p2.then(function(value) {
console.log('Finished');
});
捕获错误
在之前的示例中,完成处理程序或拒绝处理程序中可能发生错误,而 Promise 链可以用来捕获这些错误。例如:
let p1 = new Promise(function(resolve, reject) {
resolve(42);
});
p1.then(function(value) {
throw new Error('Boom!');
}).catch(function(error) {
console.log(error.message); // "Boom!"
});
let p1 = new Promise(function(resolve, reject) {
throw new Error('Explosion!');
});
p1.catc(function(errpr) {
console.log(error.message); // "Explosion!"
throw new Error('Boom!');
}).catch(function(error) {
console.log(error.message); // "Boom!"
});
务必在 Promise 链的末尾留一个拒绝处理程序以确保能够正确处理所有可能发生的错误。
Promise 链的返回值
Promise 链的另一个重要特效就是可以给下游 Promise 传递数据,我们已经看到来从执行器 resolve() 处理程序到Promise 完成处理程序的数据传递过程,如果在完成处理程序中指定一个返回值,则可以沿着这条链继续传递数据。例如:
let p1 = new Promise(function(resolve, reject) {
resolve(42);
});
p1.then(function(value) {
console.log(value); // 42
return value + 1;
}).then(function(value) {
console.log(value); // 43
});
let p1 = new Promise(function(resolve, reject) {
reject(42);
});
p1.catch(function(value) {
// 第一个完成处理程序
console.log(value); // 42
return value + 1;
}).then(function(value) {
// 第二个完成处理程序
console.log(value); // 43
});
Promise 链中返回 Promise
请看以下示例:
let p1 = new Promise(function(resolve, reject) {
resolve(42);
});
let p2 = new Promise(function(resolve, reject) {
resolve(43);
});
p1.then(function(value) {
// 第一个完成处理程序
console.log(value); // 42
return p2;
}).then(function(value) {
// 第二个完成处理程序
console.log(value); // 43
});
let p1 = new Promise(function(resolve, reject) {
resolve(42);
});
let p2 = new Promise(function(resolve, reject) {
reject(43);
});
p1.then(function(value) {
// 第一个完成处理程序
console.log(value); // 42
return p2;
}).then(function(value) {
// 第二个完成处理程序
console.log(value); // 从未调用
});
let p1 = new Promise(function(resolve, reject) {
resolve(42);
});
let p2 = new Promise(function(resolve, reject) {
reject(43);
});
p1.then(function(value) {
// 第一个完成处理程序
console.log(value); // 42
return p2;
}).catch(function(value) {
// 第二个完成处理程序
console.log(value); // 43
});
响应多个 Promise
Promise.all()方法
Promise.all() 方法只接受一个参数并返回一个 Promise,该参数是一个含有多个受监视 Promise 的可迭代对象(列如,一个数组),只有当可迭代对象中所有 Promise 都被解决后返回 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.all() 方法的 Promise 只要有一个被拒绝,那么返回的 Promise 没等所有 Promise 都完成就立即被拒绝。但是,Promise.all方法中的Promise还是会继续执行,只是Promise.all会提前返回而已,如果在p3里延时三秒打印a,在Promise.all([p1, p2, p3]
).reject()中打印b,那么就会先获取到p2的reject然后执行reject打印b,然后过三秒再打印a。
let p1 = new Promise(function(resolve, reject) {
resolve(42);
});
let p2 = new Promise(function(resolve, reject) {
reject(43);
});
let p3 = new Promise(function(resolve, reject) {
resolve(44);
});
let p4 = Promise.all([p1, p2, p3]);
p4.catch(function(value) {
console.log(Array.isArray(value)); // false
console.log(value); // 43
});
Promise.allSettled() 方法
有时候,我们希望等到一组异步操作都结束了,不管每一个操作是成功还是失败,再进行下一步操作。显然Promise.all(其只要是一个失败了,结果即进入失败状态)不太适合,所以有了Promise.allSettled
Promise.allSettled()方法接受一个数组作为参数,数组的每个成员都是一个 Promise 对象,并返回一个新的 Promise 对象。只有等到参数数组的所有 Promise 对象都发生状态变更(不管是fulfilled还是rejected),返回的 Promise 对象才会发生状态变更,一旦发生状态变更,状态总是fulfilled,不会变成rejected
还是以上面的例子为例, 我们看看与Promise.all有什么不同
const p1 = Promise.resolve(1) const p2 = new Promise((resolve) => { setTimeout(() => resolve(2), 1000) }) const p3 = new Promise((resolve) => { setTimeout(() => resolve(3), 3000) }) const p4 = Promise.reject('err4') const p5 = Promise.reject('err5') // 1. 所有的Promise都成功了 const p11 = Promise.allSettled([ p1, p2, p3 ]) .then((res) => console.log(JSON.stringify(res, null, 2))) // 输出 /* [ { "status": "fulfilled", "value": 1 }, { "status": "fulfilled", "value": 2 }, { "status": "fulfilled", "value": 3 } ] */ // 2. 有一个Promise失败了 const p12 = Promise.allSettled([ p1, p2, p4 ]) .then((res) => console.log(JSON.stringify(res, null, 2))) // 输出 /* [ { "status": "fulfilled", "value": 1 }, { "status": "fulfilled", "value": 2 }, { "status": "rejected", "reason": "err4" } ] */ // 3. 有两个Promise失败了 const p13 = Promise.allSettled([ p1, p4, p5 ]) .then((res) => console.log(JSON.stringify(res, null, 2))) // 输出 /* [ { "status": "fulfilled", "value": 1 }, { "status": "rejected", "reason": "err4" }, { "status": "rejected", "reason": "err5" } ] */
可以看到:
- 不管是全部成功还是有部分失败,最终都会进入Promise.allSettled的.then回调中
- 最后的返回值中,成功和失败的项都有status属性,成功时值是fulfilled,失败时是rejected
- 最后的返回值中,成功含有value属性,而失败则是reason属性
Promise.race() 方法
Promise.race() 方法监听多个 Promise 的方法稍有不同:它也接受含多个受监视 Promise 的可迭代对象作为唯一参数并返回一个 Promise,但只要有一个 Promise 被解决返回的 Promise 就被解决,无需等到所有 Promise 都被完成。一旦数组中某个 Promise 被完成,Promise.race() 方法也会像 Promise.all() 方法一样返回一个特定的 Promise,例如:
let p1 = new 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.race() 方法的 Promise 会进行竞选,以决出那一个先被解决,如果先解决的是已完成 Promise,则返回已完成 Promise;如果先解决的是已拒绝 Promise,则返回已拒绝 Promise。这里是一段拒绝示例:
let p1 = new Promise(function(resolve, reject) {
setTimeout(function( ) {resolve(42);}, 0);
});
let p2 = Promise.reject(43);
let p3 = new Promise(function(resolve, reject) {
resolve(44);
});
let p4 = Promise.race([p1, p2, p3]);
p4.catch(function(value) {
console.log(value); // 43
});
总的来说,Promise.all、Promise.allSettled、Promise.race这三个方法,对于他们要处理的promise是没有影响的,都会同步进行。
区别就在于Promise.all是全部完成就执行resolve,有一个失败就立刻执行reject(没有用.catch或 catch{}捕捉的话就会在控制台报错)。
Promise.allSettled是等全部完成(无论成功失败)都执行resolve。
Promise.race是谁先完成(无论成功失败)就返回谁,先返回的是成功的就执行resolve,先返回的是失败的就执行reject。