[书籍翻译] 《JavaScript并发编程》第三章 使用Promises实现同步

本文是我翻译《JavaScript Concurrency》书籍的第三章 使用Promises实现同步,该书主要以Promises、Generator、Web workers等技术来说解JavaScript并发编程方面的实践。javascript

完整书籍翻译地址:github.com/yzsunlei/ja… 。因为能力有限,确定存在翻译不清楚甚至翻译错误的地方,欢迎朋友们提issue指出,感谢。java

Promises几年前就在JavaScript类库中实现了。这一切都始于Promises/A+规范。这些类库的实现都有它们本身的形式,直到最近(确切地说是ES6),Promises规范才被JavaScript语言归入。如标题那样 - 它帮助咱们实现同步原则。node

在本章中,咱们将首先简单介绍Promises中各类术语,以便更容易理解本章的后面部份内容。而后,经过各类方式,咱们将使用Promises来解决目前的一些问题,并让并发处理更容易。准备好了吗?git

Promise相关术语

在咱们深刻研究代码以前,让咱们花一点时间确保咱们紧紧掌握Promises有关的术语。有Promise实例,可是还有各类状态和方法。若是咱们可以弄清楚Promise这些术语,那么后面的章节会更易理解。这些解释简短易懂,因此若是您已经使用过Promises,您能够快速看下这些术语,就当复习下。程序员

Promise

顾名思义,Promise是一种承诺。将Promise视为尚不存在的值的代理。Promise让咱们更好的编写并发代码,由于咱们知道值会在未来某个时刻存在,而且咱们没必要编写大量的状态检查样板代码。github

状态(State)

Promises老是处于如下三种状态之一:算法

• 等待:这是Promise建立后的第一个状态。它一直处于等待状态,直到它完成或被拒绝。编程

• 完成:该Promise值已经处理完成,并能为它提供then()回调函数。json

• 拒绝:处理Promise的值出了问题。如今没有数据。后端

Promise状态的一个有趣特性是它们只转换一次。它们要么从等待状态到完成,要么从等待状态到被拒绝。一旦它们进行了这种状态转换,后面就会锁定在这种状态。

执行器(Executor)

执行器函数负责以某种方式解析值并将处于等待状态。建立Promise后当即调用此函数。它须要两个参数:resolver函数和rejector函数。

解析器(Resolver)

解析器是一个做为参数传递给执行器函数的函数。实际上,这很是方便,由于咱们能够将解析器函数传递给另外一个函数,依此类推。调用解析器函数的位置并不重要,可是当它被调用时,Promise会进入一个完成状态。状态的这种改变将触发then()回调 - 这些咱们将在后面看到。

拒绝器(Rejector)

拒绝器与解析器类似。它是传递给执行器函数的第二个参数,能够从任何地方调用。当它被调用时,Promise从等待状态改变到拒绝状态。这种状态的改变将调用错误回调函数,若是有的话,会传递给then()或catch()。

Thenable

若是对象具备接受完成回调和拒绝回调做为参数的then()方法,则该对象就是Thenable。换句话说,Promise是Thenable。可是在某些状况下,咱们可能但愿实现特定的解析语义。

完成和拒绝Promises

若是上一节刚刚介绍的几个术语听起来让你困惑,那别担忧。从本节开始,咱们将看到全部这些Promises术语的应用实践。在这里,咱们将展现一些简单的Promise解决和拒绝的示例。

完成Promises

解析器是一个函数,顾名思义,它完成了咱们的Promise。这不是完成Promise的惟一方法 - 咱们将在后面探索更高级的方式。但到目前为止,这种方法是最多见的。它做为第一个参数传递给执行器函数。这意味着执行器能够经过简单地调用解析器直接完成Promise。但这并不怎么实用,不是吗?

更常见的状况是Promise执行器函数设置即将发生的异步操做 - 例如拨打网络电话。而后,在这些异步操做的回调函数中,咱们能够完成这个Promise。在咱们的代码中传递一个解析函数,刚开始可能感受有点违反直觉,可是一旦咱们开始使用它们就会发现颇有意义。

解析器函数是一个相对Promise来讲比较难懂的函数。它只能完成一次Promise。咱们能够调用解析器不少次,但只在第一次调用会改变Promise的状态。下面是一个图描述了Promise的可能状态;它还显示了状态之间是如何变化的:

image060.gif

如今,咱们来看一些Promise代码。在这里,咱们将完成一个promise,它会调用then()完成回调函数:

//咱们的Promise使用的执行器函数。
//第一个参数是解析器函数,在1秒后调用完成Promise。
function executor(resolve) {
	setTimeout(resolve, 1000);
}

//咱们Promise的完成回调函数。
//这个简单地在咱们的执行程序函数运行后,中止那个定时器。
function fulfilled() {
	console.timeEnd('fulfillment');
}

//建立promise,并当即运行,
//而后启动一个定时器来查看调用完成函数须要多长时间。
var promise = new Promise(executor);
promise.then(fulfilled);
console.time('fulfillment');
复制代码

