淺談ES6 Generator函數(shù)的異步應(yīng)用與co模塊的實現(xiàn)原理

一.Generator函數(shù)的概念

????Generator函數(shù)是 ES6 提供的一種異步編程解決方案。前面討論過的Promise對象也是ES6提供的異步解決方案,為什么還要提出Generator呢。
????使用Promise對象處理異步固然有不少優(yōu)勢,尤其是可以將回調(diào)地獄的處理變?yōu)閠hen的鏈?zhǔn)秸{(diào)用。但也不可避免的存在一些缺點,例如經(jīng)過Promise包裝的異步會包含大量的Promise名詞(resolve,reject,then...),可讀性不好。
????其實,異步任務(wù)的最佳處理方式應(yīng)當(dāng)是像操作同步任務(wù)那樣操作異步任務(wù),即異步任務(wù)之后的代碼直接寫在異步下面,而不是寫在回調(diào)函數(shù)或then方法中。Generator 函數(shù)的提出就是為了解決這個問題。如何做到將異步的操作同步化呢。試想,我們?nèi)绻苜x予函數(shù)'暫停'執(zhí)行的功能,即遇到異步任務(wù)時,將當(dāng)前上下文的狀態(tài)暫存起來,等到異步任務(wù)結(jié)束后,拿到異步結(jié)果再繼續(xù)向下執(zhí)行,這樣就能實現(xiàn)上述需求。這就是Generator 函數(shù)的異步處理思想。
????如何能實現(xiàn)函數(shù)的‘暫停'執(zhí)行?這里要引出Iterator接口(遍歷器)的概念

二.Iterator的概念

????Iterator是一種接口,它為不同的數(shù)據(jù)結(jié)構(gòu)提供統(tǒng)一的訪問機(jī)制。任何數(shù)據(jù)結(jié)構(gòu)只要部署了Iterator 接口,就可以完成遍歷操作。
????Iterator可以認(rèn)為是一個指針對象,通過next方法對數(shù)據(jù)結(jié)構(gòu)進(jìn)行遍歷,每次調(diào)用next方法,指針就指向數(shù)組的下一個成員并返回數(shù)據(jù)結(jié)構(gòu)的當(dāng)前成員的信息。該信息是一個對象,包含value和done兩個屬性。其中,value屬性是當(dāng)前成員的值,done屬性是一個布爾值,表示遍歷是否結(jié)束。
????ES6規(guī)定,Iterator 接口部署在數(shù)據(jù)結(jié)構(gòu)的Symbol.iterator屬性,調(diào)用這個接口,就會返回一個遍歷器對象。
下面用數(shù)組為栗子演示

let arr = [1, 2, 3];
// 返回遍歷器對象
let it = arr[Symbol.iterator]();
// 通過next方法遍歷
console.log(it.next()) // { value: 1, done: false }
console.log(it.next()) // { value: 2, done: false }
console.log(it.next()) // { value: 3, done: false }
console.log(it.next()) // { value: undefined, done: true }

????并不是所有的數(shù)據(jù)結(jié)構(gòu)都原生具備 Iterator 接口。ES6中原生具備 Iterator 接口的數(shù)據(jù)結(jié)構(gòu)如下。

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函數(shù)的 arguments 對象
  • NodeList 對象

????Iterator 接口用于for...of循環(huán),也就是說,一個數(shù)據(jù)結(jié)構(gòu)只要部署了Iterator 接口,他就可以被for...of遍歷。反之則無法遍歷(如object)。
????不過,我們可以給沒有原生Iterator 接口的數(shù)據(jù)結(jié)構(gòu)手動部署該接口。具體來說,就是給其添加Symbol.iterator屬性,它是一個函數(shù),調(diào)用該函數(shù),返回遍歷器對象。這樣,我們用for...of對其遍歷時,就會手動調(diào)用我們部署的Iterator 接口。下面演示給object部署Iterator 接口。

const obj = {
  a: 'a',
  b: 'b',
  c: 'c',
  [Symbol.iterator]: function () {
    let keys = Object.keys(this);
    let index = 0;
    return {
      next: function () {
        return index < keys.length
          ? {
              value: this[keys[index++]],
              done: false,
            }
          : {
              value: undefined,
              done: true,
            };
      }.bind(this),
    };
  },
};
for (const it of obj) {
  console.log(it)
}
// a 
// b
// c

