[書籍翻譯] 《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…查看。

相關文章
相關標籤/搜索