咱们能够看到,解析器函数被调用时fulfilled()函数会被调用。执行器实际上并不调用解析器。相反,它将解析器函数传递给另外一个异步函数 - setTimeout()。执行器并非咱们试图去弄清楚的异步代码。能够将执行器视为一种协调程序,它编排异步操做并肯定什么时候执行Promise。

前面的示例未解析任何值。当某个操做的调用者须要确认它成功或失败时,这是一个有效的用例。相反,让咱们此次尝试解析一个值,以下所示:

//咱们的Promise使用的执行函数。
//建立Promise后,设置延时一秒钟调用"resolve()",
//并解析返回一个字符串值 - "done!"。
function executor(resolve) {
	setTimeout(() => {
		resolve('done!');
	}, 1000);
}

//咱们Promise的完成回调接受一个值参数。
//这个值将传递到解析器。
function fulfilled(value) {
	console.log('resolved', value);
}

//建立咱们的Promise,提供执行程序和完成回调函数。
var promise = new Promise(executor);
promise.then(fulfilled);
复制代码

咱们能够看到这段代码与前面的例子很是类似。区别在于咱们的解析器函数其实是在传递给setTimeout()的回调函数的闭包内调用的。这是由于咱们正在解析一个字符串值。还有一个将被解析的参数值传递给咱们的fulfilled()函数。

拒绝promises

Promise执行器函数并不老是定期望进行,当出现问题时,咱们须要拒绝promise。这是从等待状态转换到另外一个可能的状态。这不是进入一个完成状态而是进入一个被拒绝的状态。这会致使执行不一样的回调,与完成回调函数是分开的。值得庆幸的是,拒绝Promise的机制与完成Promise很是类似。咱们来看看这是如何实现的:

//此执行器在延时一秒后拒绝Promise。
//它使用拒绝回调函数来改变状态,
//并传递拒绝的参数值到回调函数。
function executor(resolve, reject) {
	setTimeout(() => {
		reject('Failed');
	}, 1000);
}

//用做拒绝回调的函数。
//它接收提供拒绝的参数值。
function rejected(reason) {
	console.error(reason);
}

//建立promise,并运行执行器。
//使用“catch()”方法来接收拒绝回调函数。
var promise = new Promise(executor);
promise.catch(rejected);
复制代码

这段代码看起来和在上一节中看到的代码很是类似。咱们设置了超时,而且咱们拒绝了它而不是完成它。这是使用rejector函数完成的,并做为第二个参数传递给执行器。

咱们使用catch()方法而不是then()方法来设置拒绝回调函数。咱们将在本章后面看到then()方法如何用于同时处理完成和拒绝回调函数。此示例中的拒绝回调函数仅将失败缘由打印出来。一般状况下提供此返回值很重要。当咱们完成promise时,返回值也是常见的,尽管不是必需的。另外一方面,对于拒绝函数,通常也不多有状况仅仅经过回调函数输出拒绝缘由。

让咱们看下另外一个例子,它捕获执行器中抛出的异常,并为拒绝回调函数提供更有意义的报错缘由:

//此promise执行程序抛出错误,
//并调用拒绝回调函数输出错误信息。
new Promise(() => {
	throw new Error('Problem executing promise');
}).catch((reason) => {
	console.error(reason);
});

//此promise执行程序捕获错误,
//并调用拒绝回调函数输出更有意义的错误信息。
new Promise((resolve, reject) => {
	try {
		var size = this.name.length;
	} catch (error) {
		reject(error instanceof TypeError ? 'Missing "name" property' : error);
	}
}).catch((reason) => {
	console.error(reason);
});
复制代码

前一个例子中第一个Promise的有趣之处在于它确实改变了状态,即便咱们没有使用resolve()或reject()明确地改变promise的状态。然而,最终改变promise的状态是很重要的; 咱们将在下一节中探讨这个话题。

空Promises

尽管事实上执行器函数传递了一个完成回调函数和拒绝回调函数,但并不保证promise将改变状态。有些状况下,promise只是挂起,并无触发完成回调也没有触发拒绝回调。这可能并无什么问题,事实上,简单的promises,就很容易发现和修复没有响应的promises。然而,随着咱们进入更复杂的场景后,一个promise的完成回调能够做为其余几个promise的回调结果。若是一个promises不能完成或拒绝,而后整个流程将崩溃。这种状况调试起来是很是麻烦的;下面的图能够很清楚的看到这个状况:

image061.gif

在图中,咱们能够看到哪一个promise致使依赖的promise挂起,但经过调试代码来解决这个问题并不容易。如今让咱们看看致使promise挂起的执行函数:

//这个promise可以正常运行执行器函数。
//但“then()”回调函数永远不会被执行。
new Promise(() => {
	console.log('executing promise');
}).then(() => {
	console.log('never called');
});

//此时,咱们并不知道promise出了什么问题
console.log('finished executing, promise hangs');
复制代码

可是,是否有一种更安全的方式来处理这种不肯定性呢?在咱们的代码中,咱们不须要挂起无需完成或拒绝的执行函数。让咱们来实现一个执行器包装函数,像一个安全网那样让过长时间还没完成的promises执行拒绝回调函数。这将揭开解决很差处理的promise场景的神秘面纱:

