從C#到TypeScript - Generator
上篇講了Promise
,Promise
的執(zhí)行需要不停的調(diào)用then
,雖然比callback要好些,但也顯得累贅。所以ES6里添加了Generator
來(lái)做流程控制,可以更直觀(guān)的執(zhí)行Promise,但終級(jí)方案還是ES7議案中的async await
。
當(dāng)然async await
本質(zhì)上也還是Generator
,可以算是Generator
的語(yǔ)法糖。
所以這篇先來(lái)看下Generator.
Generator語(yǔ)法
先來(lái)看個(gè)例子:
function* getAsync(id: string){ yield 'id'; yield id; return 'finish'; }let p = getAsync('123');console.info(p.next()); console.info(p.next());console.info(p.next());
先看下和普通函數(shù)的區(qū)別,function
后面多了一個(gè)*
,變成了function*
,函數(shù)體用到了yield
,這個(gè)大家比較熟悉,C#也有,返回可枚舉集合有時(shí)會(huì)用到。
在ES6里yield
同樣表示返回一個(gè)迭代器,所以用到的時(shí)候會(huì)用next()
來(lái)順序執(zhí)行返回的迭代器函數(shù)。
上面代碼返回的結(jié)果如下:
{ value: 'id', done: false }{ value: '123', done: false }{ value: 'finish', done: true }
可以看到next()
的結(jié)果是一個(gè)對(duì)象,value
表示yield
的結(jié)果,done
表示是否真正執(zhí)行完。
所以看到最后return了finish
時(shí)done
就變成true了,如果這時(shí)再繼續(xù)執(zhí)行next()
得到的結(jié)果是{ value: undefined, done: true }
.
Generator原理和使用
Generator
其實(shí)是ES6對(duì)協(xié)程的一種實(shí)現(xiàn),即在函數(shù)執(zhí)行過(guò)程中允許保存上下文同時(shí)暫停執(zhí)行當(dāng)前函數(shù)轉(zhuǎn)而去執(zhí)行其他代碼,過(guò)段時(shí)間后達(dá)到條件時(shí)繼續(xù)以上下文執(zhí)行函數(shù)后面內(nèi)容。
所謂協(xié)程其實(shí)可以看做是比線(xiàn)程更小的執(zhí)行單位,一個(gè)線(xiàn)程可以有多個(gè)協(xié)程,協(xié)程也會(huì)有自己的調(diào)用棧,不過(guò)一個(gè)線(xiàn)程里同一時(shí)間只能有一個(gè)協(xié)程在執(zhí)行。
而且線(xiàn)程是資源搶占式的,而協(xié)程則是合作式的,怎樣執(zhí)行是由協(xié)程自己決定。
由于JavaScript是單線(xiàn)程語(yǔ)言,本身就是一個(gè)不停循環(huán)的執(zhí)行器,所以它的協(xié)程是比較簡(jiǎn)單的,線(xiàn)程和協(xié)程關(guān)系是 1:N。
同樣是基于協(xié)程goroutine的go語(yǔ)言實(shí)現(xiàn)的是 M:N,要同時(shí)協(xié)調(diào)多個(gè)線(xiàn)程和協(xié)程,復(fù)雜得多。
在Generator
中碰到yield
時(shí)會(huì)暫停執(zhí)行后面代碼,碰到有next()
時(shí)再繼續(xù)執(zhí)行下面部分。
當(dāng)函數(shù)符合Generator
語(yǔ)法時(shí),直接執(zhí)行時(shí)返回的不是一個(gè)確切的結(jié)果,而是一個(gè)函數(shù)迭代器,因此也可以用for...of
來(lái)遍歷,遍歷時(shí)碰到結(jié)果done
為true則停止。
function* getAsync(id: string){ yield 'id'; yield id; return 'finish'; }let p = getAsync('123');for(let id of p){ console.info(id); }
打印的結(jié)果是:
id123
因?yàn)樽詈笠粋€(gè)finish
的done
是true,所以for...of
停止遍歷,最后一個(gè)就不會(huì)打印出來(lái)。
另外,Generator
的next()
是可以帶參數(shù)的,
function* calc(num: number){ let count = yield 1 + num; return count + 1; }let p = calc(2);console.info(p.next().value); // 3console.info(p.next().value); // NaN//console.info(p.next(3).value); // 4
上面的代碼第一個(gè)輸出是yield 1 + num
的結(jié)果,yield 1
返回1,加上傳進(jìn)來(lái)的2,結(jié)果是3.
繼續(xù)輸出第二個(gè),按正常想法,應(yīng)該輸出3,但是由于yield 1
是上一輪計(jì)算的,這輪碰到上一輪的yield
時(shí)返回的總是undefined
。
這就導(dǎo)致yield 1
返回undefined
,undefined + num返回的是NaN
,count + 1也還是NaN,所以輸出是NaN
。
注釋掉第二個(gè),使用第三個(gè)就可以返回預(yù)期的值,第三個(gè)把上一次的結(jié)果3用next(3)傳進(jìn)去,所以可以得到正確結(jié)果。
如果想一次調(diào)用所有,可以用這次方式來(lái)遞歸調(diào)用:
let curr = p.next();while(!curr.done){ console.info(curr.value); curr = p.next(curr.value); }console.info(curr.value); // 最終結(jié)果
Generator
可以配合Promise
來(lái)更直觀(guān)的完成異步操作。
function delay(): Promise<void>{ return new Promise<void>((resolve, reject)=>{setTimeout(()=>resolve(), 2000)}); }function* run(){ console.info('start'); yield delay(); console.info('finish'); }let generator = run(); generator.next().value.then(()=>generator.next());
就run
這個(gè)函數(shù)來(lái)看,從上到下執(zhí)行是很好理解的,先輸出'start',等待2秒,再輸出'finish'。
只是執(zhí)行時(shí)需要不停的使用then
,好在TJ大神寫(xiě)了CO模塊,可以方便的執(zhí)行這種函數(shù),把Generator
函數(shù)傳給co
即可。
co(run).then(()=>console.info('success'));
co的實(shí)現(xiàn)原理可以看下它的核心代碼:
function co(gen) { var ctx = this; var args = slice.call(arguments, 1); return new Promise(function(resolve, reject) { if (typeof gen === 'function') gen = gen.apply(ctx, args); if (!gen || typeof gen.next !== 'function') return resolve(gen); onFulfilled(); //最主要就是這個(gè)函數(shù),遞歸執(zhí)行next()和then() function onFulfilled(res) { var ret; try { ret = gen.next(res); // next(), res是上一輪的結(jié)果 } catch (e) { return reject(e); } next(ret); // 里面調(diào)用then,并再次調(diào)用onFulfilled()實(shí)現(xiàn)遞歸 return null; } function onRejected(err) { // 處理失敗的情況 var ret; try { ret = gen.throw(err); } catch (e) { return reject(e); } next(ret); } function next(ret) { if (ret.done) return resolve(ret.value); // done是true的話(huà)表示完成,結(jié)束遞歸 var value = toPromise.call(ctx, ret.value); if (value && isPromise(value)) return value.then(onFulfilled, onRejected); //遞歸onFulfilled return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, ' + 'but the following object was passed: "' + String(ret.value) + '"')); } });}
可以看到co的核心代碼和我上面寫(xiě)的遞歸調(diào)用Generator
函數(shù)的本質(zhì)是一樣的,不斷調(diào)用下一個(gè)Promise,直到done
為true。
縱使有co這個(gè)庫(kù),但是使用起來(lái)還是略有不爽,下篇就輪到async await
出場(chǎng),前面這兩篇都是為了更好的理解下一篇。