[书籍翻译] 《JavaScript并发编程》第四章 使用Generators实现惰性计算

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

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

惰性计算是一种编程技术,它用于当咱们但愿须要使用值的时候才去计算的场景。这样,能够确保咱们确实须要它。相反的,直接都去计算,有可能计算了咱们不须要的值。这一般没什么问题,但当咱们的应用程序的大小和复杂性增加到必定水平,这些计算形成的浪费就不可思议了。node

Generator是引入到JavaScript中一种新的原生类型并做为ES6语言规格的一部分。Generator帮助咱们在代码中实现惰性计算技术,进一步说,帮助咱们实现保护并发原则。git

咱们将经过对Generator的一些简单介绍来开始本章,先让咱们对它们的表现方式有必定了解。以后,咱们将进入更高级的惰性计算场景,并经过协程结束本章。如今让咱们开始吧。github

调用堆栈和内存分配

内存分配是任何编程语言都必不可少的。若是没有它,咱们就没有所谓的数据结构,甚至没有原生类型。如今内存虽然很便宜,通常都有足够的内存可供使用; 但这并不值得高兴。虽然今天在内存中分配更大的数据结构更加可行,可是在10年前,当咱们编程时,咱们仍然必须释放分配内存。JavaScript是一种垃圾自动收集语言,这意味着咱们的代码没必要显式地销毁内存中的对象。可是,垃圾收集器会致使CPU损耗。编程

因此这里有两个因素在起做用。咱们想在这里保存两个资源,咱们将尝试使用生成器来实现惰性计算。咱们没必要要多余的分配内存,若是咱们能避免这一点,那么就能够避开频繁的调用垃圾收集器。在本节中,我将介绍一些Generator生成器概念。后端

标记函数上下文

在一个正常的函数调用栈,一个函数返回一个值。在return语句激活一个新的执行上下文而且丢弃旧的上下文,由于返回就表明已处理完毕了。生成器函数是一个特殊的JavaScript函数语法类型,和return语句相比他们的调用栈不那么老套。这里有张图表示了生成器函数的调用,并在开始生成值时发生的事情:数组

image076.gif

正如return语句将值传递给调用上下文同样,yield语句也会返回一个值。可是,与普通函数不一样的是,生成器函数上下文不会被丢弃。事实上,它们被加上标记,以便在将控制权交还给生成器上下文时,它能够从中断处继续执行获取值,直到完成为止。这个标记很是容易,由于它只是指向咱们代码中的位置。promise

序列而不是数组

在JavaScript中,当咱们须要遍历事物,数字、字符串、对象等列表时,咱们会使用数组。数组是通用的,功能也是强大的。在惰性计算的上下文中,数组的挑战是数组自己就是数据须要分配。因此咱们的数组须要在内存中的某个位置分配元素,而且还有关于数组中元素的元数据。浏览器

若是咱们在使用大数据量的对象,则与数组相关的内存开销就很大。另外,咱们须要以某种方式将这些对象放在数组中。这是额外的步骤会增长CPU消耗。另外一种概念是序列。序列不是有形的JavaScript语言结构。它们是一个抽象的概念 - 数组但没有实际分配数组。序列有助于惰性计算。因为这个缘由,没有什么须要分配内存,而且没有初始入口。这是迭代数组所涉及的示图:

image077.gif

咱们能够看到,在咱们迭代这三个对象以前,咱们首先必须分配一个数组,而后用这些对象填充它。让咱们将这种方法与序列的概念思想进行对比,以下图所示:

image078.gif

对于序列,咱们没有为咱们感兴趣的迭代对象提供明确的容器结构。与序列关联的惟一开销是指向当前项的指针。咱们可使用生成器函数做为在JavaScript中生成序列的机制。正如咱们在上一节中看到的那样,生成器在将值返回给调用者时将其执行上下文加上标记。这是咱们目前须要的最小开销。它使咱们可以惰性地计算对象并将它们做为序列进行迭代。

建立生成器并生成值

在本节中,将介绍生成器函数语法,并将逐步介绍生成器的值。咱们还将研究能够用来迭代生成器生成值的两种方法。

生成器函数语法

生成器函数的语法几乎与普通函数相同。不一样之处在于function关键字的声明后面跟一个星号。更重要的区别是返回值,它老是返回一个生成器实例。此外,尽管建立了新对象,但不须要new关键字。下面让咱们来看看生成器函数是怎样的:

//生成器函数使用星号来表示返回生成器实例。
//咱们能够从生成器返回值,
//然而不是调用者得到该值,
//他们将永远获取生成器实例。
function* gen() {
	return 'hello world';
}