//promise执行器函数的包装器,
//在给定的超时时间后抛出错误。
function executorWrapper(func, timeout) {
	//这是实际调用的函数。
	//它须要解析器函数和拒绝器函数做为参数。
	return function executor(resolve, reject) {
		//设置咱们的计时器。
		//当时间到达时,咱们可使用超时消息拒绝promise。
		var timer = setTimeout(() => {
			reject('Promise timed out after $​​ {timeout} MS');
		}, timeout);
		
		//调用咱们原来的执行器包装函数。
		//咱们实际上也包装了完成回调函数
		//和拒绝回调函数,因此当
		//执行者调用它们时,会清除定时器。
		func((value) => {
            clearTimeout(timer);
            resolve(value);
        }, (value) => {
            clearTimeout(timer);
            reject(value);
        });
    };
}

//这个promise执行后超时,
//超时错误消息传递给拒绝回调。
new Promise(executorWrapper((resolve, reject) => {
	setTimeout(() => {
		resolve('done');
	}, 2000);
}, 1000)).catch((reason) => {
	console.error(reason);
});

//这个promise执行后按预期运行,
//在定时结束以前调用“resolve()”。
new Promise(executorWrapper((resolve, reject) => {
	setTimeout(() => {
		resolve(true);
	}, 500);
}, 1000)).then((value) => {
	console.log('resolved', value);
});
复制代码

对promises做出改进

既然咱们已经很好地理解了promises的执行机制,本节将详细介绍如何使用promises来解决特定问题。一般,这意味着当promises完成或被拒绝时,咱们会达到咱们某些目的。

咱们将首先查看JavaScript解释器中的任务队列,以及这些对咱们的解析回调函数的意义。而后,咱们将考虑使用promise的结果数据,处理错误,建立更好的抽象来响应promises,以及thenables。让咱们开始吧。

处理任务队列

JavaScript任务队列的概念在“第2章,JavaScript运行模型”中提到过。它的主要职责是初始化新的执行上下文堆栈。这是常见的任务队列。然而,还有另外一种队列,这是专用于执行promises回调的。这意味着,若是他们都存在时,算法会从这些队列中选择一个任务执行。

Promises具备内置的并发语义,并且有充分的理由。若是一个promise被用来确保某个值最终被解析,那么为对其做出响应的代码赋予高优先级是有意义的。不然,当值到达时,处理它的代码可能还要在其余任务后面等待很长的时间才能执行。让咱们编写一些代码来演示下这些并发语义:

//建立5个promise,记录它们的执行时间,
//以及当他们对返回值作出响应的时间。
for (let i = 0; i < 5; i++) {
	new Promise((resolve) => {
		console.log('execting promise');
		resolve(i);
	}).then((value) => {
		console.log('resolved', i);
	});
}

//在任何promise完成回调以前,这里会先被调用,
//由于堆栈任务须要在解释器进入promise解析回调队列以前完成,
//当前5个“then()”回调将被置后。
console.log('done executing');

//→
//execting promise
//execting promise
// ...
//done executing
//resolved 1
//resolved 2
// ...
复制代码

拒绝回调也遵循一样的语义。

使用promise的返回数据

到目前为止,咱们已经在本章中看到了一些示例,其中解析器函数完成promise后并返回值。传递给此函数的值是最终传递给完成回调函数的值。经过让执行程序设置任何异步操做的方法,例如setTimeout(),延时传递该值调用解析程序。但在这些例子中,调用者实际上并无等待任何值;咱们只使用setTimeout()做为示例异步操做。让咱们看一下咱们实际上没有值的状况,异步网络请求须要获取到它:

//用于从服务器获取资源的通用函数,
//返回一个promise。
function get(path) {
	return new Promise((resolve, reject) => {
		var request = new XMLHttpRequest();
		
		//promise解析数据加载后的JSON数据。
		request.addEventListener('load', (e) => {
			resolve(JSON.parse(e.target.responseText));
		});

		//当请求出错时,promise执行拒绝回调函数。
		request.addEventListener('error', (e) => {
			reject(e.target.statusText || '未知错误');
		});


		//若是请求被停止时,咱们调用完成回调函数
		request.addEventListener('abort', resolve);
		
		request.open('get', path);
		request.send();
	});
}

//咱们能够直接附加咱们的“then()”处理程序
//到“get()”,由于它返回一个promise。
//在解析以前,这里使用的值是一个真正的异步操做,
//由于必须发请求远程获取值。
get('api.json').then((value) => {
	console.log('hello', value.hello);
});
复制代码

使用像get()这样的函数,它们不只始终返回像promise同样的原生类型,并且还封装了一些让人讨厌的异步细节。在咱们的代码中处理XMLHttpRequest对象并不使人愉快。咱们已经简化了能够返回的各类状况。而不是老是必须为load,error和abort事件建立处理程序,咱们只须要关心一个接口 - promise。这就是同步并发原则的所有内容。

错误回调

有两种方法能够对被拒绝的promise作出处理。换句话说,提供错误回调。第一种方法是使用catch()方法,该方法使用单一回调函数。另外一种方法是将被拒绝的回调函数做为then()的第二个参数传递。

