[译]Promise的进化史

原文:Javascript高级编程4html

异步编程

开场白

同步行为与异步行为之间的对偶是计算机科学中的一个基本概念,尤为是在单线程事件循环模型(如JavaScript)中。面对高延迟操做,异步行为再也不须要针对更高的计算吞吐量进行优化。若是在计算完成时仍然能够运行其余指令而且仍保持稳定的系统,那么这样作是实用的。编程

更重要的是,异步操做不必定是计算密集型操做或高延迟操做。它能够在不须要阻塞执行线程以等待异步行为发生的任何地方使用。promise

JavsScript中的同步与异步

同步行为相似于内存中的顺序处理器指令。每条指令严格按照其出现的顺序执行,而且每条指令还可以当即检索系统本地存储的信息(例如:在处理器寄存器或系统内存中)。结果,很容易推断出代码中任何给定点的程序状态(例如,变量的值)。浏览器

一个简单的例子就是执行一个简单的算术运算:安全

let x = 3;
x = x + 4;
复制代码

在该程序的每一个步骤中,均可以推断出程序的状态,由于在完成前一条指令以前,执行不会继续进行。当最后一条指令完成时,x的计算值当即可用。全部这些指令都在单个执行线程中串行存在。服务器

相反,异步行为相似于中断,即当前进程外部的实体可以触发代码执行。一般须要异步操做,由于强制操做等待较长时间才能完成操做是不可行的(同步操做就是这种状况)。因为代码正在访问高延迟资源,例如将请求发送到远程服务器并等待响应,所以可能会发生长时间等待。闭包

一个简单的JavaScript示例将在超时内执行算术运算:异步

let x = 3;
setTimeout(() => x = x + 4, 1000);
复制代码

该程序最终执行与一个同步程序相同的工做(将两个数字加在一块儿),可是该执行线程没法确切知道x的值什么时候会更改,由于这取决于什么时候从消息队列中使回调出队并执行回调。async

上古时代的异步编程模式

长期以来,异步操做一直是JavaScript语言的痛点。在该语言的早期版本中,异步操做仅支持定义回调函数以指示异步操做已完成。异步行为的执行是一个常见的问题,一般能够经过一个充满嵌套回调函数的代码片断来解决,该代码片断一般称为“回调地狱”。异步编程

getData(function(a){
    getMoreData(a, function(b){
        getMoreData(b, function(c){
            getMoreData(c, function(d){
                getMoreData(d, function(e){
    	            // todo
    	        });
            });
        });
    });
});
复制代码

返回异步值

假设setTimeout操做返回了一个有用的值。将值传回的最佳方式是什么?普遍接受的策略是提供对异步操做的回调,其中该回调包含须要访问计算值(做为参数提供)的代码。以下所示:

function double(value, callback) {
 setTimeout(() => callback(value * 2), 1000);
}
double(3, (x) => console.log(`给我: ${x}`));
// 给我: 6 (大约1000ms后打印)
复制代码

此处,setTimeout调用在通过1000毫秒后将函数推入消息队列。此函数将由运行时出队并异步求值。回调函数及其参数仍然能够经过函数闭包在异步执行中使用。

PROMISES

Promise表示某种还没有产生结果的实体。例如最终(eventual)将来(future)延迟(delay)推迟(deferred)。全部这些都以一种或另外一种形式描述了一种用于同步程序执行的编程工具。

Promises/A+规范

一份针对健全、通用JavaScript promises对象的开放标准 — 由实现者制定,供实现者参考。

一个 promise 对象表明一个异步操做的最终结果。与promise进行交互的主要方式是经过它的 then 方法,经过该方法注册回调函数,进而接受promise对象最终的值(value)或不能完成(fulfill)的缘由(reason)。

ECMAScript 6引入了Promises/A+兼容Promise类型的一等实现。自推出以来,Promises的采用率就很是高。全部现代浏览器都彻底支持ES6 Promise类型,而且多个浏览器API(例如fetch()和Battery API)仅使用它。

Promise状态机

当将promise实例传递到console.log时,控制台输出(可能因浏览器而异)指示此promise实例处于待定(pending)状态。如前所述,promise是一个有状态对象,能够存在如下三种状态之一:

  • Pending (待定 - 还没有执行或拒绝)
  • Fulfilled (已执行 - 有时也指resolved, 与 promise 相关的操做成功)
  • Rejected (已拒绝 - 与 promise 相关的操做失败)

待定(pending)状态是promise开始的初始状态。从待定(pending)状态开始,一个promise能够转换到fulfilled状态(表示成功)或rejected状态(表示失败)。这种过渡到稳定(settled)状态是不可逆的。一旦变成已执行(fulfilled)状态或被拒绝(rejected)状态,promise的状态就永远不会改变。此外,不能保证promise未来会离开待定(pending)状态。所以,无论promise处于何种状态,即成功执行拒绝从未退出待定(pending)状态,结构良好的代码都应能正常运行。