//建立生成器实例。
var generator = gen();

//让咱们看看它是什么样的。
console.log('generator', generator);
//→generator Generator

//这是咱们得到返回值的方式。看起来很尴尬,
//由于咱们永远不会使用生成器函数只返回一个值。
console.log('return', generator.next().value);
//→return hello world
复制代码

咱们不太可能以这种方式使用生成器,但它是说明生成器函数与普通函数一些差异的好方法。例如,return语句在生成器函数中是彻底有效的,然而,正如咱们所看到的,它们为调用者产生了彻底不一样的结果。在实践中,咱们更有可能在生成器中遇到yield语句,因此让咱们接下来看看它们。

生成值

生成器函数的常见状况是产生值并控制返回调用者。将控制权交还给调用者是生成器的一个定义特征。当咱们生成值时,生成器会在代码中标记咱们的位置。这样作是由于调用者可能会从生成器请求另外一个值,而当它发生时,生成器只是从它中止的地方开始。让咱们来看一下产生几回值的生成器函数:

//此函数按顺序生成值。
//没有容器结构,就像一个数组。
//相反,每一次调用yield语句,
//控制权交回到调用者,以及函数中的位置加上标记。
function* gen() {
	yield 'first';
	yield 'second';
	yield 'third';
}

var generator = gen();

//每次调用“next()”时,控制权都会被传回到生成器函数的执行上下文。
//而后,生成器经过标记查找它最近产生值的位置。
console.log(generator.next().value);
console.log(generator.next().value);
console.log(generator.next().value);
复制代码

前面的代码才是序列真正的样子。咱们有三个值,它们是从咱们的函数中顺序产生的。它们也没有放入任何类型的容器结构中。第一个调用yield传递firstnext(),在它被调用的地方。其余两个值也是如此。事实上,行为上是惰性计算的。咱们有三次调用console.log()gen()的实现将返回一组值供咱们输出。相反,当咱们须要输出一个值时,咱们会从生成器中获取它。这是懒惰的因素;咱们会保留咱们的努力,直到他们真正须要,避免分配和计算。

咱们以前的示例不太理想之处是咱们正在重复调用console.log(),实际上,咱们想迭代序列,为其中的每项调用console.log()。让咱们如今迭代一些生成器序列。

迭代生成器

next()方法对于咱们,已不奇怪了,它返回生成器序列接下来的值。它实际返回的值由两个属性构成:生成值和是否生成器结束。可是,咱们通常不想硬编码调用next()。取而代之的是,咱们想调用它反复的从生成器生成值。下面是一个使用while循环的例子,来循环遍历一个生成器:

//基本的生成器函数产生序列值。
function* gen(){
	yield 'first';
	yield 'second';
	yield 'third';
}

//建立生成器。
var generator = gen();

//循环直到序列结束。
while(true) {
	//获取序列中的下一项。
	let item = generator.next();
	
	//有下一个值,仍是结束了?
	if(item.done) {
		break;
	}

	console.log('while', item.value);
}
复制代码

此循环将一直持续,直到yield返回值的done属性为true;在这一点上,咱们知道没有任何东西了,能够中止它。这让咱们遍历生成值的序列,而无需建立一个数组而后去迭代它。然而,在这个循环中有些重复代码,它们更多的是在管理生成器迭代而不是实际迭代它。咱们来看看另外一种方法:

//“for..of”循环消除了须要显式的调用生成器构造,
//如“next()”,“value”,“done”。
for (let item of generator) {
	console.log('for..of', item);
}
复制代码

如今要好得多。咱们将代码缩减后而且更加专一于手头任务。除了for..of语句以外,这段代码基本上与咱们的while循环彻底相同,它知道iterable是生成器时要作什么。迭代生成器是并发JavaScript应用程序中的常见模式,所以在这里优化代码和提高可读性将是明智的决定。

无限序列

一些序列是无限的,素数,斐波纳契数,奇数,等等。无限序列不限于数字组合;更抽象的概念能够被认为是无限的。例如,一组无限重复的字符串,一个无限切换的布尔值,依此类推。在本节中,咱们将探讨生成器如何使咱们可以使用无限序列。

没有尽头

从内存消耗的角度来看,从无限序列中分配项是不实际的。事实上,甚至不可能分配整个序列 - 它是无限的。内存是有限的。所以,最好是简单地彻底回避整个分配问题,并使用生成器根据须要从序列中产生值。在任何给定的时间点,咱们的应用程序只会使用无限序列的一小部分。如下是无限序列中使用的内容与这些序列潜在大小的示意图:

image079.gif

咱们能够看到,在这个序列中有大量的项咱们永远不会用到。让咱们看看一些惰性地从无限斐波纳契数列中产生项的生成器代码:

//生成无限的Fibonacci序列。
function* fib() {
	var seq = [0, 1],
		next;

	//这个循环实际上并无无限运行,
	//只当使用“next()”请求序列中的项时。
	while (true) {
		//产生序列中的下一个项。
		yield (next = seq[0] + seq[1]);
		//存储所需的状态,
		//以便计算下一次迭代中的项。
		seq[0] = seq[1];
		seq[1] = next;
	}
}

//启动生成器。这永远不会“done”生成值。
//然而,它是惰性的 - 它只是在咱们须要的时候生成值。
var generator = fib();

//获取序列的前5项。
for (let i = 0; i < 5; i++) {
	console.log('item', generator.next().value);
}
复制代码

交替序列

无限序列的变化是循环序列或交替序列。到达终点时,这些类型的序列是循环的; 他们从起点来开始。如下是两个值之间交替的序列:

image080.gif

这种类型的序列将继续无限地生成值。当咱们有一组规则来肯定序列的定义方式和生成的项集合时,这就变得颇有用了;而后,咱们从新开始这一系列。如今,让咱们看一些代码,看看如何使用生成器实现这些序列。这是一个通用的生成器函数,咱们能够用来在值之间进行交替:

//一个通用生成器将无限迭代
//提供的参数,产生每一个项。
function* alternate(...seq) {
	while (true) {
		for (let item of seq) {
			yield item;
		}
	}
}
复制代码

这是咱们第一次声明一个接受参数的生成器函数。实际上,咱们使用spread运算符来迭代传递给函数的参数。与参数不一样,咱们使用spread运算符建立的seq参数是一个真实数组。当咱们遍历这个数组时,咱们从生成器中生成每一个项。这乍一看起来彷佛并不那么有用,可是这里的while循环起了真正的做用。因为while循环永远不会退出,for循环将本身重复。也就是说,它会交替出现。这否认了明确的须要标记代码(咱们到达了序列的末尾吗?咱们如何重置计数器并回到开头?等等)让咱们看看这个生成器函数是如何工做的:

//经过提供的参数,建立一个交替的生成器。
var alternator = alternate(true, false); 

console.log('true/false', alternator.next().value);
console.log('true/false', alternator.next().value);
console.log('true/false', alternator.next().value); 
console.log('true/false', alternator.next().value);
//→
// true/false true
// true/false false
// true/false true
// true/false false
复制代码

很酷吧。所以,只要咱们继续获取值,alternator将继续生成true/false值。这里的主要好处是咱们不须要知道关于下一个值,alternator为咱们负责完成。让咱们看看这个用不一样的序列迭代的生成器函数:

//使用新值建立新的生成器实例
//来迭代每一个项。
alternator = alternator('one', 'two', 'three');

//从无限序列中获取前10个项。
for (let i = 0; i < 10; i++) {
	console.log('one/two/three', `"${alternator.next().value}"`);
}

//→
//one/two/three "one"
//one/two/three "two"
//one/two/three "three"
//one/two/three "one"
//one/two/three "two"
//one/two/three "three"
//one/two/three "one"
//one/two/three "two"
//one/two/three "three"
//one/two/three "one"
复制代码

正如咱们所看到的,alternate()函数在传递给它的任何参数之间交替生成项。

传递到其余生成器

咱们已经看到了yield语句如何可以暂停一个生成器函数执行上下文,并生成一个值返回到当前调用上下文。在yield语句上有一个变化,它容许咱们传递到其余generator函数。另外一种技术涉及到建立一个组合生成器,它由几个生成器交织在一块儿。在本节中,咱们将探讨这些方法。

选择一个策略

传递到其余生成器使咱们的函数可以在运行时决定将控制从一个生成器切换到另外一个生成器。换句话说,它容许基于策略选择更合适的生成器函数。这有一张图表示一个生成器函数,决定并传递到其余某个生成器函数:

image081.gif

咱们在整个应用程序会使用这里的三个专用生成器。也就是说,他们每个都有本身独有的方式。也许,他们有本身特定类型的输入。然而,这些生成器只是对它们给出的输入作出假设。它可能不是在用最好的方式在执行任务,因此,咱们必需要弄清楚其中的这些生成器再使用。咱们但愿避免在全部的地方执行这些决策选择的代码。若是咱们可以封装全部这些成为一个通用的生成器,能处理一般的一些状况,这将会很不错。

