在前端開發過程當中咱們常常須要先請求後端的數據,再用拿來的數據進行使用網頁頁面渲染等操做,然而請求數據是一個異步操做,而咱們的頁面渲染又是同步操做,這裏ES6中的generator
就能發揮它的做用,使用它能夠像寫同步代碼同樣寫異步代碼。下面是一個例子,先忽略下面的寫法,後面會詳細說明。若是你已經理解generator
基礎能夠直接跳過這部分和語法部分,直接看深刻理解的部分。javascript
function *foo() {
// 請求數據
var data = yield makeAjax('http://www.example.com');
render(data);
}
複製代碼
在等待數據的過程當中會繼續執行其餘部分的代碼,直到數據返回纔會繼續執行foo
中後面的代碼,這是怎麼實現的那?咱們都知道js是單線程的,就是說咱們不可能同時執行兩段代碼,要實現這種效果,咱們先來猜測下(ps:固然後面我都看過了,這裏只是幫助你們理解,帶着問題去看),咱們來假設有一個「王杖」(指代cpu的執行權),誰拿到這個「王杖」,誰就能夠作本身想作的事,如今代碼執行到foo
咱們如今拿着「王杖」而後向服務器請求數據,如今數據尚未返回,咱們不能幹等着。做爲王咱們有着高尚的馬克思主義思想,咱們先把本身的權利交出去,讓下一個須要用的人先用着,固然前提是要他們約定好一下子有須要,再把「王杖」還給咱們。等數據返回以後,咱們再把咱們的「王杖」要回來,就能夠繼續作咱們想作的事情了。 若是你理解了這個過程,那麼恭喜你,你已經基本理解了generator
的運行機制,我這麼比喻雖然有些過程不是很貼切,但基本是這麼個思路。更多的東西仍是向下看吧。前端
在用generator
以前,咱們首先要了解它的語法。在上面也看到過,它跟函數聲明很像,但後面有多了個*
號,就是function *foo() { }
,固然也能夠這麼寫function* foo() { }
。這裏兩種寫法沒有任何區別,全看我的習慣,這篇文章裏我會用第一種語法。如今咱們按這種語法聲明一個generator
函數,供後面使用。java
function *foo() {
}
複製代碼
到目前爲止,咱們還什麼也幹不了,由於咱們還缺乏了一個重要的老夥計yield
。yield
翻譯成漢語是產生的意思。yield
會讓咱們跟在後面的表達式執行,而後交出本身的控制權,停在這裏,直到咱們調用next()
纔會繼續向下執行。這裏新出現了next
咱們先跳過,先說說generator
怎麼執行。先看一個例子。git
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
函數的返回值嗎,帶着這個疑問,咱們來看下面一部分。es6
next
能夠傳入一個參數,來做爲上一次yield
的表達式的返回值,就像咱們上面說的it.next(2)
會讓a
等於2
。固然第一次執行next
也能夠傳入一個參數,但因爲它沒有上一次yield
因此沒有任何東西可以接受它,會被忽略掉,因此沒有什麼意義。github
在這部分咱們說說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));
複製代碼
執行結果:api
{ 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));
複製代碼
執行結果:promise
{ 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
的語法,這些都是基礎,下面咱們來看點不同的東西,老規矩先帶着問題才能更有目的性的看,這裏先提出幾個問題:
進行下面全部的部分以前咱們先說一說迭代器,看到如今,咱們都知道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~。