本篇博客來源於王福明博客的異步系列和《你不知道的JavaScript》
原博客地址爲:http://www.cnblogs.com/wangfupeng1988/p/6532713.html
1. 打破完整運行
在前面已經學習了promise的解決方法來處理異步操作的代碼邏輯,接下來我們來看一種順序、看似同步的異步流程控制表達風格——ES6的生成器(generator)。
先看一段代碼:
var x = 1;
function* foo(){
x++;
yield;//暫停!
console.log("x:",x);
}
function bar(){
x++;
}
//構造一個迭代器it來控制這個生成器
var it = foo();
//這裏啓動foo()!
it.next();
console.log(x); //2
bar();
console.log(x); //3
it.next(); //x: 3
這段代碼的運行過程是什麼樣呢?請往下看:
it = foo()
運算並沒有執行生成器foo()
,,而只是夠早了一個迭代器(iterator),這個迭代器會控制它的執行。【後面會接受迭代器】- 第一個
it.next()
啓動了生成器foo()
,並運行了foo()
第一行的x++
。foo()
在yield
語句處暫停,到這裏第一個it.next()
調用結束。此時的foo()
仍在運行並且是活躍的,但處於暫停狀態。 - 此時查看x的值爲2.然後我們再調用
bar()
,再次遞增x。 - 再次查看x的值,此時x的值爲3.
- 最後的
it.next()
調用從暫停處恢復了生成器foo()
的執行,並運行console.log(...)
語句,這條語句使用當前x的值3.
顯然,foo()
啓動了,但還沒有完整運行,它在yield
處暫停了。後面恢復了foo()
並讓它運行到結束。
1.1 輸入和輸出
生成器函數實際是特殊的函數,它也可以接受參數(輸入),也能夠返回值(輸出)。
function* foo(x,y){
return x * y;
}
var it = foo(6,7);
var res = it.next();
res.value; //42
我們向foo()
傳入實參6和7分別作爲參數x和y。foo()
向調用代碼返回42.
迭代消息傳遞
處理能夠接受參數並提供返回值之外,生成器還提供了更強大的內建消息輸入輸出能力,通過yield
和next(...)
實現。
function* foo(x){
var y = x*(yield);
return y;
}
var it = foo(6);
//啓動foo(...)
it.next();
var res = it.next(7);
res.value; //42
首先,傳入6作爲參數x。然後調用it.next()
,這會啓動foo()
.
在foo(...)
內部,開始執行語句var y = x …,但隨後就遇到一個yield表達式。他就會在這點上暫停foo(...)
(在賦值語句中間暫停!)並在本質上要求調用代碼爲yield
表達式提供一個結果值。接下來,調用it.next(7)
,這句把值7傳回作爲被暫停的yield
表達式的結果。
所以,這時賦值語句實際上就是var y = 6 * 7
。現在,return y
返回值42作爲調用it.next(7)
的結果。
多個迭代器
關於多個迭代器的,和前面的理解一樣。看如下代碼:
function* foo(){
var x = yield 2;
z++;
var y = yield (x*z);
console.log(x,y,z);
}
var z = 1;
var it1 = foo();
var it2 = foo();
var val1 = it1.next().value; //2 <--yield 2
var val2 = it2.next().value; //2 <--yield 2
val1 = it1.next(val2*10).value; //40 <-- x:20,z:2
val2 = it2.next(val1*5).value; //600 <-- x:200,z:3
it1.next(val2/2); //y:300
// 20 300 3
it2.next(val1/4); //y:10
// 200 10 3
能看出正確結果麼?
2、Iterator 迭代器
前面介紹了生成器的一種有趣用法是作爲一種產生值的方式。接下來,介紹一點迭代器。
2.1 Symbol數據類型簡介
Symbol是一個特殊的數據類型,和number string等並列,詳細可以看阮一峯老師的ES6入門中的介紹。
現在先記住,Symbol數據類型也可以作爲對象屬性的key。看如下代碼:
var obj = {}
obj.a = 100
obj[Symbol.iterator] = 200
console.log(obj) // {a: 100, Symbol(Symbol.iterator): 200}
[Symbol.iterator]是一個特殊的數據類型——Symbol類型,但是也可以像number string類型一樣,作爲對象的屬性key來使用。
2.2 iterable
iterable(可迭代):指一個包含可以在其值上迭代的迭代器對象。
從ES6開始,從一個iterable中提取迭代器的方法是:iterable必須支持一個函數,其名稱是專門的ES6符號值Symbol.iterator
。調用這個函數時,它會返回一個迭代器。
在ES6中,元素具有[Symbol.iterator]
屬性數據類型的有:數組、某些類似數字的對象(arguments等)、Set和Map。
原生具有[Symbol.iterator]
屬性的數據類型有一個特點:可以使用for...of
來取值。
var item
for (item of [100, 200, 300]) {
console.log(item)
}
// 打印出:100 200 300
// 注意,這裏每次獲取的 item 是數組的 value,而不是 index ,這一點和 傳統 for 循環以及 for...in 完全不一樣
// for..of循環自動調用它的Symbol.iterator函數來構建一個迭代器
2.3 生成Iterator對象
首先定義一個數組,並生成該數組的Iterator對象
const arr = [100, 200, 300]
const iterator = arr[Symbol.iterator]() // 通過執行 [Symbol.iterator] 的屬性值(函數)來返回一個 iterator 對象
生成了iterator,該如何使用呢?有兩種方式:next
和for...of
1. next
console.log(iterator.next()) // { value: 100, done: false }
console.log(iterator.next()) // { value: 200, done: false }
console.log(iterator.next()) // { value: 300, done: false }
console.log(iterator.next()) // { value: undefined, done: true }
- for…of
let i
for (i of iterator) {
console.log(i)
}
// 打印:100 200 300
2.4 Generator返回的也是Iterator對象
現在,你應該明白了,我們在第一部分說的生成器,就是生成一個Iterator對象。因此會有next()
,也可以通過for...of
來遍歷。
function* Hello() {
yield 100
yield (function () {return 200})()
return 300
}
const h = Hello()
console.log(h[Symbol.iterator]) // function [Symbol.iterator](){[native code]}
執行const h = Hello()得到的就是一個iterator對象,因爲h[Symbol.iterator]是有值的。既然是iterator對象,那麼就可以使用next()和for…of進行操作
console.log(h.next()) // { value: 100, done: false }
console.log(h.next()) // { value: 200, done: false }
console.log(h.next()) // { value: 300, done: false }
console.log(h.next()) // { value: undefined, done: true }
let i
for (i of h) {
console.log(i);//100 200
}
3、生成器的應用
3.1 yield* 語句
如果有兩個Generator,想要在第一個中包含第二個,如下需求:
function* G1() {
yield 'a'
yield 'b'
}
function* G2() {
yield 'x'
yield 'y'
}
針對以上兩個Generator,我的需求是:一次輸出a x y b,該如何做?
用for..of
解決:
var g1 = G1();
var g2 = G2();
for(var i of g1){
console.log(i);
for(var j of g2){
console.log(j);
}
}
但是,更簡潔的方式yield*表達式
function* G1() {
yield 'a'
yield* G2() // 使用 yield* 執行 G2()
yield 'b'
}
function* G2() {
yield 'x'
yield 'y'
}
for (let item of G1()) {
console.log(item)
}
yield*
後面會接一個Generator,而且會把它其中的yield按照規則來一步一步執行。
4、異步迭代生成器
生成器與異步編碼模式及解決回調問題等有什麼關係呢?接下來我們就來看這個問題。
先看一段代碼:
function foo(x, y, cb) {
ajax(
"http://some.url.1/?x="+x+"&y="+y,
cb
);
}
foo(11,31,function (err, text) {
if(err){
console.log(err);
}
else{
console.log(text);
}
});
若想要通過生成器來表達烔煬的任務流程控制,可以這樣實現:
function foo(x, y) {
ajax(
"http://some.url.1/?x="+x+"&y="+y,
function (err, data) {
if(err){
//向*main()拋出一個錯誤
it.throw(err) ;
}
else{
//用收到的data恢復*main()
it.next(data);
}
}
);
}
function* main() {
try{
var text = yield foo(11,31);
console.log(text);
}
catch(err){
console.log(err);
}
}
var it =main();
//啓動生成器
it.next();
第一眼看上去,與之前的回調代碼對比起來,代碼更長了,但是,別想得這麼簡單!
在yield foo(11,31)
中,首先調用foo(11,31)
,它沒有返回值(返回undefined
),所以我們發出了一個調用來請求數據,但實際上之後做的是yield undefined
。因爲這段代碼當前不依賴yield
出來的值來做任何事。
這裏yield
只是將其用於流程控制實現暫停/阻塞。它的消息傳遞,只是生成器恢復運行之後的單向消息傳遞。
總結一下:我們在生成器內部有了看似完全同步的代碼(出來yield),但隱藏在背後的是,在foo(..)
內的運行可以完全異步。
這樣,對於回調無法以順序同步的、符合我們大腦思考模式的方式表達異步這個問題,是一個近乎完美的解決方案。
同步錯誤處理
try{
var text = yield foo(11,31);
console.log(text);
}
catch(err){
console.log(err);
}
在前面我們已經看到yield
是如何讓賦值語句暫停來等待foo(...)
完成,使得響應完成後可以被賦給text
。精彩部分在於yield
暫停也使得生成器能夠捕獲錯誤。
通過這段代碼,把錯誤拋出到生成器中:
if(err){
//向*main()拋出一個錯誤
it.throw(err) ;
}
還可以捕獲通過throw(..)
拋入生成器的同一個錯誤,基本上也就是給生成器一個處理它的機會;如果生成器沒有處理的話,迭代器代碼就必須處理:
function* main() {
var x = yield "Hello World";
//永遠不會到達這裏
console.log(x);
}
var it = main();
it.next();
try{
// *main()會處理這個錯誤嗎?
it.throw("Oops");
}
catch (err) {
//不行,沒有處理!
console.log(err); //Oops
}
5、生成器+promise
這個看個簡單的例子吧,感覺更偏向於結構設計了,一時半會兒沒有實際應用感覺說不清楚,以後再細說吧!
簡單的例子:
function foo(x, y) {
return request(
"http://some.url.1/?x=" + x + "&y=" + y
);
}
function* main() {
try{
var text = yield foo(11,31);
console.log(text);
}
catch (err) {
console.log(err);
}
}
我們把promise
的實現細節抽象出來了,在生成器中只進行函數調用就行了。實現了promise
的隱藏