假设咱们有如下生成器函数,它们一样适用在咱们的应用程序中:

//映射对象集合到特定的属性名称的生成器。
function* iteratePropertyValues(collection, property) {
	for (let object of collection) {
		yield object[property];
	}
}

//生成给定对象的每一个值的生成器。
function* iterateObjectValues(collection) {
	for (let key of Object.keys(collection)) {
		yield collection[key];
	}
}

//生成给定数组中每一个项的生成器。
function* iterateArrayElements(collection) {
	for (let element of collection) {
		yield element;
	}
}
复制代码

这些函数简洁小巧,易于使用。麻烦的是这些函数中的每个都会对传入的集合作出判断。它是一个对象数组,每一个对象都有一个特定的属性吗?它是一个字符串数组?它是一个对象而不是一个数组?因为这些生成器函数在咱们的代码中一般用于相似的目的,咱们能够实现一个更通用的迭代器,它的工做是肯定要使用的最适合的生成器函数,而后再用它。让咱们看看这个函数是什么样的:

//这个生成器传递到其余生成器。
//但首先,它执行一些逻辑来肯定最好的生成器函数。
function* iterateNames(collection) {
	//咱们正在处理数组吗?
	if (Array.isArray(collection)) {
		
		//这是一个启发式的,咱们检查第一个
		//数组中的元素。基于此,咱们
		//对剩余元素作出假设。
		let first = collection[0];

		//这是咱们推崇其余更专业的生成器,
		//基于咱们从第一个数组元素发现的内容。
		if (first.hasOwnProperty('name')) {
			yield* iteratePropertyValues(collection, 'name');
		} else if(first.hasOwnProperty('customerName')) {
			yield* iteratePropertyValues(collection, 'customerName');
		} else {
			yield* iterateArrayElements(collection);
		}
	} else {
		yield* iterateObjectValues(collection);
	}
}
复制代码

能够将iterateNames()函数看做其余三个生成器中的任何一个的简单代理。它根据输入,并在一个集合上作出选择。咱们本能够实现一个大型生成器函数,但这将使咱们没法直接使用想要使用较小生成器的用例。若是咱们想用它们来组合新功能特性怎么办?或者另外一个复合生成器须要用吗?保持生成器函数小而专一是一个好主意。该yield* 语法容许咱们将控制权移交给更合适的生成器。

如今,让咱们看看这个通用生成器函数如何经过传递到最适合处理数据的生成器来使用:

var colection;

//迭代一串字符串名称。
collection = ['First', 'Second', 'Third'];

for (let name of iterateNames(collection)) {
	console.log('array element', `"${name}"`);
}

//迭代一个对象,其中使用值
//来命名的 - 这里的键不相关。
collection = {
	first: 'First',
	second: 'Second',
	third: 'Third'
};

for (let name of iterateNames(collection)) {
	console.log('object value', `"${name}"`);
}

//在集合中迭代每一个对象的“name”属性。
collection = [
	{name: 'First'},
	{name: 'Second'},
	{name: 'Third'}
];

for (let name of iterateNames(collection)) {
	console.log('property value', `"${name}"`);
}
复制代码

交错生成器

当生成器传递到另外一个生成器时,控制器不会返回第一个生成器,直到第二个生成器所有完成。在前面的例子中,咱们的生成器只是寻找一个更好的生成器来完成工做。可是,有时咱们会有两个或更多数据源须要一块儿使用。所以,而不是将控制权交给一个生成器,而后传递到另外一个等等,咱们会在各类来源之间交替,轮流处理数据。

这里有一个示图,说明了交错多个数据源以建立单个数据源的生成器的方法:

image082.gif

咱们的方法是循环数据源,而不是清空一个源,而后清空另外一个源,依此类推。这样的生成器将要处理的,并非一个大型集合,而是两个或更多集合。使用这种生成器技术,咱们实际上能够将多个数据源视为一个大数据源,但无需为大型结构分配内存。咱们来看下面的代码示例:

'use strict';

//将输入数组转换为生成每一个值的生成器的实用工具函数。
//若是它不是数组,假定它已是一个生成器而且传递给它。
function* toGen(array) {
	if (Array.isArray(array)) {
		for (let item of array) {
			yield item;
		}
	} else {
		yield* array;
	}
}