????經(jīng)過上面討論我們知道,對于遍歷器,只有執(zhí)行next方法,才會繼續(xù)向下遍歷。Generator 函數(shù)正是利用這一點,實現(xiàn)異步操作的同步化。進(jìn)一步講,執(zhí)行 Generator 函數(shù)會返回一個遍歷器對象。它可以遍歷Generator 函數(shù)內(nèi)部封裝的多個狀態(tài)。下面具體分析。

三.Generator函數(shù)的形式與基本使用

1. Generator 函數(shù)的形式。

Generator函數(shù)有兩個區(qū)別于普通函數(shù)的明顯特征。

  • function關(guān)鍵字與函數(shù)名之間有一個星號。
  • 函數(shù)體內(nèi)部使用yield表達(dá)式劃分不同部狀態(tài)。
function*gen(){
  yield 1
  yield 2
}
// 得到遍歷器對象
let g = gen()
console.log(g.next()) // { value: 1, done: false }
console.log(g.next()) // { value: 2, done: false }
console.log(g.next()) // { value: undefined, done: true }
2. yield表達(dá)式

????通過上面例子我們能看出,yield表達(dá)式就是用來劃分Generator 函數(shù)的各個狀態(tài),他可以理解為函數(shù)暫停的標(biāo)志。當(dāng)執(zhí)行next()方法,遇到y(tǒng)ield表達(dá)式時,就暫停執(zhí)行后面的操作,并將yield表達(dá)式的值作為next方法返回的信息對象的value屬性值。下次調(diào)用next()方法,繼續(xù)執(zhí)行yield表達(dá)式后面的操作。這一點很重要,我們將利用這一點實現(xiàn)像操作同步那樣操作異步。

function*gen(){
  yield 1+2
  yield 2+3
}
// 得到遍歷器對象
let g = gen()
console.log(g.next()) // { value: 3, done: false }
console.log(g.next()) // { value: 5, done: false }
console.log(g.next()) // { value: undefined, done: true }

四.Generator函數(shù)的異步應(yīng)用

????我們已經(jīng)了解了Generator 函數(shù)的基本特點,回到最開始的問題,如何實現(xiàn)異步操作的同步化。我們的需求是在異步操作結(jié)束后,再執(zhí)行后面的操作,而Generator 函數(shù)的特點是只有在執(zhí)行next方法后,函數(shù)從當(dāng)前狀態(tài)變?yōu)橄乱粻顟B(tài)。因此我們只需用yield,將每個異步操作劃為一個狀態(tài),這樣就可以保證遇到異步操作時函數(shù)暫停執(zhí)行。而在每個異步操作結(jié)束的時,調(diào)用next方法,使得函數(shù)繼續(xù)執(zhí)行,這就實現(xiàn)了用同步操作的邏輯來操作異步。
????要實現(xiàn)上述,還需解決兩個問題。

1. 傳遞異步結(jié)果

????我們知道,異步操作之后的處理往往需要異步的返回結(jié)果,那么一個首要問題就是如何將異步返回結(jié)果傳遞出來。
????我們要明確一點,yield表達(dá)式是沒有返回值的(undefinded),也就是說直接使用下面這種方式是行不通的。

function*gen(){
  const res = yield async1()
  yield async2(res)
}

????要傳遞結(jié)果,我們要借助next方法。next方法如果有入?yún)ⅲ搮?shù)會被當(dāng)作上一個yield表達(dá)式的返回值。

function*gen(){
  const res1 =  yield 1
  const res2 = yield res1+1
  yield res2+2
}
// 得到遍歷器對象
let g = gen()
console.log(g.next()) // { value: 1, done: false }
// next方法傳入3 認(rèn)為res1=3 3+1=4
console.log(g.next(3)) // { value: 4, done: false }
console.log(g.next(4)) // { value: 6, done: false }

因此,我們只需將異步的返回結(jié)果傳入next方法即可

2. 異步結(jié)束后調(diào)用next方法

????我們?nèi)粘Ξ惒降奶幚頍o非是回調(diào)函數(shù)和Promsie兩種方式,因此也就有兩種思路解決該問題。

2.1 基于回調(diào)函數(shù)的Generator異步流程處理

????我們只需在異步的回調(diào)函數(shù)中調(diào)用next方法,即可實現(xiàn)異步結(jié)束后繼續(xù)執(zhí)行Generator函數(shù)。

const async1 = () => {
  setTimeout(() => {
    // 執(zhí)行next方法 傳遞異步結(jié)果
    g.next(1);
  });
};
const async2 = (res) => {
  setTimeout(() => {
    console.log(res + " from async1");
    g.next(2);
  });
};
const async3 = (res) => {
  setTimeout(() => {
    console.log(res + " from async2");
  });
};
function* gen() {
  const res1 = yield async1();
  const res2 = yield async2(res1);
  yield async3(res2);
}
// 得到遍歷器對象
let g = gen();
g.next();
//1 from async1
//2 from async2