更重要的是,promise的状态是私有的,不能在JavaScript中直接检查。这样作的缘由主要是为了防止在读取promise对象时根据其状态进行同步编程处理。此外,外部JavaScript没法更改Promise的状态。

用Executor控制promise状态

因为promise状态是私有的,所以它只能在内部被维护操做。这种内部操做是在promise的执行者(executor)函数内部执行的。执行函数有两个主要职责:初始化promise的异步行为,以及控制任何最终的状态转换。经过调用状态转换的两个函数参数之一(一般命名为resolvereject)来完成对状态转换的控制。调用resolve将使状态变为已实现fulfilled;调用reject会将状态更改成拒绝rejected。调用rejected()也会引起错误。

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)
复制代码

一旦调用resolvereject,状态转移将没法撤消。试图进一步改变状态的尝试将会被忽略。以下所示:

let p = new Promise((resolve, reject) => {
 resolve();
 reject(); // 被忽略
});

setTimeout(console.log, 0, p); // Promise <resolved>
复制代码

您能够经过添加定时退出行为来避免Promise陷入待定(pending)状态。例如,您能够设置超时以在10秒后拒绝这个promise:

let p = new Promise((resolve, reject) => {
 setTimeout(reject, 10000); // 10秒后, 调用reject()
 // 执行其余代码
});
setTimeout(console.log, 0, p); // Promise <pending>
setTimeout(console.log, 11000, p); // 11秒后检查状态
// (10秒后) Uncaught error
// (11秒后) Promise <rejected>
复制代码

由于一个promise只能更改状态一次,因此此超时行为使您能够安全地设置一个能够保持在待定(pending)状态的时间的最大值。若是执行程序内部的代码要在超时以前resolvereject,则超时处理程序拒绝reject的尝试将被忽略。

使用Promise.resolve()进行Promise转换

promise不必定须要从待定(pending)状态开始并利用执行程序函数来达到稳定settled状态。经过调用Promise .resolve()静态方法,能够在resolved状态下实例化Promise。如下两个promise实例其实是等效的:

let p1 = new Promise((resolve, reject) => resolve());
let p2 = Promise.resolve();
复制代码

此已解决resolved的Promise的值将成为传递给Promise.resolve()的第一个参数。这有效地使您能够将任何值转成一个promise:

setTimeout(console.log, 0, Promise.resolve());
// Promise <resolved>: undefined
setTimeout(console.log, 0, Promise.resolve(3));
// Promise <resolved>: 3
// Additional arguments are ignored
setTimeout(console.log, 0, Promise.resolve(4, 5, 6));
// Promise <resolved>: 4
复制代码

也许此静态方法最重要的用处之一是当参数已是一个promise实例时,它能够充当传递passthrough的能力。也就是说,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
复制代码

这种幂等操做将保持传递给它的promise的状态:

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
复制代码

但请注意,此静态方法将愉快地将任何非promise(包括错误对象)包装为已解决resolved的promise,这可能会致使意外的行为:

let p = Promise.resolve(new Error('foo'));
setTimeout(console.log, 0, p);
// Promise <resolved>: Error: foo
复制代码
使用Promise.reject()拒绝Promise

与Promise.resolve()的概念相似,Promise.reject()实例化一个被拒绝rejected的promise并引起异步错误(try/catch不会捕获该异步错误,而该错误只能由拒绝处理程序捕获)。如下两个promise实例其实是等效的:

let p1 = new Promise((resolve, reject) => reject());
let p2 = Promise.reject();
复制代码

此已解决的promise的缘由(reason)字段将是传递给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()行为。若是传递了一个promise对象,它将很乐意使用该promise做为被拒绝promise的缘由(reason)字段:

setTimeout(console.log, 0, Promise.reject(Promise.resolve()));
// Promise <rejected>: Promise <resolved>
复制代码
同步/异步执行二元性(Duality)

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块抛出了一个未被捕获的错误。这彷佛是违反直觉的,由于代码彷佛是在同步建立被拒绝的Promise实例,而后在被拒绝时引起错误。可是,未捕获第二个promise的缘由是代码没有尝试在适当的异步模式下捕获错误。这种行为强调了promise的实际行为:它们是同步对象-在同步执行模式内使用-充当通往异步执行模式的桥梁。

在前面的示例中,来自被拒绝的promise的错误不是在同步执行线程中引起的,而是在浏览器的异步消息队列执行中引起的。所以,封装try/catch块不足以捕获此错误。一旦代码开始以这种异步模式执行,与之交互的惟一方法就是使用异步模式构造—更具体地说,是promise方法。

Promise实例方法

在promise实例上公开的方法用于弥合同步外部代码路径和异步内部代码路径之间的差距。这些方法可用于访问从异步操做返回的数据,处理promise的成功和失败结果,串行评估promise或添加仅在promise进入终端状态后才执行的功能。

实现Thenable接口

出于ECMAScript异步构造的目的,任何公开了then()方法的对象都被视为实现了Thenable接口。如下是实现此接口的最简单类的示例:

class MyThenable {
 then() {}
}
复制代码