//交错给定的数据源(数组或生成器)到一个生成器源。
function* weave(...sources) {
	//这控制“while”循环。
	//只要有一个产生数据的来源,
	//while循环仍然有效。
	var yielding = true;

	//咱们必须确保每个sources是一个生成器。
	var generators = sources.map(source => toGen(source));

	//启动主交错循环。它就是这样经过每一个来源,
	//从每一个源产生一个项,而后从新开始,
	//直到每个来源是空的。
	while(yield) {
		yielding = false;
		for (let origin of generator) {
			let next = source.next();
			
			//只要咱们产生数据,“yield”值就是true,
			//并且“while”循环继续。
			//当每一个来源“done”都是true,
			//“yielding”变量保持为false,
			//那么“while”循环退出。
			if (!next.done) {
				yielding = true;
				yield next.value;
			}
		}
	}
}

//一个经过迭代给定的源生成值的基本过滤器,
//而且产生项未被禁用。
function* enabled(source) {
	for (let item of source) {
		if (!item.disabled) {
			yield item;
		}
	}
}

//这些是咱们要交错的两个数据源传入一个生成器,
//而后能够由另外一个生成器过滤。
var enrolled = [
	{name: 'First'},
	{name: 'Sencond'},
	{name: 'Third', disabled: true}
];

var pending = [
	{name: 'Fourth'},
	{name: 'Fifth'},
	{name: 'Sixth', disabled: true}
];

//建立生成器,从两个数据源生成用户对象。
var users = enabled(weave(registered, pending));

//实际上执行交错和过滤。
for (let user of users) {
	console.log('name', `"${user.name}"`);
}
复制代码

将数据传递给生成器

yield语句不仅是放弃控制权返回给调用者,它也返回一个值。该值经过next()方法传递给生成器函数。这就是咱们在建立数据后将数据传递给生成器的方法。在本节中,咱们将讨论生成器的两面性,以及如何能建立反馈循环产生一些精巧代码。

复用生成器

有些生成器是通用的,在咱们的代码中常用。在这种状况下,不断建立和销毁这些生成器实例是否有意义?或者咱们能够复用它们吗?例如,考虑一个主要依赖于初始条件的序列。假设咱们想生成一个偶数序列。咱们将从2开始,当咱们迭代这个生成器时,该值将递增。下次咱们要迭代偶数时,咱们必须建立一个新的生成器。

这有点浪费,由于咱们所作的只是重置计数器。若是咱们采用不一样的方法,容许咱们继续为这些类型的序列使用相同的生成器实例,该怎么办?生成器的next()方法是此功能的可能实现方式。咱们能够传递一个值,而后重置咱们的计数器。所以,每次咱们须要迭代偶数时,没必要建立新的生成器实例,咱们能够简单地调用next(),传入的值做为重置生成器的初始条件。

yield关键字实际上会返回一个值 - 传递到next()的参数。大多数状况下,这是未定义的,例如当生成器在for..of循环中迭代时。然而,这就是咱们在开始运行后可以将参数传递给生成器的方法。这与将参数传递给生成器函数不一样,这对于执行生成器的初始配置很是方便。传递给next()的值是当咱们须要为要生成的下一个值更改某些内容时,咱们如何与生成器通讯。

让咱们看一下如何使用next()方法建立可重用的偶数序列生成器:

//这个生成器将不断生成偶数。
function* genEvens() {
	
	//初始值为2.但这能够经过在传递给“next()”的input值进行改变
	var value = 2,
		input;
		
	while (true) {
		//咱们产生值,并得到input值。
		//若是提供input值,这将做为下一个值。
		input = yield value;
		
		if (input) {
			value = input;
		} else {
			//确保下一个值是偶数。
			//处理奇数值时的状况传递给“next()”。
			value += value % 2 ? 1 : 2;
		}
	}
}

//建立“evens”生成器。
var evens = genEvens(),
	even;

//迭代偶数达到10。
while ((even = evens.next().value) <= 10) {
	console.log('even', even);
}

//→
// even 2
// even 4
// even 6
// even 8
// even 10

//重置生成器。咱们不须要建立一个新的。
evens.next(999);

//在1000 - 1024之间迭代even值。
while ((even = evens.next().value) <= 1024) {
	console.log('evens from 1000', even);
}

//→
//evens from 1000 1002
//evens from 1000 1004
//evens from 1000 1006
//evens from 1000 1008
//evens from 1000 1010
//evens from 1000 1012
//evens from 1000 1014
复制代码

若是你想知道为何咱们没有使用for..of循环来支持while循环,那是由于你使用for..of循环迭代生成器 执行此操做时,只要循环退出,生成器就会标记为已完成。所以,它将再也不可用。

轻量级map/reduce

咱们能够用next()方法作的其余事情是将一个值映射到另外一个值。例如,假设咱们有一个包含七个项的集合。要映射这些项,咱们将迭代集合,将每一个项传递给next()。正如咱们在上一节中所见,此方法能够重置生成器的状态,但它也能够用于提供输入数据流,就像它提供输出数据流同样。

