為什麼要用generator
在前端開發過程中我們經常需要先請求後端的資料,再用拿來的資料進行使用網頁頁面渲染等操作,然而請求資料是一個非同步操作,而我們的頁面渲染又是同步操作,這裡ES6中的generator
就能發揮它的作用,使用它可以像寫同步程式碼一樣寫非同步程式碼。下面是一個例子,先忽略下面的寫法,後面會詳細說明。如果你已經理解generator
基礎可以直接跳過這部分和語法部分,直接看深入理解的部分。
function *foo() {
// 請求資料
var data = yield makeAjax('http://www.example.com');
render(data);
}
複製程式碼
在等待資料的過程中會繼續執行其他部分的程式碼,直到資料返回才會繼續執行foo
中後面的程式碼,這是怎麼實現的那?我們都知道js是單執行緒的,就是說我們不可能同時執行兩段程式碼,要實現這種效果,我們先來猜想下(ps:當然後面我都看過了,這裡只是幫助大家理解,帶著問題去看),我們來假設有一個“王杖”(指代cpu的執行權),誰拿到這個“王杖”,誰就可以做自己想做的事,現在程式碼執行到foo
我們現在拿著“王杖”然後向伺服器請求資料,現在資料還沒有返回,我們不能幹等著。作為王我們有著高尚的馬克思主義思想,我們先把自己的權利交出去,讓下一個需要用的人先用著,當然前提是要他們約定好一會兒有需要,再把“王杖”還給我們。等資料返回之後,我們再把我們的“王杖”要回來,就可以繼續做我們想做的事情了。
如果你理解了這個過程,那麼恭喜你,你已經基本理解了generator
的執行機制,我這麼比喻雖然有些過程不是很貼切,但基本是這麼個思路。更多的東西還是向下看吧。
generator語法
generator函式
在用generator
之前,我們首先要了解它的語法。在上面也看到過,它跟函式宣告很像,但後面有多了個*
號,就是function *foo() { }
,當然也可以這麼寫function* foo() { }
。這裡兩種寫法沒有任何區別,全看個人習慣,這篇文章裡我會用第一種語法。現在我們按這種語法宣告一個generator
函式,供後面使用。
function *foo() {
}
複製程式碼
yield
到目前為止,我們還什麼也幹不了,因為我們還缺少了一個重要的老夥計yield
。yield
翻譯成漢語是產生的意思。yield
會讓我們跟在後面的表示式執行,然後交出自己的控制權,停在這裡,直到我們呼叫next()
才會繼續向下執行。這裡新出現了next
我們先跳過,先說說generator
怎麼執行。先看一個例子。
function *foo() {
var a = yield 1 + 1;
var b = yield 2 + a;
console.log(b);
}
var it = foo();
it.next();
it.next(2);
it.next(4);
複製程式碼
下面我們來逐步分析,首先我們定義了一個generator
函式foo
,然後我們執行它foo()
,這裡跟普通函式不同的是,它執行完之後返回的是一個迭代器,等著我們自己卻呼叫一個又一個的yield
。怎麼呼叫那,這就用到我們前面提到的next
了,它能夠讓迭代器一個一個的執行。好,現在我們呼叫第一個it.next()
,函式會從頭開始執行,然後執行到了第一個yield
,它首先計算了1 + 1
,嗯,然後停了下來。然後我們呼叫第二個it.next(2)
,注意我這裡傳入了一個2
作為next
函式的引數,這個2
傳給了a
作為它的值,你可能還有很多其他的疑問,我們詳細的後面再說。接著來,我們的it.next(2)
執行到了第二個yield
,並計算了2 + a
由於a
是2
所以就變成了2 + 2
。第三步我們再呼叫it.next(4)
,過程跟上一步相同,我們把b
賦值為4
繼續向下執行,執行到了最後列印出我們的b
為4
。這就是generator
執行的全部的過程了。現在弄明白了yield
跟next
的作用,回到剛才的問題,你可能要問,為什麼要在next
中傳入2
和4
,這裡是為了方便理解,我手動計算了1 + 1
和2 + 2
的值,那麼程式自己計算的值在哪裡?是next
函式的返回值嗎,帶著這個疑問,我們來看下面一部分。
next
next的引數
next
可以傳入一個引數,來作為上一次yield
的表示式的返回值,就像我們上面說的it.next(2)
會讓a
等於2
。當然第一次執行next
也可以傳入一個引數,但由於它沒有上一次yield
所以沒有任何東西能夠接受它,會被忽略掉,所以沒有什麼意義。
next的返回值
在這部分我們說說next
返回值,廢話不多說,我們先列印出來,看看它到底是什麼,你可以自己執行一下,也可以直接看我執行的結果。
function *foo() {
var a = yield 1 + 1;
var b = yield 2 + a;
console.log(b);
}
var it = foo();
console.log(it.next());
console.log(it.next(2));
console.log(it.next(4));
複製程式碼
執行結果:
{ value: 2, done: false }
{ value: 4, done: false }
4
{ value: undefined, done: true }
複製程式碼
看到這裡你會發現,yield
後面的表示式執行的結果確實返回了,不過是在返回值的value
欄位中,那還有done
欄位使用來做什麼用的那。其實這裡的done
是用來指示我們的迭代器,就是例子中的it
是否執行完了,仔細觀察你會發現最後一個it.next(4)
返回值是done: true
的,前面的都是false
,那麼最後一個列印值的undefined
又是什麼那,因為我們後面沒有yield
了,所以這裡沒有被計算出值,那麼怎麼讓最後一個有值那,很簡單加個return
。我們改寫下上面的例子。
function *foo() {
var a = yield 1 + 1;
var b = yield 2 + a;
return b + 1;
}
var it = foo();
console.log(it.next());
console.log(it.next(2));
console.log(it.next(4));
複製程式碼
執行結果:
{ value: 2, done: false }
{ value: 4, done: false }
{ value: 5, done: true }
複製程式碼
最後的next
的value
的值就是最終return
返回的值。到這裡我們就不再需要手動計算我們的值了,我們在改寫下我們的例子。
function *foo() {
var a = yield 1 + 1;
var b = yield 2 + a;
return b + 1;
}
var it = foo();
var value1 = it.next().value;
var value2 = it.next(value1).value;
console.log(it.next(value2));
複製程式碼
大功告成!這些基本上就完成了generator
的基礎部分。但是還有更多深入的東西需要我們進一步挖掘,看下去,相信你會有收穫的。
深入理解
前兩部分我們學習了為什麼要用generator
以及generator
的語法,這些都是基礎,下面我們來看點不一樣的東西,老規矩先帶著問題才能更有目的性的看,這裡先提出幾個問題:
- 怎樣在非同步程式碼中使用,上面的例子都是同步的啊
- 如果出現錯誤要怎麼進行錯誤的處理
- 一個個呼叫next太麻煩了,能不能迴圈執行或者自動執行那
迭代器
進行下面所有的部分之前我們先說一說迭代器,看到現在,我們都知道generator
函式執行完返回的是一個迭代器。在ES6中同樣提供了一種新的迭代方式for...of
,for...of
可以幫助我們直接迭代出每個的值,在陣列中它像這樣。
for (var i of ['a', 'b', 'c']) {
console.log(i);
}
// 輸出結果
// a
// b
// c
複製程式碼
下面我們用我們的generator
迭代器試試
function *foo() {
yield 1;
yield 2;
yield 3;
return 4;
}
// 獲取迭代器
var it = foo();
for(var i of it) {
console.log(i);
}
// 輸出結果
// 1
// 2
// 3
複製程式碼
現在我們發現for...of
會直接取出我們每一次計算返回的值,直到done: true
。這裡注意,我們的4
沒有列印出來,說明for...of
迭代,是不包括done
為true
的時候的值的。
下面我們提一個新的問題,如果在generator
中執行generator
會怎麼樣?這裡我們先認識一個新的語法yield *
,這個語法可以讓我們在yield
跟一個generator
執行器,當yield
遇到一個新的generator
需要執行,它會先將這個新的generator
執行完,再繼續執行我們當前的generator
。這樣說可能不太好理解,我們看程式碼。
function *foo() {
yield 2;
yield 3;
yield 4;
}
function * bar() {
yield 1;
yield *foo();
yield 5;
}
for ( var v of bar()) {
console.log(v);
}
複製程式碼
這裡有兩個generator
我們在bar
中執行了foo
,我們使用了yield *
來執行foo
,這裡的執行順序會是yield 1
,然後遇到foo
進入foo
中,繼續執行foo
中的yield 2
直到foo
執行完畢。然後繼續回到bar
中執行yield 5
所以最後的執行結果是:
1
2
3
4
5
複製程式碼
非同步請求
我們上面的例子一直都是同步的,但實際上我們的應用是在非同步中,我們現在來看看非同步中怎麼應用。
function request(url) {
makeAjaxCall(url, function(response) {
it.next(response);
})
}
function *foo() {
var data = yield request('http://api.example.com');
console.log(JSON.parse(data));
}
var it = foo();
it.next();
複製程式碼
這裡又回到一開頭說的那個例子,非同步請求在執行到yield
的時候交出控制權,然後等資料回撥成功後在回撥中交回控制權。所以像同步一樣寫非同步程式碼並不是說真的變同步了,只是非同步回撥的過程被封裝了,從外面看不到而已。
錯誤處理
我們都知道在js中我們使用try...catch
來處理錯誤,在generator中類似,如果在generator
內發生錯誤,如果內部能處理,就在內部處理,不能處理就繼續向外冒泡,直到能夠處理錯誤或最後一層。
內部處理錯誤:
// 內部處理
function *foo() {
try {
yield Number(4).toUpperCase();
} catch(e) {
console.log('error in');
}
}
var it = foo();
it.next();
// 執行結果:error in
複製程式碼
外部處理錯誤:
// 外部處理
function *foo() {
yield Number(4).toUpperCase();
}
var it = foo();
try {
it.next();
} catch(e) {
console.log('error out');
}
// 執行結果:error out
複製程式碼
在generator
的錯誤處理中還有一個特殊的地方,它的迭代器有一個throw
方法,能夠將錯誤丟回generator
中,在它暫停的地方報錯,再往後就跟上面一樣了,如果內部能處理則內部處理,不能內部處理則繼續冒泡。
內部處理結果:
function *foo() {
try {
yield 1;
} catch(e) {
console.log('error', e);
}
yield 2;
yield 3;
}
var it = foo();
it.next();
it.throw('oh no!');
// 執行結果:error oh no!
複製程式碼
外部處理結果:
function *foo() {
yield 1;
yield 2;
yield 3;
}
var it = foo();
it.next();
try {
it.throw('oh no!');
} catch (e) {
console.log('error', e);
}
// 執行結果:error oh no!
複製程式碼
根據測試,發現迭代器的throw
也算作一次迭代,測試程式碼如下:
function *foo() {
try {
yield 1;
yield 2;
} catch (e) {
console.log('error', e);
}
yield 3;
}
var it = foo();
console.log(it.next());
it.throw('oh no!');
console.log(it.next());
// 執行結果
// { value: 1, done: false }
// error oh no!
// { value: undefined, done: true }
複製程式碼
當用throw
丟回錯誤的時候,除了try
中的語句,迭代器迭代掉了yield 3
下次再迭代就是,就是最後結束的值了。錯誤處理到這裡就沒有了,就這麼點東西^_^。
自動執行
generator
能不能自動執行?當然能,並且有很多這樣的庫,這裡我們先自己實現一個簡單的。
function run(g) {
var it = g();
// 利用遞迴進行迭代
(function iterator(val) {
var ret = it.next(val);
// 如果沒有結束
if(!ret.done) {
// 判斷promise
if(typeof ret.value === 'object' && 'then' in ret.value) {
ret.value.then(iterator);
} else {
iterator(ret.value);
}
}
})();
}
複製程式碼
這樣我們就能自動處理執行我們的generator
了,當然我們這個很簡單,沒有任何錯誤處理,如何讓多個generator
同時執行,這其中涉及到如何進行控制權的轉換問題。我寫了一個簡單的執行器Fo
,其中包含了Kyle Simpson大神的一個ping-pong的例子,感興趣的可以看下這裡是傳送門,當然能順手star一下就更好了,see you next article ~O(∩_∩)O~。