本文已同步至個人我的主頁。歡迎訪問查看更多內容!若有錯誤或遺漏,歡迎隨時指正探討!謝謝你們的關注與支持!git
Generator
函數是ES6標準中提出的一種異步編程的解決方案。這種函數與普通函數最大的區別在於它能夠暫停執行,又能夠從暫停的位置恢復繼續執行。es6
從語法上看,Generator
函數就是一個狀態機,封裝了許多內部狀態。github
從實質上看,Generator
函數就是一個遍歷器對象生成器。(關於遍歷器對象,能夠參考阮一峯老師的這篇文章)Generator
函數返回一個遍歷器對象,遍歷這個對象,就能夠依次獲得函數內部的每個狀態。web
定義一個Generator函數和定義一個普通函數的區別在於:編程
*
(星號)。yield
來定義每個函數內部的狀態。return
語句,那麼他就是函數內部的最後一個狀態。來看一個簡單的例子:異步
// 定義 function* sayHello() { yield 'hello'; yield 'world'; return 'ending'; } // 調用 // 注意,hw獲取到的值是一個遍歷器對象 let g = sayHello();
上面的例子,定義了一個名爲sayHello
的Generator函數,它內部有兩個yield
表達式和一個return
表達式。因此,該函數內部有三個狀態:hello
,world
和 return
語句(結束執行)。最後,調用這個函數,獲得一個遍歷器對象並賦值給變量g
。async
Generator函數的調用方法與普通函數徹底同樣,函數名()
。不一樣的是:異步編程
Generator函數調用後不會當即執行,那麼,咱們如何讓它開始執行內部的代碼呢?又如何獲取它內部的每個狀態呢?此時,咱們必須調用返回的生成器對象的.next()方法,才能開始代碼的執行,而且使得指針移向下一個狀態。函數
以上面的例子爲例:學習
g.next(); // { value: 'hello', done: false } g.next(); // { value: 'world', done: false } g.next(); // { value: 'ending', done: true } g.next(); // { value: undefined, done: true }
上面的代碼中,一共調用了四次g
這個遍歷器對象的.next()
方法。第一次調用,sayHello
這個Generator函數開始執行,直到遇到第一個yield
表達式就會暫停執行。.next()
方法會返回一個對象,它的value
屬性就是當前yield
表達式的值hello
,done
屬性的值false
,表示遍歷尚未結束。
第二次再調用.next()
,就會執行到第二個yield
表達式處,並暫停執行,返回對應的對象。
第三次調用.next()
,函數執行到最後的return
語句,此時標誌着遍歷器對象g
遍歷結束,因此返回的對象中value
屬性值就是return
後面所跟的值ending
,done
屬性值爲true
,表示遍歷已經結束。
第四次以及後面在調用.next()方法,返回的都會是{value: undefined, done: true }
。
由Generator函數返回的遍歷器對象,只有調用.next()
方法纔會遍歷到下一個內部狀態,因此這實際上是提供了一種能夠暫停執行的函數,yield
表達式就是暫停標誌。
遍歷器對象的.next()
方法的運行邏輯以下。
yield
表達式,就暫停執行後面的操做,並將緊跟在yield
後面的那個表達式的值,做爲返回的對象的value
屬性值。.next()
方法時,再繼續往下執行,直到遇到下一個yield
表達式。yield
表達式,就一直運行到函數結束,直到return
語句爲止,並將return
語句後面的表達式的值,做爲返回的對象的value
屬性值。return
語句,則返回的對象的value
屬性值爲undefined
。值得注意的是:
yield
關鍵字只能出如今Generator函數中,出如今別的函數中會報錯。// 出如今普通函數中,報錯 (function () { yield 'hello'; })() // forEach不是Generator函數,報錯 [1, 2, 3, 4, 5].forEach(val => { yield val });
yield
關鍵字後面跟的表達式,是惰性求值的。 只有當調用.next()
方法、內部狀態暫停到當前yield
時,纔會計算其後面跟的表達式的值。這等於爲JavaScript提供了手動的「惰性求值」的語法功能。function* step() { yield 'step1'; // 下面的yield後面的表達式不會當即求值, // 只有暫停到這一行時,纔會計算表達式的值。 yield 'step' + 2; yield 'setp3'; return 'end'; }
yield
表達式自己是沒有返回值的,或者說它的返回值爲undefined
。使用.next()傳參能夠爲其設置返回值。(後面會講到)function* gen() { for (let i = 0; i < 5; i++) { let res = yield; // yield表達式自己沒有返回值 console.log(res); // undefined } } let g = gen(); g.next(); // {value: 0, done: false} g.next(); // {value: 1, done: false} g.next(); // {value: 2, done: false}
yield與return的異同:
相同點:
不一樣點:
前面咱們說到過,yield
表達式自身沒有返回值,或者說返回值永遠是undefined
。可是,咱們能夠經過給.next()
方法傳入一個參數,來設置上一個(是上一個)yield
表達式返回值。
來看一個例子:
function* conoleNum() { console.log('Started'); console.log(`data: ${yield}`); console.log(`data: ${yield}`); return 'Ending'; } let g = conoleNum(); g.next(); // 控制檯輸出:'Started' g.next('a'); // 控制檯輸出:'data: a' // 不傳入參數'a',就會輸出'data: undefined' g.next('b'); // 控制檯輸出:'data: b' // 不傳入參數'a',就會輸出'data: undefined'
上面的例子,須要強調一個不易理解的地方。
第一次調用.next()
,此時函數暫停在代碼第三行的yield
表達式處。記得嗎?yield
會暫停函數執行,此時打印它的console.log()
,也就是代碼第三行的console,因爲暫停並無被執行,因此不會打印出結果,只輸出了代碼第二行的'Started'。
當第二次調用.next()
方法時,傳入參數'a'
,函數暫停在代碼第四行的yield
語句處。此時參數'a'
會被當作上一個yield
表達式的返回值,也就是代碼第三行的yiled
表達式的返回值,因此此時控制檯輸出'data: a'
。而代碼第四行的console.log()
因爲暫停,沒有被輸出。
第三次調用,同理。因此輸出'data: b'
。
Generator函數返回的遍歷器對象,都有一個.throw()
方法,能夠在函數體外拋出錯誤,而後在Generator函數體內捕獲。
function* gen() { try { yield; } catch (e) { console.log('內部捕獲', e); } }; var g = gen(); // 下面執行一次.next() // 是爲了讓gen函數體執行進入try語句中的yield處 // 這樣拋出錯誤,gen函數內部的catch語句才能捕獲錯誤 g.next(); try { g.throw('a'); g.throw('b'); } catch (e) { console.log('外部捕獲', e); }
上面例子中,遍歷器對象g
在gen
函數體外連續拋出兩個錯誤。第一個錯誤被gen
函數體內的catch
語句捕獲。g
第二次拋出錯誤,因爲gen
函數內部的catch
語句已經執行過了,不會再捕捉到這個錯誤了,因此這個錯誤就會被拋出gen
函數體,被函數體外的catch
語句捕獲。
值得注意的是:
try...catch
代碼塊,那麼遍歷器對象的throw
方法拋出的錯誤,將被外部try...catch
代碼塊捕獲。try...catch
代碼塊,那麼程序將報錯,直接中斷執行。遍歷器對象的throw
方法被捕獲之後,會附帶執行一次.next()
方法,代碼執行會暫停到下一條yield
表達式處。看下面這個例子:
function* gen(){ try { yield console.log('a'); } catch (e) { console.log(e); // 'Error' } yield console.log('b'); yield console.log('c'); } var g = gen(); g.next(); // 控制檯輸出:'a' g.throw('Error'); // 控制檯輸出:'b' // throw的錯誤被內部catch語句捕獲, // 會自動在執行一次g.next() g.next(); // 控制檯輸出:'c'
Generator函數返回的遍歷器對象,還有一個.return()
方法,能夠返回給定的值,而且直接結束對遍歷器對象的遍歷。
function* gen() { yield 1; yield 2; yield 3; } var g = gen(); g.next(); // { value: 1, done: false } // 提早結束對g的遍歷。儘管yield尚未執行完 // 此時done屬性值爲true,說明遍歷結束 g.return('foo'); // { value: "foo", done: true } g.next(); // { value: undefined, done: true }
若是.return()
方法調用時,不提供參數,則返回值的value
屬性爲undefined
。
yield*
用來在一個Generator函數裏面執行另外一個Generator函數。
若是在一個Generator函數內部,直接調用另外一個Generator函數,默認狀況下是沒有效果的。
function* gen1() { yield 'a'; yield 'b'; } function* gen2() { yield 'x'; // 直接調用gen1() gen1(); yield 'y'; } // 遍歷器對象可使用for...of遍歷全部狀態 for (let v of gen2()){ 只輸出了gen1的狀態 console.log(v); // 'x' 'y' }
上面的例子中,gen1
和gen2
都是Generator函數,在gen2
裏面直接調用gen1
,是不會有效果的。
這個就須要用到 yield*
表達式。
function* gen1() { yield 'a'; yield 'b'; } function* gen2() { yield 'x'; // 用 yield* 調用gen1() yield* gen1(); yield 'y'; } for (let v of gen2()){ 輸出了gen一、gen2的狀態 console.log(v); // 'x' 'a' 'b' 'y' }
本文主要講解Generator函數的基本語法和一些細節,Generator函數的定義、yield
表達式、.next()
方法及傳參、.throw()
方法、.return()
方法以及 yield*
表達式。
文章開頭講到,Generator函數時ES6提出的異步編程的一種解決方案。在實際應用中,通常在yield
關鍵字後面會跟隨一個異步操做,當異步操做成功返回後調用.next()
方法,將異步流程交給下一個yield表達式。具體關於Generator函數的異步應用,你們能夠參考阮一峯老師的這篇文章,或參考其餘網上資料,繼續深刻學習。