ES6 新增了正式的 Promise(期约) 引用类型,支持优雅地定义和组织异步逻辑
接下来几个版本增加了使用 async 和 await 关键字定义异步函数的机制
异步行为是为了优化因计算量大而时间长的操作
如果在等待其他操作完成的同时,即使运行其他指令,系统也能保持稳定,那么这样做就是务实的
异步操作并不一定是计算量大或要等很长时间。只要你不想未等待某个异步操作而阻塞线程执行,那么任何时候都可以使用
同步行为对应内存中顺序执行的处理器指令,每条指令都会严格按照它们出现的顺序要执行
// 每条指令执行后也能立即获得存储在系统本地(如寄存器或系统内存)的信息 // 这样执行流程容易分析程序在执行到代码任意位置时的状态(比如变量的值) // 程序执行的每一步,都可以推断出程序的状态 // 因为后面的指令总是在前面的指令完成后才会执行 let x = 3; x = x + 4; // 等到最后一条指令执行完,存储在 x 的值就立即可以使用
异步行为 类似于系统终端,即当前进程外部的实体可以触发代码执行
异步操作经常是必要的,因为强制进程等待一个长时间的操作通常是不可行的(同步操作则必须要等)
如果代码要访问一些高延迟的资源,比如向远程服务器发送请求并等待响应,那么就会出现长时间的等待
let x = 3; setTimeout(() => x = x + 4, console.log(x), 1000); // 这段程序最终与同步代码执行的任务一样,都是把两个数加在一起, // 但这一次执行线程不知道 x 值何时会改变, // 取决于回调核实从消息队列出列并执行 // 设计一个能知道 x 什么时候可以读取的系统是非常难的。
异步行为是 JavaScript 的基础,但以前的实现并不理想
早期 JavaScript 中,只支持定义回调函数来表明异步操作的完成
串联多个异步操作是一个常见的问题,通常需要深度嵌套的回调函数(俗称”回调地狱“)来解决
function double(value) { setTimeout(() => setTimeout(console.log, 0, value *2), 1000); } double(3); // 6(大约 1000 毫秒之后)
假设 setTimeout 操作会返回一个有用的值
广泛接受的一个策略是给异步操作提供一个回调,这个回调中包含要使用异步返回值的代码(作为回调的参数)
function double(value, callback){ setTimeout(()=> callback(value * 2), 1000) } double(3, (x) => console.log(`I was given: ${x}`)) // I was given: 6(大约 1000 毫秒之后)
异步操作的失败处理在回调模型中也要考虑,因此自然就出现了成功回调和失败回调
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(`Failyure: ${e}`); double(3, successCallback, failureCallback); double('b', successCallback, failureCallback); // Success 6(大约1000毫秒之后) // Failure: Must provide number as first argument(大约1000毫秒之后)
这种模式已经不可取了,因为必须在初始化异步操作时定义回调。
异步函数的返回值只在短时间内存在,只有预备好将这个短时间内存在的值作为参数的回调才能接受到它
如果异步返值又依赖另一个异步返回值,那么回调的情况还会进一步变复杂,这就要求回调嵌套
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) => { double(x, (y) => console.log(`Success: ${y}`)); }; const failureCallback = (e) => console.log(`Failure: ${e}`); double(3, successCallback, failureCallback); // Success: 12(大约 1000 毫秒之后)
随着代码越来越复杂,回调策略是不具有扩展性的
"回调地狱” 这个称呼可谓实至名归。
嵌套回调的代码维护起来就是噩梦
ECMAScript 6增加了对 Promise/A+规范的完善支持,即 Promise 类型 一经推出,Promise 就大受欢迎,成为了主导性的异步变成机制 所有现在浏览器都支持 ES6 期约,很多其他浏览器 API(如 fetch() 和 Battery Status API)也以期约为基础
ECMAScript6 新增的引用类型 Promise, 可以通过 new 操作符来实例化 创建新期约时需要传入执行器(executor)函数作为参数
let p = new Promise(() => {}); setTimeout(console.log, 0, p); // 如果不提供执行器函数,会抛出 SyntaxError
把一个期约实例传给 console.log() 时,控制台输出表明该实例处于 (pending) 状态。 期约的状态是私有的,不能直接通过 JavaScript 检测到。 期约故意将异步行为封装起来,从而隔离外部的同步代码 期约是一个有状态的对象 - 待定(padding) - 期约的最初状态 - 期约可以落定(settled)为代表成功的兑现(fulfilled)状态,或者代表失败拒绝(rejected)状态 - 无论哪种状态都是不可逆的 - 只要从待定转换为兑现或拒绝,期约的状态就不再改变 - 也不能保证期约必然会脱离待定状态 - 兑现(fulfilled,有时候也称为 ”解决“,resolved) - 拒绝(rejected)
期约主要有两大用途 - 抽象地表示一个异步操作。 - 期约的状态代表期约是否完成 - ”待定“表示尚未开始或者正在执行中 - ”兑现“表示已经可以成功完成 - ”拒绝“则表示没有成功完成 期约封装的异步操作会实际生成某个值,而程序起来期约状态改变时可以访问这个值 如果期约被拒绝,程序就会期待状态改变时可以拿到拒绝的理由。 每个期约只要状态切换为兑现,就会有一个私有的内部值(value) 每个期约只要状态切换为拒绝,就会有一个私有的内部理由(reason) 无论是值还是理由,都是包含原始值或兑现的不可修改的引用。 二者都是可选的,且默认值为 undefined 在期约到达某个落定状态时执行的异步代码始终会收到这个值或理由
期约状态是私有的,所以只能在内部进行操作 内部操作在期约的执行器函数中完成 执行函数主要有两个职责 - 初始化期约的异步行为 - 控制状态的最终转换 - 通过调用它的两个函数参数实现的 - 这两个函数命名为 resolve() 和 reject() - 调用 resolve() 会把状态切换为兑现 - 调用 reject() 会把状态切换为拒绝 - 调用 reject() 也会抛出错误
let p1 = new Promise((resolve, reject) => resolve()); setTimeout(console.log, 0, p1) // Promise {: undefined} let p2 = new Promise((resove, reject) => reject()); setTimeout(console.log, 0, p2) // Promise {: undefined} // 这个例子中并没有什么异步操作,因为在初始化期约时,执行器函数以及改变了每个期约的状态 // 关键在于,执行器函数是同步执行的 // 因为执行器函数是期约的初始化程序
new Promise(()=> setTimeout(console.log, 0, 'executor')); setTimeout(console.log, 0, 'promise initialized');
添加 setTimeout 可以推迟切换状态
let p = new Promise((resolve, reject) => setTimeout(resolve, 1000)); // 在 console.log 打印期约实例的时候,还不会执行超时回调(即 resolve()) setTimeout(console.log, 0, p); setTimeout(console.log, 1500, p);
无论 resolve() 和 reject() 中的哪个被调用,状态转换都不可撤销
继续修改状态会静默失败
let p = new Promise((resolve, reject) => { resolve(); reject(); // 没有效果 }); setTimeout(console.log, 0, p); // Promise
为了避免期约卡在待定状态,可以添加一个定时退出功能。
可以通过 setTimeout 设置一个 10 秒钟后无论如何都会拒绝期约的回调
let p = new Promise((resolve, reject) => { setTimeout(reject, 10000); // 10秒钟后调用 reject() // 执行函数的逻辑 }) setTimeout(console.log, 0, p); // Promise setTimeout(console.log, 11000, p); // 11秒后再检查状态
期约的状态只能改变一次,这里的超时拒绝逻辑中可以放心地设置让期约处于待定状态的最长时间
如果执行器中的代码在超时之前已经解决或拒绝,那么超时回调再尝试拒绝也会静默失败
期约并非一开始就必须处于待定状态,通过执行器函数才能转换为落定状态
通过调用 promise.resolve() 静态方法,可以实例化一个解决的期约
let p1 = new Promise((resolve, reject) => resolve()); let p2 = new Promise.resolve();
解决的期约的值对应着传给 Promise.resolve() 的第一个参数
使用这个静态方法,实际上可以把任何值都转换为一个期约
setTimeout(console.log, 0, Promise.resolve()); setTimeout(console.log, 0, Promise.resolve(3)); // 多余的参数会忽略 setTimeout(console.log, 0, Promise.resolve(4, 5, 6));
对于静态方法而言,如果传入的参数本身是一个期约,那它的行为就类似于一个空包装
Promise.resolve() 可以说是一个幂等方法
let p = Promise.resolve(7); setTimeout(console.log, 0, p === Promise.resolve(p)); setTimeout(console.log, 0, p === Promise.resolve(Promise.resolve(p)));
这个幂等性会保留传入期约的状态
let p = new Promise(() => {}); setTimeout(console.log, 0, p); setTimeout(console.log, 0, Promise.resolve(p)); setTimeout(console.log, 0, p === Promise.resolve(p));
这个静态方法能够包装任何非期约值,包括错误对象,并将其转换为解决的期约
也可能导致不符合预期的行为
let p1 = Promise.resolve((resolve, reject) => reject()); let p2 = Promise.reject();
Promise.reject() 会实例化一个拒绝的期约并抛出一个异步错误(不能通过 try catch 捕获,只能通过拒绝处理程序捕获)
let p1 = new Promise((resolve, reject) => reject()); let p2 = new Promise.reject();
这个拒绝的期约的理由就是传给 Promise.reject() 的第一个参数
这个参数也会传给后续的拒绝处理程序
let p = Promise.resolve(new Error('foo')); setTimeout(console.log, 0, p); p.then(null, (e) => setTimeout(console.log, 0, e));
Promise.reject() 如果给它传一个期约对象,则这个期约会成为它返回的拒绝期约的理由
setTimeout(console.log, 0, Promise.reject(Promise.resolve()));
Promise 的设计很大程度上会导致一种完全不同于 JS 的计算模式
try{ throw new Error('foo'); }catch(e){ console.log(e); // Error: foo } try{ Promise.reject(new Error('foo')); // 拒绝期约的错误没有抛到执行的同步代码的线程里,而是通过浏览器异步消息队列来处理的 // try/catch块并不能捕获该错误 // 代码一旦开始以异步模式执行 // 唯一与之交互的方式就是使用异步结构--期约的方法 }catch(e) { console.log(e); }
期约的实例方法是连接外部同步代码与内部异步代码之间的桥梁 这些方法可以访问异步操作返回的数据 - 处理期约成功和失败的结果,连续对期约求值,或者添加只有期约进入终止状态时才会执行的代码
在 ECMAScript 暴露的异步结构中,任何对象都有 then() 方法 这个方法被认为实现了 Thenable 接口
// 实现这一接口最简单的类 class MyThenable { then (){} } // ECMAScript 的 Promise 类型实现了 Thenable 接口 // 这个简化的接口跟 TypeScript 或其他包中的接口或类型定义不同, // 它们都设定了 Thenable 接口更具体的形式
Promise.prototype.then() 是为期约实例添加处理程序的主要方法
then() 方法接收最多两个参数 - onResolved 处理程序 - onReject 处理程序 两个参数都是可选的,如果提供的话 则会在期约分别进入“兑现”和“拒绝”状态时执行
function onResolved(id) { setTimeout(console.log, 0, id, 'resolved'); } function onReject(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('onResolved p1'), () => onReject('onReject p1') ); p2.then( () => onResolved('onResolved p2'), () => onReject('onReject p2') );
因为期约只能转换为最终状态一次,所以这两个操作一定是互斥的
两个处理程序参数都是可选的 传给 then() 的任何非函数类型的参数都会被静默忽略。 如果只想提供 onRejected 参数,那就要在 onResolved 参数的位置上传入 undefined 这样有助于避免在内存中创建多余的对象,对期待函数参数的类型系统也是一个交代
function onResolved(id) { setTimeout(console.log, 0, id, 'resolved'); } function onReject(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, () => onReject('p2'))
Promise.prototyoe.then() 方法返回一个新的期约实例
let p1 = new Promise(() => {}); let p2 = p1.then(); setTimeout(console.log, 0, p1); // Promise setTimeout(console.log, 0, p2); // Promise setTimeout(console.log, 0, p1 === p2); // false
新期约实例基于 onResolve 处理程序的返回值构建
// 该处理程序的返回值会通过 Promise.resolve() 包装来生成新的期约 // 如果没有提供这个处理程序,则 Promise.resolve() 就会包装上一个期约解决之后的值 // 如果没有显式的返回语句,则 Promise.resolve() 会包装默认的返回值 undefined let p1 = Promise.resolve('foo'); // 若调用 then() 不传处理程序,则原样向后传 let p2 = p1.then(); setTimeout(console.log, 0, p2); // Promise {: 'foo'} // 这些都一样 let p3 = p1.then(() => undefined); let p4 = p1.then(() => {}); let p5 = p1.then(() => Promise.resolve()); setTimeout(console.log, 0, p3); // Promise {: undefined} setTimeout(console.log, 0, p4); // Promise {: undefined} setTimeout(console.log, 0, p5); // Promise {: undefined} // 如果有显式的返回值,则 Promise.resolve() 会包装这个值 let p6 = p1.then(() => 'bar'); let p7 = p1.then(() => Promise.resolve('bar')); setTimeout(console.log, 0, p6); // Promise {: 'bar'} setTimeout(console.log, 0, p7); // Promise {: 'bar'} // Promise.resolve() 保留返回的期约 let p8 = p1.then(() => new Promise(() => {})); let p9 = p1.then(() => Promise.resolve()); setTimeout(console.log, 0, p8); // Promise {} setTimeout(console.log, 0, p9); // Promise {: 'baz'} // 抛出异常会返回拒绝的期约 let p10 = p1.then(() => {throw 'baz'}); setTimeout(console.log, 0, p10); // Promise {: 'baz'} // 返回错误值不会触发拒绝行为,而会把错误对象包装在一个解决的期约中 let p11 = p1.then(() => Error('qux')); setTimeout(console.log, 0, p11); // Promise {: Error: qux
onRejected 处理程序也与之类似
onRejected 处理程序返回的值也会被 Promise.resolve() 包装
let p1 = Promise.resolve('foo'); // 调用 then() 时不传处理程序则原样向后传 let p2 = p1.then(); setTimeout(console.log, 0, p2); // Promise foo; // 这些都一样 let p3 = p1.then(null, () => undefined); let p4 = p1.then(null, () => {}); let p5 = p1.then(null, () => Promise.resolve()); setTimeout(console.log, 0, p3); setTimeout(console.log, 0, p4); setTimeout(console.log, 0, p5); // 这些都一样 let p6 = p1.then(null, () => 'bar'); let p7 = p1.then(null, () => Promise.resolve('bar')); setTimeout(console.log, 0, p6); setTimeout(console.log, 0, p7); // Promise.resolve() 保留返回的期约 let p8 = p1.then(null, () => new Promise(() => {})); let p9 = p1.then(null, () => Promise.resolve('bar')); setTimeout(console.log, 0, p8); setTimeout(console.log, 0, p9); let p10 = p1.then(null, () => Error('qux')); setTimeout(console.log, 0, p10);
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); p.then(onRejected);
Promise.prototype.catch() 返回一个新的期约实例
let p1 = new Promise(() => {}); let p2 = p1.catch(); setTimeout(console.log, 0, p1); setTimeout(console.log, 0, p2); setTimeout(console.log, 0, p1 === p2);
Promise.prototype.finally() 方法用于给期约添加 onFinally 处理程序
这个处理程序在期约转换为解决或拒绝状态时都会执行 这个方法可以避免 onResolved 和 onRejected 处理程序中出现冗余代码 但 onFinally 处理程序没有办法知道期约的状态是解决还是拒绝 所以这个方法主要用于添加清理代码
let p1 = Promise.resolve(); let p2 = Promise.reject(); let onFinally = function(){ setTimeout(console.log, 0, 'Finally!'); }; p1.finally(onFinally) p2.finally(onFinally)
Promise.finally 方法返回新的期约实例
let p1 = new Promise(() => {}); let p2 = p1.finally(); setTimeout(console.log, 0, p1); setTimeout(console.log, 0, p2); setTimeout(console.log, 0, p1 === p2);
这个新期约实例不同于 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')); setTimeout(console.log, 0, p2) // Promise {: 'foo'} setTimeout(console.log, 0, p3) // Promise {: 'foo'} setTimeout(console.log, 0, p4) // Promise {: 'foo'} setTimeout(console.log, 0, p5) // Promise {: 'foo'} setTimeout(console.log, 0, p6) // Promise {: 'foo'} setTimeout(console.log, 0, p7) // Promise {: 'foo'} setTimeout(console.log, 0, p8) // Promise {: 'foo'}
如果返回的是一个待定的期约,或者 onFinally 处理程序抛出了错误(显式抛出或返回一个拒绝期约),则会返回相应的期约(待定或拒绝)
let p1 = Promise.resolve('foo'); // Promise.resolve() 保留返回的期约 let p9 = p1.finally(() => new Promise(() => {})); let p10 = p1.finally(() => Promise.reject()); setTimeout(console.log, 0, p9); // Promise {} setTimeout(console.log, 0, p10); // Promise {: undefined} let p11 = p1.finally(() => { throw 'baz'; }); setTimeout(console.log, 0, p11); // Promise {: 'baz'}
返回待定期约的情形并不常见,这是因为只要期约一解决,新期约仍让会原样后传初始的期约
let p1 = Promise.resolve('foo'); // 忽略解决的值 let p2 = p1.finally(() => new Promise((resolve, reject) => setTimeout(() => resolve('bar'), 100))); setTimeout(console.log, 0, p2); // Promise {} setTimeout(()=> setTimeout(console.log, 0, p2), 200); // Promise {: 'foo'}
当期约进入落定状态时,与该状态相关的处理程序仅仅会被排期,而非立即执行 跟在添加这个处理程序的代码之后的同步代码一定会在处理程序之前先执行。 即使期约一开始就是与附加处理程序关联的状态,执行顺序也是这样的。 这个特性由 JavaScript 运行时保证,被称为 “非重入” (non-reentrancy) 特性
// 创建解决的期约 let p = Promise.resolve(); // 添加解决处理程序 // 直觉上,这个处理程序会等期约一解决就执行 p.then(() => console.log('onResolved handler')); // 同步输出,证明 then() 已经返回 console.log('then() return'); // 实际上 // then() return // onResolved handler // 在一个解决期约上调用 then() 会把 onResolved 处理程序推进消息队列 // - 这个处理程序在当前线程上的同步代码执行完成前不会执行 // - 跟在 then 后面的同步代码已经先于处理程序执行
// 先添加处理程序后解决期约也是一样的 // 如果添加处理程序后,同步代码才改变期约状态,那么处理程序仍然会基于该状态表现出非重入特性 let synchronousResolve; // 创建一个期约并将解决函数保存在一个局部变量中 let p = new Promise((resolve) => { synchronousResolve = function() { console.log('1: resolve') resolve(); console.log('2: resolve() returns'); }; }); p.then(() => console.log('4: then() handler executes')); synchronousResolve() console.log('3: synchronousResolve() returns') // 1: resolve // 2: resolve() returns // 3: synchronousResolve() returns // 4: then() handler executes // 即使期约状态变化发生在添加处理程序之后 // 处理程序也会等到运行的消息队列让它出列时才会执行
非重入适用于 onResolved/onRejected 处理程序, catch()/ finally() 处理程序
let p1 = Promise.resolve(); p1.then(() => console.log('p1.then() onResolved')); console.log('p1.then() returns'); let p2 = Promise.reject(); p2.then(null, () => console.log('p2.then() onRejected')); console.log('p2.then() returns'); let p3 = Promise.reject(); p3.catch(() => console.log('p3.then() onRejected')); console.log('p3.then() returns'); let p4 = Promise.resolve(); p4.finally(() => console.log('p4.finally() onFinally')); console.log('p4.then() returns') // p1.then() returns // p2.then() returns // p3.then() returns // p4.then() returns // p1.then() onResolved // p2.then() onRejected // p3.then() onRejected // p4.finally() onFinally
如果给期约添加了多个处理程序,当期约状态变化时,相关处理程序会按照添加它们的顺序依次执行 无论是 then(), catch() 还是 finally() 添加的处理程序都是如此
let p1 = Promise.resolve(); let p2 = Promise.reject(); p1.then(() => setTimeout(console.log, 0, 1)); p2.then(null, () => setTimeout(console.log, 0, 2)); p2.then(null, () => setTimeout(console.log, 0, 3)); p2.then(null, () => setTimeout(console.log, 0, 4)); p2.catch(() => setTimeout(console.log, 0, 5)); p2.catch(() => setTimeout(console.log, 0, 6)); p1.finally(() => setTimeout(console.log, 0, 7)); p1.finally(() => setTimeout(console.log, 0, 8));
到了落定状态后
期约会提供其解决值(如果兑现)或其拒绝理由(如果拒绝)给相关状态的处理程序
拿到返回值后,就可以进一步对这个值进行操作
在执行函数中,解决的值和拒绝的理由分别作为 resolve() 和 reject() 的第一个参数往后传的。 这些值又会传给它们各自的处理程序,作为 onResolved 和 onReject 处理程序的唯一参数
let p1 = new Promise((resolve, reject) => resolve('p1 foo')); p1.then((value) => console.log(value)); let p2 = new Promise((resolve, reject) => reject('p2 bar')); p2.catch((value) => console.log(value));
Promise.resolve() 和 Promise.reject()
在被调用时就会接收解决值和拒绝理由
它们返回的期约也会像执行器一样把这些值传给 onResolved 和 onRejected 处理程序
let p1 = Promise.resolve('p1 foo'); p1.then((value) => console.log(value)); let p2 = Promise.reject('p2 bar'); p2.catch((reason) => console.log(reason));
拒绝期约类似于 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 {: Error: foo setTimeout(console.log, 0, p2); // Promise {: Error: foo setTimeout(console.log, 0, p3); // Promise {: Error: foo setTimeout(console.log, 0, p4); // Promise {: Error: foo // 期约可以以任何理由拒绝,包括undefined,但最好统一使用错误对象 // 主要时因为创建错误对象可以让浏览器捕获错误对象中的栈跟踪信息 // 而这些信息对调试是非常关键的 // 所有错误都是异步抛出且未处理的,通过错误对象捕获的栈跟踪信息展示了错误发生的路径 // 注意错误的顺序:Promise.resolve().then() 的错误才最后出现,因为它需要在运行时消息队列中添加处理程序 // 在最终抛出未捕获错误之前它还会创建另一个期约
// 正常情况下,在通过 throw() 关键字抛出错误时 // JavaScript 运行时的错误处理机制会停止执行抛出错误之后的任何指令 throw Error('foo'); console.log('bar'); // 这一行不会执行
在期约中抛出错误时,并不会阻止运行时继续执行的同步指令
// 在期约中抛出错误,实际上是从消息队列中异步抛出的 Promise.reject(Error('foo')); console.log('bar');
异步错误只能通过异步的 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);
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') 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'); });
多个期约组合在一起可以构成强大的代码逻辑 - 期约连锁 - 一个期约接一个期约地拼接 - 期约合成 - 将多个期约组合为一个期约
把期约逐个地串联起来是一种非常有用的编程模式
每个期约实例的方法(then(), catch(), finally()) 都会返回一个新的期约对象 新的期约对象又有自己的实例方法 这样连缀方法调用就可以构成所谓的 “期约连锁“
let p = new Promise((resolve, reject) => { console.log('Promise first'); resolve(); }) p.then(() => console.log('Promise second')) .then(() => console.log('Promise third')) .then(() => console.log('Promise fourth'));
这种方式执行的任务没有那么有用,分别使用 4 个同步函数也可以做到
(() => console.log('Thread first'))(); (() => console.log('Thread second'))(); (() => console.log('Thread third'))(); (() => console.log('Thread fourth'))();
真正执行异步任务,可以改写前面的例子,这样就可以让每个后续期约都等待刷新之前的期约,也就是串行化异步任务
let p1 = new Promise((resolve, reject) => { console.log('p1 executor'); setTimeout(resolve, 1000); }); p1.then(() => new Promise((resolve, reject) => { console.log('p2 executor'); setTimeout(resolve, 1000) })).then(() => new Promise((resolve, reject) => { console.log('p3 executor'); setTimeout(resolve, 1000); })).then(() => new Promise((resolve, reject) => { console.log('p4 executor') setTimeout(resolve, 1000); }))
把生成期约的代码提取到一个工厂函数中
function delayedResolve(str){ return new Promise((resolve, reject) => { console.log(str); setTimeout(resolve, 1000); }) } delayedResolve('p1 executor') .then(()=> delayedResolve('p2 executor')) .then(()=> delayedResolve('p3 executor')) .then(()=> delayedResolve('p4 executor')) .then(()=> delayedResolve('p5 executor'))
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'));
一个期约可以有任意多个处理程序,期约连锁可以构建有向非循环图的结构 - 使用实例方法添加的处理程序则是有向顶点。 - 图中的每个节点都会等待前一个节点落定,图的方向就是期约的解决或拒绝顺序
/* 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')); // 日志的输出是对二叉树层序遍历。 // 期约的处理程序是按照它们添加的顺序执行的 // 期约的处理程序是先添加到消息队列,然后才逐个执行,因此构成了层序遍历
Promise.all() 静态方法创建的期约会在一组期约全部解决之后再解决 这个静态方法接收一个可迭代对象,返回一个新期约
let p1 = Promise.all([ Promise.resolve(), Promise.resolve() ]) // 可迭代对象中的元素会通过 Prmise.resolve() 转换为期约 let p2 = Promise.all([3, 4]); // 空的可迭代对象等价于 Promise.resolve() let p3 = Promise.all([]) // 无效的语法 let p4 = Promise.all(); // TypeError: connot read Symbol.iterator of undefined
合成的期约只会在每个包含的期约都解决之后才解决
let p = Promise.all([ Promise.resolve(), new Promise((resolve, reject) => setTimeout(resolve, 1000)) ]) setTimeout(console.log, 0, p); p.then(() => setTimeout(console.log, 0, 'all() resolved!'));
如果至少有一个包含的期约待定,则合成的期约也会待定
// 永远待定 let p1 = Promise.all([ new Promise(() => {}) ]) setTimeout(console.log, 0, p1);
如果有一个包含的期约拒绝,则合成的期约也会拒绝
// 一次拒绝会导致最终期约拒绝 let p2 = Promise.all([ Promise.resolve(), Promise.reject(), Promise.resolve() ]) setTimeout(console.log, 0, p2)
如果所有期约都成功解决,则合成的期约解决值就是所有包含期约解决值的数组,按照迭代顺序
let p = Promise.all([ Promise.resolve(3), Promise.resolve(), Promise.resolve(4) ]) p.then((values) => setTimeout(console.log, 0, values));
如果有期约拒绝,则第一个拒绝的期约会将拒绝的理由作为合成期约的拒绝理由。
之后再拒绝的期约不会影响最终期约的拒绝理由
这并不影响所有包含期约的正常的拒绝操作,合成的期约会静默处理所有包含期约的拒绝操作
// 虽然只有第一个期约拒绝理由会进入拒绝处理程序, // 第二个期约的拒绝也会被静默处理,不会有错误跑掉 let p = Promise.all([ Promise.reject(3), new Promise((resolve, reject) => setTimeout(reject, 1000)) ]) p.catch((reason) => setTimeout(console.log, 0, reason)) // 没有未处理的错误
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: cannot read Symbol.iterator of undefined
Promise.race() 不会对解决或拒绝的期约区别对待。
无论是解决还是拒绝,只要是第一个落定的期约,
Promise.race() 就会包装其解决值或拒绝理由并返回新期约
// 解决先发生,超时后拒绝被忽略 let p1 = Promise.race([ Promise.resolve(3), new Promise((resolve, reject) => setTimeout(reject, 1000)) ]); setTimeout(console.log, 0, p1); // Promise {: 3} // 拒绝先发生,超时后的解决被忽略 let p2 = Promise.race([ Promise.reject(4), new Promise((resolve, reject) => setTimeout(resolve, 1000)) ]); setTimeout(console.log, 0, p2); // Promise {: 4} // 迭代顺序决定了落定顺序 let p3 = Promise.race([ Promise.resolve(5), Promise.resolve(6), Promise.resolve(7) ]); setTimeout(console.log, 0, p3); // Promise {: 5}
如果有一个期约拒绝,只要它是第一个落定的,就会成为拒绝合成期约的理由 之后再拒绝的期约不会影响最终期约的拒绝理由。 但是,这并不影响所有包含期约正常的拒绝操作 与 Promise.all() 类似,合成的期约会静默处理所有包含期约的拒绝操作
// 虽然只有第一个期约的拒绝理由会进入拒绝处理程序, // 第二个期约的拒绝也会被静默处理,不会有错误跑掉 let p = Promise.race([ Promise.reject(3), new Promise((resolve, reject) => setTimeout(reject, 1000)) ]) p.catch((reason) => setTimeout(console.log, 0, reason)); // 3 // 没有未处理的错误
异步产生值并将其传给处理程序
基于后续期约使用之前期约的返回值来串联期约是期约的基本功能
很像函数合成,即将多个函数合成为一个函数
function addTwo(x) {return x +2;} function addThree(x) {return x +3;} function addFive(x) {return x + 5;} function addTen(x) { return addFive(addTwo(addThree(x))); } console.log(addTen(7))
有 3 个函数基于一个值合成为一个函数
期约也可以像这样合成起来,渐进地消费一个值,并返回结果
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)
使用 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)
这种模式可以提炼出一个通用函数,可以把任意多个函数作为处理程序合成一个连续传值的期约连锁
function addTwo(x) { return x + 2; } function addThree(x) { return x + 3; } function addFive(x) { return x + 5; } function compose(...fns){ return (x) => fns.reduce((promise, fn) => promise.then(fn), Promise.resolve(x)) } let addTen = compose(addTwo, addThree, addFive); addTen(8).then(console.log)
异步函数,也成为 "async/await" (语法关键字),是 ECMAScript 函数中的应用。 async / await 是 ES8 规范新增的 让以同步方式写的代码能够异步执行
let p = new Promise((resolve, reject) => setTimeout(resolve, 1000, 3)); // 这个期约在 1000 毫秒之后解决为数值3
如果程序中其他代码要在这个值可用时访问它,则需要写一个解决处理程序
let p = new Promise((resolve, reject) => setTimeout(resolve, 1000, 3)); p.then((x) => console.log(x));
这其实是很不方便的,可以把处理程序定义为一个函数
function handler(x){ console.log(x) }; let p = new Promise((resolve, reject) => setTimeout(resolve, 1000, 3)); p.then(handler);
ES8 的 async / await 旨在解决利用异步结构组织代码的问题
ECMAScript 对函数进行了扩展,为其增加了两个新关键字: async / await
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);
异步函数如果使用 return 关键字返回了值,这个值会被 Promise.resolve() 包装成一个期约对象
如果没有 return 则会返回 undefined
异步函数始终返回期约对象,在函数外部调用个函数可以得到它返回的期约
async function foo() { console.log(1); return 3; } // 给返回的期约添加一个解决处理程序 foo().then(console.log); console.log(2)
直接返回一个期约对象也是一样的
async function foo(){ console.log(1); return Promise.resolve(3); } // 给返回的期约添加一个解决处理程序 foo().then(console.log); console.log(2);
异步函数的返回值期待(但实际上并不要求)一个实现 thenable 接口的对象,常规的值也可以
如果返回的是实现 thenable 接口的对象,则这个对象可以由提供给 then() 的处理程序 “解包”
如果不是,则返回值就被当作已经解决的期约
// 返回一个原始值 async function foo() { return 'foo'; } foo().then(console.log); // 返回一个没有实现 thenable 接口的对象 async function bar() { return ['bar']; } bar().then(console.log); // 返回一个实现了 thenable 接口的非期约对象 async function baz() { return { then(callback) { callback('baz'); } }; } baz().then(console.log); // 返回一个期约 async function qux() { return Promise.resolve('qux'); } qux().then(console.log);
与在期约处理程序中一样,在异步函数中抛出错误会返回拒绝的期约
// 与在期约处理程序中一样,但异步函数中抛出错误会返回拒绝的期约 async function foo(){ console.log(1); throw 3; } // 给返回的期约添加一个拒绝处理程序 foo().catch(console.log) console.log(2)
拒绝期约的错误不会被异步函数捕获
async function foo() { console.log(1); return Promise.reject(3); } foo().catch(console.log); console.log(2);
await 关键字会暂停执行异步函数后面的代码,让出 JavaScript 运行时的执行线程 这个行为与生成器函数中的 yield 关键字是一样的 await 关键字同样是尝试 “解包” 对象的值,然后将这个值传给表达式,再异步恢复异步函数的执行
await 关键字的用法与 JavaScript 的一元操作一样
它可以单独使用,也可以在表达式中使用
// 异步打印 foo async function foo() { console.log(await Promise.resolve('foo')); } foo(); // 异步打印 bar async function bar() { return await Promise.resolve('bar'); } bar().then(console.log); // 1000 毫秒后异步打印 baz async function baz() { await new Promise((resolve, reject) => setTimeout(console.log, 1000, 'baz')); } baz()
await 关键字期待一个实现 thenable 接口的对象,常规的值也可以
如果是实现 thenable 接口的对象,这个对象可以由 awati 来 “解包”
如果不是,则这个值就被当作已经解决的期约
// 等待一个原始值 async function foo() { console.log(await 'foo'); } foo(); // 等待一个没有实现 thenable 接口的对象 async function bar() { console.log(await ['bar']); } bar(); // 等待一个实现了 thenable 接口的非期约对象 async function baz() { const thenable = { then(callback) { callback('baz') } } console.log(await thenable) } baz(); // 等待一个期约 async function qux() { console.log(await Promise.resolve('qux')); } qux();
等待会抛出错误的同步操作,会返回拒绝的期约
async function foo() { console.log(1); await (() => {throw 3})(); } // 给返回的期约添加一个拒绝处理程序 foo().catch(console.log); console.log(2)
单独的 Promise.reject() 不会被异步捕获,而会抛出未捕获的错误
对拒绝的期约使用 await 则会释放(unwrap)错误值(将拒绝的期约返回)
async function foo() { console.log(1); await Promise.reject(3); console.log(4); // 这行代码不会执行 } // 给返回的期约添加一个拒绝处理程序 foo().catch(console.log); console.log(2);
await 关键字必须在异步函数中使用
不能在顶级上下文如