本文是我翻譯《JavaScript Concurrency》書籍的第四章 使用Generators實作惰性計算,該書主要以Promises、Generator、Web workers等技術來講解JavaScript并發編程方面的實踐,
完整書籍翻譯地址:https://github.com/yzsunlei/javascript_concurrency_translation ,由于能力有限,肯定存在翻譯不清楚甚至翻譯錯誤的地方,歡迎朋友們提issue指出,感謝,
惰性計算是一種編程技術,它用于當我們希望需要使用值的時候才去計算的場景,這樣,可以確保我們確實需要它,相反的,直接都去計算,有可能計算了我們不需要的值,這通常沒什么問題,但當我們的應用程式的大小和復雜性增長到一定水平,這些計算造成的浪費就難以想象了,
Generator是引入到JavaScript中一種新的原生型別并作為ES6語言規格的一部分,Generator幫助我們在代碼中實作惰性計算技術,進一步說,幫助我們實作保護并發原則,
我們將通過對Generator的一些簡單介紹來開始本章,先讓我們對它們的表現方式有一定了解,之后,我們將進入更高級的惰性計算場景,并通過協程結束本章,現在讓我們開始吧,
呼叫堆疊和記憶體分配
記憶體分配是任何編程語言都必不可少的,如果沒有它,我們就沒有所謂的資料結構,甚至沒有原生型別,現在記憶體雖然很便宜,一般都有足夠的記憶體可供使用; 但這并不值得高興,雖然今天在記憶體中分配更大的資料結構更加可行,但是在10年前,當我們編程時,我們仍然必須釋放分配記憶體,JavaScript是一種垃圾自動收集語言,這意味著我們的代碼不必顯式地銷毀記憶體中的物件,但是,垃圾收集器會導致CPU損耗,
所以這里有兩個因素在起作用,我們想在這里保存兩個資源,我們將嘗試使用生成器來實作惰性計算,我們不必要多余的分配記憶體,如果我們能避免這一點,那么就可以避開頻繁的呼叫垃圾收集器,在本節中,我將介紹一些Generator生成器概念,
標記函式背景關系
在一個正常的函式呼叫堆疊,一個函式回傳一個值,在return陳述句激活一個新的執行背景關系并且丟棄舊的背景關系,因為回傳就代表已處理完畢了,生成器函式是一個特殊的JavaScript函式語法型別,和return陳述句相比他們的呼叫堆疊不那么老套,這里有張圖表示了生成器函式的呼叫,并在開始生成值時發生的事情:
正如return陳述句將值傳遞給呼叫背景關系一樣,yield陳述句也會回傳一個值,但是,與普通函式不同的是,生成器函式背景關系不會被丟棄,事實上,它們被加上標記,以便在將控制權交還給生成器背景關系時,它可以從中斷處繼續執行獲取值,直到完成為止,這個標記非常容易,因為它只是指向我們代碼中的位置,
序列而不是陣列
在JavaScript中,當我們需要遍歷事物,數字、字串、物件等串列時,我們會使用陣列,陣列是通用的,功能也是強大的,在惰性計算的背景關系中,陣列的挑戰是陣列本身就是資料需要分配,所以我們的陣列需要在記憶體中的某個位置分配元素,并且還有關于陣列中元素的元資料,
如果我們在使用大資料量的物件,則與陣列相關的記憶體開銷就很大,另外,我們需要以某種方式將這些物件放在陣列中,這是額外的步驟會增加CPU消耗,另一種概念是序列,序列不是有形的JavaScript語言結構,它們是一個抽象的概念 - 陣列但沒有實際分配陣列,序列有助于惰性計算,由于這個原因,沒有什么需要分配記憶體,并且沒有初始入口,這是迭代陣列所涉及的示圖:
我們可以看到,在我們迭代這三個物件之前,我們首先必須分配一個陣列,然后用這些物件填充它,讓我們將這種方法與序列的概念思想進行對比,如下圖所示:
對于序列,我們沒有為我們感興趣的迭代物件提供明確的容器結構,與序列關聯的唯一開銷是指向當前項的指標,我們可以使用生成器函式作為在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傳遞first到next(),在它被呼叫的地方,其他兩個值也是如此,事實上,行為上是惰性計算的,我們有三次呼叫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應用程式中的常見模式,因此在這里優化代碼和提升可讀性將是明智的決定,
無限序列
一些序列是無限的,素數,斐波納契數,奇數,等等,無限序列不限于數字組合;更抽象的概念可以被認為是無限的,例如,一組無限重復的字串,一個無限切換的布林值,依此類推,在本節中,我們將探討生成器如何使我們能夠使用無限序列,
沒有盡頭
從記憶體消耗的角度來看,從無限序列中分配項是不實際的,事實上,甚至不可能分配整個序列 - 它是無限的,記憶體是有限的,因此,最好是簡單地完全回避整個分配問題,并使用生成器根據需要從序列中產生值,在任何給定的時間點,我們的應用程式只會使用無限序列的一小部分,以下是無限序列中使用的內容與這些序列潛在大小的示意圖:
我們可以看到,在這個序列中有大量的項我們永遠不會用到,讓我們看看一些惰性地從無限斐波納契數列中產生項的生成器代碼:
//生成無限的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);
}
交替序列
無限序列的變化是回圈序列或交替序列,到達終點時,這些型別的序列是回圈的; 他們從起點來開始,以下是兩個值之間交替的序列:
這種型別的序列將繼續無限地生成值,當我們有一組規則來確定序列的定義方式和生成的項集合時,這就變得很有用了;然后,我們重新開始這一系列,現在,讓我們看一些代碼,看看如何使用生成器實作這些序列,這是一個通用的生成器函式,我們可以用來在值之間進行交替:
//一個通用生成器將無限迭代
//提供的引數,產生每個項,
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函式,另一種技術涉及到創建一個組合生成器,它由幾個生成器交織在一起,在本節中,我們將探討這些方法,
選擇一個策略
傳遞到其他生成器使我們的函式能夠在運行時決定將控制從一個生成器切換到另一個生成器,換句話說,它允許基于策略選擇更合適的生成器函式,這有一張圖表示一個生成器函式,決定并傳遞到其他某個生成器函式:
我們在整個應用程式會使用這里的三個專用生成器,也就是說,他們每一個都有自己獨有的方式,也許,他們有自己特定型別的輸入,然而,這些生成器只是對它們給出的輸入做出假設,它可能不是在用最好的方式在執行任務,所以,我們必須要弄清楚其中的這些生成器再使用,我們希望避免在所有的地方執行這些決策選擇的代碼,如果我們能夠封裝所有這些成為一個通用的生成器,能處理通常的一些情況,這將會很不錯,
假設我們有以下生成器函式,它們同樣適用在我們的應用程式中:
//映射物件集合到特定的屬性名稱的生成器,
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}"`);
}
交錯生成器
當生成器傳遞到另一個生成器時,控制器不會回傳第一個生成器,直到第二個生成器全部完成,在前面的例子中,我們的生成器只是尋找一個更好的生成器來完成作業,但是,有時我們會有兩個或更多資料源需要一起使用,因此,而不是將控制權交給一個生成器,然后傳遞到另一個等等,我們會在各種來源之間交替,輪流處理資料,
這里有一個示圖,說明了交錯多個資料源以創建單個資料源的生成器的方法:
我們的方法是回圈資料源,而不是清空一個源,然后清空另一個源,依此類推,這樣的生成器將要處理的,并不是一個大型集合,而是兩個或更多集合,使用這種生成器技術,我們實際上可以將多個資料源視為一個大資料源,但無需為大型結構分配記憶體,我們來看下面的代碼示例:
'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 = https://www.cnblogs.com/yzsunlei/p/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中實作協同函式所需的大部分內容; 他們可以暫停并繼續執行,我們只需要在生成器周圍實作一些細微的抽象,這樣我們正在使用的函式實際上就像呼叫協程函式,而不是迭代生成器,以下大致說明我們希望協程在呼叫時的行為:
這個方法是呼叫協程函式從一個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 onm ouseMove = 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', onm ouseMove);
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 = https://www.cnblogs.com/yzsunlei/p/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 - 第一次看看如何在瀏覽器環境中使用并發,
最后補充下書籍章節目錄
- 《JavaScript并發編程》第一章 JavaScript并發簡介
- 《JavaScript并發編程》第二章 JavaScript運行模型
- 《JavaScript并發編程》第三章 使用Promises實作同步
- 《JavaScript并發編程》第四章 使用Generators實作惰性計算
- 《JavaScript并發編程》第五章 使用Web Workers
- 《JavaScript并發編程》第六章 實用的并發
- 《JavaScript并發編程》第七章 抽取并發邏輯
另外還有講解兩章nodeJs后端并發方面的,和一章專案實戰方面的,這里就不再貼了,有興趣可轉向https://github.com/yzsunlei/javascript_concurrency_translation查看,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/117305.html
標籤:Html/Css
上一篇:網頁設計和開發中,關于字體的常識
下一篇:測驗
