1、异步编程
- 异步行为是为了优化因计算量大而时间长的操作。
- 在等待其他操作完成的同时,即使运行其他指令,系统也能保存稳定。
- 异步操作不一定要计算量大或者时间长,不想为等待某个操作而阻塞线程执行,那么任何时候都可以使用。
1、同步与异步
-
同步行为对应内存中顺序执行的处理器指令。
-
异步行为类似于系统中断,即当前进程外部的实体可以出发代码执行。
-
异步操作是必要的,因为强制等待一个长时间操作不可行。
let x = 3 setTimeout(()=> x = x + 4, 1000) 复制代码
- 什么时候x会更新?如何通知?
2、以往的异步编程模式
- 以往只支持定义回调函数表明异步操作完成。
- 需要深度嵌套回调函数,因此产生「回调地狱」
1、异步返回值
-
如何把异步操作setTimeout 的返回值传给需要的地方?
-
以往需要一个回调,回调包含要使用异步返回值的代码(作为回调参数)
function double(value, callback) { setTimeout(()=> callback(value*2), 1000) } double(3,(x)=> console.log('I was given: ${x}')) // I was given 6 (1000ms后) 复制代码
2、失败处理
- 成功回调和失败回调
function double(value, success, failure) {
setTimeout(() => {
try {
if (typeof value !== 'number') {
throw 'Must provide number as first argument';
}
success(2 * value);
} catch (e) {
failure(e);
}
}, 1000);
}
const successCallback = (x) => console.log(`Success: ${x}`);
const failureCallback = (e) => console.log(`Failure: ${e}`);
double(3, successCallback, failureCallback);
double('b', successCallback, failureCallback);
// Success: 6(大约 1000 毫秒之后)
// Failure: Must provide number as first argument(大约 1000 毫秒之后)
复制代码
- 不可取,必须在初始化异步操作时定义回调,异步函数的返回值只在短时间内存在,只有预备好将这个短时间存在的值作为参数的回调才能接收到它。
3、嵌套异步函数
- 噩梦一般的 嵌套?
2、期约
- 期约,承诺(promise)
1、Promise/A+ 规范
- ES6起完善支持
2、期约基础
- 引用类型Promise,可以用new操作符来实例化
- 创建时需要传入执行器函数作为参数
1、期约状态机
- 把一个Promise实例传给console.log() 时,控制台输出表明该实例处于**待定(pending)**状态
- 三种状态
- 待定(pending)
- 最初始状态,待定状态下可以落定(settled)为代表成功兑现状态,或者失败的拒绝状态
- 状态修改不可逆
- 不能保证期约必然会脱离待定状态
- 兑现(fulfilled,有时也称解决,resolved)
- 拒绝(rejected)
- 待定(pending)
- 期约的状态是私有的,不能外部检测到,为了避免读取到期约状态,用同步的方式处理期约对象。
- 期约的状态是不能被外部修改。
2、解决值、拒绝理由及期约用例
- 期约用途
- 抽象的表示一个异步操作,期约状态代表期约是否完成,这个状态就是有用的信息。
- 期约封装的异步操作会实际生成某个值,程序期待期约状态改变时可以访问这个值。
- 为了满足这两种用例,每个期约只要状态切换为兑现,就会有一个私有的内部值(value)。每个期约只要切换为拒绝,就会有一个私有的内部理由(reason)。默认是undefined
3、通过执行函数控制期约状态
-
resolve() 会把状态切换为兑现,reject() 会切换为拒绝
let p1 = new Promise((resolve, reject) => resolve()); setTimeout(console.log, 0, p1); // Promise <fulfilled> let p2 = new Promise((resolve, reject) => reject()); setTimeout(console.log, 0, p2); // Promise <fulfilled> // Uncaught error (in promise) 复制代码
-
无论resolve() 和 reject() 哪个状态被调用了,状态转换都不可撤销,修改状态会静默失败
-
为了避免卡在约定状态,可以定时退出。
4、Promise.resolve()
-
期约并非一开始必须处于待定状态,可以通过Promise.resolve()静态方法,实例化一个解决的期约。
let p1 = new Promise((resolve, reject) => resolve()) let p2 = Promise.resolve() 复制代码
-
这个解决的期约的值对应着传给Promise.resolve() 的第一个参数
setTimeout(console.log, 0, Promise.resolve()); // Promise <fulfilled>: undefined setTimeout(console.log, 0, Promise.resolve(3)); // Promise <fulfilled>: 3 // 多余的参数会忽略 setTimeout(console.log, 0, Promise.resolve(4, 5, 6)); // Promise <fulfilled>: 4 复制代码
-
如果传入的参数本身是一个期约,那么它的行为类似空包装,可以说是个幂等方法
let p = Promise.resolve(7) p === Promise.resolve(p) p === Promise.resolve(Promise.resolve(p) 复制代码
-
这个幂等性会保留传入期约的状态
let p = new Promise(() => {}) setTimeout(console.log, 0, p) // Promise <pending> setTimeout(console.log, 0, Promise.resolve(p)) // Promise <pending> setTimeout(console.log,0,p === Promise.resolve(p)) // true 复制代码
-
可以包装任何非期约值,包含错误对象,并将其转换为解决的期约。
let p = Promise.resolve(new Error('foo')); setTimeout(console.log, 0, p); // Promise <fulfilled>: Error: foo 复制代码
5、Promise.reject()
-
与Promise.resolve() 类似,Promise.reject()会实例化一个拒绝的期约并抛出一个异步错误(无法通过try/catch捕获,只能通过拒绝处理程序捕获)
let p1 = new Promise((resolve, reject)=> reject()) let p2 = Promise.reject() 复制代码
-
拒绝的期约理由就是传给Promise.reject() 的第一个参数
let p = Promise.reject(3); setTimeout(console.log, 0, p); // Promise <rejected>: 3 p.then(null, (e) => setTimeout(console.log, 0, e)); // 3 复制代码
-
如果传入期约对象,这个期约会成为它返回的拒绝的理由
setTimeout(console.log, 0, Promise.reject(Promise.resolve())); // Promise <rejected>: Promise <resolved> 复制代码
- Promise.resolve() 会保留传入状态
- Promise.reject() 无论传入什么都会返回失败
6、同步/异步执行的二元性
try {
throw new Error('foo');
} catch (e) {
console.log(e); // Error: foo
}
try {
Promise.reject(new Error('bar'));
} catch (e) {
console.log(e);
}
// Uncaught (in promise) Error: bar
复制代码
- try/catch同步代码之所以没有捕获期约抛出的错误,是因为没有通过异步模式捕获错误。
- 期约的真正特性:它们是同步对象(在同步执行模式中使用),但也是异步执行模式的媒介
3、期约的实例方法
- 期约实例方法可以访问异步操作返回的数据,处理期约成功和失败的结果,连续对期约求值,或者添加只有期约进入终止状态才会执行的代码。
1、实现Thenable接口
-
异步结构,任何对象都有then() 方法,实现了Thenable接口
class MyThenable { then() {} } 复制代码
-
Promise类型实现了Thenable接口。这个简化的接口跟TypeScript获取他包里的接口或类型定义不同,都设定了Thenable接口更具体的形式。
2、Promise.prototype.then()
-
为期约实例添加处理程序的主要方法
-
then() 方法
- 最多接收两个参数 onResolved和onRejected处理程序
- 都是可选的,如果提供的话则会在期约分别进入”兑现”和“拒绝”状态时执行。
function onResolved(id) { setTimeout(console.log, 0, id, 'resolved') } function onRejected(id) { setTimeout(console.log, 0, id, 'rejected') } let p1 = new Promise((resolve, reject)=> { setTimeout(resolve, 3000) }) let p2 = new Promise((resolve, reject)=> { setTimeout(reject, 3000) }) p1.then(() => onResolved('p1'), () => onRejected('p1')) p2.then(() => onResolved('p2'), () => onRejected('p2')) // 3s后 // p1 resolved // p2 rejected 复制代码
- 期约如果只提供onRejected处理程序,第一个参数要传null,这样有助于避免在内存中创建多余对象。
function onResolved(id) { setTimeout(console.log, 0, id, 'resolved'); } function onRejected(id) { setTimeout(console.log, 0, id, 'rejected'); } let p1 = new Promise((resolve, reject)=> { setTimeout(resolve, 3000) }) let p2 = new Promise((resolve, reject)=> { setTimeout(reject, 3000) }); // 非函数处理程序会被静默忽略,不推荐 p1.then('gobbeltygook'); // 不传 onResolved 处理程序的规范写法 p2.then(null, () => onRejected('p2')); // p2 rejected(3 秒后) 复制代码
- Promise.prototype.then()方法返回一个新的期约实例
let p1 = new Promise(() => {}) let p2 = p1.then() setTimeout(console.log, 0, p1) //Promise <pending> setTimeout(console.log, 0, p1) //Promise <pending> setTimeout(console.log, 0, p1===p2) // false 复制代码
- 这个新期约实例基于onResolved 处理程序的返回值构建。
- 该处理程序的返回值会通过Promise.resolve() 包装来生成新期约。
- 如果没有提供这个处理程序,则Promise.resolve()就会包装上一个期约解决之后的值。
- 如果没有显式返回语句,则Promise.resolve()会包装默认的返回值undefined
let p1 = Promise.resolve('foo') // 若调用then()时不传处理程序,则原样向后传 let p2 = p1.then() setTimeout(console.log, 0, p2) // Promise <fulfilled>: foo // 没有显式返回语句都一样 返回undefined let p3 = p1.then(()=> undefined) let p4 = p1.then(()=> {}) let p5 = p1.then(()=> Promise.resolve()) // ...Promise <fulfilled>: undefined // 如果有显式返回值,就会包装这个值 let p6 = p1.then(()=> 'bar') let p7 = p1.then(()=> Promise.resolve('bar')) // ...Promise <fulfilled>: bar // Promise.resolve() 保留返回的期约 let p8 = p1.then(()=> new Promise(()=>{})) let p9 = p1.then(()=> Promise.reject()) setTimeout(console.log, 0, p8) // Promise <pending> setTimeout(console.log, 0, p9) // Promise <rejected>: undefined // 抛出异常会返回拒绝的期约 let p10 = p1.then(()=> {throw 'baz'}) // Uncaught (in promise) baz setTimeout(console.log, 0, p10) // Promise <rejected>: baz // 返回错误值不会触发上面拒绝行为而是会包装在一个解决期约中 let p11 = p1.then(()=> Error('qux')) setTimeout(console.log, 0, p11) // Promise <fulfilled>: Error: qux 复制代码
- onRejected处理程序类似。onRejected处理程序返回的值也会被Promise.resolve() 包装。
- onRejected处理程序任务就是捕获异步错误,使用不抛出异常是符合期约的行为,应该返回一个解决期约。
let p1 = Promise.reject('foo') // 调用then() 不传处理程序则原样向后传 let p2 = p1.then() // Uncaught (in promise) foo setTimeout(console.log, 0, p2) // Promise <rejected>: foo // 没有显式返回语句都一样 返回undefined let p3 = p1.then(null, ()=> undefined) let p4 = p1.then(null, ()=> {}) let p5 = p1.then(null, ()=> Promise.resolve()) // ...Promise <fulfilled>: undefined // 如果有显式返回值,就会包装这个值 let p6 = p1.then(null,()=> 'bar') let p7 = p1.then(null,()=> Promise.resolve('bar')) // ...Promise <fulfilled>: bar // Promise.resolve() 保留返回的期约 let p8 = p1.then(null, ()=> new Promise(()=>{})) let p9 = p1.then(null, ()=> Promise.reject()) setTimeout(console.log, 0, p8) // Promise <pending> setTimeout(console.log, 0, p9) // Promise <rejected>: undefined // 抛出异常会返回拒绝的期约 let p10 = p1.then(null, ()=> {throw 'baz'}) // Uncaught (in promise) baz setTimeout(console.log, 0, p10) // Promise <rejected>: baz // 返回错误值不会触发上面拒绝行为而是会包装在一个解决期约中 let p11 = p1.then(null, ()=> Error('qux')) setTimeout(console.log, 0, p11) // Promise <fulfilled>: Error: qux 复制代码
3、Promise.prototype.catch()
-
用于给期约添加拒绝处理程序,直接收一个参数onRejected处理程序。
-
语法糖,相当于调用Promise.prototype.then(null, onRejected)
let p = Promise.reject() let onRejected = function(e) { setTimeout(console.log, 0, 'rejected') } p.then(null, onRejected) // rejected p.catch(onRejected) // rejected 复制代码
-
Promise.prototype.catch() 返回了一个新期约
let p1 = new Promise(()=> {}) let p2 = p1.catch() setTimeout(console.log, 0, p1) //Promise <pending> setTimeout(console.log, 0, p1) //Promise <pending> setTimeout(console.log, 0, p1===p2) //false 复制代码
-
新期约的行为与Promise.prototype.then() 的onRejected处理程序一样。
4、Promise.prototype.finally()
-
用于给期约添加onFinally处理程序,这个处理程序在期约传换为解决或拒绝状态都会执行。
-
可以避免onResolved和onRejected处理程序出现冗余代码
-
onFinally处理程序无法知道期约状态是解决还是拒绝,所以主要用于清理代码。
let p1 = Promise.resolve() let p2 = Promise.resolve() let onFinally = function(e) { setTimeout(console.log, 0, 'Finally') } p1.finally(onFinally) // Finally p2.finally(onFinally) // Finally 复制代码
-
Promise.prototype.finally() 返回了一个新期约
let p1 = new Promise(()=> {}) let p2 = p1.finally() setTimeout(console.log, 0, p1) //Promise <pending> setTimeout(console.log, 0, p1) //Promise <pending> setTimeout(console.log, 0, p1===p2) //false 复制代码
-
这个新期约实例不同于then() 或 catch() 方式返回的实例。是一个与状态无关的方法,指表现为父期约的传递。
let p1 = Promise.resolve('foo') // 原样往后传 let p2 = p1.finally() let p3 = p1.finally(()=> undefined) let p4 = p1.finally(()=> {}) let p5 = p1.finally(()=> Promise.resolve()) let p6 = p1.finally(()=> 'bar') let p7 = p1.finally(()=> Promise.resolve('bar')) let p8 = p1.finally(()=> Error('qux')) //... Promise <fulfilled>: foo 复制代码
-
如果返回一个待定期约,或者onFinally处理程序抛出了错误,则会返回相应期约(待定或拒绝)
// Promise.resolve() 保留返回的期约 let p9 = p1.finally(()=> new Promise(()=> {})) let p10 = p1.finally(()=> Promise.reject()) // Uncaught (in promise): undefined setTimeout(console.log, 0, p9) // Promise <pending> setTimeout(console.log, 0, p10) // Promise <rejected>: undefined // 抛出异常会返回拒绝的期约 let p11 = p1.finally(()=> {throw 'baz'}) // Uncaught (in promise) baz setTimeout(console.log, 0, p11) // Promise <rejected>: baz 复制代码
-
返回待定期约的情形不常见,因为只要期约一解决,新期约仍然会原样后传初始的期约。
let p1 = Promise.resolve('foo') let p2 = p1.finally(()=> { new Promise((resolve, reject)=> { setTimeout(()=> resolve('bar'), 100) }) }) setTimeout(console.log, 0, p2) // Promise <pending> setTimtout(()=> setTimeout(console.log, 0, p2), 200) // 200ms后 // Promise <fulfilled>: foo 复制代码
5、非重入期约方法
-
期约进入落定状态时,该状态相关的处理程序仅仅会被排期,而不是立即执行。之后的同步代码一定会在处理程序之前先执行。
-
即使期约一开始就与附加处理程序关联,也是这样,称为“非重入”
// 创建解决的期约 let p = Promise.resolve() // 添加解决处理程序 p.then(() => console.log('onResolved handler')) console.log('then() returns') // then() returns // onResolved handler 复制代码
let synchronousResolve; let p = new Promise((resolve)=> { synchronousResolve = function() { console.log('1: invoking resolve()') resolve() console.log('2: resolve() returns') } }) p.then(() => console.log('4: then() handler executes')) synchronousResolve() console.log('3 synchronousResolve() returns') //1234 复制代码
-
非重入适用于onResolved/onRejected处理程序、catch()处理程序和finally() 处理程序。
6、邻近处理程序的执行顺序
-
如果添加了多个处理程序,期约变化时,相关处理程序会按照添加顺序依次执行。无论then() catch() 还是 finally()
let p1 = Promise.resolve() let p2 = Promise.reject() p1.then(()=> setTimeout(console.log, 0, 1)) p1.then(()=> setTimeout(console.log, 0, 2)) // 1 // 2 p2.then(null, ()=> setTimeout(console.log, 0, 3)) p2.then(null, ()=> setTimeout(console.log, 0, 4)) // 3 // 4 p2.catch(()=> setTimeout(console.log, 0, 5)) p2.catch(()=> setTimeout(console.log, 0, 6)) // 5 // 6 p1.finally(()=> setTimeout(console.log, 0, 7)) p1.finally(()=> setTimeout(console.log, 0, 8)) // 7 // 8 复制代码
7、传递解决值和拒绝理由
-
落定状态,期约会提供解决值或拒绝理由,进行进一步操作。
-
解决的值和拒绝的理由分别作为resolve() 和 reject() 的第一个参数
let p1 = new Promise((resolve, reject) => resolve('foo')); p1.then((value) => console.log(value)); // foo let p2 = new Promise((resolve, reject) => reject('bar')); p2.catch((reason) => console.log(reason)); // bar 复制代码
-
Promise.resolve() 和 Promise.reject() 在被调用时就会接收解决值和拒绝理由。也会传给处理程序
let p1 = Promise.resolve('foo'); p1.then((value) => console.log(value)); // foo let p2 = Promise.reject('bar'); p2.catch((reason) => console.log(reason)); // bar 复制代码
8、拒绝期约与拒绝错误处理
-
类似throw() 表示中断,处理函数中抛出错误会导致拒绝。
let p1 = new Promise((resolve, reject) => reject(Error('foo'))); let p2 = new Promise((resolve, reject) => { throw Error('foo'); }); let p3 = Promise.resolve().then(() => { throw Error('foo'); }); let p4 = Promise.reject(Error('foo')); setTimeout(console.log, 0, p1); // Promise <rejected>: Error: foo setTimeout(console.log, 0, p2); // Promise <rejected>: Error: foo setTimeout(console.log, 0, p3); // Promise <rejected>: Error: foo setTimeout(console.log, 0, p4); // Promise <rejected>: Error: foo // 也有四个未捕获错误 复制代码
-
理由可以任意,最好统一使用错误对象。
-
Promise.resolve().then() 的错误最后抛出,因为要创建另一个期约
-
同步中 throw() 抛出错误后,不会继续执行后面语句。
-
异步中 throw() 抛出错误后,并不会阻止运行时继续执行同步指令
-
异步错误只能通过异步的onRejected处理程序捕获
// 正确 Promise.reject(Error('foo')).catch((e)=> {}) // 错误 try{ Promise.reject(Error('foo')) }.catch(e){} 复制代码
-
在解决或拒绝之前的捕获执行函数里,仍可以使用try/catch在执行函数中捕获错误。
let p = new Promise((resolve, reject) => { try { throw Error('foo') }catch(e) {} resolve('bar') }) setTimeout(console.log, 0, p); // promise <fulfilled>: bar 复制代码
-
then() 和 catch() 的 onRejected 处理程序在语义上相当于try/catch
- 出发点都是为了捕获错误后隔离,不影响正常逻辑
- 因此onRejected处理程序的任务应该是在捕获异步错误之后返回一个解决的期约
console.log('begin synchronous execution'); try { throw Error('foo'); } catch (e) { console.log('caught error', e); } console.log('continue synchronous execution'); // begin synchronous execution // caught error Error: foo // continue synchronous execution new Promise((resolve, reject) => { console.log('begin asynchronous execution'); reject(Error('bar')); }).catch((e) => { console.log('caught error', e); }).then(() => { console.log('continue asynchronous execution'); }); // begin asynchronous execution // caught error Error: bar // continue asynchronous execution 复制代码
4、期约连锁与期约合成
- 期约连锁就是一个期约接一个期约拼接。
- 期约合成是将多个期约组合成为一个期约
1、期约连锁
-
因为每个期约实例方法(then()、catch()、finally()) 都会返回一个新的期约对象,新的期约对象又有自己的实例方法。
let p = new Promise((resolve, reject) => { console.log('first'); resolve() }) p.then(()=>console.log('second')) .then(()=>console.log('third')) .then(()=>console.log('fourth')) // first second third fourth 复制代码
- 最终执行了一连串的同步任务,相当于同步函数。
-
真正执行异步任务,可以让每个执行器都返回期约实例,可以让每个后续期约都等待之前的期约。
let p1 = new Promise((resolve, reject) => { console.log('p1') setTimeout(resolve, 1000) }) p1.then(() => new Promise(resolve, reject)=> { console.log('p2') setTimeout(resolve, 1000) }).then(() => new Promise(resolve, reject)=> { console.log('p3') setTimeout(resolve, 1000) }).then(() => new Promise(resolve, reject)=> { console.log('p4') setTimeout(resolve, 1000) }) // p1 1s后 // p2 2s后 // p3 3s后 // p4 4s后 复制代码
-
提前到工厂函数
function delayedResolve(str) { return new Promise((resolve, reject)=> { console.log(str) setTimeout(resolve, 1000) }) } delayedResolve('p1') .then(()=> delayedResolve('p2')) .then(()=> delayedResolve('p3')) .then(()=> delayedResolve('p4')) 复制代码
-
then()、catch()、finally() 都返回期约
let p = new Promise((resolve, reject) => { console.log('initial promise rejects'); reject(); }); p.catch(() => console.log('reject handler')) .then(() => console.log('resolve handler')) .finally(() => console.log('finally handler')); // initial promise rejects // reject handler // resolve handler // finally handler 复制代码
2、期约图
-
一个期约可以有任意多个处理程序,可以构建有向非循环图的结构。
-
每个期约都是图中的一个节点,使用实例方法添加的处理程序是有向节点。图的方向就是期约的解决或拒绝顺序。
// A // / \ // B C // / \ / \ // D E F G let A = new Promise((resolve, reject) => { console.log('A'); resolve(); }); let B = A.then(() => console.log('B')); let C = A.then(() => console.log('C')); B.then(() => console.log('D')); B.then(() => console.log('E')); C.then(() => console.log('F')); C.then(() => console.log('G')); 复制代码
-
输出语句是对二叉树的层序遍历。是按照他们添加的顺序执行的。
3、Promise.all() 和 Promise().race()
1、Promise.all()
-
Promise.all() 静态方法创建的期约会在一组期约全部解决之后再解决。接收一个可迭代对象返回一个新期约。
let p1 = Promise.all([ Promise.resolve(), Promise.resolve() ]) // 可迭代对象中的元素会通过Promise.resolve() 转换为期约 let p2 = Promise.all([3, 4]) // 空的可迭代对象等价于Promise.resolve() let p3 = Promise.all([]) // 无效 let p4 = Promise.all() // TypeError: ... 复制代码
-
合成期约只会在每个包含的期约都解决后才解决
let p = Promise.all([ Promise.resolve(), new Promise((resolve, reject) => setTimeout(resolve, 1000)) ]); setTimeout(console.log, 0, p); // Promise <pending> p.then(() => setTimeout(console.log, 0, 'all() resolved!')); // all() resolved!(大约 1 秒后) 复制代码
-
如果至少有一个包含的期约待定,合成期约也待定
-
如果有一个包含的期约拒绝,合成期约也拒绝
// 永远待定 let p1 = Promise.all([new Promise(() => {})]); setTimeout(console.log, 0, p1); // Promise <pending> // 一次拒绝会导致最终期约拒绝 let p2 = Promise.all([ Promise.resolve(), Promise.reject(), Promise.resolve() ]); setTimeout(console.log, 0, p2); // Promise <rejected> // Uncaught (in promise) undefined 复制代码
-
如果都解决,合成期约的解决值就是所有包含期约解决值的数组,按迭代器顺序。
let p = Promise.all([ Promise.resolve(3), Promise.resolve(), Promise.resolve(4) ]) p.then((values)=> setTimeout(console.log,0,values)) // [3, undefined, 4] 复制代码
-
如果有期约拒绝,则第一个拒绝的期约会将自己的理由作为合成期约的拒绝理由,之后拒绝不会影响最后期约拒绝理由
-
不会影响所有包含期约正常的拒绝操作,合成的期约会静默处理所有包含期约的拒绝操作。
// 虽然只有第一个期约的拒绝理由会进入 // 拒绝处理程序,第二个期约的拒绝也 // 会被静默处理,不会有错误跑掉 let p = Promise.all([ Promise.reject(3), new Promise((resolve, reject) => setTimeout(reject, 1000)) ]); p.catch((reason) => setTimeout(console.log, 0, reason)); // 3 // 没有未处理的错误 复制代码
2、Promise.race()
-
Promise.race() 静态方法返回一个包装期约,是一组集合中最先解决或拒绝期约的镜像。
let p1 = Promise.race([ Promise.resolve(), Promise.resolve() ]) // 可迭代对象中的元素会通过Promise.resolve() 转换为期约 let p2 = Promise.race([3, 4]) // 空的可迭代对象等价于new Promise(()=> {}) let p3 = Promise.race([]) // 无效 let p4 = Promise.race() // TypeError: ... 复制代码
-
不会对解决或拒绝的期约区别对待,无论解决还是拒绝,第一个落定的期约,Promise.race() 就会包装其解决值或拒绝理由并返回新期约。
// 解决先发生,超时后的拒绝被忽略 let p1 = Promise.race([ Promise.resolve(3), new Promise((resolve, reject) => setTimeout(reject, 1000)) ]); setTimeout(console.log, 0, p1); // Promise <fulfilled>: 3 // 拒绝先发生,超时后的解决被忽略 let p2 = Promise.race([ Promise.reject(4), new Promise((resolve, reject) => setTimeout(resolve, 1000)) ]); setTimeout(console.log, 0, p2); // Promise <rejected>: 4 // 迭代顺序决定了落定顺序 let p3 = Promise.race([ Promise.resolve(5), Promise.resolve(6), Promise.resolve(7) ]); setTimeout(console.log, 0, p3); // Promise <fulfilled>: 5 复制代码
-
如果有一个期约先拒绝,只要他是第一个落定的,就会成为拒绝理由。之后在拒绝不会影响最终的拒绝理由。但不会影响正常的拒绝操作,合成的期约会静默处理所有包含期约的拒绝操作。
4、串行期约合成
-
类似函数合成
function addTwo(x) { return x + 2; } function addThree(x) { return x + 3; } function addFive(x) { return x + 5; } function addTen(x) { return Promise.resolve(x) .then(addTwo) .then(addThree) .then(addFive); } addTen(8).then(console.log); // 18 // Array.prototype.reduce() function addTwo(x) { return x + 2; } function addThree(x) { return x + 3; } function addFive(x) { return x + 5; } function addTen(x) { return [addTwo, addThree, addFive] .reduce((promise, fn)=> promise.then(fn), Promise.resolve(x)) } addTen(8).then(console.log); // 18 // 提出公共函数 function addTwo(x) { return x + 2; } function addThree(x) { return x + 3; } function addFive(x) { return x + 5; } function compose(...fns) { return (x)=> fns.reduces((promise, fn)=> promise.then(fn), Promise.resolve(x)) } let addTen = compose(addTwo, addThree, addFive) addTen(8).then(console.log); // 18 复制代码
5、期约扩展
- 第三方期约库不具备 期约取消和进度追踪
1、期约取消
-
通过”取消令牌”, 其实就是一层临时封装,触发取消。
class CancelToken { constructor(cancelFn) { this.promise = new Promise((resolve, reject)=> { cancelFn(resolve) }) } } function cancelableDelayResolve() { } const id = setTimeout(()=> { resolve() }, delay) const cancelToken = new CancelToken((cancelCb)=> { xxxx.addEventListener('click', cancelCb) }) cancelToken.promise.then(()=>clearTimeout(id)) 复制代码
2、期约进度通知
-
通过扩展监控期约执行进度。
-
扩展Promise类,为它添加notify()方法
class TrackablePromise extends Promise { constructor(executor) { const notifyHandlers = [] super((resolve, reject)=> { return executor(resolve, reject, (status) => { notifyHandlers.map((handler) => handler(status)) }) }) this.notifyHandlers = notifyHandlers } notify(notifyHandler) { this.notifyHandlers.push(notifyHandler) return this } } // 这样就可以执行函数时使用notify了。 let p = new TrackablePromise((resolve, reject, notify)=> { function countdown(x) { if(x > 0) { notify(`${20 * x}% remaining`) setTimeout(() => countdown(x - 1), 1000) }else { resolve() } } countdown(5) }) p.notify((x)=> setTimeout(console.log, 0, 'progress:', x)) p.then(()=> setTimeout(console.log, 0 , 'completed')) // (约 1 秒后)80% remaining // (约 2 秒后)60% remaining // (约 3 秒后)40% remaining // (约 4 秒后)20% remaining // (约 5 秒后)completed 复制代码
3、异步函数
-
async/await,ES8 新增,让同步方式写的代码能够异步执行。
// 以往 let p = new Promise((resolve, reject)=>setTimeout(resolve, 1000, 3)) p.then((x)=>console.log(x)) 复制代码
1、异步函数
1、async
-
async 关键词用于声明异步函数,可用在函数声明、函数表达式、箭头函数和方法上。
async function foo() {} let bar = async function() {} let baz = async () => {} class Qux { async qux() {} } 复制代码
-
使用async可让函数具有异步特征,但总体上仍然是同步求值的
-
在参数和闭包方面,异步函数仍然就有普通JavaScript函数的正常行为
async function foo() { console.log(1) } foo() console.log(2) // 1 // 2 复制代码
-
异步函数通过return 返回的值(没有则为undefined),这个值会被Promise.resolve()包装成一个期约对象。异步函数始终返回期约对象。在函数外部调用这个函数可以得到它返回的期约。
async function foo() { console.log(1) return 3 } foo().then(console.log) console.log(2) // 1 2 3 // 直接返回一个期约对象 async function foo() { console.log(1) return Promise.resolve(3) } foo().then(console.log) console.log(2) // 1 2 3 复制代码
-
异步函数返回值期待一个实现(不要求)thenable接口的对象,常规值也可以。如果返回是实现thenable接口的对象,则可以由提供给then()的处理程序“解包”。 否则就当做已解决期约。
// 返回一个原始值 async function foo() { return 'foo'; } foo().then(console.log); // foo // 返回一个没有实现 thenable 接口的对象 async function bar() { return ['bar']; } bar().then(console.log); // ['bar'] // 返回一个实现了 thenable 接口的非期约对象 async function baz() { const thenable = { then(callback) { callback('baz'); } }; return thenable; } baz().then(console.log); // baz // 返回一个期约 async function qux() { return Promise.resolve('qux'); } qux().then(console.log); // qux 复制代码
-
与在期约处理程序中一样, 在异步函数中抛出错误会返回拒绝的期约
async function foo() { console.log(1) throw 3 } foo.catch(console.log) console.log(2) //1 //2 //3 复制代码
-
拒绝期约不会被异步函数捕获
async function foo() { console.log(1) Promise.reject(3) } // Attach a rejected handler to the returned promise foo().catch(console.log) console.log(2) //1 //2 //Uncaught (in promise): 3 复制代码
2、await
-
await 可以暂停异步函数代码的执行,等待期约解决
let p = new Promise((resolve, reject)=>setTimeout(resolve, 1000, 3)) p.then((x)=> console.log(x)) // 3 // ----> async function foo() { let p = new Promise((resolve,reject)=>setTimeout(resolve, 1000, 3)) console.log(await p) } foo() // 3 复制代码
-
await 这个行为与 yield 一样。同样尝试“解包”对象的值。
-
await可以单独使用也可以在表达式使用。
// 异步打印 foo async function foo() { console.log(await Promise.resolve('foo')) } foo() // foo // 异步打印 bar async function bar() { return await Promise.resolve('bar') } bar().then(console.log) // bar // 1000ms 后异步打印 baz async function baz() { await new Promise((resolve, reject) => { return setTimeout(resolve, 1000) }) console.log('baz') } baz() // baz (1000ms 后) 复制代码
-
await期待(不要求)一个实现thenable接口对象。如果是thenable接口对象则进行“解包”,否则当作已解决值
// 等待一个原始值 async function foo() { console.log(await 'foo'); } foo(); // foo // 等待一个没有实现 thenable 接口的对象 async function bar() { console.log(await ['bar']); } bar(); // ['bar'] // 等待一个实现了 thenable 接口的非期约对象 async function baz() { const thenable = { then(callback) { callback('baz'); } }; console.log(await thenable); } baz(); // baz // 等待一个期约 async function qux() { console.log(await Promise.resolve('qux')); } qux(); // qux 复制代码
-
等待会抛出错误的同步操作,会返回拒绝期约
async function foo() { console.log(1) await (()=> {throw 3})() console.log(4) // 不会执行 } foo().catch(console.log) console.log(2) // 1 // 2 // 3 复制代码
-
单独的Promise.reject()不会被异步函数捕获,而会抛出未捕获错误。对拒绝的期约使用await会释放错误值。
async function foo() { console.log(1) await Promise.reject(3) console.log(4) // 不会执行 } foo().catch(console.log) console.log(2) // 1 // 2 // 3 复制代码
3、await的限制
- 必须在异步函数中使用,不能在顶级上下文使用。
- 可以定义并立即调用
- 异步函数特质不会扩展到潜逃函数,await只能直接出现在异步函数的定义中
2、停止和恢复执行
-
顺序问题
async function foo() { console.log(await Promise.resolve('foo')); } async function bar() { console.log(await 'bar'); } async function baz() { console.log('baz'); } foo(); bar(); baz(); // baz // foo // bar // baz bar foo (旧) 复制代码
-
异步函数里不包含await关键字,和普通函数没区别
-
在运行时遇到await 时,会记录在哪里暂停执行,等到await右边的值可用了,会向消息队列中推送任务,这个任务会恢复异步函数执行
-
即使await跟着立即可用的值,函数其他部分也是会异步求值
async function foo() { console.log(2) await null console.log(4) } console.log(1) foo() console.log(3) // 1 2 3 4 复制代码
- 打印1
- 调用异步函数foo()
- 在foo() 中 打印 2
- 在foo() 中 await关键字暂停执行,为立即可用值null向消息队列添加任务
- foo() 退出
- 打印3
- 同步线程的代码执行完毕
- JavaScript运行时从消息队列中取出任务,恢复异步函数执行
- 在foo() 中恢复执行,await 取得null值 (没有使用)
- 在foo() 中 打印4
- foo() 返回
-
若后面是一个期约,则为了执行异步函数,实际会有两个任务被添加到消息队列并被异步求值(新版浏览器只会生成一个异步任务)
async function foo() { console.log(2) // 旧版会多一步添加恢复执行foo()函数的任务 console.log(await Promise.resolve(8)) console.log(9) } async function bar() { console.log(4) console.log(await 6) console.log(7) } console.log(1) foo() console.log(3) bar() console.log(5) // 123456789 旧 // 123458967 新 复制代码
-
3、异步函数策略
1、实现sleep()
async function sleep(delay) {
return new Promise((resolve)=> setTimeout(resolve,delay))
}
async function foo() {
const t0 = Date.now()
await sleep(1500)
console.log(Date.now() - t0)
}
foo()
// 1502
复制代码
2、利用平行执行
-
平行加速的机会
async function randomDelay(id) { // 延迟 0~1000 毫秒 const delay = Math.random() * 1000; return new Promise((resolve) => setTimeout(() => { console.log(`${id} finished`); resolve(); }, delay)); } async function foo() { const t0 = Date.now(); await randomDelay(0); await randomDelay(1); await randomDelay(2); await randomDelay(3); await randomDelay(4); console.log(`${Date.now() - t0}ms elapsed`); } foo(); // 0 finished // 1 finished // 2 finished // 3 finished // 4 finished // 877ms elapsed // for 循环 async function foo() { const t0 = Date.now(); for(let i = 0; i < 5; ++i) { await randomDelay(i) } console.log(`${Date.now() - t0}ms elapsed`); } 复制代码
-
如果期约之间没有依赖,异步函数也会依次暂停保证执行顺序,但总时间也变长。如果无需保证顺序 可以一次性初始化所有契约,分别等待结果。
async function randomDelay(id) { // 延迟 0~1000 毫秒 const delay = Math.random() * 1000; return new Promise((resolve) => setTimeout(() => { setTimout(console.log, 0, `${id} finished`); resolve(); }, delay)); } async function foo() { const t0 = Date.now() const p0 = randomDelay(0) const p1 = randomDelay(1) const p2 = randomDelay(2) const p3 = randomDelay(3) const p4 = randomDelay(4) await p0 await p1 await p2 await p3 await p4 setTimeout(console.log, 0 ,`${Date.now() - t0}ms elapsed`); } foo() // 1 finished // 4 finished // 3 finished // 0 finished // 2 finished // 877ms elapsed // for 循环 async function foo() { const t0 = Date.now() const promises = Array(5).full(null).map((_, i)=>randomDelay(i)) for(const p of promises) { await p } setTimeout(console.log, 0 ,`${Date.now() - t0}ms elapsed`); } 复制代码
-
虽然期约没有按照顺序执行,但await按顺序收到了每个期约的值
async function randomDelay(id) { // 延迟 0~1000 毫秒 const delay = Math.random() * 1000; return new Promise((resolve) => setTimeout(() => { console.log(`${id} finished`); resolve(id); }, delay)); } async function foo() { const t0 = Date.now(); const promises = Array(5).fill(null).map((_, i) => randomDelay(i)); for (const p of promises) { console.log(`awaited ${await p}`); } console.log(`${Date.now() - t0}ms elapsed`); } foo(); 复制代码
3、串行执行期约
function addTow(x) {return x + 2}
function addThree(x) {return x + 2}
function addFive(x) {return x + 5}
async function addTen(x) {
for(const fn of [addTow, addThree, addFive]) {
x = await fn(x)
}
return x
}
addTen(9).then(console.log) // 19
复制代码
4、栈追踪与内存管理
-
期约和异步函数在内存中的差别很大。
function fooPromiseExecutor(resolve, reject) { setTimeout(reject, 1000, 'bar') } function foo() { new Promise(fooPromiseExecutor) } foo() // Uncaught (in promise) bar // setTimeout (async) // fooPromiseExecutor // foo 复制代码
- 有些已经返回的函数,还是出现了。有计算和存储成本
function fooPromiseExecutor(resolve, reject) { setTimeout(reject, 1000, 'bar'); } async function foo() { await new Promise(fooPromiseExecutor); } foo(); // Uncaught (in promise) bar // foo // async function (async) // foo 复制代码
- JavaScript 运行时可以简单的在嵌套韩素中存储指向包含函数的指针,就和对待同步函数调用栈一样。
- 这个指针实际上存储在内存中,可用于再出错事生成栈追踪信息。