從C#到TypeScript - Generator

上篇講了PromisePromise的執(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è)finishdone是true,所以for...of停止遍歷,最后一個(gè)就不會(huì)打印出來(lái)。
另外,Generatornext()是可以帶參數(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),前面這兩篇都是為了更好的理解下一篇。