将then()方法用来处理拒绝回调函数在某些状况下表现的更好,它应该被用来替代catch()函数。第一个场景是编写promises和thenable对象能够互换的代码。catch()方法不是thenable必要的一部分。第二个场景是当咱们创建回调链时,咱们将在本章后面探讨。

让咱们看一些代码,它们比较了两种为promises提供拒绝回调函数的方法:

//这个promise执行器将随机执行完成回调或拒绝回调
function executor(resolve, reject) {
	cnt++;
	Math.round(Math.random()) ? 
		resolve(`fulfilled promise ${cnt}`) :
		reject(`rejected promise ${cnt}`);
}

//让“log()”和“error()”函数做为简单回调函数
var log = console.log.bind(console),
	error = console.error.bind(console),
	cnt = 0;

//建立一个promise,而后经过“catch()”方法传入拒绝回调。
new Promise(executor).then(log).catch(error);

//建立一个promise,而后经过“then()”方法传入拒绝回调。
new Promise(executor).then(log, error);
复制代码

咱们能够看到这两种方法实际上很是类似。在代码美观上,也没有哪一个有真正的优点。然而,当涉及到使用thenables时,then()方法有一个优点,咱们后面会看到。可是,因为咱们实际上并无以任何方式使用promise实例,除了添加回调以外,实际上没有必要担忧catch()和then()用于注册拒绝回调。

始终响应

Promises最终老是结束于完成状态或拒绝状态。咱们一般为每一个状态传入不一样的回调函数。可是,咱们极可能但愿为这两个状态执行一些相同的操做。例如,若是使用promise的组件在promise等待时更改状态,咱们要确保在完成或拒绝promise后清除状态。

咱们能够用这样的方式编写代码:完成和拒绝状态的每一个回调都去执行这些操做,或者他们每一个均可以调用执行一些公用的清理函数。下面这种方式的示图:

image065.gif

将清理任务分配给promise是否有意义,而不是将其分配给其它个别结果?这样,在解析promise时运行的回调函数专一于它须要对值执行的操做,而拒绝回调则专一于处理错误。让咱们看看是否可使用always()方法编写一些扩展promises的代码:

//在promise原型上扩展使用“always()”方法。
//无论promise是完成仍是拒绝,始终会调用给定的函数。
Promise.prototype.always = function(func) {
	return this.then(func, func);
};

//建立promise随机完成或被拒绝。
var promise = new Promise((resolve, reject) => {
	Math.round(Math.random()) ? 
	resolve('fullfilled') : reject('rejected');
});

//传递promise完成和拒绝回调。
promise.then((value) => {
	console.log(value);
}, (reason) => {
	console.error(reason);
});

//这个回调函数老是会在上面的回调执行以后调用。
promise.always((value) => {
	console.log('cleaning up...');
});
复制代码

请注意,在这里顺序很重要。若是咱们在then()以前调用always(),那么函数仍然会运行,但它会在 回调提供给then()以前运行。咱们实际上能够在then()以前和以后都调用always(),以便在完成或拒绝回调 以前以及以后运行代码。

处理其余promises

到目前为止,咱们在本章中看到的大多数promise都是由执行程序函数直接完成的,或者是当值准备完成时从异步操做中调用解析器的结果。像这样传递回调函数实际上很是灵活。例如,执行程序甚至没必要执行任何任务,除了将解析器函数存储在某处以便稍后调用它来解析promise。

当咱们发现本身处于须要多个值的更复杂的同步场景时,这可能特别有用,这些值已经被传递给调用者。若是咱们有处理回调函数,咱们就能够处理promise。让咱们看看,在存储代码的解析函数的多个promises,使每个promise均可以在后面处理:

//存储一系列解析器函数的列表。
var resolvers = [];

//在执行器中建立5个新的promise,
//解析器被推到了“resolvers”数组。
//咱们能够给每个promise执行回调。
for(let i = 0; i < 5; i++) {
	new Promise(() => {
		resolvers.push(resolve);
	}).then((value) => {
		console.log(`resolved ${i + 1}`, value);
	});
}

//设置一个2s以后延时运行函数,
//当它运行时,咱们遍历“解析器”数组中的每个解析器函数,
//而且传入一个返回值来调用它。
setTimeout(() => {
	for(resolver of resolvers) {
		resolver(true);
	}
}, 2000);
复制代码

正如这个例子所代表的那样,咱们没必要在executor函数内处理它们。事实上,咱们甚至不须要在建立和设置执行程序和完成函数以后显式引用promise实例。解析器函数已存储在某处,它包含对promise的引用。

类Promise对象

Promise类是一种原生的JavaScript类型。可是,咱们并不老是须要建立新的promise实例来实现相同的同步操做。咱们可使用静态Promise.resolve()方法来解析这些对象。让咱们看看如何使用此方法:

//“Promise.resolve()”方法能够处理thenable对象。
//这是一个带有“then()”方法的相似于执行器的对象。
//这个执行器将随机完成或拒绝promise。
Promise.resolve({then: (resolve, reject) => {
	Math.round(Math.random()) ? resolve('fulfilled') : reject('rejected');

	//这个方法返回一个promise,因此咱们可以
	//设置已完成和被拒绝的回调函数。
}}).then((value) => {
	console.log('resolved', value);
}, (reason) => {
	console.error('reason', reason);
});
复制代码