????上面的代碼基本實現(xiàn)了需求,我們發(fā)現(xiàn)Generator函數(shù)內(nèi)部的異步邏輯處理,如果去掉yield就基本和同步操作一樣了。
????不過,上面代碼的問題也很明顯,我們需要對每個異步的回調(diào)進(jìn)行處理。這樣是很低效的,因為我們發(fā)現(xiàn)在回調(diào)中做的其實是同一件事,即執(zhí)行next方法并傳入異步返回結(jié)果。我們?nèi)绻軐⑦@個過程抽離出來,并自動執(zhí)行。將使得代碼邏輯大為簡化。下面依次解決這兩個問題。

  • 抽取回調(diào)函數(shù)的處理

????如何能將回調(diào)函數(shù)的處理抽離出來?
????以setTimeot函數(shù)為例,它接受兩個參數(shù),分別是回調(diào)函數(shù)和延時時間。而我們希望將這個兩個參數(shù)分開傳入,單獨處理。這里就可以想到前面討論過的柯里化函數(shù)。柯里化函數(shù)可以將接受多個參數(shù)的函數(shù)變換成接受一個單一參數(shù),并返回接受余下參數(shù)的函數(shù)。
????還是以setTimeot函數(shù)為例,如果經(jīng)過柯里化處理,我們可以先傳入延時時間,再向返回的函數(shù)中傳入回調(diào),這就實現(xiàn)了上述需求。像下面這樣

function currying(time) {
  return (cb) => {
    return setTimeout(cb, time);
  };
}
const curryTimeout = currying(500);
curryTimeout(() => {
  console.log("timeOut");
});

????接下來的問題是,在什么地方處理異步回調(diào)。我們知道,next方法返回值的value屬性,就是yield表達(dá)式的執(zhí)行結(jié)果。我們?nèi)绻趛ield后面執(zhí)行經(jīng)過柯里化處理過的異步(如上例中的currying(500)),就會使得next方法返回值的value屬性是一個函數(shù),可以傳入異步的回調(diào)。因此我們只需將回調(diào)函數(shù)傳入next方法的value屬性即可。下面就基于上述對上例進(jìn)行改造。

function currying(time) {
  return (cb) => {
    return setTimeout(cb, time);
  };
}

function* gen() {
  const res1 = yield currying(500);
  const res2 = yield currying(res1);
  yield currying(res2);
}

const g = gen();
g.next().value(() => {
  console.log("async1");
  g.next(500).value(() => {
    console.log("async2");
    g.next(500).value(() => {
      console.log("async3");
    });
  });
});
//每隔0.5秒依次打印async1 async2 async3

????可以看到代碼邏輯清晰了很多。這里還要說明一點,事實上前面所謂的經(jīng)過柯里化處理的異步,就是Thunk 函數(shù)。所謂的Thunk 函數(shù),其實就是一個臨時函數(shù),它可以把一個多參數(shù)函數(shù),替換成一個只接受回調(diào)函數(shù)作為參數(shù)的單參數(shù)函數(shù)。如上例中的curryTimeout函數(shù),它就是一個只接受回調(diào)函數(shù)作為參數(shù)的中間函數(shù),也就是Thunk 函數(shù)。用阮一峰老師的話說就是:任何函數(shù),只要參數(shù)有回調(diào)函數(shù),就能寫成 Thunk 函數(shù)的形式。上面的例子相當(dāng)于手動實現(xiàn)了一個丐版Thunk 函數(shù)轉(zhuǎn)換器,生產(chǎn)環(huán)境中一般使用nodejs的Thunkify模塊,它可以實現(xiàn)Thunk 函數(shù)的轉(zhuǎn)換。
????接下來要做的就是變手動執(zhí)行為自動執(zhí)行。

  • 自動執(zhí)行

????仔細(xì)觀察手動執(zhí)行Generator 函數(shù)的代碼會返現(xiàn),我們做的其實只有一件事,即把同一個回調(diào)傳入next方法的value屬性,而回調(diào)要做的就是執(zhí)行next方法并傳遞異步結(jié)果。
????基于上述,我們可以實現(xiàn)Generator 函數(shù)按照既定邏輯自動執(zhí)行的程序。它只需判斷next方法返回值的done屬性,只要不為true,就一直將回調(diào)傳入next方法的value屬性。
????下面用node.js fs模塊的readFileAPI演示,使用thunkify模塊將異步API轉(zhuǎn)換為Thunk函數(shù)。準(zhǔn)備兩個文本文件,內(nèi)容分別是'對酒當(dāng)歌' '人生幾何'。

