当前位置: 首页 > news >正文

怎么做网站关键词视频怎么制作网站编辑页面

怎么做网站关键词视频,怎么制作网站编辑页面,连江厦门网站建设公司,背景网页设计ECMAScript 6 及之后的几个版本逐步加大了对异步编程机制的支持#xff0c;提供了令人眼前一亮的新特性。ECMAScript 6 新增了正式的 Promise#xff08;期约#xff09;引用类型#xff0c;支持优雅地定义和组织异步逻辑。接下来几个版本增加了使用 async 和 await 关键字…ECMAScript 6 及之后的几个版本逐步加大了对异步编程机制的支持提供了令人眼前一亮的新特性。ECMAScript 6 新增了正式的 Promise期约引用类型支持优雅地定义和组织异步逻辑。接下来几个版本增加了使用 async 和 await 关键字定义异步函数的机制。 注意 本章示例将大量使用异步日志输出的方式 setTimeout(console.log, 0, ... params)旨在演示执行顺序及其他异步行为。异步输出的内容看起来虽然像是同步输出的但实际上是异步打印的。这样可以让期约等返回的值达到其最终状态。 此外浏览器控制台的输出经常能打印出 JavaScript 运行中无法获取的对象信息比如期约的状态。这个特性在示例中广泛使用以便辅助读者理解相关概念。 异步编程 同步行为和异步行为的对立统一是计算机科学的一个基本概念。特别是在 JavaScript 这种单线程事 件循环模型中同步操作与异步操作更是代码所要依赖的核心机制。异步行为是为了优化因计算量大而时间长的操作。如果在等待其他操作完成的同时即使运行其他指令系统也能保持稳定那么这样做就是务实的。 重要的是异步操作并不一定计算量大或要等很长时间。只要你不想为等待某个异步操作而阻塞线 程执行那么任何时候都可以使用。 同步与异步 同步行为对应内存中顺序执行的处理器指令。每条指令都会严格按照它们出现的顺序来执行而每条指令执行后也能立即获得存储在系统本地如寄存器或系统内存的信息。这样的执行流程容易分析程序在执行到代码任意位置时的状态比如变量的值。 同步操作的例子可以是执行一次简单的数学计算 let x 3; x x 4; 在程序执行的每一步都可以推断出程序的状态。这是因为后面的指令总是在前面的指令完成后才 会执行。等到最后一条指定执行完毕存储在 x 的值就立即可以使用。 这两行 JavaScript 代码对应的低级指令从 JavaScript 到 x86并不难想象。首先操作系统会在栈 内存上分配一个存储浮点数值的空间然后针对这个值做一次数学计算再把计算结果写回之前分配的内存中。所有这些指令都是在单个线程中按顺序执行的。在低级指令的层面有充足的工具可以确定系统状态。 相对地异步行为类似于系统中断即当前进程外部的实体可以触发代码执行。异步操作经常是必 要的因为强制进程等待一个长时间的操作通常是不可行的同步操作则必须要等。如果代码要访问一些高延迟的资源比如向远程服务器发送请求并等待响应那么就会出现长时间的等待。 异步操作的例子可以是在定时回调中执行一次简单的数学计算 let x 3; setTimeout(() x x 4, 1000); 这段程序最终与同步代码执行的任务一样都是把两个数加在一起但这一次执行线程不知道 x 值 何时会改变因为这取决于回调何时从消息队列出列并执行。 异步代码不容易推断。虽然这个例子对应的低级代码最终跟前面的例子没什么区别但第二个指令 块加操作及赋值操作是由系统计时器触发的这会生成一个入队执行的中断。到底什么时候会触发这个中断这对 JavaScript 运行时来说是一个黑盒因此实际上无法预知尽管可以保证这发生在当前线程的同步代码执行之后否则回调都没有机会出列被执行。无论如何在排定回调以后基本没办法知道系统状态何时变化。 为了让后续代码能够使用 x异步执行的函数需要在更新 x 的值以后通知其他代码。如果程序不需 要这个值那么就只管继续执行不必等待这个结果了。 设计一个能够知道 x 什么时候可以读取的系统是非常难的。JavaScript 在实现这样一个系统的过程 中也经历了几次迭代。 setTimeout语法 setTimeout(callbackFunction, delayTime, parameter1, parameter2, ...); //callbackFunction 是要在延迟时间后执行的函数。 //delayTime 是延迟的毫秒数表示从调用 setTimeout 到执行 callbackFunction 的时间间隔。 //parameter1, parameter2, 等等是要传递给 callbackFunction 的参数。这些参数是可选的你可以根据需要传递任意数量的参数。以往的异步编程模式 异步行为是 JavaScript 的基础但以前的实现不理想。在早期的 JavaScript 中只支持定义回调函数 来表明异步操作完成。串联多个异步操作是一个常见的问题通常需要深度嵌套的回调函数俗称“回 调地狱”来解决。 假设有以下异步函数使用了 setTimeout 在一秒钟之后执行某些操作 function double(value) { setTimeout(() setTimeout(console.log, 0, value * 2), 1000); } double(3); // 6大约 1000 毫秒之后这里的代码没什么神秘的但关键是理解为什么说它是一个异步函数。setTimeout 可以定义一个在指定时间之后会被调度执行的回调函数。 对这个例子而言1000 毫秒之后JavaScript 运行时会把回调函数推到自己的消息队列上去等待执行。推到队列之后回调什么时候出列被执行对 JavaScript 代码就完全不可见了。还有一点double()函数在 setTimeout 成功调度异步操作之后会立即退出。 1.异步返回值 假设 setTimeout 操作会返回一个有用的值。有什么好办法把这个值传给需要它的地方广泛接受 的一个策略是给异步操作提供一个回调这个回调中包含要使用异步返回值的代码作为回调的参数。 function double(value, callback) { setTimeout(() callback(value * 2), 1000); //函数体运行完后执行回调函数 } double(3, (x) console.log(I was given: ${x})); // I was given: 6大约 1000 毫秒之后 这里的 setTimeout 调用告诉 JavaScript 运行时在 1000 毫秒之后把一个函数推到消息队列上。这 个函数会由运行时负责异步调度执行。而位于函数闭包中的回调及其参数在异步执行时仍然是可用的。 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. 嵌套异步回调 如果异步返值又依赖另一个异步返回值那么回调的情况还会进一步变复杂。在实际的代码中这 就要求嵌套回调 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 毫秒之后 显然随着代码越来越复杂回调策略是不具有扩展性的。“回调地狱”这个称呼可谓名至实归。 嵌套回调的代码维护起来就是噩梦。 这种回调函数模式在过去是常见的但随着 JavaScript 的发展出现了一些更加灵活和优雅的异步处理方法比如 Promise 和 async/await。 Promise(期约) 期约是对尚不存在结果的一个替身。期约promise这个名字最早是由 Daniel Friedman和 David Wise在他们于 1976 年发表的论文“The Impact of Applicative Programming on Multiprocessing”中提出来的。但直到十几年以后Barbara Liskov 和 Liuba Shrira 在 1988 年发表了论文“Promises: Linguistic Support for Efficient Asynchronous Procedure Calls in Distributed Systems”这个概念才真正确立下来。同一时期的计算机科学家还使用了“终局”eventual、“期许”future、“延迟”delay和“迟付”deferred等术语指代同样的概念。所有这些概念描述的都是一种异步程序执行的机制。 Promises/A规范 早期的期约机制在 jQuery 和 Dojo 中是以 Deferred API 的形式出现的。到了 2010 年CommonJS 项 目实现的 Promises/A 规范日益流行起来。Q 和 Bluebird 等第三方 JavaScript 期约库也越来越得到社区认可虽然这些库的实现多少都有些不同。为弥合现有实现之间的差异2012 年 Promises/A组织分叉fork了 CommonJS 的 Promises/A 建议并以相同的名字制定了 Promises/A规范。这个规范最终成为了ECMAScript 6 规范实现的范本。 ECMAScript 6 增加了对 Promises/A规范的完善支持即 Promise 类型。一经推出Promise 就大受欢迎成为了主导性的异步编程机制。所有现代浏览器都支持 ES6 期约很多其他浏览器 API如fetch()和 Battery Status API也以期约为基础。 期约基础 ECMAScript 6 新增的引用类型 Promise可以通过 new 操作符来实例化。创建新期约时需要传入 执行器executor函数作为参数后面马上会介绍下面的例子使用了一个空函数对象来应付一下解释器 let p new Promise(() {}); setTimeout(console.log, 0, p); // Promise pending 之所以说是应付解释器是因为如果不提供执行器函数就会抛出 SyntaxError。 1.期约状态机 在把一个期约实例传给 console.log()时控制台输出可能因浏览器不同而略有差异表明该实例处于待定pending状态。如前所述期约是一个有状态的对象可能处于如下 3 种状态之一 待定pending兑现fulfilled有时候也称为“解决”resolved拒绝rejected 待定pending是期约的最初始状态。在待定状态下期约可以落定settled为代表成功的兑现fulfilled状态或者代表失败的拒绝rejected状态。无论落定为哪种状态都是不可逆的。只要从待定转换为兑现或拒绝期约的状态就不再改变。而且也不能保证期约必然会脱离待定状态。因此组织合理的代码无论期约解决resolve还是拒绝reject甚至永远处于待定pending状态都应该具有恰当的行为。 重要的是期约的状态是私有的不能直接通过 JavaScript 检测到。这主要是为了避免根据读取到的期约状态以同步方式处理期约对象。另外期约的状态也不能被外部 JavaScript 代码修改。这与不能读取该状态的原因是一样的期约故意将异步行为封装起来从而隔离外部的同步代码。 2.解决值、拒绝理由及期约用例 期约主要有两大用途。首先是抽象地表示一个异步操作。期约的状态代表期约是否完成。“待定”表示尚未开始或者正在执行中。“兑现”表示已经成功完成而“拒绝”则表示没有成功完成。 某些情况下这个状态机就是期约可以提供的最有用的信息。知道一段异步代码已经完成对于其他代码而言已经足够了。比如假设期约要向服务器发送一个 HTTP 请求。请求返回 200 ~ 299 范围内的状态码就足以让期约的状态变为“兑现”。类似地如果请求返回的状态码不在 200 ~ 299 这个范围内那么就会把期约状态切换为“拒绝”。 在另外一些情况下期约封装的异步操作会实际生成某个值而程序期待期约状态改变时可以访问 这个值。相应地如果期约被拒绝程序就会期待期约状态改变时可以拿到拒绝的理由。比如假设期约向服务器发送一个 HTTP 请求并预定会返回一个 JSON。如果请求返回范围在 200 ~ 299 的状态码则足以让期约的状态变为兑现。此时期约内部就可以收到一个 JSON 字符串。类似地如果请求返回的状态码不在 200 ~ 299 这个范围内那么就会把期约状态切换为拒绝。此时拒绝的理由可能是一个 Error对象包含着 HTTP 状态码及相关错误消息。 为了支持这两种用例每个期约只要状态切换为兑现就会有一个私有的内部值value。类似地 每个期约只要状态切换为拒绝就会有一个私有的内部理由reason。无论是值还是理由都是包含原始值或对象的不可修改的引用。二者都是可选的而且默认值为 undefined。在期约到达某个落定状态时执行的异步代码始终会收到这个值或理由。 3. 通过执行函数控制期约状态 由于期约的状态是私有的所以只能在内部进行操作。内部操作在期约的执行器函数中完成。执行 器函数主要有两项职责初始化期约的异步行为和控制状态的最终转换。其中控制期约状态的转换是通过调用它的两个函数参数实现的。这两个函数参数通常都命名为 resolve()和 reject()。调用 resolve()会把状态切换为兑现调用 reject()会把状态切换为拒绝。另外调用 reject()也会抛出错误后面会讨论这个错误。 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) 在前面的例子中并没有什么异步操作因为在初始化期约时执行器函数已经改变了每个期约的状态。这里的关键在于执行器函数是同步执行的。这是因为执行器函数是期约的初始化程序。通过下面的例子可以看出上面代码的执行顺序 new Promise(() setTimeout(console.log, 0, executor)); setTimeout(console.log, 0, promise initialized); // executor // promise initialized 添加 setTimeout 可以推迟切换状态 let p new Promise((resolve, reject) setTimeout(resolve, 1000)); // 在 console.log 打印期约实例的时候还不会执行超时回调即 resolve() setTimeout(console.log, 0, p); // Promise pending 无论 resolve()和 reject()中的哪个被调用状态转换都不可撤销了。于是继续修改状态会静默失败如下所示 let p new Promise((resolve, reject) { resolve(); reject(); // 没有效果 }); setTimeout(console.log, 0, p); // Promise resolved 为避免期约卡在待定状态可以添加一个定时退出功能。比如可以通过 setTimeout 设置一个10 秒钟后无论如何都会拒绝期约的回调 let p new Promise((resolve, reject) { setTimeout(reject, 10000); // 10 秒后调用 reject() // 执行函数的逻辑 }); setTimeout(console.log, 0, p); // Promise pending setTimeout(console.log, 11000, p); // 11 秒后再检查状态 // (After 10 seconds) Uncaught error // (After 11 seconds) Promise rejected 因为期约的状态只能改变一次所以这里的超时拒绝逻辑中可以放心地设置让期约处于待定状态的 最长时间。如果执行器中的代码在超时之前已经解决或拒绝那么超时回调再尝试拒绝也会静默失败。 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 resolved: undefined setTimeout(console.log, 0, Promise.resolve(3)); // Promise resolved: 3 // 多余的参数会忽略 setTimeout(console.log, 0, Promise.resolve(4, 5, 6)); // Promise resolved: 4 对这个静态方法而言如果传入的参数本身是一个期约那它的行为就类似于一个空包装(setTimeout实际上并没有延迟任何东西)。因此Promise.resolve()可以说是一个幂等方法如下所示 let p Promise.resolve(7); setTimeout(console.log, 0, p Promise.resolve(p)); // true setTimeout(console.log, 0, p Promise.resolve(Promise.resolve(p))); // true 这个幂等性(注:通常用来描述一个操作无论执行多少次结果都是相同的)会保留传入期约的状态 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 resolved: 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 关键在于Promise.reject()并没有照搬 Promise.resolve()的幂等逻辑。如果给它传一个期约对象则这个期约会成为它返回的拒绝期约的理由 setTimeout(console.log, 0, Promise.reject(Promise.resolve())); // Promise rejected: Promise resolved 6. 同步/异步执行的二元性 Promise 的设计很大程度上会导致一种完全不同于 JavaScript 的计算模式。下面的例子完美地展示了这一点其中包含了两种模式下抛出错误的情形 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 抛出并捕获了错误第二个 try/catch 抛出错误却没有捕获到。乍一看这可能有点违反直觉因为代码中确实是同步创建了一个拒绝的期约实例而这个实例也抛出了包含拒绝理由的错误。这里的同步代码之所以没有捕获期约抛出的错误是因为它没有通过异步模式捕获错误。从这里就可以看出期约真正的异步特性它们是同步对象在同步执行模式中使用但也是异步执行模式的媒介。 在前面的例子中拒绝期约的错误并没有抛到执行同步代码的线程里而是通过浏览器异步消息队列来处理的。因此try/catch 块并不能捕获该错误。代码一旦开始以异步模式执行则唯一与之交互的方式就是使用异步结构——更具体地说就是期约的方法。 期约的实例方法 期约实例的方法是连接外部同步代码与内部异步代码之间的桥梁。这些方法可以访问异步操作返回的数据处理期约成功和失败的结果连续对期约求值或者添加只有期约进入终止状态时才会执行的代码。 1. 实现 Thenable 接口 在 ECMAScript 暴露的异步结构中任何对象都有一个 then()方法。这个方法被认为实现了Thenable 接口。下面的例子展示了实现这一接口的最简单的类 class MyThenable { then() {} } ECMAScript 的 Promise 类型实现了 Thenable 接口。这个简化的接口跟 TypeScript 或其他包中的接口或类型定义不同它们都设定了 Thenable 接口更具体的形式。 注意 本章后面再介绍异步函数时还会再谈到 Thenable 接口的用途和目的。 2. Promise.prototype.then() 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)); //3 秒后 // p1 resolved // p2 rejected 因为期约只能转换为最终状态一次所以这两个操作一定是互斥的。 如前所述两个处理程序参数都是可选的。而且传给 then()的任何非函数类型的参数都会被静 默忽略。如果想只提供 onRejected 参数那就要在 onResolved 参数的位置上传入 undefined。这样有助于避免在内存中创建多余的对象对期待函数参数的类型系统也是一个交代。 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 rejected3 秒后 Promise.prototype.then()方法返回一个新的期约实例 let p1 new Promise(() {}); let p2 p1.then(); setTimeout(console.log, 0, p1); // Promise pending setTimeout(console.log, 0, p2); // Promise pending setTimeout(console.log, 0, p1 p2); // false 这个新期约实例基于 onResovled 处理程序的返回值构建。换句话说该处理程序的返回值会通过Promise.resolve()包装来生成新期约。如果没有提供这个处理程序则 Promise.resolve()就会 包装上一个期约解决之后的值。如果没有显式的返回语句则 Promise.resolve()会包装默认的返回值 undefined。 let p1 Promise.resolve(foo); // 若调用 then()时不传处理程序则原样向后传 let p2 p1.then(); setTimeout(console.log, 0, p2); // Promise resolved: foo // 这些都一样 let p3 p1.then(() undefined); let p4 p1.then(() {}); let p5 p1.then(() Promise.resolve()); setTimeout(console.log, 0, p3); // Promise resolved: undefined setTimeout(console.log, 0, p4); // Promise resolved: undefined setTimeout(console.log, 0, p5); // Promise resolved: undefined 如果有显式的返回值则 Promise.resolve()会包装这个值 // 这些都一样 let p6 p1.then(() bar); let p7 p1.then(() Promise.resolve(bar)); setTimeout(console.log, 0, p6); // Promise resolved: bar setTimeout(console.log, 0, p7); // Promise resolved: bar // Promise.resolve()保留返回的期约 let p8 p1.then(() new Promise(() {})); let p9 p1.then(() Promise.reject()); // Uncaught (in promise): undefined 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 resolved: Error: qux onRejected 处理程序也与之类似onRejected 处理程序返回的值也会被 Promise.resolve()包装。 乍一看这可能有点违反直觉但是想一想onRejected 处理程序的任务不就是捕获异步错误吗 因此拒绝处理程序在捕获错误后不抛出异常是符合期约的行为应该返回一个解决期约。 下面的代码片段展示了用 Promise.reject()替代之前例子中的 Promise.resolve()之后的结果 let p1 Promise.reject(foo); // 调用 then()时不传处理程序则原样向后传 let p2 p1.then(); // Uncaught (in promise) foo setTimeout(console.log, 0, p2); // Promise rejected: foo // 这些都一样 let p3 p1.then(null, () undefined); let p4 p1.then(null, () {}); let p5 p1.then(null, () Promise.resolve()); setTimeout(console.log, 0, p3); // Promise resolved: undefined setTimeout(console.log, 0, p4); // Promise resolved: undefined setTimeout(console.log, 0, p5); // Promise resolved: undefined // 这些都一样 let p6 p1.then(null, () bar); let p7 p1.then(null, () Promise.resolve(bar)); setTimeout(console.log, 0, p6); // Promise resolved: bar setTimeout(console.log, 0, p7); // Promise resolved: bar // Promise.resolve()保留返回的期约 let p8 p1.then(null, () new Promise(() {})); let p9 p1.then(null, () Promise.reject()); // Uncaught (in promise): undefined 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 resolved: Error: qux 3. Promise.prototype.catch() 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, p2); // Promise pending setTimeout(console.log, 0, p1 p2); // false 在返回新期约实例方面Promise.prototype.catch()的行为与 Promise.prototype.then()的 onRejected 处理程序是一样的。 4. Promise.prototype.finally() 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); // 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, p2); // Promise pending setTimeout(console.log, 0, p1 p2); // false 这个新期约实例不同于 then()或 catch()方式返回的实例。因为 onFinally 被设计为一个状态 无关的方法所以在大多数情况下它将表现为父期约的传递。对于已解决状态和被拒绝状态都是如此。 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 resolved: foo setTimeout(console.log, 0, p3); // Promise resolved: foo setTimeout(console.log, 0, p4); // Promise resolved: foo setTimeout(console.log, 0, p5); // Promise resolved: foo setTimeout(console.log, 0, p6); // Promise resolved: foo setTimeout(console.log, 0, p7); // Promise resolved: foo setTimeout(console.log, 0, p8); // Promise resolved: 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 setTimeout(() setTimeout(console.log, 0, p2), 200); // 200 毫秒后 // Promise resolved: foo 5. 非重入期约方法 当期约进入落定状态时与该状态相关的处理程序仅仅会被排期而非立即执行。跟在添加这个处理程序的代码之后的同步代码一定会在处理程序之前先执行。即使期约一开始就是与附加处理程序关联的状态执行顺序也是这样的。这个特性由 JavaScript 运行时保证被称为“非重入”non-reentrancy特性。 个人补充 “非重入”non-reentrancy特性是指在 JavaScript 中当一个 Promise 进入 resolved 或 rejected 状态时与该状态相关的处理程序then 或 catch会被安排在微任务队列中而不会立即执行。即使 Promise 最初就是在添加处理程序之前就已经达到了 resolved 或 rejected 状态相关的处理程序也会按照规定的顺序执行。 下面的例子演示了这个特性 // 创建解决的期约 let p Promise.resolve(); // 添加解决处理程序 // 直觉上这个处理程序会等期约一解决就执行 p.then(() console.log(onResolved handler)); // 同步代码证明 then()已经返回 console.log(then() returns); // 实际的输出 // then() returns // onResolved handler 在这个例子中在一个解决期约上调用 then()会把 onResolved 处理程序推进消息队列。但这个处理程序在当前线程上的同步代码执行完成前不会执行。因此跟在 then()后面的同步代码一定先于处理程序执行。 先添加处理程序后解决期约也是一样的。如果添加处理程序后同步代码才改变期约状态那么处理程序仍然会基于该状态变化表现出非重入特性。下面的例子展示了即使先添加了 onResolved 处理程序再同步调用 resolve()处理程序也不会进入同步线程执行 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); // 实际的输出 // 1: invoking 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.catch() onRejected)); console.log(p3.catch() returns); let p4 Promise.resolve(); p4.finally(() console.log(p4.finally() onFinally)); console.log(p4.finally() returns); // p1.then() returns // p2.then() returns // p3.catch() returns // p4.finally() returns // p1.then() onResolved // p2.then() onRejected // p3.catch() onRejected // p4.finally() onFinally 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. 传递解决值和拒绝理由 到了落定状态后期约会提供其解决值如果兑现或其拒绝理由如果拒绝给相关状态的处理程序。拿到返回值后就可以进一步对这个值进行操作。比如第一次网络请求返回的 JSON 是发送第二次请求必需的数据那么第一次请求返回的值就应该传给 onResolved 处理程序继续处理。当然失败的网络请求也应该把 HTTP 状态码传给 onRejected 处理程序。 在执行函数中解决的值和拒绝的理由是分别作为 resolve()和 reject()的第一个参数往后传的。然后这些值又会传给它们各自的处理程序作为 onResolved 或 onRejected 处理程序的唯一参数。下面的例子展示了上述传递过程 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()在被调用时就会接收解决值和拒绝理由。同样地它们返回的期约也会像执行器一样把这些值传给 onResolved 或 onRejected 处理程序 let p1 Promise.resolve(foo); p1.then((value) console.log(value)); // foo let p2 Promise.reject(bar); p2.catch((reason) console.log(reason)); // bar let p1 new Promise((resolve, reject) resolve(foo)); p1.then((value) console.log(value)); // foo 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 // 也会抛出 4 个未捕获错误 期约可以以任何理由拒绝包括 undefined但最好统一使用错误对象。这样做主要是因为创建错误对象可以让浏览器捕获错误对象中的栈追踪信息而这些信息对调试是非常关键的。例如前面例子中抛出的 4 个错误的栈追踪信息如下 Uncaught (in promise) Error: foo at Promise (test.html:5) at new Promise (anonymous) at test.html:5 Uncaught (in promise) Error: foo at Promise (test.html:6) at new Promise (anonymous) at test.html:6 Uncaught (in promise) Error: foo at test.html:8 Uncaught (in promise) Error: foo at Promise.resolve.then (test.html:7) 所有错误都是异步抛出且未处理的通过错误对象捕获的栈追踪信息展示了错误发生的路径。注意错误的顺序Promise.resolve().then()的错误最后才出现这是因为它需要在运行时消息队列中添加处理程序也就是说在最终抛出未捕获错误之前它还会创建另一个期约。 这个例子同样揭示了异步错误有意思的副作用。正常情况下在通过 throw()关键字抛出错误时JavaScript 运行时的错误处理机制会停止执行抛出错误之后的任何指令 throw Error(foo); console.log(bar); // 这一行不会执行 // Uncaught Error: foo 但是在期约中抛出错误时因为错误实际上是从消息队列中异步抛出的所以并不会阻止运行时继续执行同步指令 Promise.reject(Error(foo)); console.log(bar); // bar // Uncaught (in promise) Error: foo 如本章前面的 Promise.reject()示例所示异步错误只能通过异步的 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 resolved: 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 期约连锁与期约合成 多个期约组合在一起可以构成强大的代码逻辑。这种组合可以通过两种方式实现期约连锁与期约合成。前者就是一个期约接一个期约地拼接后者则是将多个期约组合为一个期约。 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 这个实现最终执行了一连串同步任务。正因为如此这种方式执行的任务没有那么有用毕竟分别 使用 4 个同步函数也可以做到 (() console.log(first))(); (() console.log(second))(); (() console.log(third))(); (() 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); })); // p1 executor1 秒后 // p2 executor2 秒后 // p3 executor3 秒后 // p4 executor4 秒后 把生成期约的代码提取到一个工厂函数中就可以写成这样 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)) // p1 executor1 秒后 // p2 executor2 秒后 // p3 executor3 秒后 // p4 executor4 秒后 每个后续的处理程序都会等待前一个期约解决然后实例化一个新期约并返回它。这种结构可以简洁地将异步任务串行化解决之前依赖回调的难题。假如这种情况下不使用期约那么前面的代码可能就要这样写了 function delayedExecute(str, callback null) { setTimeout(() { console.log(str); callback callback(); }, 1000) } delayedExecute(p1 callback, () { delayedExecute(p2 callback, () { delayedExecute(p3 callback, () { delayedExecute(p4 callback); }); }); }); // p1 callback1 秒后 // p2 callback2 秒后 // p3 callback3 秒后 // p4 callback4 秒后 心明眼亮的开发者会发现这不正是期约所要解决的回调地狱问题吗 因为 then()、catch()和 finally()都返回期约所以串联这些方法也很直观。下面的例子同时使用这 3 个实例方法 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)); // A // B // C // D // E // F // G 注意日志的输出语句是对二叉树的层序遍历。如前所述期约的处理程序是按照它们添加的顺序执行的。由于期约的处理程序是先添加到消息队列然后才逐个执行因此构成了层序遍历。 树只是期约图的一种形式。考虑到根节点不一定唯一且多个期约也可以组合成一个期约通过下一节介绍的 Promise.all()和 Promise.race()所以有向非循环图是体现期约连锁可能性的最准确表达。 3. Promise.all()和 Promise.race() Promise 类提供两个将多个期约实例组合成一个期约的静态方法Promise.all()和 Promise.race()。而合成后期约的行为取决于内部期约的行为。 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: cannot read Symbol.iterator of undefined合成的期约只会在每个包含的期约都解决之后才解决 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 // 没有未处理的错误 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: 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 resolved: 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 resolved: 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 // 没有未处理的错误 4. 串行期约合成 到目前为止我们讨论期约连锁一直围绕期约的串行执行忽略了期约的另一个主要特性异步产生值并将其传给处理程序。基于后续期约使用之前期约的返回值来串联期约是期约的基本功能。这很像函数合成即将多个函数合成为一个函数比如 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)); // 17 在这个例子中有 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); // 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.reduce((promise, fn) promise.then(fn), Promise.resolve(x)) } let addTen compose(addTwo, addThree, addFive); addTen(8).then(console.log); // 18 期约扩展 ES6 期约实现是很可靠的但它也有不足之处。比如很多第三方期约库实现中具备而 ECMAScript规范却未涉及的两个特性期约取消和进度追踪。 1. 期约取消 我们经常会遇到期约正在处理过程中程序却不再需要其结果的情形。这时候如果能够取消期约就好了。某些第三方库比如 Bluebird就提供了这个特性。实际上TC39 委员会也曾准备增加这个特性 但相关提案最终被撤回了。结果ES6 期约被认为是“激进的”只要期约的逻辑开始执行就没有办法阻止它执行到完成。 实际上可以在现有实现基础上提供一种临时性的封装以实现取消期约的功能。这可以用到 Kevin Smith 提到的“取消令牌”cancel token。生成的令牌实例提供了一个接口利用这个接口可以取消期约同时也提供了一个期约的实例可以用来触发取消后的操作并求值取消状态。 下面是 CancelToken 类的一个基本实例 class CancelToken { constructor(cancelFn) { this.promise new Promise((resolve, reject) { cancelFn(resolve); }); } } 这个类包装了一个期约把解决方法暴露给了 cancelFn 参数。这样外部代码就可以向构造函数中传入一个函数从而控制什么情况下可以取消期约。这里期约是令牌类的公共成员因此可以给它添加处理程序以取消期约。 这个类大概可以这样使用 button idstartStart/button button idcancelCancel/button script class CancelToken { constructor(cancelFn) { this.promise new Promise((resolve, reject) { cancelFn(() { setTimeout(console.log, 0, delay cancelled); resolve(); }); }); } } const startButton document.querySelector(#start); const cancelButton document.querySelector(#cancel); function cancellableDelayedResolve(delay) { setTimeout(console.log, 0, set delay); return new Promise((resolve, reject) { const id setTimeout((() { setTimeout(console.log, 0, delayed resolve); resolve(); }), delay); const cancelToken new CancelToken((cancelCallback) cancelButton.addEventListener(click, cancelCallback)); cancelToken.promise.then(() clearTimeout(id)); }); } startButton.addEventListener(click, () cancellableDelayedResolve(1000)); /script 每次单击“Start”按钮都会开始计时并实例化一个新的 CancelToken 的实例。此时“Cancel”按钮一旦被点击就会触发令牌实例中的期约解决。而解决之后单击“Start”按钮设置的超时也会被取消。 2. 期约进度通知 执行中的期约可能会有不少离散的“阶段”在最终解决之前必须依次经过。某些情况下监控期约的执行进度会很有用。ECMAScript 6 期约并不支持进度追踪但是可以通过扩展来实现。 一种实现方式是扩展 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; } } 这样TrackablePromise 就可以在执行函数中使用 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); }); 这个期约会连续5次递归地设置1000毫秒的超时。每个超时回调都会调用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 notify()函数会返回期约所以可以连缀调用连续添加处理程序。多个处理程序会针对收到的每条消息分别执行一遍如下所示 ... p.notify((x) setTimeout(console.log, 0, a:, x)) .notify((x) setTimeout(console.log, 0, b:, x)); p.then(() setTimeout(console.log, 0, completed)); // 约 1 秒后 a: 80% remaining // 约 1 秒后 b: 80% remaining // 约 2 秒后 a: 60% remaining // 约 2 秒后 b: 60% remaining // 约 3 秒后 a: 40% remaining // 约 3 秒后 b: 40% remaining // 约 4 秒后 a: 20% remaining // 约 4 秒后 b: 20% remaining // 约 5 秒后 completed 总体来看这还是一个比较粗糙的实现但应该可以演示出如何使用通知报告进度了。 注意 ES6 不支持取消期约和进度通知一个主要原因就是这样会导致期约连锁和期约合成 过度复杂化。比如在一个期约连锁中如果某个被其他期约依赖的期约被取消了或者发出了 通知那么接下来应该发生什么完全说不清楚。毕竟如果取消了 Promise.all()中的一个 期约或者期约连锁中前面的期约发送了一个通知那么接下来应该怎么办才比较合理呢
http://www.pierceye.com/news/874471/