咱们将在本章的最后一节中再次讨论Promise.resolve()方法,以了解更多用例。

创建回调链

咱们在本章前面介绍的每种promise方法都会返回promise。这容许咱们在返回值上再次调用这些方法,从而产生then().then()调用的链,依此类推。链式promise具备挑战性的一个方面是promise方法返回的是新实例。也就是说,咱们将在本节中探讨promise在必定程度上的不变性。

随着咱们的应用程序变得愈来愈大,并发性挑战随之增长。这意味着咱们须要考虑更好的方法来利用原生同步语义,例如promises。正如JavaScript中的任何其余原始值同样,咱们能够将它们从函数传递给函数。咱们必须以一样的方式处理promises - 传递它们,并创建在回调函数链上。

Promises只改变状态一次

Promise初始时是等待状态,而且它们结束于已完成或被拒绝的状态。一旦promise转变为其中一种状态,它们就会锁定在这种状态。这有两个有趣的反作用。

首先,屡次尝试完成或拒绝promise将被忽略。换句话说,解析器和拒绝器是幂等的 - 只有第一次调用对promise有影响。让咱们看看这代码如何执行:

//此执行器函数尝试解析promise两次,
//但完成的回调只调用一次。
new Promise((resolve, reject) => {
	resolve('fulfilled');
	resolve('fulfilled');
}).then((value) => {
	console.log('then', value);
});

//这个执行器函数尝试拒绝promise两次,
//但拒绝的回调只调用一次。
new Promise((resolve, reject) => {
	reject('rejected');
	reject('rejected');
}).catch((reason) => {
	console.error('reason');
});
复制代码

promises仅改变状态一次的另外一个含义是promise能够在添加完成或拒绝回调以前处理。竞争条件,例如这个,是并发编程的残酷现实。一般,回调函数会在建立时添加到promise中。因为JavaScript是运行到完成的,所以在添加回调以前,不会处理promise解析回调的任务队列。可是,若是promise当即在执行中解析怎么办?若是将回调添加到另外一个JavaScript执行上下文的promise中会怎样?让咱们看看是否能够用一些代码来更好地说明这些状况:

//此执行器函数当即解析promise。添加“then()”回调时,
//promise已经解析了。但回调函数仍然会使用已解析的值进行调用。
new Promise((resolve, reject) => {
	resolve('done');
	console.log('executor', 'resolved');
}).then((value) => {
	console.log('then', value);
});

//建立一个当即解析的新promise执行器函数。
var promise = new Promise((resolve, reject) => {
	resolve('done');
	console.log('executor', 'resolved');
});

//这个回调是promise解析后就当即执行了。
promise.then((value) => {
	console.log('then 1', value);
});

//此回调在promise解析后未添加到另外一个的promise中,
//它仍然被当即调用并得到已解析的值。
setTimeout(() => {
	promise.then((value) => {
		console.log('then 2', value);
	});
}, 1000);
复制代码

此代码说明了promises的一个很是重要的特性。不管什么时候将执行回调添加到promise中,不管是处于暂时挂起状态仍是解析状态,使用promise的代码都不会更改。从表面上看,这彷佛不是什么大不了的事。可是这种竞争条件检查的类型须要更多的并发代码来保护本身。相反,Promise原生语法为咱们处理这个问题,咱们能够开始将异步值视为原始类型。

不可改变的promises

promises并不是真正不可改变。它们改变状态,then()方法将回调函数添加到promise。可是,有一些不可改变的promises特征值得在这里讨论,由于它们会在某些状况下影响咱们的promise代码。

从技术上讲,then()方法实际上并无改变promise对象。它建立了所谓的promise能力,它是一个引用promise的内部JavaScript记录,以及咱们添加的函数。所以,它不是JavaScript语言中的真正语法。

这是一张图,说明当咱们连接两个或更多then()一块儿调用时会发生什么:

image069.gif

咱们能够看到,then()方法不会返回与上下文一块儿调用的相同实例。相反,then()建立一个新的promise实例并返回它。让咱们看一些代码,来进一步的说明当咱们使用then()将promises连接在一块儿时会发生的事情:

//建立一个当即解析的promise,
//而且存储在“promise1”中。
var promise1 = new Promise((resolve, reject) => {
	resolve('fulfilled');
});

//使用“promise1”的“then()”方法建立一个
//新的promise实例,存储在“promise2”中。
var promise2 = promise1.then((value) => {
	console.log('then 1', value);
	//→then 1 fulfilled
});

//为“promise2”建立一个“then()”回调。这实际上
//建立第三个promise实例,但咱们不用它作任何事情。
promise2.then((value) => {
	console.log('then 2', value);
	//→then 2 undefined
});

//确信“promise1”和“promise2”其实是不一样的对象
console.log('equal', promise1 === promise2);
//→equal false
复制代码

咱们能够清楚地看到这两个建立promise的实例在这个例子中是独立的promise对象。值得指出的是第二个promise执行前时,必定是它执行了第一个promise。可是,咱们能够看到的是该值不会传递到第二个promise。咱们将在下一节中解决此问题。

