介紹
Generator 函數(shù)是 ES6 提供的一種異步編程解決方案,語法行為與傳統(tǒng)函數(shù)完全不同
生成器對象是由一個 generator function 返回的,并且它符合可迭代協(xié)議和迭代器協(xié)議。
generator和函數(shù)不同的是,generator由function*定義(注意多出的*號),并且,除了return語句,還可以用yield返回多次。
回顧下上文提到的解決異步的手段:
- 回調(diào)函數(shù)
- promise
那么,上文我們提到promsie已經(jīng)是一種比較流行的解決異步方案,那么為什么還出現(xiàn)Generator?甚至async/await呢?
該問題我們留在后面再進行分析,下面先認識下Generator
Generator函數(shù)
執(zhí)行 Generator 函數(shù)會返回一個遍歷器對象,可以依次遍歷 Generator 函數(shù)內(nèi)部的每一個狀態(tài)
形式上,Generator函數(shù)是一個普通函數(shù),但是有兩個特征:
- function關(guān)鍵字與函數(shù)名之間有一個星號
- 函數(shù)體內(nèi)部使用yield表達式,定義不同的內(nèi)部狀態(tài)
function* helloWorldGenerator() { yield ‘hello’; yield ‘world’; return ‘ending’;}
函數(shù)只能返回一次,但是,如果換成generator,就可以一次返回一個數(shù),不斷返回多次。
什么是生成器?
生成器是在函數(shù)內(nèi)部運行的一些代碼
- 返回值后,它會自行暫停,并且——
- 調(diào)用程序可以要求取消暫停并返回另一個值
這種“返回”不是傳統(tǒng)的從函數(shù) return。所以它被賦予了一個特殊的名稱——yield。
生成器語法因語言而異。Javascript 的生成器語法類似于 PHP,但是區(qū)別也很大,如果你希望它們的作用相同,那么最終你會感到非常困惑。
在 javascript 中,如果想要使用生成器,則需要:
- 定義特殊的生成器函數(shù)
- 調(diào)用該函數(shù)創(chuàng)建一個生成器對象
- 在循環(huán)中使用該生成器對象,或直接調(diào)用其 next 方法
方法
Generator.prototype.next()返回一個由 yield表達式生成的值。
Generator.prototype.return()返回給定的值并結(jié)束生成器。
Generator.prototype.throw()向生成器拋出一個錯誤。
使用
Generator 函數(shù)會返回一個遍歷器對象,即具有Symbol.iterator屬性,并且返回給自己
function* gen(){ // some code}var g = gen();g[Symbol.iterator]() === g// true
通過yield關(guān)鍵字可以暫停generator函數(shù)返回的遍歷器對象的狀態(tài)
function* helloWorldGenerator() { yield ‘hello’; yield ‘world’; return ‘ending’;}var hw = helloWorldGenerator();
上述存在三個狀態(tài):hello、world、return
通過next方法才會遍歷到下一個內(nèi)部狀態(tài),其運行邏輯如下:
- 遇到y(tǒng)ield表達式,就暫停執(zhí)行后面的操作,并將緊跟在yield后面的那個表達式的值,作為返回的對象的value屬性值。
- 下一次調(diào)用next方法時,再繼續(xù)往下執(zhí)行,直到遇到下一個yield表達式
- 如果沒有再遇到新的yield表達式,就一直運行到函數(shù)結(jié)束,直到return語句為止,并將return語句后面的表達式的值,作為返回的對象的value屬性值。
- 如果該函數(shù)沒有return語句,則返回的對象的value屬性值為undefined
hw.next()// { value: ‘hello’, done: false }hw.next()// { value: ‘world’, done: false }hw.next()// { value: ‘ending’, done: true }hw.next()// { value: undefined, done: true }
done用來判斷是否存在下個狀態(tài),value對應狀態(tài)值
yield表達式本身沒有返回值,或者說總是返回undefined
通過調(diào)用next方法可以帶一個參數(shù),該參數(shù)就會被當作上一個yield表達式的返回值
function* foo(x) { var y = 2 * (yield (x + 1)); var z = yield (y / 3); return (x + y + z);}var a = foo(5);a.next() // Object{value:6, done:false}a.next() // Object{value:NaN, done:false}a.next() // Object{value:NaN, done:true}var b = foo(5);b.next() // { value:6, done:false }b.next(12) // { value:8, done:false }b.next(13) // { value:42, done:true }
正因為Generator函數(shù)返回Iterator對象,因此我們還可以通過for…of進行遍歷
function* foo() { yield 1; yield 2; yield 3; yield 4; yield 5; return 6;}for (let v of foo()) { console.log(v);}// 1 2 3 4 5
原生對象沒有遍歷接口,通過Generator函數(shù)為它加上這個接口,就能使用for…of進行遍歷了
function* objectEntries(obj) { let propKeys = Reflect.ownKeys(obj); for (let propKey of propKeys) { yield [propKey, obj[propKey]]; }}let jane = { first: ‘Jane’, last: ‘Doe’ };for (let [key, value] of objectEntries(jane)) { console.log(`${key}: ${value}`);}// first: Jane// last: Doe
異步解決方案
回顧之前展開異步解決的方案:
- 回調(diào)函數(shù)
- Promise 對象
- generator 函數(shù)
- async/await
這里通過文件讀取案例,將幾種解決異步的方案進行一個比較:
回調(diào)函數(shù)
所謂回調(diào)函數(shù),就是把任務的第二段單獨寫在一個函數(shù)里面,等到重新執(zhí)行這個任務的時候,再調(diào)用這個函數(shù)
fs.readFile(‘/etc/fstab’, function (err, data) { if (err) throw err; console.log(data); fs.readFile(‘/etc/shells’, function (err, data) { if (err) throw err; console.log(data); });});
readFile函數(shù)的第三個參數(shù),就是回調(diào)函數(shù),等到操作系統(tǒng)返回了/etc/passwd這個文件以后,回調(diào)函數(shù)才會執(zhí)行
傳統(tǒng)的生成器對象
Firefox (SpiderMonkey) 在 JavaScript 1.7 中也實現(xiàn)了一個較早版本的生成器,其中函數(shù)聲明中的星號(*)不是必需的 (只需在函數(shù)體中使用yield 關(guān)鍵字)。但是,舊式生成器已棄用。不要使用它們;他們將被刪除 (bug 1083482)。
傳統(tǒng)的生成器方法
Generator.prototype.next() Non-Standard返回 yield 表達式產(chǎn)生的值。與 ES2015 生成器對象的 next() 方法對應。
Generator.prototype.close() Non-Standard關(guān)閉生成器,因此執(zhí)行該函數(shù)后調(diào)用next() 函數(shù)時將會拋出 StopIteration 錯誤。與 ES2015 生成器對象的 return() 方法對應..
Generator.prototype.send() Non-Standard用于將值發(fā)送到生成器。 該值由 yield 表達式返回,并且返回下一個 yield 表達式產(chǎn)生的值。send(x) 對應于 ES2015 生成器對象中的 next(x)
Generator.prototype.throw() Non-Standard向生成器拋出錯誤。與 ES2015 生成器對象的 throw() 方法對應。
generator函數(shù)中的this問題
// 正常函數(shù)function F () { this.a = 1}const f = new F()// 調(diào)用構(gòu)造函數(shù)F,返回實例對象f// 將構(gòu)造函數(shù)內(nèi)部中的this指向這個實例對象// 將構(gòu)造函數(shù)中的原型對象賦值給實例對象的原型// 執(zhí)行構(gòu)造函數(shù)中的代碼// 調(diào)用Generator函數(shù)會返回遍歷器對象,而不是實例對象,因此無法獲取到this指向的實例對象上的私有屬性和方法。但是這個遍歷器對象可以繼承Generator函數(shù)的prototype原型對象上的屬性和方法(公有屬性和方法)。function* g() {}g.prototype.hello = function () { return ‘hi!’;};let obj = g();obj instanceof g // trueobj.hello() // ‘hi!’
上面代碼表明,Generator 函數(shù)g返回的遍歷器obj,是g的實例,而且繼承了g.prototype。但是,如果把g當作普通的構(gòu)造函數(shù),并不會生效,因為g返回的總是遍歷器對象,而不是this對象。
應用generator 數(shù)組扁平化
function* flatten3(arr) { let length = arr.length; for (let i=0; i<length; i++) { let item = arr[i]; if (Array.isArray(item)) { yield* flatten3(item); } else { yield item; } }} let res = [];for (let f of flatten3 (arr)) { res.push(f);}console.log(res)
迭代方法
function flatten2(arr) { const stack = […arr]; const res = []; while (stack.length) { // 從棧里取出 const next = stack.pop(); if (Array.isArray(next)) { // 把next扁平化,然后放入stack中 stack.push(…next); } else { res.push(next); } } // reverse to restore input order return res.reverse();}console.log(flatten2(arr))
Promise
Promise就是為了解決回調(diào)地獄而產(chǎn)生的,將回調(diào)函數(shù)的嵌套,改成鏈式調(diào)用
const fs = require(‘fs’);const readFile = function (fileName) { return new Promise(function (resolve, reject) { fs.readFile(fileName, function(error, data) { if (error) return reject(error); resolve(data); }); });};readFile(‘/etc/fstab’).then(data =>{ console.log(data) return readFile(‘/etc/shells’)}).then(data => { console.log(data)})
這種鏈式操作形式,使異步任務的兩段執(zhí)行更清楚了,但是也存在了很明顯的問題,代碼變得冗雜了,語義化并不強
generator
yield表達式可以暫停函數(shù)執(zhí)行,next方法用于恢復函數(shù)執(zhí)行,這使得Generator函數(shù)非常適合將異步任務同步化
const gen = function* () { const f1 = yield readFile(‘/etc/fstab’); const f2 = yield readFile(‘/etc/shells’); console.log(f1.toString()); console.log(f2.toString());};
async/await
將上面Generator函數(shù)改成async/await形式,更為簡潔,語義化更強了
const asyncReadFile = async function () { const f1 = await readFile(‘/etc/fstab’); const f2 = await readFile(‘/etc/shells’); console.log(f1.toString()); console.log(f2.toString());};
區(qū)別:
通過上述代碼進行分析,將promise、Generator、async/await進行比較:
- promise和async/await是專門用于處理異步操作的
- Generator并不是為異步而設(shè)計出來的,它還有其他功能(對象迭代、控制輸出、部署Interator接口…)
- promise編寫代碼相比Generator、async更為復雜化,且可讀性也稍差
- Generator、async需要與promise對象搭配處理異步情況
- async實質(zhì)是Generator的語法糖,相當于會自動執(zhí)行Generator函數(shù)
- async使用上更為簡潔,將異步代碼以同步的形式進行編寫,是處理異步編程的最終方案
使用場景
Generator是異步解決的一種方案,最大特點則是將異步操作同步化表達出來
function* loadUI() { showLoadingScreen(); yield loadUIDataAsynchronously(); hideLoadingScreen();}var loader = loadUI();// 加載UIloader.next()// 卸載UIloader.next()
包括redux-saga中間件也充分利用了Generator特性
import { call, put, takeEvery, takeLatest } from ‘redux-saga/effects’import Api from ‘…’function* fetchUser(action) { try { const user = yield call(Api.fetchUser, action.payload.userId); yield put({type: “USER_FETCH_SUCCEEDED”, user: user}); } catch (e) { yield put({type: “USER_FETCH_FAILED”, message: e.message}); }}function* mySaga() { yield takeEvery(“USER_FETCH_REQUESTED”, fetchUser);}function* mySaga() { yield takeLatest(“USER_FETCH_REQUESTED”, fetchUser);}export default mySaga;
還能利用Generator函數(shù),在對象上實現(xiàn)Iterator接口
function* iterEntries(obj) { let keys = Object.keys(obj); for (let i=0; i < keys.length; i++) { let key = keys[i]; yield [key, obj[key]]; }}let myObj = { foo: 3, bar: 7 };for (let [key, value] of iterEntries(myObj)) { console.log(key, value);}// foo 3// bar 7
給大家分享我收集整理的各種學習資料,前端小白交流、學習交流,也可以直接問我,我會組織大家一起做項目練習,幫助大家匹配一位學習伙伴互相監(jiān)督學習-下面是學習資料參考。
前端學習交流、自學、學習資料等推薦 – 知乎