相关文章:

  • 建个网站视频教程小程序开发是前端还是后端
  • 广州分享网站建设网站速度查询
  • 做电商网站价钱传奇类网页游戏大全
  • 如何选择南京网站建设网站制作能赚多少钱
  • 一站式网站设计已有域名如何在花生壳网站做二级域名托管
  • 哪个网站可以接图纸做返利网站怎么做的
  • 旅游网站建设国内外现状辽阳专业建设网站公司
  • 免费视频模板网站wordpress不写代码
  • 设计网站公司 露 联湖南岚鸿小程序网站开发公司
  • 聊城网站设计seo公司重庆
  • 网站布局技术厦门网站建设680元
  • 深圳物流公司网站建e网怎么做效果图
  • 做营销网站公司建个个人网站一年多少钱
  • 阆中网站网站建设代理网络服务器
  • 企业网站新模式seo排名推广工具
  • 山东做网站三五个人网页设计作品简单
  • 福州网站建设软件网站做了301怎么查看跳转前网站
  • 网站开发竞品分析网站开发与规划
  • 香山红叶建设有限公司网站网络营销方式落后的表现
  • 合肥百姓网网站建设263云通信官方网站
  • 深圳建设网站seo 手机电商数据分析师
  • 网站内外链怎么做公司建设包括哪些方面
  • 织梦网站环境搭建电子邮件怎么注册
  • 企业营销类专业网站app设计尺寸规范
  • 奈曼旗建设局网站建设旅游门户网站
  • 网站设计一般会遇到哪些问题wordpress文章关闭缩略图
  • 优质东莞网站制作公司thinkphp网站源码下载
  • 公司网站做一下多少钱最吉利旺财的公司名字
  • 网站建设维护及使用管理办法营销策划的步骤
  • 优秀网站设计案例在家开个人工作室违法吗