让咱们看看是否能够经过next()将它们传入生成器来编写一些执行此映射集合项的代码:

//这个生成器只要调用“next()”,将继续迭代。
//这也是期待的结果,以便它能够调用
//“iteratee()”函数就能够生成结果。
function* genMapNext(iteratee) {
	var input = yield null;
	while (true) {
		input = yield iteratee(input);
	}
}

//咱们想要映射的数组。
var array = ['a', 'b', 'c', 'b', 'a'];

//一个“mapper”生成器。咱们传递一个iteratee函数,
//做为“genMapNext()”的参数。
var mapper = genMapNext(x => x.toUpperCase());

//咱们迭代的起点
var reduced = {};

//咱们必须调用“next()”来开始生成器。
mapper.next();

//如今咱们能够开始迭代数组了。
//“mapped”值来自生成器。
//咱们想要映射的值经过将其传递给“next()”进入生成器。
for (let item of array) {
	let mapped = mapper.next(item).value;
	
	//咱们的简化逻辑采用映射值,
	//并将其添加到“reduced”对象中,
	//计算重复键的数量。
	if (reduced.hasOwnProperty(mapped)) {
		reduced[mapped]++;
	} else {
		reduced[mapped] = 1;
	}
}

console.log('reduced', reduced);
//→reduced {A: 2, B: 2, C: 1}
复制代码

咱们能够看到,这确实是可能的。咱们可以使用这种方法执行轻量级的map/reduce任务。映射生成器具备iteratee函数,该函数应用于集合中的每一项。当咱们遍历数组时,咱们能够经过将这些项传递给next()方法来将这些项提供给生成器做为一个参数。

可是,有一些关于前一种方法的东西感受并非最好 - 必须像这样启动生成器,而且为每次迭代显式调用next()都会感受很笨拙。实际上,咱们不能直接应用iteratee函数,而是非得调用next()吗?在使用生成器时,咱们须要注意这些事情;特别是在将数据传递给生成器时。仅仅由于咱们可以实现,并不意味着这是一个好主意。

若是咱们像对待全部其余生成器同样简单地迭代生成器,mapping和reducing可能会感受更天然。咱们仍然但愿生成器为咱们提供的轻量级映射,以免内存分配。让咱们尝试一种不一样的方法 - 一种不须要next()的方法:

//这个生成器是一个比“genMapNext()”更有用的映射器,
//由于它不依赖于值经过“next()”进入生成器。

//相反,这个生成器接受一个iterable,
//和一个iteratee函数。iterable是iterated-over,
//以及iteratee的结果是能够生成的。
function* genMap(iterable, iteratee) {
	for (let item of iterable) {
		yield iteratee(item);
	}
}

//使用iterable的数据源建立咱们的“mapped”生成器和iteratee函数。
var mapped = genMap(array, x => x.toUpperCase());
var reduced = {};

//如今咱们能够简单地迭代咱们的生成器而不是调用“next()”。
//每一个循环迭代的工做都是执行reduction逻辑,而不是调用“next()”。
for (let item of mapped) {
	if (reduced.hasOwnProperty(item)) {
		reduced[item]++;
	} else {
		reduced[item] = 1;
	}
}

console.log('reduced', reduced);
//→reduced improved {A: 2, B: 2, C: 1}
复制代码

这看起来像是一种改进。代码更少,生成器的流程更容易理解。不一样之处在于咱们将数组和iteratee函数预先传递给生成器。而后,当咱们遍历生成器时,每一个项都会被惰性地映射。将此数组迭代为对象的代码也更易于阅读。

咱们刚刚实现的这个genMap()函数是通用的,他对咱们颇有用。在实际应用中,映射比大写转换更复杂。更有可能的是,将有多个级别的映射。也就是说,咱们映射的集合,映射它N屡次。若是咱们能对咱们的代码作一个良好的设计,而后,咱们要以较小的迭代功能来组合生成器。

可是咱们怎样才能保持这种通用和惰性呢?方法是使用几个生成器,每一个生成器做为下一个生成器的输入。这意味着,当咱们的reducer代码遍历这些生成器时,只有一个项能够经过各类映射层到达代码。让咱们来实现这个:

