yield
yield*
進行遞迴
next()
傳送值
yield
鬆散繫結return()
和 throw()
return()
終止產生器throw()
訊號錯誤yield*
:完整說明IteratorPrototype
this
值yield
function*
而不是 generator
?yield
是關鍵字嗎?您可以將產生器視為可以暫停和繼續的程序(程式碼片段)
function
*
genFunc
()
{
// (A)
console
.
log
(
'First'
);
yield
;
console
.
log
(
'Second'
);
}
請注意新的語法:function*
是 產生器函式 的新「關鍵字」(也有 產生器方法)。yield
是產生器可以用來暫停自己的運算子。此外,產生器也可以透過 yield
接收輸入和傳送輸出。
當您呼叫產生器函式 genFunc()
時,您會取得一個 產生器物件 genObj
,您可以使用它來控制程序
const
genObj
=
genFunc
();
程序最初在 A 行暫停。genObj.next()
繼續執行,genFunc()
內部的 yield
會暫停執行
genObj
.
next
();
// Output: First
genObj
.
next
();
// output: Second
產生器有四種
function
*
genFunc
()
{
···
}
const
genObj
=
genFunc
();
const
genFunc
=
function
*
()
{
···
};
const
genObj
=
genFunc
();
const
obj
=
{
*
generatorMethod
()
{
···
}
};
const
genObj
=
obj
.
generatorMethod
();
class
MyClass
{
*
generatorMethod
()
{
···
}
}
const
myInst
=
new
MyClass
();
const
genObj
=
myInst
.
generatorMethod
();
產生器傳回的物件是可迭代的;每個 yield
都會貢獻到迭代值的序列。因此,您可以使用產生器來實作可迭代物件,而這些物件可以使用各種 ES6 語言機制來使用:for-of
迴圈、展開運算子 (...
) 等。
以下函式傳回物件屬性的可迭代物件,每個屬性一組 [key, value]
function
*
objectEntries
(
obj
)
{
const
propKeys
=
Reflect
.
ownKeys
(
obj
);
for
(
const
propKey
of
propKeys
)
{
// `yield` returns a value and then pauses
// the generator. Later, execution continues
// where it was previously paused.
yield
[
propKey
,
obj
[
propKey
]];
}
}
objectEntries()
的使用方式如下
const
jane
=
{
first
:
'Jane'
,
last
:
'Doe'
};
for
(
const
[
key
,
value
]
of
objectEntries
(
jane
))
{
console
.
log
(
`
${
key
}
:
${
value
}
`
);
}
// Output:
// first: Jane
// last: Doe
objectEntries()
的運作方式詳細說明於 專門的章節 中。在沒有產生器的情況下實作相同的功能需要更多工作。
你可以使用產生器大幅簡化使用 Promise 的工作。讓我們看看一個基於 Promise 的函數 fetchJson()
,以及如何透過產生器來改善它。
function
fetchJson
(
url
)
{
return
fetch
(
url
)
.
then
(
request
=>
request
.
text
())
.
then
(
text
=>
{
return
JSON
.
parse
(
text
);
})
.
catch
(
error
=>
{
console
.
log
(
`ERROR:
${
error
.
stack
}
`
);
});
}
有了 函式庫 co 和一個產生器,這個非同步程式碼看起來像同步程式碼
const
fetchJson
=
co
.
wrap
(
function
*
(
url
)
{
try
{
let
request
=
yield
fetch
(
url
);
let
text
=
yield
request
.
text
();
return
JSON
.
parse
(
text
);
}
catch
(
error
)
{
console
.
log
(
`ERROR:
${
error
.
stack
}
`
);
}
});
ECMAScript 2017 將會有非同步函數,它們在內部是基於產生器的。有了它們,程式碼看起來像這樣
async
function
fetchJson
(
url
)
{
try
{
let
request
=
await
fetch
(
url
);
let
text
=
await
request
.
text
();
return
JSON
.
parse
(
text
);
}
catch
(
error
)
{
console
.
log
(
`ERROR:
${
error
.
stack
}
`
);
}
}
所有版本都可以像這樣呼叫
fetchJson
(
'http://example.com/some_file.json'
)
.
then
(
obj
=>
console
.
log
(
obj
));
產生器可以透過 yield
從 next()
接收輸入。這表示你可以隨時喚醒一個產生器,只要有新的資料非同步地到達,而對產生器而言,它感覺就像同步接收資料一樣。
產生器 是可以暫停和恢復的函數(想想協作式多工處理或協程),這使得各種應用程式成為可能。
作為第一個範例,考慮以下名稱為 genFunc
的產生器函數
function
*
genFunc
()
{
// (A)
console
.
log
(
'First'
);
yield
;
// (B)
console
.
log
(
'Second'
);
// (C)
}
有兩件事讓 genFunc
與一般的函數宣告不同
function*
開頭。yield
(B 行)暫停自己。呼叫 genFunc
不會執行它的主體。相反地,你會得到一個所謂的產生器物件,你可以用它來控制主體的執行
> const genObj = genFunc();
genFunc()
在主體之前(A 行)最初會暫停。方法呼叫 genObj.next()
會繼續執行,直到下一個 yield
> genObj.next()
First
{ value: undefined, done: false }
正如你在最後一行看到的,genObj.next()
也會傳回一個物件。我們現在先忽略它。它在稍後會很重要。
genFunc
現在在 B 行暫停。如果我們再次呼叫 next()
,執行會恢復,並且會執行 C 行
> genObj.next()
Second
{ value: undefined, done: true }
之後,函數結束,執行已離開主體,而進一步呼叫 genObj.next()
沒有任何作用。
產生器可以扮演三個角色
yield
都能透過 next()
傳回一個值,這表示產生器能透過迴圈和遞迴產生一系列的值。由於產生器物件實作了介面 Iterable
(在 迭代章節 中有說明),這些序列可以由任何支援可迭代項目的 ECMAScript 6 結構處理。兩個範例是:for-of
迴圈和展開運算子 (...
)。yield
也能從 next()
(透過參數)接收一個值。這表示產生器會變成資料消費者,在透過 next()
將新值推入之前會暫停。下一個章節會更深入地說明這些角色。
如前所述,產生器物件可以是資料產生器、資料消費者或兩者兼具。此章節將它們視為資料產生器,它們同時實作介面 Iterable
和 Iterator
(如下所示)。這表示產生器函式的結果既是可迭代項目,也是迭代器。產生器物件的完整介面將在稍後顯示。
interface
Iterable
{
[
Symbol
.
iterator
]()
:
Iterator
;
}
interface
Iterator
{
next
()
:
IteratorResult
;
}
interface
IteratorResult
{
value
:
any
;
done
:
boolean
;
}
我省略了介面 Iterable
的方法 return()
,因為它與此章節無關。
產生器函式透過 yield
產生一系列的值,資料消費者透過迭代器方法 next()
消耗這些值。例如,以下產生器函式產生值 'a'
和 'b'
function
*
genFunc
()
{
yield
'a'
;
yield
'b'
;
}
此互動顯示如何透過產生器物件 genObj
擷取已產生的值
> const genObj = genFunc();
> genObj.next()
{ value: 'a', done: false }
> genObj.next()
{ value: 'b', done: false }
> genObj.next() // done: true => end of sequence
{ value: undefined, done: true }
由於產生器物件是可迭代的,因此支援可迭代項目的 ES6 語言結構可以套用在它們身上。以下三個特別重要。
首先,for-of
迴圈
for
(
const
x
of
genFunc
())
{
console
.
log
(
x
);
}
// Output:
// a
// b
其次,展開運算子 (...
),它會將迭代的序列轉換成陣列的元素(請參閱 參數處理章節 以取得關於此運算子的更多資訊)
const
arr
=
[...
genFunc
()];
// ['a', 'b']
第三,解構
> const [x, y] = genFunc();
> x
'a'
> y
'b'
前一個產生器函數不包含明確的 return
。明確的 return
等同於傳回 undefined
。我們來檢視一個有明確 return
的產生器
function
*
genFuncWithReturn
()
{
yield
'a'
;
yield
'b'
;
return
'result'
;
}
傳回的值顯示在 next()
傳回的最後一個物件中,其屬性 done
為 true
> const genObjWithReturn = genFuncWithReturn();
> genObjWithReturn.next()
{ value: 'a', done: false }
> genObjWithReturn.next()
{ value: 'b', done: false }
> genObjWithReturn.next()
{ value: 'result', done: true }
然而,大多數與可迭代物件一起運作的建構忽略 done
物件中的值
for
(
const
x
of
genFuncWithReturn
())
{
console
.
log
(
x
);
}
// Output:
// a
// b
const
arr
=
[...
genFuncWithReturn
()];
// ['a', 'b']
yield*
,一個用於進行遞迴產生器呼叫的運算子,會考量 done
物件中的值。稍後會說明。
如果例外離開產生器的本體,則 next()
會擲回它
function
*
genFunc
()
{
throw
new
Error
(
'Problem!'
);
}
const
genObj
=
genFunc
();
genObj
.
next
();
// Error: Problem!
這表示 next()
可以產生三個不同的「結果」
x
,它會傳回 { value: x, done: false }
z
的迭代序列結束,它會傳回 { value: z, done: true }
我們來看一個範例,說明產生器在實作可迭代物件方面有多麼方便。下列函數 objectEntries()
傳回一個物件屬性的可迭代物件
function
*
objectEntries
(
obj
)
{
// In ES6, you can use strings or symbols as property keys,
// Reflect.ownKeys() retrieves both
const
propKeys
=
Reflect
.
ownKeys
(
obj
);
for
(
const
propKey
of
propKeys
)
{
yield
[
propKey
,
obj
[
propKey
]];
}
}
這個函數讓你可以透過 for-of
迴圈來迭代物件 jane
的屬性
const
jane
=
{
first
:
'Jane'
,
last
:
'Doe'
};
for
(
const
[
key
,
value
]
of
objectEntries
(
jane
))
{
console
.
log
(
`
${
key
}
:
${
value
}
`
);
}
// Output:
// first: Jane
// last: Doe
做個比較 – 不使用產生器的 objectEntries()
實作複雜多了
function
objectEntries
(
obj
)
{
let
index
=
0
;
let
propKeys
=
Reflect
.
ownKeys
(
obj
);
return
{
[
Symbol
.
iterator
]()
{
return
this
;
},
next
()
{
if
(
index
<
propKeys
.
length
)
{
let
key
=
propKeys
[
index
];
index
++
;
return
{
value
:
[
key
,
obj
[
key
]]
};
}
else
{
return
{
done
:
true
};
}
}
};
}
yield
產生器的重大限制是,你只能在(靜態地)位於產生器函數內時 yield
。也就是說,在回呼函式中 yield
是不行的
function
*
genFunc
()
{
[
'a'
,
'b'
].
forEach
(
x
=>
yield
x
);
// SyntaxError
}
在非產生器函數中不允許 yield
,這就是為什麼前一個程式碼會造成語法錯誤。在這種情況下,很容易改寫程式碼,使其不使用回呼函式(如下所示)。但很不幸地,這並不總是可行的。
function
*
genFunc
()
{
for
(
const
x
of
[
'a'
,
'b'
])
{
yield
x
;
// OK
}
}
這個限制的好處是 稍後說明:它使產生器更容易實作,並與事件迴圈相容。
yield*
遞迴 你只能在產生器函式中使用 yield
。因此,如果你想用產生器實作遞迴演算法,你需要一種從一個產生器呼叫另一個產生器的方法。本節將說明這比聽起來的更複雜,這就是為什麼 ES6 有個特殊運算子 yield*
來處理這件事。現在,我只說明如果兩個產生器都產生輸出,yield*
如何運作,我稍後會說明如果涉及輸入時,事情如何運作。
一個產生器如何遞迴呼叫另一個產生器?假設你寫了一個產生器函式 foo
function
*
foo
()
{
yield
'a'
;
yield
'b'
;
}
你會如何從另一個產生器函式 bar
呼叫 foo
?以下方法無法運作!
function
*
bar
()
{
yield
'x'
;
foo
();
// does nothing!
yield
'y'
;
}
呼叫 foo()
會傳回一個物件,但並未實際執行 foo()
。這就是為什麼 ECMAScript 6 有運算子 yield*
來進行遞迴產生器呼叫
function
*
bar
()
{
yield
'x'
;
yield
*
foo
();
yield
'y'
;
}
// Collect all values yielded by bar() in an array
const
arr
=
[...
bar
()];
// ['x', 'a', 'b', 'y']
在內部,yield*
大致上如下運作
function
*
bar
()
{
yield
'x'
;
for
(
const
value
of
foo
())
{
yield
value
;
}
yield
'y'
;
}
yield*
的運算元不一定要是產生器物件,它可以是任何可迭代的
function
*
bla
()
{
yield
'sequence'
;
yield
*
[
'of'
,
'yielded'
];
yield
'values'
;
}
const
arr
=
[...
bla
()];
// ['sequence', 'of', 'yielded', 'values']
yield*
考慮迭代結束值 支援可迭代的大部分建構都忽略迭代結束物件中包含的值(其屬性 done
為 true
)。產生器透過 return
提供該值。yield*
的結果是迭代結束值
function
*
genFuncWithReturn
()
{
yield
'a'
;
yield
'b'
;
return
'The result'
;
}
function
*
logReturned
(
genObj
)
{
const
result
=
yield
*
genObj
;
console
.
log
(
result
);
// (A)
}
如果我們想要到 A 行,我們必須先迭代 logReturned()
產生的所有值
> [...logReturned(genFuncWithReturn())]
The result
[ 'a', 'b' ]
使用遞迴迭代樹狀結構很簡單,用傳統方式撰寫樹狀結構的迭代器很複雜。這就是為什麼產生器在此發揮作用:它們讓你透過遞迴實作迭代器。舉例來說,考慮以下二元樹的資料結構。它是可迭代的,因為它有一個金鑰為 Symbol.iterator
的方法。該方法是一個產生器方法,在呼叫時會傳回一個迭代器。
class
BinaryTree
{
constructor
(
value
,
left
=
null
,
right
=
null
)
{
this
.
value
=
value
;
this
.
left
=
left
;
this
.
right
=
right
;
}
/** Prefix iteration */
*
[
Symbol
.
iterator
]()
{
yield
this
.
value
;
if
(
this
.
left
)
{
yield
*
this
.
left
;
// Short for: yield* this.left[Symbol.iterator]()
}
if
(
this
.
right
)
{
yield
*
this
.
right
;
}
}
}
以下程式碼建立一個二元樹並透過 for-of
迭代它
const
tree
=
new
BinaryTree
(
'a'
,
new
BinaryTree
(
'b'
,
new
BinaryTree
(
'c'
),
new
BinaryTree
(
'd'
)),
new
BinaryTree
(
'e'
));
for
(
const
x
of
tree
)
{
console
.
log
(
x
);
}
// Output:
// a
// b
// c
// d
// e
作為資料的消費者,產生器物件符合產生器介面的後半部分,Observer
interface
Observer
{
next
(
value
?
:
any
)
:
void
;
return
(
value
?
:
any
)
:
void
;
throw
(
error
)
:
void
;
}
作為觀察者,產生器會暫停,直到它收到輸入。有透過介面指定的輸入方法傳輸的三種輸入
next()
傳送一般輸入。return()
終止產生器。throw()
傳遞錯誤。next()
傳送值 如果你使用產生器作為觀察者,你會透過 next()
傳送值給它,它會透過 yield
接收那些值
function
*
dataConsumer
()
{
console
.
log
(
'Started'
);
console
.
log
(
`1.
${
yield
}
`
);
// (A)
console
.
log
(
`2.
${
yield
}
`
);
return
'result'
;
}
讓我們互動式地使用這個產生器。首先,我們建立一個產生器物件
> const genObj = dataConsumer();
現在我們呼叫 genObj.next()
,這會啟動產生器。執行會持續到第一個 yield
,這是產生器暫停的地方。next()
的結果是 A 行中產生的值 (undefined
,因為 yield
沒有運算元)。在此區段中,我們對 next()
回傳的內容不感興趣,因為我們只用它來傳送值,而不是擷取值。
>
genObj
.
next
()
Started
{
value
:
undefined
,
done
:
false
}
我們再呼叫 next()
兩次,目的是傳送值 'a'
給第一個 yield
,以及值 'b'
給第二個 yield
>
genObj
.
next
(
'a'
)
1.
a
{
value
:
undefined
,
done
:
false
}
>
genObj
.
next
(
'b'
)
2.
b
{
value
:
'result'
,
done
:
true
}
最後一次 next()
的結果是從 dataConsumer()
回傳的值。done
為 true
表示產生器已完成。
很不幸地,next()
是不對稱的,但這無可避免:它總是將值傳送給目前暫停的 yield
,但會回傳下一個 yield
的運算元。
next()
當將產生器用作觀察者時,請務必注意,第一次呼叫 next()
的唯一目的是啟動觀察者。它之後才會準備好輸入,因為第一次呼叫會將執行推進到第一個 yield
。因此,您透過第一次 next()
傳送的任何輸入都會被忽略
function
*
gen
()
{
// (A)
while
(
true
)
{
const
input
=
yield
;
// (B)
console
.
log
(
input
);
}
}
const
obj
=
gen
();
obj
.
next
(
'a'
);
obj
.
next
(
'b'
);
// Output:
// b
最初,執行會在 A 行暫停。第一次呼叫 next()
next()
的引數 'a'
提供給產生器,但產生器沒有辦法接收它 (因為沒有 yield
)。這就是它會被忽略的原因。yield
並暫停執行。yield
的運算元 (undefined
,因為它沒有運算元)。第二次呼叫 next()
next()
的引數 'b'
提供給產生器,它會透過 B 行的 yield
接收它,並將它指定給變數 input
。next()
會回傳該 yield
的運算元 (undefined
)。下列的工具函式修復了這個問題
/**
* Returns a function that, when called,
* returns a generator object that is immediately
* ready for input via `next()`
*/
function
coroutine
(
generatorFunction
)
{
return
function
(...
args
)
{
const
generatorObject
=
generatorFunction
(...
args
);
generatorObject
.
next
();
return
generatorObject
;
};
}
為了了解 coroutine()
的運作方式,讓我們比較一個包裝過的產生器和一個正常的產生器
const
wrapped
=
coroutine
(
function
*
()
{
console
.
log
(
`First input:
${
yield
}
`
);
return
'DONE'
;
});
const
normal
=
function
*
()
{
console
.
log
(
`First input:
${
yield
}
`
);
return
'DONE'
;
};
包裝過的產生器會立即準備好輸入
> wrapped().next('hello!')
First input: hello!
正常的產生器需要額外的 next()
才會準備好輸入
> const genObj = normal();
> genObj.next()
{ value: undefined, done: false }
> genObj.next('hello!')
First input: hello!
{ value: 'DONE', done: true }
yield
會鬆散繫結 yield
繫結非常鬆散,因此我們不必將其運算元放在括號中
yield
a
+
b
+
c
;
這被視為
yield
(
a
+
b
+
c
);
而不是
(
yield
a
)
+
b
+
c
;
因此,許多運算元繫結比 yield
更緊密,如果你想將其用作運算元,則必須將 yield
放在括號中。例如,如果你將未加括號的 yield
作為加號的運算元,則會得到一個 SyntaxError
console
.
log
(
'Hello'
+
yield
);
// SyntaxError
console
.
log
(
'Hello'
+
yield
123
);
// SyntaxError
console
.
log
(
'Hello'
+
(
yield
));
// OK
console
.
log
(
'Hello'
+
(
yield
123
));
// OK
如果 yield
是函式或方法呼叫中的直接引數,則不需要括號
foo
(
yield
'a'
,
yield
'b'
);
如果你在指定項目的右側使用 yield
,則也不需要括號
const
input
=
yield
;
yield
在 ECMAScript 6 規格 中的下列語法規則中,可以看到 yield
周圍需要括號。這些規則描述如何分析表達式。我將它們從一般(鬆散繫結,較低優先順序)列到具體(緊密繫結,較高優先順序)。無論何時需要某種類型的表達式,你也可以使用更具體的表達式。相反的說法不成立。層級以 ParenthesizedExpression
結束,這表示如果你將任何表達式放在括號中,你可以在任何地方提到它。
Expression :
AssignmentExpression
Expression , AssignmentExpression
AssignmentExpression :
ConditionalExpression
YieldExpression
ArrowFunction
LeftHandSideExpression = AssignmentExpression
LeftHandSideExpression AssignmentOperator AssignmentExpression
···
AdditiveExpression :
MultiplicativeExpression
AdditiveExpression + MultiplicativeExpression
AdditiveExpression - MultiplicativeExpression
MultiplicativeExpression :
UnaryExpression
MultiplicativeExpression MultiplicativeOperator UnaryExpression
···
PrimaryExpression :
this
IdentifierReference
Literal
ArrayLiteral
ObjectLiteral
FunctionExpression
ClassExpression
GeneratorExpression
RegularExpressionLiteral
TemplateLiteral
ParenthesizedExpression
ParenthesizedExpression :
( Expression )
AdditiveExpression
的運算元是 AdditiveExpression
和 MultiplicativeExpression
。因此,使用(更具體的)ParenthesizedExpression
作為運算元是可以的,但使用(更一般的)YieldExpression
則不行。
return()
和 throw()
產生器物件有兩個額外的函式,return()
和 throw()
,類似於 next()
。
讓我們回顧一下 next(x)
的運作方式(在第一次呼叫之後)
yield
運算元。x
傳送到該 yield
,這表示它等於 x
。yield
、return
或 throw
yield x
導致 next()
返回 { value: x, done: false }
return x
導致 next()
返回 { value: x, done: true }
throw err
(未在產生器內捕獲)導致 next()
擲出 err
。return()
和 throw()
的運作方式類似於 next()
,但它們在步驟 2 中執行不同的操作
return(x)
在 yield
的位置執行 return x
。throw(x)
在 yield
的位置執行 throw x
。return()
終止產生器 return()
在導致產生器最後一次暫停的 yield
的位置執行 return
。讓我們使用以下產生器函數來看看它是如何運作的。
function
*
genFunc1
()
{
try
{
console
.
log
(
'Started'
);
yield
;
// (A)
}
finally
{
console
.
log
(
'Exiting'
);
}
}
在以下互動中,我們首先使用 next()
來啟動產生器並進行處理,直到 A 行的 yield
。然後我們透過 return()
從該位置返回。
> const genObj1 = genFunc1();
> genObj1.next()
Started
{ value: undefined, done: false }
> genObj1.return('Result')
Exiting
{ value: 'Result', done: true }
如果您在 finally
子句中產生 (在該子句中使用 return
陳述式也是可行的),您可以防止 return()
終止產生器。
function
*
genFunc2
()
{
try
{
console
.
log
(
'Started'
);
yield
;
}
finally
{
yield
'Not done, yet!'
;
}
}
這次,return()
沒有退出產生器函數。因此,它所傳回物件的 done
屬性為 false
。
> const genObj2 = genFunc2();
> genObj2.next()
Started
{ value: undefined, done: false }
> genObj2.return('Result')
{ value: 'Not done, yet!', done: false }
您可以再次呼叫 next()
。與非產生器函數類似,產生器函數的傳回值是在進入 finally
子句之前排隊的值。
> genObj2.next()
{ value: 'Result', done: true }
允許從新生產生器 (尚未啟動) 傳回值
> function* genFunc() {}
> genFunc().return('yes')
{ value: 'yes', done: true }
throw()
傳送錯誤訊號 throw()
在導致產生器最後一次暫停的 yield
的位置擲回例外。讓我們透過以下產生器函數來檢視它是如何運作的。
function
*
genFunc1
()
{
try
{
console
.
log
(
'Started'
);
yield
;
// (A)
}
catch
(
error
)
{
console
.
log
(
'Caught: '
+
error
);
}
}
在以下互動中,我們首先使用 next()
來啟動產生器並進行處理,直到 A 行的 yield
。然後我們從該位置擲回例外。
>
const
genObj1
=
genFunc1
();
>
genObj1
.
next
()
Started
{
value
:
undefined
,
done
:
false
}
>
genObj1
.
throw
(
new
Error
(
'Problem!'
))
Caught
:
Error
:
Problem
!
{
value
:
undefined
,
done
:
true
}
throw()
的結果 (顯示在最後一行) 來自我們使用隱含 return
離開函數。
允許在新生產生器 (尚未啟動) 中擲回例外
> function* genFunc() {}
> genFunc().throw(new Error('Problem!'))
Error: Problem!
由於產生器作為觀察者在等待輸入時會暫停,因此它們非常適合依需求處理非同步接收的資料。設定用於處理的產生器鏈的模式如下
target
。它透過 yield
接收資料並透過 target.next()
傳送資料。target
,只接收資料。整個鏈由一個非產生器函數為前綴,該函數發出非同步請求,並透過 next()
將結果推送到產生器鏈中。
舉例來說,讓我們鏈結產生器來處理非同步讀取的檔案。
下列程式碼設定串連:它包含產生器 splitLines
、numberLines
和 printLines
。資料透過非產生器函式 readFile
推入串連中。
readFile
(
fileName
,
splitLines
(
numberLines
(
printLines
())));
我會在展示這些函式的程式碼時說明它們的功能。
如前所述,如果產生器透過 yield
接收輸入,則產生器物件上的第一個 next()
呼叫不會執行任何動作。這就是我使用 先前展示的輔助函式 coroutine()
在此建立非同步函式的緣故。它會為我們執行第一個 next()
。
readFile()
是啟動所有動作的非產生器函式
import
{
createReadStream
}
from
'fs'
;
/**
* Creates an asynchronous ReadStream for the file whose name
* is `fileName` and feeds it to the generator object `target`.
*
* @see ReadStream https://node.dev.org.tw/api/fs.html#fs_class_fs_readstream
*/
function
readFile
(
fileName
,
target
)
{
const
readStream
=
createReadStream
(
fileName
,
{
encoding
:
'utf8'
,
bufferSize
:
1024
});
readStream
.
on
(
'data'
,
buffer
=>
{
const
str
=
buffer
.
toString
(
'utf8'
);
target
.
next
(
str
);
});
readStream
.
on
(
'end'
,
()
=>
{
// Signal end of output sequence
target
.
return
();
});
}
產生器串連從 splitLines
開始
/**
* Turns a sequence of text chunks into a sequence of lines
* (where lines are separated by newlines)
*/
const
splitLines
=
coroutine
(
function
*
(
target
)
{
let
previous
=
''
;
try
{
while
(
true
)
{
previous
+=
yield
;
let
eolIndex
;
while
((
eolIndex
=
previous
.
indexOf
(
'\n'
))
>=
0
)
{
const
line
=
previous
.
slice
(
0
,
eolIndex
);
target
.
next
(
line
);
previous
=
previous
.
slice
(
eolIndex
+
1
);
}
}
}
finally
{
// Handle the end of the input sequence
// (signaled via `return()`)
if
(
previous
.
length
>
0
)
{
target
.
next
(
previous
);
}
// Signal end of output sequence
target
.
return
();
}
});
請注意一個重要的模式
readFile
使用產生器物件方法 return()
來表示它所傳送的區塊序列的結尾。readFile
在 splitLines
透過 yield
等待輸入時,在無限迴圈內傳送該訊號。return()
會中斷該迴圈。splitLines
使用 finally
子句來處理序列結尾。下一個產生器是 numberLines
//**
*
Prefixes
numbers
to
a
sequence
of
lines
*
/
const
numberLines
=
coroutine
(
function
*
(
target
)
{
try
{
for
(
const
lineNo
=
0
;
;
lineNo
++
)
{
const
line
=
yield
;
target
.
next
(
`
${
lineNo
}
:
${
line
}
`
);
}
}
finally
{
// Signal end of output sequence
target
.
return
();
}
});
最後一個產生器是 printLines
/**
* Receives a sequence of lines (without newlines)
* and logs them (adding newlines).
*/
const
printLines
=
coroutine
(
function
*
()
{
while
(
true
)
{
const
line
=
yield
;
console
.
log
(
line
);
}
});
這段程式碼的優點是所有動作都是延遲執行的(依需求):當行到來時,會將其分割、編號並列印出來;我們不必等到所有文字都到齊才能開始列印。
yield*
:完整說明 根據經驗法則,yield*
會執行(等同於)從一個產生器(呼叫者)到另一個產生器(被呼叫者)的函式呼叫。
到目前為止,我們只看過 yield
的一個面向:它會將被呼叫者產生的值傳播到呼叫者。現在我們有興趣讓產生器接收輸入,另一個面向就變得相關:yield*
也會將呼叫者接收到的輸入轉發給被呼叫者。在某種程度上,被呼叫者會變成主動產生器,而且可以透過呼叫者的產生器物件來控制。
yield*
轉發 next()
下列產生器函式 caller()
透過 yield*
呼叫產生器函式 callee()
。
function
*
callee
()
{
console
.
log
(
'callee: '
+
(
yield
));
}
function
*
caller
()
{
while
(
true
)
{
yield
*
callee
();
}
}
callee
會記錄透過 next()
接收到的值,這讓我們可以檢查它是否接收我們傳送給 caller
的值 'a'
和 'b'
。
> const callerObj = caller();
> callerObj.next() // start
{ value: undefined, done: false }
> callerObj.next('a')
callee: a
{ value: undefined, done: false }
> callerObj.next('b')
callee: b
{ value: undefined, done: false }
throw()
和 return()
以類似方式轉送。
yield*
語意 我將透過說明在 JavaScript 中實作 yield*
的方式,來解釋其完整的語意。
以下陳述
let
yieldStarResult
=
yield
*
calleeFunc
();
大致等於
let
yieldStarResult
;
const
calleeObj
=
calleeFunc
();
let
prevReceived
=
undefined
;
while
(
true
)
{
try
{
// Forward input previously received
const
{
value
,
done
}
=
calleeObj
.
next
(
prevReceived
);
if
(
done
)
{
yieldStarResult
=
value
;
break
;
}
prevReceived
=
yield
value
;
}
catch
(
e
)
{
// Pretend `return` can be caught like an exception
if
(
e
instanceof
Return
)
{
// Forward input received via return()
calleeObj
.
return
(
e
.
returnedValue
);
return
e
.
returnedValue
;
// “re-throw”
}
else
{
// Forward input received via throw()
calleeObj
.
throw
(
e
);
// may throw
}
}
}
為了簡化起見,此程式碼中缺少幾項內容
yield*
的運算元可以是任何可迭代值。return()
和 throw()
是可選的迭代器方法。我們僅應在它們存在時呼叫它們。throw()
,但存在 return()
,則會呼叫 return()
(在擲回例外狀況之前),以提供 calleeObject
清理的機會。calleeObj
可以拒絕關閉,方法是傳回其屬性 done
為 false
的物件。然後,呼叫方也必須拒絕關閉,而 yield*
必須繼續反覆運算。我們已看到產生器用作資料來源或接收器。對於許多應用程式而言,嚴格區分這兩個角色是很好的做法,因為這可以讓事情變得更簡單。本節說明完整的產生器介面(結合這兩個角色),以及需要這兩個角色的一個使用案例:協同多工處理,其中任務必須能夠同時傳送和接收資訊。
產生器物件的完整介面 Generator
處理輸出和輸入
interface
Generator
{
next
(
value
?
:
any
)
:
IteratorResult
;
throw
(
value
?
:
any
)
:
IteratorResult
;
return
(
value
?
:
any
)
:
IteratorResult
;
}
interface
IteratorResult
{
value
:
any
;
done
:
boolean
;
}
介面 Generator
結合了我們先前看過的兩個介面:用於輸出的 Iterator
和用於輸入的 Observer
。
interface
Iterator
{
// data producer
next
()
:
IteratorResult
;
return
?
(
value
?
:
any
)
:
IteratorResult
;
}
interface
Observer
{
// data consumer
next
(
value
?
:
any
)
:
void
;
return
(
value
?
:
any
)
:
void
;
throw
(
error
)
:
void
;
}
合作式多工是產生器的應用,我們需要它們來處理輸出和輸入。在我們深入探討其運作方式之前,讓我們先回顧 JavaScript 中並行處理的現況。
JavaScript 在單一程序中執行。有兩種方法可以廢除這種限制
兩個用例受益於合作式多工,因為它們涉及的控制流程大多是順序的,偶爾會暫停
幾個基於 Promise 的函式庫透過產生器簡化非同步程式碼。產生器是 Promise 的理想客戶端,因為它們可以在結果到達之前暫停。
以下範例示範了如果使用 T.J. Holowaychuk 的函式庫 co會是什麼樣子。我們需要兩個函式庫(如果我們透過 babel-node
執行 Node.js 程式碼)
import
fetch
from
'isomorphic-fetch'
;
const
co
=
require
(
'co'
);
co
是用於合作式多工處理的實際函式庫,isomorphic-fetch
是新的基於 Promise 的 fetch
API 的多重載入(XMLHttpRequest
的替代品;閱讀 Jake Archibald 的「That’s so fetch!」以取得更多資訊)。fetch
讓撰寫 getFile
函式變得容易,該函式透過 Promise 傳回 url
中檔案的文字
function
getFile
(
url
)
{
return
fetch
(
url
)
.
then
(
request
=>
request
.
text
());
}
我們現在具備使用 co
的所有要素。下列工作會讀取兩個檔案的文字,解析其中的 JSON 並記錄結果。
co
(
function
*
()
{
try
{
const
[
croftStr
,
bondStr
]
=
yield
Promise
.
all
([
// (A)
getFile
(
'https://127.0.0.1:8000/croft.json'
),
getFile
(
'https://127.0.0.1:8000/bond.json'
),
]);
const
croftJson
=
JSON
.
parse
(
croftStr
);
const
bondJson
=
JSON
.
parse
(
bondStr
);
console
.
log
(
croftJson
);
console
.
log
(
bondJson
);
}
catch
(
e
)
{
console
.
log
(
'Failure to read: '
+
e
);
}
});
請注意,即使在 A 行中進行非同步呼叫,這段程式碼看起來很同步。將產生器當成工作會透過讓排程器函式 co
產生 Promise 來進行非同步呼叫。產生會暫停產生器。一旦 Promise 傳回結果,排程器就會透過 next()
將結果傳遞給產生器,讓產生器繼續執行。co
的簡化版本如下所示。
function
co
(
genFunc
)
{
const
genObj
=
genFunc
();
step
(
genObj
.
next
());
function
step
({
value
,
done
})
{
if
(
!
done
)
{
// A Promise was yielded
value
.
then
(
result
=>
{
step
(
genObj
.
next
(
result
));
// (A)
})
.
catch
(
error
=>
{
step
(
genObj
.
throw
(
error
));
// (B)
});
}
}
}
我忽略了 next()
(A 行)和 throw()
(B 行)可能會引發例外情況(每當例外情況跳脫產生器函式的本體時)。
常式 是沒有限制的合作式多工處理工作:在常式中,任何函式都可以暫停整個常式(函式啟動本身、函式呼叫者的啟動、呼叫者的呼叫者,等等)。
相反地,你只能從產生器內部直接暫停產生器,而且只會暫停目前的函式啟動。由於這些限制,產生器偶爾會被稱為 淺層常式 [3]。
產生器的限制有兩個主要優點
JavaScript 已經具備非常簡單的合作式多工處理樣式:事件迴圈,它會在佇列中排程執行工作。每個工作都是透過呼叫函式啟動,並在函式完成後結束。事件、setTimeout()
和其他機制會將工作加入佇列。
這種多工處理樣式提供一個重要的保證:執行至完成;每個函式都可以依賴在完成之前不會被其他工作中斷。函式會變成交易,而且可以在沒有人看到其在中間狀態操作資料的情況下執行完整的演算法。並發存取共用資料會讓多工處理變得複雜,而且 JavaScript 的並發模型不允許這麼做。這就是執行至完成是一件好事的原因。
唉,協程會妨礙執行至完成,因為任何函式都可能會暫停其呼叫者。例如,下列演算法包含多個步驟
step1(sharedData);
step2(sharedData);
lastStep(sharedData);
如果 step2
要暫停演算法,其他工作可以在演算法的最後一個步驟執行之前執行。那些工作可能會包含應用程式的其他部分,而這些部分會看到 sharedData
處於未完成狀態。產生器會保留執行至完成,它們只會暫停自己並返回給呼叫者。
co
和類似的函式庫會提供大部分協程的效能,而且沒有它們的缺點
yield*
執行時才能暫停。這會讓呼叫者控制暫停。本節提供產生器可以用於哪些用途的幾個範例。
在 迭代章節 中,我「手動」實作了幾個可迭代物件。在本節中,我改用產生器。
take()
take()
將(潛在無限的)反覆值序列轉換為長度為 n
的序列
function
*
take
(
n
,
iterable
)
{
for
(
const
x
of
iterable
)
{
if
(
n
<=
0
)
return
;
n
--
;
yield
x
;
}
}
以下為使用範例
const
arr
=
[
'a'
,
'b'
,
'c'
,
'd'
];
for
(
const
x
of
take
(
2
,
arr
))
{
console
.
log
(
x
);
}
// Output:
// a
// b
不使用產生器的 take()
實作較為複雜
function
take
(
n
,
iterable
)
{
const
iter
=
iterable
[
Symbol
.
iterator
]();
return
{
[
Symbol
.
iterator
]()
{
return
this
;
},
next
()
{
if
(
n
>
0
)
{
n
--
;
return
iter
.
next
();
}
else
{
maybeCloseIterator
(
iter
);
return
{
done
:
true
};
}
},
return
()
{
n
=
0
;
maybeCloseIterator
(
iter
);
}
};
}
function
maybeCloseIterator
(
iterator
)
{
if
(
typeof
iterator
.
return
===
'function'
)
{
iterator
.
return
();
}
}
請注意,可迭代組合器 zip()
透過產生器實作並未獲益太多,因為涉及多個可迭代物件,且無法使用 for-of
。
naturalNumbers()
傳回所有自然數的可迭代物件
function
*
naturalNumbers
()
{
for
(
let
n
=
0
;;
n
++
)
{
yield
n
;
}
}
此函式通常與組合器搭配使用
for
(
const
x
of
take
(
3
,
naturalNumbers
()))
{
console
.
log
(
x
);
}
// Output
// 0
// 1
// 2
以下是供您比較用的非產生器實作
function
naturalNumbers
()
{
let
n
=
0
;
return
{
[
Symbol
.
iterator
]()
{
return
this
;
},
next
()
{
return
{
value
:
n
++
};
}
}
}
map
、filter
陣列可透過 map
和 filter
方法轉換。這些方法可廣義化,將可迭代物件作為輸入和輸出。
map()
這是 map
的廣義化版本
function
*
map
(
iterable
,
mapFunc
)
{
for
(
const
x
of
iterable
)
{
yield
mapFunc
(
x
);
}
}
map()
可搭配無限可迭代物件使用
> [...take(4, map(naturalNumbers(), x => x * x))]
[ 0, 1, 4, 9 ]
filter()
這是 filter
的廣義化版本
function
*
filter
(
iterable
,
filterFunc
)
{
for
(
const
x
of
iterable
)
{
if
(
filterFunc
(
x
))
{
yield
x
;
}
}
}
filter()
可搭配無限可迭代物件使用
> [...take(4, filter(naturalNumbers(), x => (x % 2) === 0))]
[ 0, 2, 4, 6 ]
接下來兩個範例說明如何使用產生器處理字元串流。
/^[A-Za-z0-9]+$/
的字串。非字元字元會被忽略,但會用來區隔字詞。此步驟的輸入為字元串流,輸出為字詞串流。/^[0-9]+$/
的字詞,並將其轉換為數字。最棒的是,所有運算都是延遲(逐步且依需求)執行的:運算會在第一個字元抵達時開始。例如,我們不必等到收到所有字元才能取得第一個字詞。
使用產生器的延遲拉取運作方式如下。實作步驟 1-3 的三個產生器會以下列方式串連
addNumbers
(
extractNumbers
(
tokenize
(
CHARS
)))
鏈結中的每個成員會從來源拉取資料,並產生一連串項目。處理會從 tokenize
開始,其來源為字串 CHARS
。
以下技巧讓程式碼更簡單:序列結束迭代器的結果(其屬性 done
為 false
)轉換成哨兵值 END_OF_SEQUENCE
。
/**
* Returns an iterable that transforms the input sequence
* of characters into an output sequence of words.
*/
function
*
tokenize
(
chars
)
{
const
iterator
=
chars
[
Symbol
.
iterator
]();
let
ch
;
do
{
ch
=
getNextItem
(
iterator
);
// (A)
if
(
isWordChar
(
ch
))
{
let
word
=
''
;
do
{
word
+=
ch
;
ch
=
getNextItem
(
iterator
);
// (B)
}
while
(
isWordChar
(
ch
));
yield
word
;
// (C)
}
// Ignore all other characters
}
while
(
ch
!==
END_OF_SEQUENCE
);
}
const
END_OF_SEQUENCE
=
Symbol
();
function
getNextItem
(
iterator
)
{
const
{
value
,
done
}
=
iterator
.
next
();
return
done
?
END_OF_SEQUENCE
:
value
;
}
function
isWordChar
(
ch
)
{
return
typeof
ch
===
'string'
&&
/^[A-Za-z0-9]$/
.
test
(
ch
);
}
這個產生器如何做到延遲?當你透過 next()
要求它提供一個詞彙時,它會根據需要拉取其 iterator
(A 和 B 行)以產生一個詞彙,然後產生那個詞彙(C 行)。接著,它會暫停,直到再次要求它提供一個詞彙。這表示詞彙化會在第一個字元可用時立即開始,這對於串流來說很方便。
我們來試試詞彙化。請注意,空格和句點是非詞彙。它們會被忽略,但它們會區分詞彙。我們利用字串可以迭代字元(Unicode 編碼點)的事實。tokenize()
的結果是可以迭代詞彙的物件,我們透過展開運算子(...
)將其轉換成陣列。
> [...tokenize('2 apples and 5 oranges.')]
[ '2', 'apples', 'and', '5', 'oranges' ]
這個步驟相對簡單,我們只會 yield
僅包含數字的詞彙,在透過 Number()
將它們轉換成數字後。
/**
* Returns an iterable that filters the input sequence
* of words and only yields those that are numbers.
*/
function
*
extractNumbers
(
words
)
{
for
(
const
word
of
words
)
{
if
(
/^[0-9]+$/
.
test
(
word
))
{
yield
Number
(
word
);
}
}
}
你再次可以看到延遲:如果你透過 next()
要求一個數字,你會在 words
中遇到一個數字後立即取得一個數字(透過 yield
)。
我們從一個詞彙陣列中萃取數字
> [...extractNumbers(['hello', '123', 'world', '45'])]
[ 123, 45 ]
請注意,字串會轉換成數字。
/**
* Returns an iterable that contains, for each number in
* `numbers`, the total sum of numbers encountered so far.
* For example: 7, 4, -1 --> 7, 11, 10
*/
function
*
addNumbers
(
numbers
)
{
let
result
=
0
;
for
(
const
n
of
numbers
)
{
result
+=
n
;
yield
result
;
}
}
我們來試試一個簡單的範例
>
[...
addNumbers
([
5
,
-
2
,
12
])]
[
5
,
3
,
15
]
產生器鏈本身不會產生輸出。我們需要透過展開運算子主動拉取輸出
const
CHARS
=
'2 apples and 5 oranges.'
;
const
CHAIN
=
addNumbers
(
extractNumbers
(
tokenize
(
CHARS
)));
console
.
log
([...
CHAIN
]);
// [ 2, 7 ]
輔助函式 logAndYield
讓我們可以檢查事情是否真的延遲運算
function
*
logAndYield
(
iterable
,
prefix
=
''
)
{
for
(
const
item
of
iterable
)
{
console
.
log
(
prefix
+
item
);
yield
item
;
}
}
const
CHAIN2
=
logAndYield
(
addNumbers
(
extractNumbers
(
tokenize
(
logAndYield
(
CHA
\
RS
)))),
'-> '
);
[...
CHAIN2
];
// Output:
// 2
//
// -> 2
// a
// p
// p
// l
// e
// s
//
// a
// n
// d
//
// 5
//
// -> 7
// o
// r
// a
// n
// g
// e
// s
// .
輸出顯示 addNumbers
在收到字元 '2'
和 ' '
後立即產生結果。
將先前的拉取式演算法轉換成推播式演算法不需要太多工作。步驟相同。但我們不是透過拉取結束,而是透過推播開始。
如前所述,如果產生器透過 yield
接收輸入,則產生器物件上的第一個 next()
呼叫不會執行任何動作。這就是我使用 先前展示的輔助函式 coroutine()
在此建立非同步函式的緣故。它會為我們執行第一個 next()
。
以下函式 send()
負責推播。
/**
* Pushes the items of `iterable` into `sink`, a generator.
* It uses the generator method `next()` to do so.
*/
function
send
(
iterable
,
sink
)
{
for
(
const
x
of
iterable
)
{
sink
.
next
(
x
);
}
sink
.
return
();
// signal end of stream
}
當產生器處理串流時,它需要知道串流的結尾,以便可以適當地清理。對於拉取,我們透過特殊串流結尾哨兵執行此操作。對於推入,串流結尾會透過 return()
發出訊號。
讓我們透過只輸出它接收的所有內容的產生器來測試 send()
/**
* This generator logs everything that it receives via `next()`.
*/
const
logItems
=
coroutine
(
function
*
()
{
try
{
while
(
true
)
{
const
item
=
yield
;
// receive item via `next()`
console
.
log
(
item
);
}
}
finally
{
console
.
log
(
'DONE'
);
}
});
讓我們透過字串(這是 Unicode 編碼點的 iterable)傳送三個字元給 logItems()
。
> send('abc', logItems());
a
b
c
DONE
請注意此產生器如何透過兩個 finally
子句對串流結尾(透過 return()
發出訊號)做出反應。我們依賴於傳送 return()
到兩個 yield
中的任何一個。否則,產生器永遠不會終止,因為從 A 行開始的無限迴圈永遠不會終止。
/**
* Receives a sequence of characters (via the generator object
* method `next()`), groups them into words and pushes them
* into the generator `sink`.
*/
const
tokenize
=
coroutine
(
function
*
(
sink
)
{
try
{
while
(
true
)
{
// (A)
let
ch
=
yield
;
// (B)
if
(
isWordChar
(
ch
))
{
// A word has started
let
word
=
''
;
try
{
do
{
word
+=
ch
;
ch
=
yield
;
// (C)
}
while
(
isWordChar
(
ch
));
}
finally
{
// The word is finished.
// We get here if
// - the loop terminates normally
// - the loop is terminated via `return()` in line C
sink
.
next
(
word
);
// (D)
}
}
// Ignore all other characters
}
}
finally
{
// We only get here if the infinite loop is terminated
// via `return()` (in line B or C).
// Forward `return()` to `sink` so that it is also
// aware of the end of stream.
sink
.
return
();
}
});
function
isWordChar
(
ch
)
{
return
/^[A-Za-z0-9]$/
.
test
(
ch
);
}
這次,惰性是由推入驅動的:一旦產生器收到足夠的字元來組成一個字詞(在 C 行),它就會將字詞推入 sink
(D 行)。也就是說,產生器不會等到收到所有字元。
tokenize()
證明產生器很適合作為線性狀態機器的實作。在這種情況下,機器有兩個狀態:「在字詞內」和「不在字詞內」。
讓我們標記化一個字串
> send('2 apples and 5 oranges.', tokenize(logItems()));
2
apples
and
5
oranges
這個步驟很簡單。
/**
* Receives a sequence of strings (via the generator object
* method `next()`) and pushes only those strings to the generator
* `sink` that are “numbers” (consist only of decimal digits).
*/
const
extractNumbers
=
coroutine
(
function
*
(
sink
)
{
try
{
while
(
true
)
{
const
word
=
yield
;
if
(
/^[0-9]+$/
.
test
(
word
))
{
sink
.
next
(
Number
(
word
));
}
}
}
finally
{
// Only reached via `return()`, forward.
sink
.
return
();
}
});
事情再次變得惰性:一旦遇到數字,就會將其推入 sink
。
我們從一個詞彙陣列中萃取數字
> send(['hello', '123', 'world', '45'], extractNumbers(logItems()));
123
45
DONE
請注意,輸入是字串序列,而輸出是數字序列。
這次,我們透過推入單一值然後關閉 sink 來對串流結尾做出反應。
/**
* Receives a sequence of numbers (via the generator object
* method `next()`). For each number, it pushes the total sum
* so far to the generator `sink`.
*/
const
addNumbers
=
coroutine
(
function
*
(
sink
)
{
let
sum
=
0
;
try
{
while
(
true
)
{
sum
+=
yield
;
sink
.
next
(
sum
);
}
}
finally
{
// We received an end-of-stream
sink
.
return
();
// signal end of stream
}
});
讓我們試試這個產生器
> send([5, -2, 12], addNumbers(logItems()));
5
3
15
DONE
產生器鏈從 tokenize
開始,以 logItems
結束,它會記錄它接收的所有內容。我們透過 send
將字元序列推入鏈中
const
INPUT
=
'2 apples and 5 oranges.'
;
const
CHAIN
=
tokenize
(
extractNumbers
(
addNumbers
(
logItems
())));
send
(
INPUT
,
CHAIN
);
// Output
// 2
// 7
// DONE
以下程式碼證明處理確實是惰性發生的
const
CHAIN2
=
tokenize
(
extractNumbers
(
addNumbers
(
logItems
({
prefix
:
'-> '
})
\
)));
send
(
INPUT
,
CHAIN2
,
{
log
:
true
});
// Output
// 2
//
// -> 2
// a
// p
// p
// l
// e
// s
//
// a
// n
// d
//
// 5
//
// -> 7
// o
// r
// a
// n
// g
// e
// s
// .
// DONE
輸出顯示 addNumbers
在字元 '2'
和 ' '
被推入後立即產生結果。
在此範例中,我們建立一個顯示在網頁上的計數器。我們改善初始版本,直到我們有一個合作式多工版本,不會封鎖主執行緒和使用者介面。
這是網頁中應顯示計數器的部分
<
body
>
Counter: <
span
id
=
"counter"
></
span
>
</
body
>
此函式顯示一個持續計數的計數器5
function
countUp
(
start
=
0
)
{
const
counterSpan
=
document
.
querySelector
(
'#counter'
);
while
(
true
)
{
counterSpan
.
textContent
=
String
(
start
);
start
++
;
}
}
如果您執行此函式,它會完全封鎖執行它的使用者介面執行緒,而其分頁將會沒有回應。
讓我們透過一個會定期暫停的產生器來實作相同的功能,透過 yield
(此產生器的執行排程函式稍後會顯示)
function
*
countUp
(
start
=
0
)
{
const
counterSpan
=
document
.
querySelector
(
'#counter'
);
while
(
true
)
{
counterSpan
.
textContent
=
String
(
start
);
start
++
;
yield
;
// pause
}
}
讓我們增加一個小小的改進。我們將使用者介面的更新移到另一個產生器 displayCounter
,我們透過 yield*
呼叫它。由於它是一個產生器,它也可以處理暫停。
function
*
countUp
(
start
=
0
)
{
while
(
true
)
{
start
++
;
yield
*
displayCounter
(
start
);
}
}
function
*
displayCounter
(
counter
)
{
const
counterSpan
=
document
.
querySelector
(
'#counter'
);
counterSpan
.
textContent
=
String
(
counter
);
yield
;
// pause
}
最後,這是一個排程函式,我們可以使用它來執行 countUp()
。產生器的每個執行步驟都由一個透過 setTimeout()
建立的個別任務處理。這表示使用者介面可以在其間排程其他任務,而且會保持回應。
function
run
(
generatorObject
)
{
if
(
!
generatorObject
.
next
().
done
)
{
// Add a new task to the event queue
setTimeout
(
function
()
{
run
(
generatorObject
);
},
1000
);
}
}
在 run
的協助下,我們得到一個(幾乎)無限的計數器,不會封鎖使用者介面
run
(
countUp
());
如果您呼叫產生器函式(或方法),它無法存取其產生器物件;它的 this
是它如果是非產生器函式時會有的 this
。一個解決方法是透過 yield
將產生器物件傳遞到產生器函式中。
下列 Node.js 腳本使用此技術,但將產生器物件包裝在一個回呼中(next
,A 行)。它必須透過 babel-node
執行。
import
{
readFile
}
from
'fs'
;
const
fileNames
=
process
.
argv
.
slice
(
2
);
run
(
function
*
()
{
const
next
=
yield
;
for
(
const
f
of
fileNames
)
{
const
contents
=
yield
readFile
(
f
,
{
encoding
:
'utf8'
},
next
);
console
.
log
(
'##### '
+
f
);
console
.
log
(
contents
);
}
});
在 A 行,我們取得一個回呼,我們可以使用它與遵循 Node.js 回呼約定的函式。回呼使用產生器物件喚醒產生器,如您在 run()
的實作中所見
function
run
(
generatorFunction
)
{
const
generatorObject
=
generatorFunction
();
// Step 1: Proceed to first `yield`
generatorObject
.
next
();
// Step 2: Pass in a function that the generator can use as a callback
function
nextFunction
(
error
,
result
)
{
if
(
error
)
{
generatorObject
.
throw
(
error
);
}
else
{
generatorObject
.
next
(
result
);
}
}
generatorObject
.
next
(
nextFunction
);
// Subsequent invocations of `next()` are triggered by `nextFunction`
}
函式庫 js-csp
將通訊順序處理(CSP)帶到 JavaScript,這是一種合作式多工處理,類似於 ClojureScript 的 core.async 和 Go 的 goroutines。js-csp
有兩個抽象
go()
來實作。chan()
建立的。舉例來說,讓我們使用 CSP 來處理 DOM 事件,以類似於函數式反應式程式設計的方式。下列程式碼使用函式 listen()
(稍後會顯示)建立一個輸出 mousemove
事件的通道。然後它在一個無限迴圈中透過 take
持續擷取輸出。感謝 yield
,處理程序會封鎖,直到通道有輸出。
import
csp
from
'js-csp'
;
csp
.
go
(
function
*
()
{
const
element
=
document
.
querySelector
(
'#uiElement1'
);
const
channel
=
listen
(
element
,
'mousemove'
);
while
(
true
)
{
const
event
=
yield
csp
.
take
(
channel
);
const
x
=
event
.
layerX
||
event
.
clientX
;
const
y
=
event
.
layerY
||
event
.
clientY
;
element
.
textContent
=
`
${
x
}
,
${
y
}
`
;
}
});
listen()
的實作如下。
function
listen
(
element
,
type
)
{
const
channel
=
csp
.
chan
();
element
.
addEventListener
(
type
,
event
=>
{
csp
.
putAsync
(
channel
,
event
);
});
return
channel
;
}
這是 ECMAScript 6 中各種物件如何連接的圖表(它基於 ECMAScript 規格中 Allen Wirf-Brock 的圖表)
圖例
x
到 y
的白色箭頭表示 Object.getPrototypeOf(x) === y
。x
到 y
的 instanceof
箭頭表示 x instanceof y
。
o instanceof C
等於 C.prototype.isPrototypeOf(o)
。x
到 y
的 prototype
箭頭表示 x.prototype === y
。此圖表揭露了兩個有趣的事實
首先,產生器函式 g
非常像建構函式(不過,您無法透過 new
呼叫它;那會導致 TypeError
):它建立的產生器物件是它的實例,新增到 g.prototype
的方法會變成原型方法,等等。
>
function
*
g
()
{}
>
g
.
prototype
.
hello
=
function
()
{
return
'hi!'
};
>
const
obj
=
g
();
>
obj
instanceof
g
true
>
obj
.
hello
()
'hi!'
其次,如果您想要讓所有產生器物件都可以使用這些方法,最好將它們新增到 (Generator).prototype
。存取該物件的方法之一如下
const
Generator
=
Object
.
getPrototypeOf
(
function
*
()
{});
Generator
.
prototype
.
hello
=
function
()
{
return
'hi!'
};
const
generatorObject
=
(
function
*
()
{})();
generatorObject
.
hello
();
// 'hi!'
IteratorPrototype
圖表中沒有 (Iterator)
,因為沒有這樣的物件。但是,由於 instanceof
的運作方式,而且因為 (IteratorPrototype)
是 g1()
的原型,所以您仍然可以說 g1()
是 Iterator
的實例。
ES6 中的所有反覆運算器在它們的原型鏈中都有 (IteratorPrototype)
。該物件是可反覆運算的,因為它有下列方法。因此,所有 ES6 反覆運算器都是可反覆運算的(因此,您可以對它們套用 for-of
等)。
[
Symbol
.
iterator
]()
{
return
this
;
}
規格建議使用下列程式碼存取 (IteratorPrototype)
const
proto
=
Object
.
getPrototypeOf
.
bind
(
Object
);
const
IteratorPrototype
=
proto
(
proto
([][
Symbol
.
iterator
]()));
您也可以使用
const
IteratorPrototype
=
proto
(
proto
(
function
*
()
{}.
prototype
));
引用 ECMAScript 6 規格
ECMAScript 代碼也可以定義繼承自
IteratorPrototype
的物件。IteratorPrototype
物件提供一個地方,可以新增適用於所有迭代器物件的其他方法。
IteratorPrototype
可能會在 ECMAScript 的後續版本中直接存取,並包含工具方法,例如 map()
和 filter()
(來源).
this
值 產生器函式結合了兩個考量
這就是為什麼產生器內部 this
的值並不明顯。
在函式呼叫和方法呼叫中,this
的值與 gen()
不是產生器函式,而是常態函式時相同
function
*
gen
()
{
'use strict'
;
// just in case
yield
this
;
}
// Retrieve the yielded value via destructuring
const
[
functionThis
]
=
gen
();
console
.
log
(
functionThis
);
// undefined
const
obj
=
{
method
:
gen
};
const
[
methodThis
]
=
obj
.
method
();
console
.
log
(
methodThis
===
obj
);
// true
如果你在透過 new
呼叫的產生器中存取 this
,你會得到一個 ReferenceError
(來源:ES6 規格)
function
*
gen
()
{
console
.
log
(
this
);
// ReferenceError
}
new
gen
();
一個解決方法是將產生器包裝在一個常態函式中,透過 next()
將產生器傳遞給其產生器物件。這表示產生器必須使用其第一個 yield
來擷取其產生器物件
const
generatorObject
=
yield
;
星號格式化的合理且合法的變化包括
function * foo(x, y) { ··· }
function *foo(x, y) { ··· }
function* foo(x, y) { ··· }
function*foo(x, y) { ··· }
讓我們找出這些變化中哪些適用於哪些建構,以及原因。
在此,星號僅用於 generator
(或類似字詞)不可用作關鍵字。如果可用,則產生器函式宣告會如下所示
generator
foo
(
x
,
y
)
{
···
}
ECMAScript 6 使用星號標記 function
關鍵字,而不是 generator
。因此,function*
可以視為 generator
的同義字,建議如下撰寫產生器函式宣告。
function
*
foo
(
x
,
y
)
{
···
}
匿名產生器函數表達式將會以這種方式格式化
const
foo
=
function
*
(
x
,
y
)
{
···
}
在撰寫產生器方法定義時,我建議以以下方式格式化星號。
const
obj
=
{
*
generatorMethod
(
x
,
y
)
{
···
}
};
在星號後加上空白有三個論點支持。
首先,星號不應該是方法名稱的一部分。一方面,它不是產生器函數名稱的一部分。另一方面,只有在定義產生器時才會提到星號,而不會在使用時提到。
其次,產生器方法定義是以下語法的縮寫。(為了說明我的觀點,我也會多餘地為函數表達式命名。)
const
obj
=
{
generatorMethod
:
function
*
generatorMethod
(
x
,
y
)
{
···
}
};
如果方法定義是要省略 function
關鍵字,那麼星號後面應該加上空白。
第三,產生器方法定義在語法上類似於 getter 和 setter(ECMAScript 5 中已經有了)
const
obj
=
{
get
foo
()
{
···
}
set
foo
(
value
)
{
···
}
};
關鍵字 get
和 set
可以視為普通方法定義的修改器。可以說,星號也是這樣的修改器。
yield
以下是產生器函數遞迴產生其自己的產生值的範例
function
*
foo
(
x
)
{
···
yield
*
foo
(
x
-
1
);
···
}
星號標記了不同類型的 yield
算子,這就是上述寫法有意義的原因。
Kyle Simpson (@getify) 提出了有趣的建議:由於我們在撰寫函數和方法(例如 Math.max()
)時,通常會加上括號,那麼在撰寫產生器函數和方法時,在前面加上星號是否合理?例如:我們是否應該撰寫 *foo()
來指稱前一個小節中的產生器函數?讓我反對這個說法。
在撰寫會傳回可迭代項目的函數時,產生器只是其中一種選項。我認為最好不要透過標記函數名稱來提供這個實作細節。
此外,在呼叫產生器函數時不會使用星號,但會使用括號。
最後,星號沒有提供有用的資訊 – yield*
也可以用於傳回可迭代項目的函數。但標記傳回可迭代項目的函數和方法(包括產生器)的名稱可能是合理的。例如,透過後綴 Iter
。
function*
而不是 generator
? 由於向後相容性,使用關鍵字generator
並非選項。例如,以下程式碼(一個假設的 ES6 匿名產生器表達式)可以是 ES5 函式呼叫後接程式碼區塊。
generator
(
a
,
b
,
c
)
{
···
}
我發現星號命名方案很好地延伸到yield*
。
yield
是關鍵字嗎? yield
僅在嚴格模式中是保留字。有一個技巧可以將它帶到 ES6 隨意模式:它變成語境關鍵字,僅在產生器內可用。
我希望本章讓您確信產生器是一個有用且多功能的工具。
我喜歡產生器讓您實作協同多工任務,這些任務在進行非同步函式呼叫時會封鎖。在我看來,那是非同步呼叫的正確心智模式。希望 JavaScript 未來朝這個方向進一步發展。
本章來源
[1] “非同步產生器提案” by Jafar Husain
[2] “協同程序和並發性的好奇課程” by David Beazley
[3] “為什麼協同程序無法在網路上運作” by David Herman