JavaScript高级程序设计(4)
第11章 期约与异步函数
### 11.1 异步编程 322 ##### 11.1.1 同步与异步 > 同步行为对应内存中顺序执行的处理器指令,每条指令都会严格按照它们出现的顺序来执行。 > > 异步行为类似于系统中断,即当前进程外部的实体可以触发代码执行。 ##### 11.1.2 以往的异步编程模式 - 异步返回值 ```javascript function double(value, callback) { setTimeout(() => callback(value * 2), 1000); } double(3, (x) => console.log(`I was given: ${x}`)); ``` - 失败处理 这种模式已经不可取了,因为必须在初始化异步操作时定义回调。异步函数的返回值只在短时间内存在,只有预备好将这个短时间内存在的值作为参数的回调才能接收到它。 - 嵌套异步回调 如果异步返值又依赖另一个异步返回值,那么回调的情况还会进一步变复杂。显然,随着代码越来越复杂,回调策略是不具有扩展性的。“回调地狱”这个称呼可谓名至实归。嵌套回调的代码维护起来就是噩梦。 ### 11.2 期约 325 ##### 11.2.1 Promises/A+规范 早期的期约机制在 jQuery 和 Dojo 中是以 Deferred API 的形式出现的。到了 2010 年,CommonJS 项目实现的 Promises/A 规范日益流行起来。2012 年 Promises/A+组织分叉(fork)了 CommonJS 的 Promises/A 建议,并以相同的名字制定了 Promises/A+规范。这个规范最终成为了ECMAScript 6 规范实现的范本,即 Promise 类型。一经推出,Promise 就大受欢迎,成为了主导性的异步编程机制。 ##### 11.2.2 期约基础 - 期约状态机 - 待定(pending):表示尚未开始或者正在执行中。 - 兑现(fulfilled,有时候也称为“解决”,resolved):表示已经成功完成。 - 拒绝(rejected):表示没有成功完成。 待定(pending)是期约的最初始状态。在待定状态下,期约可以落定(settled)为代表成功的兑现(fulfilled)状态,或者代表失败的拒绝(rejected)状态。无论落定为哪种状态都是不可逆的。只要从待定转换为兑现或拒绝,期约的状态就不再改变。而且,也不能保证期约必然会脱离待定状态。因此,组织合理的代码无论期约解决(resolve)还是拒绝(reject),甚至永远处于待定(pending)状态,都应该具有恰当的行为。 重要的是,期约的状态是私有的,不能直接通过 JavaScript 检测到。这主要是为了避免根据读取到的期约状态,以同步方式处理期约对象。另外,期约的状态也不能被外部 JavaScript 代码修改。这与不能读取该状态的原因是一样的:期约故意将异步行为封装起来,从而隔离外部的同步代码。 - 解决值、拒绝理由及期约用例 每个期约只要状态切换为兑现,就会有一个私有的内部值(value)。每个期约只要状态切换为拒绝,就会有一个私有的内部理由(reason)。无论是值还是理由,都是包含原始值或对象的不可修改的引用。二者都是可选的,而且默认值为 undefined。在期约到达某个落定状态时执行的异步代码始终会收到这值或理由。 - 通过执行函数控制期约状态 控制期约状态的转换是通过调用它的两个函数参数实现的。这两个函数参数通常都命名为 resolve()和 reject()。调用resolve()会把状态切换为兑现,调用 reject()会把状态切换为拒绝。另外,调用 reject()也会抛出错误(后面会讨论这个错误)。 ```javascript 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> // Uncaught error (in promise) ``` - **Promise.resolve()**:实例化一个解决的期约 ```javascript //下面两个期约实例实际上是一样的 let p1 = new Promise((resolve, reject) => resolve()); let p2 = Promise.resolve(); setTimeout(console.log, 0, Promise.resolve()); // Promise <resolved>: undefined setTimeout(console.log, 0, Promise.resolve(3)); // Promise <resolved>: 3 setTimeout(console.log, 0, Promise.resolve(4, 5, 6)); // Promise <resolved>: 4 ``` * **Promise.reject()**:实例化一个拒绝的期约并抛出一个异步错误(该错误不能通过 try/catch 捕获,只能通过拒绝处理程序捕获)。 ```javascript //下面两个期约实例实际上是一样的 let p1 = new Promise((resolve, reject) => reject()); let p2 = 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 ``` - 同步/异步执行的二元性 >期约真正的异步特性:它们是同步对象(在同步执行模式中使用),但也是异步执行模式的媒介。 ##### 11.2.3 期约的实例方法 1. 实现 **Thenable** 接 > 在暴露的异步结构中,任何对象都有一个 then()方法。这个方法被认为实现了Thenable 接口。 Promise 类型实现了 Thenable 接口。 ```javascript class MyThenable { then() {} } ``` 2. **Promise.prototype.then()** 是为期约实例添加处理程序的主要方法。接收最多两个参数:onResolved 处理程序和 onRejected 处理程序。两个参数可选,如果提供的话,则会在期约分别进入“兑现”和“拒绝”状态时执行。 ```javascript 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')); ``` 3. **Promise.prototype.catch()** >用于给期约添加拒绝处理程序。只接收一个参数:onRejected 处理程序。事实上,这个方法就是一个语法糖,调用它就相当于调用 Promise.prototype. then(null, onRejected)。返回一个新的期约实例。 ```javascript let p = Promise.reject(); let onRejected = function(e) { setTimeout(console.log, 0, 'rejected'); }; // 这两种添加拒绝处理程序的方式是一样的: p.then(null, onRejected); // rejected p.catch(onRejected); // rejected ``` 4. **Promise.prototype.finally()** > 用于给期约添加 onFinally 处理程序,这个处理程序在期约转换为解决或拒绝状态时都会执行。这个方法可以避免 onResolved 和 onRejected 处理程序中出现冗余代码。但 onFinally 处理程序没有办法知道期约的状态是解决还是拒绝,所以这个方法主要用于添加清理代码。因为 onFinally 被设计为一个状态无关的方法,所以在大多数情况下它将表现为父期约的传递。对于已解决状态和被拒绝状态都是如此。 5. 非重入期约方法 > 当期约进入落定状态时,与该状态相关的处理程序仅仅会被排期,而非立即执行。跟在添加这个处理程序的代码之后的同步代码一定会在处理程序之前先执行。即使期约一开始就是与附加处理程序关联的状态,执行顺序也是这样的。这个特性由 JavaScript 运行时保证,被称为“非重入”(non-reentrancy)特性。 6. 邻近处理程序的执行顺序 > 如果给期约添加了多个处理程序,当期约状态变化时,相关处理程序会按照添加它们的顺序依次执行。无论是 then()、catch()还是 finally()添加的处理程序都是如此。 7. 传递解决值和拒绝理由 > 在执行函数中,解决的值和拒绝的理由是分别作为 resolve()和 reject()的第一个参数往后传的。然后,这些值又会传给它们各自的处理程序,作为 onResolved 或 onRejected 处理程序的唯一参数。 > > Promise.resolve()和 Promise.reject()在被调用时就会接收解决值和拒绝理由。同样地,它们返回的期约也会像执行器一样把这些值传给 onResolved 或 onRejected 处理程序. 8. 拒绝期约与拒绝错误处理 > 在期约的执行函数或处理程序中抛出错误会导致拒绝,对应的错误对象会成为拒绝的理由。所有错误都是异步抛出且未处理的,通过错误对象捕获的栈追踪信息展示了错误发生的路径。 ##### 11.2.4 期约连锁与期约合成 >多个期约组合在一起可以构成强大的代码逻辑。这种组合可以通过两种方式实现:期约连锁与期约合成。前者就是一个期约接一个期约地拼接,后者则是将多个期约组合为一个期约 1. **期约连锁** ```javascript let p = new Promise((resolve, reject) => { console.log('first'); resolve(); }); p.then(() => console.log('second')) .then(() => console.log('third')) .then(() => console.log('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); })); ``` 2. **期约图** >因为一个期约可以有任意多个处理程序,所以期约连锁可以构建有向非循环图的结构。这样,每个期约都是图中的一个节点,而使用实例方法添加的处理程序则是有向顶点。因为图中的每个节点都会等待前一个节点落定,所以图的方向就是期约的解决或拒绝顺序。 3. **Promise.all()**和 **Promise.race()** >Promise 类提供两个将多个期约实例组合成一个期约的静态方法:Promise.all()和 Promise.race()。而合成后期约的行为取决于内部期约的行为。这两个静态方法都是接收一个可迭代对象,返回一个新期约。 > >Promise.all():创建的期约会在一组期约全部解决之后再解决。 > >Promise.race():返回一个包装期约,是一组集合中最先解决或拒绝的期约的镜像。 ```javascript let p1 = Promise.all([ Promise.resolve(), Promise.resolve() ]); let p1 = Promise.race([ Promise.resolve(), Promise.resolve() ]); ``` 4. **串行期约合成** >基于后续期约使用之前期约的返回值来串联期约是期约的基本功能。这很像函数合成,即将多个函数合成为一个函数。类似地,期约也可以像这样合成起来,渐进地消费一个值,并返回一个结果: ```javascript 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 addTen(x) { return [addTwo, addThree, addFive] .reduce((promise, fn) => promise.then(fn), Promise.resolve(x)); } ``` ##### 11.2.5 期约扩展 >ES6 期约实现是很可靠的,但它也有不足之处。比如,很多第三方期约库实现中具备而 ECMAScript规范却未涉及的两个特性:期约取消和进度追踪。 1. 期约取消 > 我们经常会遇到期约正在处理过程中,程序却不再需要其结果的情形。这时候如果能够取消期约就好了。实际上,可以在现有实现基础上提供一种临时性的封装,以实现取消期约的功能。 2. 期约进度通知 >执行中的期约可能会有不少离散的“阶段”,在最终解决之前必须依次经过。某些情况下,监控期约的执行进度会很有用。ECMAScript 6 期约并不支持进度追踪,但是可以通过扩展来实现。一种实现方式是扩展 Promise 类,为它添加 notify()方法,如下所示: ```javascript 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; } } ``` ### 11.3 异步函数 347 ##### 11.3.1 异步函数 - 为在解决利用异步结构组织代码的问题,ECMAScript 对函数进行了扩展,为其增加了两个新关键字:async 和 await。 - async 关键字用于声明异步函数。这个关键字可以用在函数声明、函数表达式、箭头函数和方法上。因为异步函数主要针对不会马上完成的任务,所以自然需要一种暂停和恢复执行的能力。使用 await关键字可以暂停异步函数代码的执行,等待期约解决。 - 使用 async 关键字可以让函数具有异步特征,但总体上其代码仍然是同步求值的。而在参数或闭包方面,异步函数仍然具有普通 JavaScript 函数的正常行为。 - 异步函数如果使用 return 关键字返回了值(如果没有 return 则会返回 undefined),这个值会被 Promise.resolve()包装成一个期约对象。异步函数始终返回期约对象。在函数外部调用这个函数可以得到它返回的期约。 - await 关键字会暂停执行异步函数后面的代码,让出 JavaScript 运行时的执行线程。这个行为与生成器函数中的 yield 关键字是一样的。await 关键字同样是尝试“解包”对象的值,然后将这个值传给表达式,再异步恢复异步函数的执行。 - await 关键字的用法与 JavaScript 的一元操作一样。它可以单独使用,也可以在表达式中使用。 - await 关键字必须在异步函数中使用,不能在顶级上下文如<script>标签或模块中使用。此外,异步函数的特质不会扩展到嵌套函数。因此,await 关键字也只能直接出现在异步函数的定义中。在同步函数内部使用 await 会抛出 SyntaxError。 ```javascript //async 关键字可以用在函数声明、函数表达式、箭头函数和方法上 async function foo() {} let bar = async function() {}; let baz = async () => {}; class Qux { async qux() {} } //返回的期约 async function foo() { console.log(1); return 3; //return Promise.resolve(3); } foo().then(console.log); //3 // 返回一个原始值 async function foo() { return 'foo'; } // 返回一个没有实现 thenable 接口的对象 async function bar() { return ['bar']; } // 异步打印"foo" async function foo() { console.log(await Promise.resolve('foo')); } // 异步打印"bar" async function bar() { return await Promise.resolve('bar'); } // 1000 毫秒后异步打印"baz" async function baz() { await new Promise((resolve, reject) => setTimeout(resolve, 1000)); console.log('baz'); } ``` ##### 11.3.2 停止和恢复执行 > async/await 中真正起作用的是 await。async 关键字,无论从哪方面来看,都不过是一个标识符。毕竟,异步函数如果不包含 await 关键字,其执行基本上跟普通函数没有什么区别。 ```javascript //使用 await 关键字之后的区别其实比看上去的还要微妙一些。比如,下面的例子中按顺序调用了 3个函数,但它们的输出结果顺序是相反的: 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 // bar // foo ``` 要完全理解 await 关键字,必须知道它并非只是等待一个值可用那么简单。JavaScript 运行时在碰到 await 关键字时,会记录在哪里暂停执行。等到 await 右边的值可用了,JavaScript 运行时会向消息队列中推送一个任务,这个任务会恢复异步函数的执行。因此,即使 await 后面跟着一个立即可用的值,函数的其余部分也会被异步求值。 ```javascript async function foo() { console.log(2); await null; console.log(4); } console.log(1); foo(); console.log(3); // 1 // 2 // 3 // 4 控制台中输出结果的顺序很好地解释了运行时的工作过程: (1) 打印 1; (2) 调用异步函数 foo(); (3)(在 foo()中)打印 2; (4)(在 foo()中)await 关键字暂停执行,为立即可用的值 null 向消息队列中添加一个任务; (5) foo()退出; (6) 打印 3; (7) 同步线程的代码执行完毕; (8) JavaScript 运行时从消息队列中取出任务,恢复异步函数执行; (9)(在 foo()中)恢复执行,await 取得 null 值(这里并没有使用); (10)(在 foo()中)打印 4; (11) foo()返回。 ``` ##### 11.3.3 异步函数策略 1. 实现 **sleep()** ```javascript async function sleep(delay) { return new Promise((resolve) => setTimeout(resolve, delay)); } async function foo() { const t0 = Date.now(); await sleep(1500); // 暂停约 1500 毫秒 console.log(Date.now() - t0); } ``` 2. 利用平行执行 3. 串行执行期约 4. 栈追踪与内存管理
顶部
收展
底部
[TOC]
目录
第1章 JavaScript简介
第2章 在 HTML中使用JavaScript
第3章 语言基础(1)语法变量
第3章 语言基础(2)数据类型
第3章 语言基础(3)操作符
第3章 语言基础(4)语句
第4章 变量、作用域与内存
第5章 基本引用类型
第6章 集合引用类型
第7章 迭代器与生成器
第8 章对象、类与面向对象编程
第9章 代理与反射
第10章 函数
第11章 期约与异步函数
第12章 BOM
第13章 客户端检测
第14章 DOM
第15章 DOM 扩展
第16章 DOM2 和 DOM3
第17章 事件
第18章 动画与 Canvas 图形
第19章 表单脚本
第20章 JavaScript API
第21章 错误处理与调试
第22章 处理 XML
第23章 JSON
第24章 网络请求与远程资源
第25章 客户端存储
第26章 模块
第27章 工作者线程
第28章 最佳实践
相关推荐
WebSocket