//此函数经过iterable组成一个生成器。
//这个方法是为每一个iteratee创造生成器,
//以便每一个项来自原始的可迭代,向下传递,
//经过每一个iteratee,在映射下一个项以前。
function composeGenMap(...iteratees) {

	//咱们正在返回一个生成器函数。
	//那样,可使用相同的映射组合,
	//能够应用于多个迭代,而不只仅是一个。
	return function* (iterable) {

		//为每一个iteratee建立生成器传递给函数。
		//下一个生成器将前一个生成器做为“itarable”参数
		for (let iteratee of iteratees) {
			iterable = genMap(iterable, iteratee);
		}

		//简单地传递咱们建立的最后一个迭代。
		yield* iterable;
	}
}

//咱们的可迭代数据源 
var array = [1, 2, 3];

//使用3个iteratee函数建立“composed”映射生成器。
var composed = composeGenMap(
	x => x + 1,
	x => x * x,
	x => x - 2
);

//如今咱们能够迭代组合的生成器,
//传递它到咱们的迭代和惰性的映射值。
for (let item of composed(array)) {
	console.log('composed', item);
}

//→
// composed 2
// composed 7
// composed 14
复制代码

协程

协程是一种容许协做式多任务处理的并发技术。这意味着若是咱们应用程序的一部分须要执行一些任务,它能够这样作,而后将控制权移交给应用程序的另外一部分。想一想一个子程序,或者更接近的,一个函数。这些子程序一般依赖于其余子程序。然而,它们不只仅是连续运行,而是相互合做。

在JavaScript中,没有内在的协程机制。生成器不是协程,但它们具备类似的属性。例如,生成器能够暂停执行一个函数,去控制另外一个执行上下文,而后从新得到控制。这让咱们有些想象空间,可是生成器只是用于生成值,它并非咱们了解协程所必须的。在本节中,咱们将介绍使用生成器在JavaScript中实现协程的一些方法。

建立协程函数

生成器为咱们提供了在JavaScript中实现协同函数所需的大部份内容; 他们能够暂停并继续执行。咱们只须要在生成器周围实现一些细微的抽象,这样咱们正在使用的函数实际上就像调用协程函数,而不是迭代生成器。如下大体说明咱们但愿协程在调用时的行为:

image086.gif

这个方法是调用协程函数从一个yield语句移动到下一个。咱们能够经过传递一个参数来为协程提供输入,而后由yield语句返回。这须要记住不少,因此让咱们在函数包装器中归纳这些协程概念:

//取自:http://syzygy.st/javascript-coroutines/
//该工具函数接受一个生成器函数,而后返回
//协程函数。任什么时候候协程被调用,
//它的工做都是在生成器上调用“next()”。
//
//结果是生成器函数能够无限地运行,
//只到当它命中“yield”语句时暂停。
function coroutine(func) {
	//建立生成器,并移动函数
	//在第一个“yield”声明以前。
	var gen = func();
	gen.next();

	//“val”经过“yield”语句传递给生成器函数。
	//而后从那里恢复,直到它到达另外一个yield。
	return function(val) {
		gen.next(val);
	}
}
复制代码

很是简单 - 五行代码,但它也很强大。Harold的包装器返回的函数只是将生成器推动到下一个yield语句,若是提供了参数,则将参数提供给next()。声明工具函数是一种方法,但让咱们实际使用它来实现协程函数:

//在调用时建立一个coroutine函数,
//进入到下一个yield语句。
var coFirst = coroutine(function* () {
	var input;
	
	//输入来自yield语句,
	//并且是传递给“coFirst()”的参数值。
	input = yield;
	console.log('step1', input);
	input = yield; 
	console.log('step3', input);
});


//与上面建立的协程同样工做... 
var coSecond = coroutine(function* () {
	var input;
	input = yield;
	console.log('step2', input);
	input = yield;
	console.log('step4', input);
});

//这两个协程彼此合做,按预期输出。
//咱们能够看到对每一个协程的第二次调用,
//会找到上一个yield语句暂停的位置。
coFirst('the money');
coSecond('the show');
coFirst('get ready');
coSecond('go');
//→
// step1 the money
// step2 the show
// step3 get ready
// step4 go
复制代码

当完成某项任务涉及一系列步骤时,咱们一般须要标记代码,临时值等。协程不须要这些,由于函数只是暂停,任何本地状态都保持不变。换句话说,当协程为咱们隐藏这些细节时,没有必要将并发逻辑与咱们的应用程序逻辑交织在一块儿。

处理DOM事件

咱们可使用协程的其余地方是DOM做为事件处理程序。这经过将相同的coroutine()函数做为事件侦听器添加到多个元素来工做。让咱们回想一下,对这些协程函数的每次调用都与单个生成器进行通讯。这意味着咱们设置为处理DOM事件的协程将做为流传入。这几乎就像咱们在迭代这些事件同样。