有多少个then()回调,就有多少个promise对象

正如咱们在上一节中看到的那样,使用then()建立的promise将绑定到它们的建立者。也就是说,当第一个promise完成时,绑定它的promise也会完成,依此类推。可是,咱们也发现了一个小问题。已解析的值不会使其传递到第一个回调函数。这样作的缘由是为响应promise解析而运行的每一个回调都是第一个回调的返回值被送入第二个回调,依此类推。咱们的第一个回调将值做为参数的缘由是由于这在promise机制中显然会发生的。

咱们来看看另外一个promise链示例。这一次,咱们将显式返回回调函数中的值:

//建立一个新promise随机调用解析回调或拒绝回调。
new Promise((resolve, reject) => {
	Math.round(Math.random()) ?
	resolve('fulfilled') : reject('rejected');
}).then((value) => {
	//在完成原始promise时调用返回值,
	//以防另外一个promise连接到这一个。
	console.log('then 1', value); 
	return value;
}).catch((reason) => {
	//连接到第二个promise,
	//当拒绝回调时执行。
	console.error('catch 1', reason);
}).then((value) => {
	//连接到第三个promise,
	//按预期获得值,并返回值给任何下个promise回调使用。
	console.log('then 2', value);
	return value;
}).catch((reason) => {
	//这里永不会被调用,
	//拒绝回调不会经过promise链传递。
	console.error('catch 2', reason);
});
复制代码

这看起来不错。咱们能够看到已解析的值经过promise链传递。有一个异常 - 拒绝回调不会向后传递。相反,只有链中的第一个promise拒绝回调会执行。其他的promise回调只是完成,而不是拒绝。这意味着最后一个catch()回调永远不会运行。

当咱们以这种方式将promise连接在一块儿时,咱们的执行回调函数须要可以处理错误条件。例如,已解析的值可能具备error属性,能够检查其具体问题。

promises传递

在本节中,咱们讲讲promise做为原始值的用法。咱们常常用原始值作的事情是将它们做为参数传递给函数,并从函数中返回它们。promise和其余原生语法之间的关键区别在于咱们如何使用它们。其余值是始终都存在,而promise的值到将来某个时间点才存在。所以,咱们须要经过回调函数定义一些操做过程,当值得到时去执行。

promises的好处是用于提供这些回调函数的接口小巧且一致。当咱们将值与将做用于它的代码耦合时,咱们不须要再去自主创造同步机制。这些单元能够像任何其余值同样在咱们的应用程序中运用,而且并发语义是常见的。这是几个promise函数相互传递的示图:

image071.gif

在这个函数堆栈调用结束时,咱们获得一个完成几个promise的解析的promise对象。整个promise链是从第一个promise完成而开始的。好比何遍历promise链的机制​​更重要的是全部这些函数均可以自由使用这个promise传递的值而不影响其余函数。

在这里有两个并发原则。首先,咱们经过执行异步操做仅只能处理该值一次; 每一个回调函数均可以自由使用此解析值。其次,咱们在抽象同步机制方面作得很好。换句话说,代码并无带有不少重复代码。让咱们看看传递promise的代码实际的样子:

//简单实用的工具函数,
//将多个较小的函数组合成一个函数。
function compose(...funcs) {
	return function(value) {
		var result = value;
		
		for(let func of funcs) {
			result = func(value);
		}
		return result;
	};
}

//接受一个promise或一个完成值。
//若是这是一个promise,它添加了一个“then()”回调并返回一个新的promise。
//不然,它会执行“update”并返回值。
function updateFirstName(value) {
	if (value instanceof Promise) {
		return value.then(updateFirstName);
	}

	console.log('first name', value.first); 
	return value;
}

//与上面的函数相似,
//只是它执行不一样的UI“update”。
function updateLastName(value) {
	if (value instanceof Promise) {
		return value.then(updateLastName);
	} 

	console.log('last name', value.last); 
	return value;
}

//与上面的函数相似,除了它
//只是它执行不一样的UI“update”。
function updateAge(value) {
	if (value instanceof Promise) {
		return value.then(updateAge);
	}

	console.log('age', value.age);
	return value;
}

//一个promise对象,
//它在延时一秒钟以后,
//携带一个数据对象完成promise。
var promise = new Promise((resolve, reject) => {
	setTimeout(() => {
		resolve({
			first: 'John',
			last: 'Smith',
			age: 37
		});
	});
}, 1000);

//咱们组装一个“update()”函数来更新各类UI组件。
var update = compose(
	updateFirstName,
	updateLastName,
	updateAge
);

//使用promise调用咱们的更新函数。
update(promise);
复制代码

这里的关键函数是咱们的更新函数 - updateFirstName(),updateLastName()和updateAge()。他们很是灵活,接受一个promise或promise返回值。若是这些函数中的任何一个将promise做为参数,它们会经过添加then()回调函数来返回新的promise。请注意,它添加了相同的函数。updateFirstName()将添加updateFirstName()做为回调。当回调触发时,它将与这次用于更新UI的普通对象一块儿使用。所以,promise若是失败,咱们能够继续更新UI。

