從C#到TypeScript - Generator
上篇講了Promise
,Promise
的執行需要不停的調用then
,雖然比callback要好些,但也顯得累贅。所以ES6里添加了Generator
來做流程控制,可以更直觀的執行Promise,但終級方案還是ES7議案中的async await
。
當然async await
本質上也還是Generator
,可以算是Generator
的語法糖。
所以這篇先來看下Generator.
Generator語法
先來看個例子:
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());
先看下和普通函數的區別,function
后面多了一個*
,變成了function*
,函數體用到了yield
,這個大家比較熟悉,C#也有,返回可枚舉集合有時會用到。
在ES6里yield
同樣表示返回一個迭代器,所以用到的時候會用next()
來順序執行返回的迭代器函數。
上面代碼返回的結果如下:
{ value: 'id', done: false } { value: '123', done: false } { value: 'finish', done: true }
可以看到next()
的結果是一個對象,value
表示yield
的結果,done
表示是否真正執行完。
所以看到最后return了finish
時done
就變成true了,如果這時再繼續執行next()
得到的結果是{ value: undefined, done: true }
.
Generator原理和使用
Generator
其實是ES6對協程的一種實現,即在函數執行過程中允許保存上下文同時暫停執行當前函數轉而去執行其他代碼,過段時間后達到條件時繼續以上下文執行函數后面內容。
所謂協程其實可以看做是比線程更小的執行單位,一個線程可以有多個協程,協程也會有自己的調用棧,不過一個線程里同一時間只能有一個協程在執行。
而且線程是資源搶占式的,而協程則是合作式的,怎樣執行是由協程自己決定。
由於JavaScript是單線程語言,本身就是一個不停循環的執行器,所以它的協程是比較簡單的,線程和協程關系是 1:N。
同樣是基於協程goroutine的go語言實現的是 M:N,要同時協調多個線程和協程,復雜得多。
在Generator
中碰到yield
時會暫停執行后面代碼,碰到有next()
時再繼續執行下面部分。
當函數符合Generator
語法時,直接執行時返回的不是一個確切的結果,而是一個函數迭代器,因此也可以用for...of
來遍歷,遍歷時碰到結果done
為true則停止。
function* getAsync(id: string){ yield 'id'; yield id; return 'finish'; } let p = getAsync('123'); for(let id of p){ console.info(id); }
打印的結果是:
id 123
因為最后一個finish
的done
是true,所以for...of
停止遍歷,最后一個就不會打印出來。
另外,Generator
的next()
是可以帶參數的,
function* calc(num: number){ let count = yield 1 + num; return count + 1; } let p = calc(2); console.info(p.next().value); // 3 console.info(p.next().value); // NaN //console.info(p.next(3).value); // 4
上面的代碼第一個輸出是yield 1 + num
的結果,yield 1
返回1,加上傳進來的2,結果是3.
繼續輸出第二個,按正常想法,應該輸出3,但是由於yield 1
是上一輪計算的,這輪碰到上一輪的yield
時返回的總是undefined
。
這就導致yield 1
返回undefined
,undefined + num返回的是NaN
,count + 1也還是NaN,所以輸出是NaN
。
注釋掉第二個,使用第三個就可以返回預期的值,第三個把上一次的結果3用next(3)傳進去,所以可以得到正確結果。
如果想一次調用所有,可以用這次方式來遞歸調用:
let curr = p.next(); while(!curr.done){ console.info(curr.value); curr = p.next(curr.value); } console.info(curr.value); // 最終結果
Generator
可以配合Promise
來更直觀的完成異步操作。
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
這個函數來看,從上到下執行是很好理解的,先輸出'start',等待2秒,再輸出'finish'。
只是執行時需要不停的使用then
,好在TJ大神寫了CO模塊,可以方便的執行這種函數,把Generator
函數傳給co
即可。
co(run).then(()=>console.info('success'));
co的實現原理可以看下它的核心代碼:
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(); //最主要就是這個函數,遞歸執行next()和then() function onFulfilled(res) { var ret; try { ret = gen.next(res); // next(), res是上一輪的結果 } catch (e) { return reject(e); } next(ret); // 里面調用then,並再次調用onFulfilled()實現遞歸 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的話表示完成,結束遞歸 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的核心代碼和我上面寫的遞歸調用Generator
函數的本質是一樣的,不斷調用下一個Promise,直到done
為true。
縱使有co這個庫,但是使用起來還是略有不爽,下篇就輪到async await
出場,前面這兩篇都是為了更好的理解下一篇。