南漳网站建设,土木工程毕设代做网站,做网站建设的注意事项,关闭小程序apiECMAScript 6及之后的几个版本逐步加大了对异步编程机制的支持#xff0c;提供了令人眼前一亮的新特性。ECMAScript 6新增了正式的Promise#xff08;期约#xff09;引用类型#xff0c;支持优雅地定义和组织异步逻辑。接下来几个版本增加了使用async和await关键字定义异步… ECMAScript 6及之后的几个版本逐步加大了对异步编程机制的支持提供了令人眼前一亮的新特性。ECMAScript 6新增了正式的Promise期约引用类型支持优雅地定义和组织异步逻辑。接下来几个版本增加了使用async和await关键字定义异步函数的机制。 11.1 异步编程 同步行为和异步行为的对立统一是计算机科学的一个基本概念。特别是在JavaScript这种单线程事件循环模型中同步操作与异步操作更是代码所要依赖的核心机制。异步行为是为了优化因计算量大而时间长的操作。如果在等待其他操作完成的同时即使运行其他指令系统也能保持稳定那么这样做就是务实的。 重要的是异步操作并不一定计算量大或要等很长时间。只要你不想为等待某个异步操作而阻塞线程执行那么任何时候都可以使用。 11.1.1 同步与异步 同步行为对应内存中顺序执行的处理器指令。每条指令都会严格按照它们出现的顺序来执行而每条指令执行后也能立即获得存储在系统本地如寄存器或系统内存的信息。这样的执行流程容易分析程序在执行到代码任意位置时的状态比如变量的值。 同步操作的例子可以是执行一次简单的数学计算
let x 3;
x x 4; 在程序执行的每一步都可以推断出程序的状态。这是因为后面的指令总是在前面的指令完成后才会执行。等到最后一条指定执行完毕存储在x的值就立即可以使用。 这两行JavaScript代码对应的低级指令从JavaScript到x86并不难想象。首先操作系统会在栈内存上分配一个存储浮点数值的空间然后针对这个值做一次数学计算再把计算结果写回之前分配的内存中。所有这些指令都是在单个线程中按顺序执行的。在低级指令的层面有充足的工具可以确定系统状态。 相对地异步行为类似于系统中断即当前进程外部的实体可以触发代码执行。异步操作经常是必要的因为强制进程等待一个长时间的操作通常是不可行的同步操作则必须要等。如果代码要访问一些高延迟的资源比如向远程服务器发送请求并等待响应那么就会出现长时间的等待。 异步操作的例子可以是在定时回调中执行一次简单的数学计算
let x 3;
setTimeout(() x x 4, 1000); 这段程序最终与同步代码执行的任务一样都是把两个数加在一起但这一次执行线程不知道x值何时会改变因为这取决于回调何时从消息队列出列并执行。 异步代码不容易推断。虽然这个例子对应的低级代码最终跟前面的例子没什么区别但第二个指令块加操作及赋值操作是由系统计时器触发的这会生成一个入队执行的中断。到底什么时候会触发这个中断这对JavaScript运行时来说是一个黑盒因此实际上无法预知尽管可以保证这发生在当前线程的同步代码执行之后否则回调都没有机会出列被执行。无论如何在排定回调以后基本没办法知道系统状态何时变化。 为了让后续代码能够使用x异步执行的函数需要在更新x的值以后通知其他代码。如果程序不需要这个值那么就只管继续执行不必等待这个结果了。 设计一个能够知道x什么时候可以读取的系统是非常难的。JavaScript在实现这样一个系统的过程中也经历了几次迭代。 11.1.2 以往的异步编程模式 异步行为是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毫秒之后 显然随着代码越来越复杂回调策略是不具有扩展性的。“回调地狱”这个称呼可谓名至实归。嵌套回调的代码维护起来就是噩梦。 11.2 期约 期约是对尚不存在结果的一个替身。期约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等术语指代同样的概念。所有这些概念描述的都是一种异步程序执行的机制。 11.2.1 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也以期约为基础。 11.2.2 期约基础 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 resolvedlet 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: undefinedsetTimeout(console.log, 0, Promise.resolve(3));
// Promise resolved: 3// 多余的参数会忽略
setTimeout(console.log, 0, Promise.resolve(4, 5, 6));
// Promise resolved: 4 对这个静态方法而言如果传入的参数本身是一个期约那它的行为就类似于一个空包装。因此Promise.resolve()可以说是一个幂等方法如下所示
let p Promise.resolve(7);setTimeout(console.log, 0, p Promise.resolve(p));
// truesetTimeout(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 pendingsetTimeout(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: 3p.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块并不能捕获该错误。代码一旦开始以异步模式执行则唯一与之交互的方式就是使用异步结构——更具体地说就是期约的方法。 11.2.3 期约的实例方法 期约实例的方法是连接外部同步代码与内部异步代码之间的桥梁。这些方法可以访问异步操作返回的数据处理期约成功和失败的结果连续对期约求值或者添加只有期约进入终止状态时才会执行的代码。
未完待续。。。