promise检查每一个函数都须要三行,这并非很是突兀的。最终结果是易读且灵活的代码。顺序可有可无; 咱们能够用不一样的顺序包装咱们的update()函数,而且UI组件都将以相同的方式更新。咱们能够将普通对象直接传递给update(),一切都会一样执行。看起来不像并发代码的并发代码是咱们在这里取得的重大成功。

同步多个promises

在本章前面,咱们已经探究了单个promise实例,它解析一个值,触发回调,并可能传递给其余promises处理。在本节中,咱们将介绍几种静态Promise方法,它们能够帮助咱们处理须要同步多个promise值的状况。

首先,咱们将处理咱们开发的组件须要同步访问多个异步资源的状况。而后,咱们将看一下不常见的状况,如异步操做在处理以前因为UI中发生的事件而变得没有意义。

等待promises

在咱们等待处理多个promise的状况下,也许是将多个数据源转换后提供给一个UI组件使用,咱们可使用Promise.all()方法。它将promise实例的集合做为输入,并返回一个新的promise实例。仅当完成了全部输入的promise时,才会返回一个新实例。

then()函数是咱们为Promise提供的建立新promise的回调。给出一组解析值做为输入。这些值对应于索引输入promise的位置。这是一个很是强大的同步机制,它能够帮助咱们实现同步并发原则,由于它隐藏了全部的处理记录。

咱们不须要几个回调,让每一个回调都协调它们所绑定的promise状态,咱们只需一个回调,它具备咱们须要的全部解析数据。这个示例展现如何同步多个promise:

//用于发送“GET”HTTP请求的工具函数,
//并返回带有已解析的数据的promise。
function get(path) {
	return new Promise((resolve, reject) => {
		var request = new XMLHttpRequest();
		
		//当数据加载时,完成解析了JSON数据的promise
		request.addEventListener('load', (e) => {
			resolve(JSON.parse(e.target.responseText));
		});

		//当请求出错时,
		//promise被适当的缘由拒绝。
		request.addEventListener('error', (e) => {
			reject(e.target.statusText || 'unknown error');
		});

		//若是请求被停止,咱们继续完成处理请求 
		request.addEventListener('abort', resolve);
		
		request.open('get', path);
		request.send();
	});
}


//保存咱们的请求promises。
var requests = [];

//发出5个API请求,并将相应的5个
//promise放在“requests”数组中。
for (let i = 0; i < 5; i++) {
	requests.push(get('api.json'));
}

//使用“Promise.all()”让咱们传入一个数组promises,
//当全部promise完成时,返回一个已经完成的新promise。
//咱们的回调获得一个数组对应于promises的已解析值。
Promise.all(requests).then((values) => {
	console.log('first', values.map(x => x[0])); 
	console.log('second', values.map(x => x[1]));
});
复制代码

取消promises

到目前为止,咱们在本书中已看到的XHR请求具备停止请求的处理程序。这是由于咱们能够手动停止请求并阻止任何load回调函数运行。须要此功能的典型场景是用户单击取消按钮,或导航到应用程序的其余部分,从而使请求变得毫无心义。

若是咱们是要在抽象promise上更上一层楼,在一样的原则也适用。而一些可能发生的并发操做的执行让promise变得毫无心义。promises和XHR请求的过程当中之间的区别,是前者没有abort()方法。最后咱们要作的一件事是在咱们的promise回调中开始引入可能并没必要要的取消逻辑。

Promise.race()方法在这里能够帮助咱们。顾名思义,该方法返回一个新的promise,它由第一个要解析的输入promise决定。这可能你听的很少,但实现Promise.race()的逻辑并不容易。它其实是同步原则,隐藏了应用程序代码中的并发复杂性。咱们来看看这个方法是怎么能够帮助咱们处理因用户交互而取消的promise:

//用于取消数据请求的解析器​​函数。
var cancelResolver;

//一个简单的“常量”值,用于处理取消promise
var CANCELED = {};

//咱们的UI组件
var buttonLoad = document.querySelector('button.load'),
	buttonCancel = document.querySelector('button.cancel');

//请求数据,返回一个promise。
function getDataPromise() {
	//建立取消promise。
	//执行器传入“resolve”函数为“cancelResolver”,
	//因此它稍后能够被调用。
	var cancelPromise = new Promise((resolve) => {
		cancelResolver = resolve;
	});

	//咱们实际想要的数据
	//这一般是一个HTTP请求,
	//但咱们在这里使用setTimeout()简单模拟一下。
	var dataPromise = new Promise((resolve) => {
		setTimeout(() => {
			resolve({hello: 'world'});
		}, 3000);
	});

	//“Promise.race()”方法返回一个新的promise,
	//而且不管输入promise是什么,它均可以完成处理
	return Promise.race([cancelPromise, dataPromise]);
}

//单击取消按钮时,咱们使用
//“cancelResolver()”函数来处理取消promise
buttonCancel.addEventListener('click', () => {
	cancelResolver(CANCELLED);
});