因为这些协程函数使用相同的生成器,所以元素可使用此技术轻松地互相通讯。DOM事件的典型方法涉及回调函数,这些函数与元素之间共享的某种中心源进行通讯并维护状态。使用协程,元素通讯的状态隐含在咱们的函数代码中。让咱们在DOM事件处理程序的上下文中使用咱们的协程包装器:

//与mousemove一块儿使用的协程函数
var onMouseMove = coroutine(function* () {
	var e;

	//这个循环无限地执行。
	//事件对象经过yield语句传入。
	while (true) {
		e = yield;
		
		//若是元素被禁用,则不执行任何操做。
		//不然,输出记录消息。
		if (e.target.disabled) {
			continue;
		}
		console.log('mousemove', e.target.textContent);
	}
});

//与点击事件一块儿使用的协程函数。
var onClick = coroutine(function* () {
	//保存对咱们两个按钮的引用。
	//协程是有状态的,它们永远都是可用的。
	var first = document.querySelector('button:first-of-type');
	var second = document.querySelector('button:last-of-type');
	var e;
	
	while (true) {
		e = yield;
		
		//按钮被单击后禁用。
		e.target.disabled = true;
		
		//若是单击了第一个按钮,
		//则切换第二个按钮的状态。
		if(Object.is(e.target, first)) {
			second.disabled = !second.disabled;
			continue;
		}

		//若是单击了第二个按钮,
		//则切换第一个按钮的状态。
		if(Object.is(e.target, second)) {
			first.disabled = !first.disabled;
		}
	}
});

//设置事件处理程序 - 咱们的协程函数。
for (let document of document.querySelectorAll('button')) {
	button.addEventListener('mousemove', onMouseMove);
	button.addEventListener('click', onClick);
}
复制代码

处理promise的值

在上一节中,咱们了解了如何使用coroutine()函数来处理DOM事件。咱们使用相同的coroutine()函数,将事件视为数据流,而不是随意添加响应DOM事件的回调函数。DOM事件处理程序更容易相互协做,由于它们共享相同的生成器上下文。

咱们能够将相同的方法应用于promise的then()回调,它的工做方式与DOM协程方法相似。咱们将协程传递给then(),而不是传递普通函数。当promise解析时,协程将进到下一个yield语句以及已解析的值。咱们来看看下面的代码:

//一系列promise的数组。
var promises = [];

//咱们的完成回调是一个协程。
//这意味着每次调用它时,都会有新的promise完成值显示在这里。
var onFulfilled = coroutine(function* () {
	var data;

	//当他们返回时继续处理已完成的promise值
	while (true) {
		data = yield;
		console.log('data', data);
	}
});

//在1到5秒之间,建立5个随机解析的promises。
for (let i = 0; i < 5; i++) {
	promises.push(new Promise((resolve, reject) => {
		setTimeout(() => {
			resolve(i);
		}, Math.floor(Math.random() * (5000 - 1000)) + 1000);
	}));
}

//将咱们的完成协程附加为“then()”回调。
for (let promise of promises) {
	promise.then(onFulfilled);
}
复制代码

这很是有用,由于它提供了静态promise方法所不具有的功能。该Promise.all()方法迫使咱们等待全部的promise完成,在处理返回promise以前。可是,在已解析的promise值彼此不相关的状况下,咱们能够简单地迭代它们,在它们按任何顺序解析时进行响应。

咱们能够经过将原生函数附加到then()做为回调来相似的实现,可是,当它们完成时,咱们就不会有共享上下文给promise值来处理。另外一种方法是咱们能够经过将promises与协程相结合来采用声明一系列协程响应不一样的协程,具体取决于它们响应的数据类型。这些协程将在整个应用程序期间继续存在,并在建立时传递给promise。

小结

这一章向你介绍了生成器的概念,ES6的新结构,这让咱们可以实现惰性计算。生成器帮助咱们实现了并发原则,让咱们可以避免计算和内存分配的浪费。有一些与生成器关联的新语法形式。首先,是生成器函数,它老是返回一个生成器实例。这些声明不一样于普通函数。这些函数是用于生成值,依赖于yield关键字。

而后,咱们探索了更高级的生成器和惰性计算话题,包括传递到其余生成器,实现map/reduce工具函数,以及将数据传递到生成器。在本章的结尾,咱们看了如何使用生成器来实现协程。

在下一章中,咱们将介绍Web workers - 第一次看看如何在浏览器环境中使用并发。

最后补充下书籍章节目录

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

相关文章
相关标签/搜索