const thunkify = require("thunkify");
const fs = require("fs");
const readFileThunk = thunkify(fs.readFile)

function* gen() {
  yield readFileThunk("./text1.txt");
  yield readFileThunk("./text2.txt");
}

function run(fn) {
  const gen = fn();
  function next(err, data) {
    // 錯誤優(yōu)先的回調(diào)
    if (data) console.log(data.toString());
    const res = gen.next(data);
    if (res.done) return;
    res.value(next);
  }
  next();
}
run(gen);
// 對酒當(dāng)歌
// 人生幾何

????可以看到,有了自動執(zhí)行器,我們只管在Generator函數(shù)內(nèi)部處理異步,最后直接把 Generator 函數(shù)傳入run函數(shù)即可,當(dāng)然前提是yield表達(dá)式必須是Thunk函數(shù)。

2.2 基于Promise的Generator異步流程處理

????通過觀察前面實現(xiàn)的基于回調(diào)的Generator函數(shù)自動執(zhí)行器不難看出,自動執(zhí)行的關(guān)鍵其實就是在異步結(jié)束后調(diào)用next方法,讓Generator函數(shù)繼續(xù)執(zhí)行。同樣,利用Promise.then方法也能做到這一點。
????沿用上面例子對其進(jìn)行改造,我們要做的其實很簡單

  • 將readFile函數(shù)包裝為Promise
  • 利用Promise.then方法自動執(zhí)行
const fs = require("fs");
function promisify_readFile(path) {
  return new Promise((resolve, reject) => {
    fs.readFile(path, (err, data) => {
      if (err) reject(err);
      else resolve(data);
    });
  });
}

function* gen() {
  yield promisify_readFile("./text1.txt");
  yield promisify_readFile("./text2.txt");
}

function run(fn) {
  const gen = fn();
  function next(data) {
    if (data) console.log(data.toString());
    const res = gen.next(data);
    if (res.done) return;
    //res.value返回的是Promise,可以通過then方法繼續(xù)執(zhí)行Generator
    res.value.then(next,(r)=>console.log(r))
  }
  next();
}
run(gen);

????至此我們已經(jīng)基本實現(xiàn)了像文章開頭的需求,并實現(xiàn)了自動執(zhí)行,其實這就是著名的co模塊的核心實現(xiàn)原理。

五.co模塊及其實現(xiàn)原理

????co模塊一個著名的用于Generator函數(shù)自動執(zhí)行的模塊。它的使用非常簡單,只需將Generator函數(shù)傳入co,即可自動執(zhí)行。

const co = require("co");
function* gen() {
  const res1 = yield promisify_readFile("./text1.txt");
  console.log(res1.toString())
  const res2 = yield promisify_readFile("./text2.txt");
  console.log(res2.toString())
}
co(gen)
// 對酒當(dāng)歌
// 人生幾何
實現(xiàn)原理

????其實,經(jīng)過上面對Generator函數(shù)自動執(zhí)行的討論我們能夠知道,co模塊核心實現(xiàn)原理就是我們實現(xiàn)的run函數(shù)的擴(kuò)展。具體如下

  • co返回的是Promise 對象,因此要添加一些改變Promise狀態(tài)的邏輯
  • 要確保每一步的返回值都是Promise

下面實現(xiàn)一個丐版的co模塊

function co(gen) {
  return new Promise(function (resolve, reject) {
    gen = gen();
    if (!gen || typeof gen.next !== "function") return resolve(gen);
    function next(data) {
      const res = gen.next(data);
      if (res.done) {
        return resolve(res.value);
      } else {
        // 確保每一步的返回值都是Promise
        const value = Promise.resolve(res.value);
        value.then(next, (r) => reject(r));
      }
    }
    next();
  });
}
// 由于co返回的是Promise,因此可以指定then方法使得
// 在Generator執(zhí)行完成后進(jìn)行一些操作
co(gen).then(()=>console.log('end'))
// 對酒當(dāng)歌
// 人生幾何
// end

????co模塊是async/await關(guān)鍵字的前身,async/await被譽為異步編程的終極解決方案,后面會著重介紹。

參考:https://es6.ruanyifeng.com/#docs/generator-async

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容