//单击加载按钮时,咱们使用
//“getDataPromise()”发出请求获取数据。
buttonLoad.addEventListener('click', () => {
	buttonLoad.disabled = true;
	getDataPromise().then((value) => {
		buttonLoad.disabled = false;
		//promise获得了执行,但那是由于
		//用户取消了请求。因此咱们这里
		//经过返回CANCELED “constant”退出。
		//不然,咱们有数据可使用。
		if (Object.is(value, CANCELED)) {
			return value;
		}
		
		console.log('loaded data', value);
	});
});
复制代码

做为练习,尝试想象一个更复杂的场景,其中dataPromise是由Promise.all()建立的promise。咱们的 cancelResolver()函数能够一次取消许多复杂的异步操做。

没有执行器的promises

在最后一节中,咱们将介绍Promise.resolve()和Promise.reject()方法。咱们已经在本章前面看到Promise.resolve()如何处理thenable对象。它还能够直接处理值或其余promises。当咱们实现一个可能同步也可能异步的函数时,这些方法会派上用场。这不是咱们想要使用具备模糊并发语义函数的状况。

例如,这是一个可能同步也可能异步的函数,让人感到迷惑,几乎确定会在之后出现错误:

//一个示例函数,它可能从缓存中返回“value”,
//也可能经过“fetchs”异步获取值。
function getData(value) {
	//若是它存在于缓存中,咱们直接返回这个值
	var index = getData.cache.indexOf(value);
	if(index > -1) {
		return getData.cache[index];
	}

	//不然,咱们必须经过“fetch”异步获取它。
	//这个“resolve()”调用一般是会在网络发起请求的回调函数
	return new Promise((resolve) => {
		getData.cache.push(value);
		resolve(value);
	});
}

//建立缓存。
getData.cache = [];

console.log('getting foo', getData('foo'));
//→getting foo Promise
console.log('getting bar', getData('bar'));
//→getting bar Promise
console.log('getting foo', getData('foo'));
//→getting foo foo
复制代码

咱们能够看到最后一次调用返回的是缓存值,而不是一个promise。这很直观,由于咱们不须要经过promise获取最终的值,咱们已经拥有这个值!问题是咱们让使用getData()函数的任何代码表现出不一致性。也就是说,调用getData()的代码须要处理并发语义。此代码不是并发的。让咱们经过引入Promise.resolve()来改变它:

//一个示例函数,它可能从缓存中返回“value”,
//也可能经过“fetchs”异步获取值。
function getData(value) {
	var cache = getData.cache;
	//若是这个函数没有缓存,
	//那就拒绝promise。
	if(!Array.isArray(cache)) {
		return Promise.reject('missing cache');
	}

	//若是它存在于缓存中,
	//咱们直接使用缓存的值返回完成的promise
	var index = getData.cache.indexOf(value);
	
	if (index > -1) {
		return Promise.resolve(getData.cache[index]);
	}

	//不然,咱们必须经过“fetch”异步获取它。
	//这个“resolve()”调用一般是会在网络发起请求的回调函数
	return new Promise((resolve) => {
		getData.cache.push(value);
		resolve(value);
	});
}

//建立缓存。
getData.cache = [];

//每次调用“getData()”返回都是一致的。
//甚至当使用同步值时,
//它们仍然返回获得解析完成的promise。
getData('foo').then((value) => {
	console.log('getting foo', `“${value}”`);
}, (reason) => {
	console.error(reason);
});

getData('bar').then((value) => {
	console.log('getting bar', `“${value}”`);
}, (reason) => {
	console.error(reason);
});

getData('foo').then((value) => {
	console.log('getting foo', `“${value}”`);
}, (reason) => {
	console.error(reason);
});
复制代码

这样更好。使用Promise.resolve()和Promise.reject(),任何使用getData()的代码默认都是并发的,即便数据获取操做是同步的。

小结

本章介绍了ES6中引入的Promise对象的大量细节内容,以帮助JavaScript程序员处理困扰该语言多年的同步问题。大量的使用异步回调,这会产生回调地狱,于是咱们要尽可能避免它。

Promise经过实现一个足以解决任何值的通用接口来帮助咱们处理同步问题。promise老是处于三种状态之一 - 等待,完成或拒绝,而且它们只会改变一次状态。当这些状态发生改变时,将触发回调。promise有一个执行器函数,其做用是设置使用promise的异步操做resolver函数或rejector函数来改变promise的状态。

promise带来的大部分价值在于它们如何帮助咱们简化复杂的场景。由于,若是咱们只需处理一个运行带有解析值回调的异步操做,那么使用promises就不值得。这是不常见的状况。常见的状况是几个异步操做,每一个操做都须要解析返回值;而且这些值须要同步处理和转换。Promises有方法帮助咱们这样作,所以,咱们可以更好地将同步并发原则应用于咱们的代码。

在下一章中,咱们将介绍另外一个新引入的语法 - Generator。与promises相似,生成器是帮助咱们应用另外一个并发原则的机制 - 保护。

最后补充下书籍章节目录

另外还有讲解两章nodeJs后端并发方面的,和一章项目实战方面的,这里就再也不贴了,有兴趣可转向github.com/yzsunlei/ja…查看。

相关文章
相关标签/搜索