系列文章 -- ES6筆記系列
接觸過Ajax請求的會遇到過異步調用的問題,為了保證調用順序的正確性,一般我們會在回調函數中調用,也有用到一些新的解決方案如Promise相關的技術。
在異步編程中,還有一種常用的解決方案,它就是Generator生成器函數。顧名思義,它是一個生成器,它也是一個狀態機,內部擁有值及相關的狀態,生成器返回一個迭代器Iterator對象,我們可以通過這個迭代器,手動地遍歷相關的值、狀態,保證正確的執行順序。
一、簡單使用
1. 聲明
Generator的聲明方式類似一般的函數聲明,只是多了個*號,並且一般可以在函數內看到yield關鍵字
function* showWords() { yield 'one'; yield 'two'; return 'three'; } var show = showWords(); show.next() // {done: false, value: "one"} show.next() // {done: false, value: "two"} show.next() // {done: true, value: "three"} show.next() // {done: true, value: undefined}
如上代碼,定義了一個showWords的生成器函數,調用之后返回了一個迭代器對象(即show)
調用next方法后,函數內執行第一條yield語句,輸出當前的狀態done(迭代器是否遍歷完成)以及相應值(一般為yield關鍵字后面的運算結果)
每調用一次next,則執行一次yield語句,並在該處暫停,return完成之后,就退出了生成器函數,后續如果還有yield操作就不再執行了
2. yield和yield*
有時候,我們會看到yield之后跟了一個*號,它是什么,有什么用呢?
類似於生成器前面的*號,yield后面的星號也跟生成器有關,舉個大栗子:
function* showWords() { yield 'one'; yield showNumbers(); return 'three'; } function* showNumbers() { yield 10 + 1; yield 12; } var show = showWords(); show.next() // {done: false, value: "one"} show.next() // {done: false, value: showNumbers} show.next() // {done: true, value: "three"} show.next() // {done: true, value: undefined}
增添了一個生成器函數,我們想在showWords中調用一次,簡單的 yield showNumbers()之后發現並沒有執行函數里面的yield 10+1
因為yield只能原封不動地返回右邊運算后值,但現在的showNumbers()不是一般的函數調用,返回的是迭代器對象
所以換個yield* 讓它自動遍歷進該對象
function* showWords() { yield 'one'; yield* showNumbers(); return 'three'; } function* showNumbers() { yield 10 + 1; yield 12; } var show = showWords(); show.next() // {done: false, value: "one"} show.next() // {done: false, value: 11} show.next() // {done: false, value: 12} show.next() // {done: true, value: "three"}
要注意的是,這yield和yield* 只能在generator函數內部使用,一般的函數內使用會報錯
function showWords() { yield 'one'; // Uncaught SyntaxError: Unexpected string }
雖然換成yield*不會直接報錯,但使用的時候還是會有問題,因為’one'字符串中沒有Iterator接口,沒有yield提供遍歷
function showWords() { yield* 'one'; } var show = showWords(); show.next() // Uncaught ReferenceError: yield is not defined
在爬蟲開發中,我們常常需要請求多個地址,為了保證順序,引入Promise對象和Generator生成器函數,看這個簡單的栗子:
var urls = ['url1', 'url2', 'url3']; function* request(urls) { urls.forEach(function(url) { yield req(url); }); // for (var i = 0, j = urls.length; i < j; ++i) { // yield req(urls[i]); // } } var r = request(urls); r.next(); function req(url) { var p = new Promise(function(resolve, reject) { $.get(url, function(rs) { resolve(rs); }); }); p.then(function() { r.next(); }).catch(function() { }); }
上述代碼中forEach遍歷url數組,匿名函數內部不能使用yield關鍵字,改換成注釋中的for循環就行了
3. next()調用中的傳參
參數值有注入的功能,可改變上一個yield的返回值,如
function* showNumbers() { var one = yield 1; var two = yield 2 * one; yield 3 * two; } var show = showNumbers(); show.next().value // 1 show.next().value // NaN show.next(2).value // 6
第一次調用next之后返回值one為1,但在第二次調用next的時候one其實是undefined的,因為generator不會自動保存相應變量值,我們需要手動的指定,這時two值為NaN,在第三次調用next的時候執行到yield 3 * two,通過傳參將上次yield返回值two設為2,得到結果
另一個栗子:
由於ajax請求涉及到網絡,不好處理,這里用了setTimeout模擬ajax的請求返回,按順序進行,並傳遞每次返回的數據
1 var urls = ['url1', 'url2', 'url3']; 2 3 function* request(urls) { 4 var data; 5 6 for (var i = 0, j = urls.length; i < j; ++i) { 7 data = yield req(urls[i], data); 8 } 9 } 10 11 var r = request(urls); 12 r.next(); 13 14 function log(url, data, cb) { 15 setTimeout(function() { 16 cb(url); 17 }, 1000); 18 19 } 20 21 22 function req(url, data) { 23 var p = new Promise(function(resolve, reject) { 24 log(url, data, function(rs) { 25 if (!rs) { 26 reject(); 27 } else { 28 resolve(rs); 29 } 30 }); 31 }); 32 33 p.then(function(data) { 34 console.log(data); 35 r.next(data); 36 }).catch(function() { 37 38 }); 39 }
達到了按順序請求三個地址的效果,初始直接r.next()無參數,后續通過r.next(data)將data數據傳入
注意代碼的第16行,這里參數用了url變量,是為了和data數據做對比
因為初始next()沒有參數,若是直接將url換成data的話,就會因為promise對象的數據判斷 !rs == undefined 而reject
所以將第16行換成 cb(data || url);
通過模擬的ajax輸出,可了解到next的傳參值,第一次在log輸出的是 url = 'url1'值,后續將data = 'url1'傳入req請求,在log中輸出 data = 'url1'值
4. for...of循環代替.next()
除了使用.next()方法遍歷迭代器對象外,通過ES6提供的新循環方式for...of也可遍歷,但與next不同的是,它會忽略return返回的值,如
function* showNumbers() { yield 1; yield 2; return 3; } var show = showNumbers(); for (var n of show) { console.log(n) // 1 2 }
此外,處理for...of循環,具有調用迭代器接口的方法方式也可遍歷生成器函數,如擴展運算符...的使用
function* showNumbers() { yield 1; yield 2; return 3; } var show = showNumbers(); [...show] // [1, 2, length: 2]
5. 更多使用
更多使用可參考 MDN - Generator