ECMAScript Promise类型实现了Thenable接口。不要将这种简单化的接口与诸如TypeScript之类的包中的其余接口或类型定义相混淆,后者提供了thenable接口的更具体形式。

Promise.prototype.then()

Promise.prototype.then()方法是用于将处理程序附加到Promise实例的主要方法。then()方法最多接受两个参数:一个可选的onResolved处理函数和一个可选的onRejected处理函数。每一个仅在定义了它们的promise达到其各自的已实现(fulfilled)已拒绝(rejected)状态时才执行。

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
复制代码

由于一个promise只能转换一次到最终状态,因此能够保证这些处理函数的执行是互斥的。

如前所述,两个处理程序参数都是彻底可选的。做为then()的参数提供的任何非函数类型都将被静默忽略。若是只想显式地提供onRejected处理程序,则将undefined做为onResolved参数是一种典型选择。这样能够避免在内存中建立临时对象,以避免被解释器忽略。

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('hello');
// 显式跳过onResolved处理程序
p2.then(null, () => onRejected('p2'));
// p2 rejected (3秒后) 
复制代码

Promise.prototype.then()方法返回一个新的Promise实例:

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
复制代码

这个新的Promise实例p2是从onResolved处理程序的返回值派生的。处理程序的返回值包装在Promise.resolve()中以生成新的Promise。若是未提供处理函数,则该方法将直接传递初始promise的已解决的值。若是没有显式的return语句,则默认的返回值是undefined,并包装在Promise.resolve()中。

let p1 = Promise.resolve('foo');

// 调用then()方法时,没有提供处理函数参数,p1.then()的结果是直接返回p1给p2
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

// 1, 处理程序的返回值包装在Promise.resolve()中以生成新的Promise
// 2, Promise.resolve()保留返回的promise
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
复制代码

抛出异常将返回被拒绝的promise:

let p10 = p1.then(() => { throw 'baz'; });
// Uncaught (in promise) baz
setTimeout(console.log, 0, p10); // Promise <rejected> baz
复制代码

更重要的是,返回错误不会触发相同的拒绝行为,而是将错误对象包装在已解决的Promise中:

let p11 = p1.then(() => Error('qux'));
setTimeout(console.log, 0, p11); // Promise <resolved>: Error: qux 
复制代码

onRejected处理函数的处理方式也相同:从onRejected处理函数返回的值包装在Promise.resolve()中。乍一看,这彷佛违反直觉,可是onRejected处理程序正在执行其工做以捕获异步错误。所以,该拒绝处理函数在不引起其余错误的状况下完成执行应视为预期的promise行为,并所以返回已解决的promise。

如下Promise.reject()代码片断和使用Promise.resolve()的先前示例相似:

let p1 = Promise.reject('foo');
// 调用then()方法时,没有提供处理函数参数,p1.then()的结果是直接返回p1给p2
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()保留返回的promise
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 
复制代码
Promise.prototype.catch()

Promise.prototype.catch()方法只能用于将拒绝处理函数附加到Promise。它只须要一个参数,即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()方法返回一个新的Promise实例:

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实例,Promise.prototype.catch()的行为与Promise.prototype.then()的onRejected处理程序相同。

Promise.prototype.finally()

Promise.protoype.finally()方法可用于附加onFinally处理程序,该处理程序在promise达到已解决或已拒绝状态时执行。这对于避免onResolved和onRejected处理程序之间的代码重复颇有用。重要的是,处理程序没有任何方法能够肯定promise是否已解决或被拒绝,所以该方法旨在用于清除之类的事情。

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()方法返回一个新的Promise实例:

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
复制代码

这个新的Promise实例是经过不一样于then()或catch()的方式派生的。由于onFinally旨在成为状态未知的方法,因此在大多数状况下,它将做为直接传递父promose的做用。不管是已解决状态仍是被拒绝状态,都是如此。

let p1 = Promise.resolve('foo');
// 这些都充当直接传递以前的promise, 即p1
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
复制代码

惟一的例外是它返回待定的promise或引起错误(经过显式throw或返回被拒绝的promise)。在这些状况下,将返回相应的promise(待定或拒绝),以下所示:

// Promise.resolve()保留返回的promise
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
复制代码

返回待定的promise是一种不常见的状况,由于一旦promise解决,新的promise仍将充当初始promise来传递:

let p1 = Promise.resolve('foo');

// resolve('bar')将被忽略
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
复制代码

待续。。。

总结

长期以来,在单线程JavaScript运行时内部掌握异步行为一直是一项艰巨的任务。随着ES6中引入Promise和ES7中引入async/await ,ECMAScript中的异步构造获得了极大的加强。Promise和async/await不只启用了之前难以实现或没法实现的模式,并且还带来了一种全新的JavaScript编写方式,该方式更加简洁,简短,易于理解和调试。它们是现代JavaScript工具箱中最重要的工具之一。

阿里云,云服务器,仅86元/年,有须要的同窗能够直接点击连接参团购买

相关文章
相关标签/搜索