JS 生成器 vs. Lua 協(xié)程

“協(xié)程(coroutine)”于我而言還是比較新的概念,Lua 也是剛接觸不久怔鳖。不過(guò)碰巧這段時(shí)間我又在看 ES6 生成器的文章:

然后很自然地發(fā)現(xiàn)兩者其實(shí)是相似的東西摹菠。整理盒卸、對(duì)比的過(guò)程,肯定也會(huì)加深自己的理解次氨,所以盡管是初學(xué)蔽介,還是貿(mào)然一試了。

JS 生成器

先來(lái)看一個(gè)簡(jiǎn)單的例子:

function *g() {
  yield 1
  yield 2
  yield 3
}

通過(guò) function* 聲明了一個(gè)生成器函數(shù)煮寡,這是 ES6/ES2015 引入的新的函數(shù)類(lèi)型虹蓄,也是下面主要分析的對(duì)象。

可以看到洲押,函數(shù)體內(nèi)還有新的 yield 關(guān)鍵字武花。yield 這里在函數(shù)執(zhí)行時(shí)會(huì)產(chǎn)生“中斷”,而中斷時(shí)函數(shù)的執(zhí)行環(huán)境(變量等)會(huì)被保存下來(lái)杈帐,然后在某個(gè)時(shí)刻体箕,可以返回中斷的位置繼續(xù)執(zhí)行函數(shù),同時(shí)執(zhí)行環(huán)境被還原挑童。這是我理解的生成器函數(shù)的主要特性累铅。

也就是說(shuō),和一般的函數(shù)不同站叼,生成器函數(shù)的執(zhí)行過(guò)程可能是這樣的:

  • 開(kāi)始執(zhí)行
  • 暫停
  • 繼續(xù)
  • 暫停
  • 繼續(xù)
  • ...
  • 執(zhí)行結(jié)束

而且暫停和繼續(xù)之間娃兽,其他的代碼可以獲得控制權(quán)進(jìn)行執(zhí)行,并且決定在什么時(shí)候繼續(xù)生成器函數(shù)的執(zhí)行尽楔,甚至可以永遠(yuǎn)不繼續(xù)執(zhí)行生成器函數(shù)投储。

我們來(lái)運(yùn)行一個(gè)完整的示例,看下生成器函數(shù)的用法:

var o = g()
console.log(o.next()) // {value: 1, done: false}
console.log(o.next()) // {value: 2, done: false}
console.log(o.next()) // {value: 3, done: false}
console.log(o.next()) // {value: undefined, done: true}

與普通函數(shù)不同阔馋,調(diào)用生成器函數(shù)并不是真正執(zhí)行了函數(shù)玛荞,而是返回了一個(gè)“生成器對(duì)象”。從這一點(diǎn)上來(lái)看呕寝,生成器函數(shù)有點(diǎn)類(lèi)似“構(gòu)造函數(shù)”的感覺(jué)勋眯,每次調(diào)用返回一個(gè)新的對(duì)象。

當(dāng)然下梢,這個(gè)新的對(duì)象也是比較特殊的客蹋,它是一個(gè)迭代器對(duì)象,遵循“iterator”協(xié)議孽江。從迭代的角度讶坯,也可以稱(chēng)之為“迭代器對(duì)象”。

iterator 協(xié)議也是 ES6 新增的岗屏,它要求支持該協(xié)議(或者說(shuō)接口吧闽巩,雖然 JS 中并沒(méi)有接口)的對(duì)象提供一個(gè) next() 方法钧舌,每次調(diào)用時(shí)可以返回一個(gè)結(jié)果對(duì)象。這個(gè)迭代結(jié)果對(duì)象包含 valuedone 兩個(gè)屬性涎跨。在較新版本的 Chrome 控制臺(tái)執(zhí)行下示例代碼,可以看到崭歧,value 是返回的值隅很,done 表示迭代是否執(zhí)行完成。

看到這里率碾,貌似生成器的確和其名字一樣叔营,可以生成一些值,只不過(guò)這些值是斷斷續(xù)續(xù)地返回的所宰,需要其他的代碼主動(dòng)去獲取绒尊。基于這些仔粥,我們可以做一個(gè)能夠持續(xù)不斷返回?cái)?shù)據(jù)的生成器函數(shù):

function *num() {
  var i = 0
  while (true) {
    yield i++
  }
}

有點(diǎn)暴力婴谱,不過(guò)的確可行:

var n = num()
console.log(n.next()) // {value: 0, done: false}
console.log(n.next()) // {value: 1, done: false}
console.log(n.next()) // {value: 2, done: false}

可以一直調(diào)用 n.next() 下去,和前面的 *g() 不同躯泰,這個(gè)生成器函數(shù)貌似不會(huì)主動(dòng)結(jié)束谭羔。

ES6 還引入了 for ... of 語(yǔ)句,專(zhuān)門(mén)用于迭代麦向,例如瘟裸,可以這樣:

for (var i of g()) {
  console.log(i)
}

for (var n of num()) {
  if (n < 10) {
    console.log(n)
  } else {
    break
  }
}

這個(gè)也很容易理解,不多說(shuō)了诵竭。

不過(guò)话告,這還不是有關(guān)生成器的全部,如果僅僅是這樣卵慰,那和后面要介紹的 Lua 的協(xié)程的區(qū)別就太大了沙郭,比人家的能力差得太多。

生成器還支持在生成器函數(shù)內(nèi)外進(jìn)行數(shù)據(jù)傳遞呵燕,來(lái)看一個(gè)例子:

function *query(name) {
  var age = yield getAgeByName(name)
  console.log('name: `' + name + '` age: ' + age)
}

我們把生成器函數(shù) *foo() 作為普通函數(shù)來(lái)看待棠绘,它封裝了一段的邏輯。在運(yùn)行時(shí)再扭,外部傳入一個(gè)名字(name)氧苍,通過(guò)調(diào)用 getAgeByName() 來(lái)獲得名字對(duì)應(yīng)的年齡,然后打印出來(lái)泛范。

我們假設(shè) getAgeByName() 是這樣的:

function getAgeByName(name) {
  var people = [{
    name: 'luobo',
    age: 18
  }, {
    name: 'tang',
    age: 20
  }]
 
  var person = people.find(p => p.name === name)
  return person.age
}

由于生成器函數(shù)與普通函數(shù)的執(zhí)行過(guò)程不同让虐,我們定義一個(gè)執(zhí)行生成器函數(shù)的函數(shù):

function run(g, arg) {
  var o = g(arg)

  next()

  function next() {
    var result = o.next(arg)
    arg = result.value
    if (!result.done) {
      next()
    }
  }
}

函數(shù) run() 用于生成器函數(shù) g,可以傳入一個(gè)初始參數(shù) arg罢荡,之后每次調(diào)用生成器對(duì)象的 next() 時(shí)赡突,會(huì)將上一次調(diào)用的返回值傳入对扶。

yield 暫停生成器函數(shù)執(zhí)行時(shí)惭缰,會(huì)將一個(gè)值返回到生成器外部浪南,而外部程序在調(diào)用生成器對(duì)象的 next() 方法時(shí)可以傳入一個(gè)參數(shù),這個(gè)參數(shù)的值會(huì)作為 yield 表達(dá)式的值使用络凿,然后繼續(xù)執(zhí)行生成器函數(shù)。

下面我們來(lái)實(shí)際執(zhí)行一下上面的 *foo()

run(query, 'luobo') // name: `luobo` age: 18
run(query, 'tang') // name: `tang` age: 20

好像沒(méi)什么了不起昂羡,而且本來(lái)很簡(jiǎn)單的過(guò)程絮记,使用了生成器函數(shù)好像還有點(diǎn)復(fù)雜了。

還是上面的例子虐先,如果我們改變下 getAgeByName() 函數(shù)的實(shí)現(xiàn):

function getAgeByName(name) {
  return fetch('/person?name=' + name).then(res => res.json().age)
}

現(xiàn)在根據(jù)名字查找年齡的過(guò)程是異步的了怨愤,需要向服務(wù)器獲取數(shù)據(jù)。如果是通過(guò)普通函數(shù)實(shí)現(xiàn) *query() 的邏輯蛹批,那我們需要修改函數(shù)的實(shí)現(xiàn)撰洗,因?yàn)橥将@取數(shù)據(jù)和異步是不同的編程方式,通常需要改用回調(diào)函數(shù)般眉。

不過(guò)生成器本身的執(zhí)行就是“異步”的了赵,而且生成器支持?jǐn)?shù)據(jù)傳遞。所以甸赃,借助這個(gè)特性柿汛,我們其實(shí)可以不必修改 *query() 的邏輯,而是在 run() 上做一下處理:

function run(g, arg) {
  var o = g(arg)

  next()

  function next() {
    var result = o.next(arg)
    arg = result.value
    if (!result.done) {
      // 返回值可能是 Promise 對(duì)象
      if (arg && typeof arg.then === 'function') {
        arg.then(val => {
          arg = val
          next()
        })
      } else {
        next()
      }
    }
  }
}

將對(duì)異步狀態(tài)的處理拆分到了與邏輯無(wú)關(guān)的控制函數(shù) run() 中埠对,而具體的邏輯部分(*query())不需要修改代碼络断。

看到這里,是不是有點(diǎn)意思了项玛?當(dāng)然貌笨,或許你早就知道這些了。

稍微總結(jié)一下 JS 生成器吧襟沮。

JS 生成器除了作為“生成器”來(lái)使用锥惋,還可以作為一種改善代碼編寫(xiě)方式的技術(shù),它可以使得我們能夠?qū)懗鲱?lèi)似“同步”執(zhí)行的異步代碼开伏,這樣的代碼畢竟更易讀和維護(hù)膀跌。

有關(guān)生成器的特性,其實(shí)還有很多固灵,本文前面列出的四篇文章中有比較全面的介紹捅伤,這里不再贅述。

下面巫玻,我們來(lái)看 Lua 中的協(xié)程丛忆。

Lua 協(xié)程

先看一個(gè)例子:

co = coroutine.create(function ()
    coroutine.yield(1)
    coroutine.yield(2)
    coroutine.yield(3)
  end)

print(coroutine.resume(co)) --> true    1
print(coroutine.resume(co)) --> true    2
print(coroutine.resume(co)) --> true    3
print(coroutine.resume(co)) --> true
print(coroutine.resume(co)) --> false   cannot resume dead coroutine

這個(gè)例子和 JS 生成器一節(jié)的第一個(gè)例子類(lèi)似祠汇,不過(guò)有一些區(qū)別:

  • Lua 的協(xié)程通過(guò) coroutine 庫(kù)來(lái)創(chuàng)建,coroutine.create() 接收的是普通函數(shù)熄诡,而 JS 則是新增了生成器函數(shù)這一新的函數(shù)類(lèi)型
  • Lua 的協(xié)程對(duì)應(yīng)的是 thread 類(lèi)型的值可很,JS 的生成器函數(shù)調(diào)用后返回的是迭代器對(duì)象
  • Lua 的協(xié)程通過(guò) coroutine.resume(co) 的模式來(lái)執(zhí)行,JS 則是利用迭代器接口的 next() 方法來(lái)執(zhí)行
  • Lua 的協(xié)程內(nèi)部通過(guò) coroutine.yield() 來(lái)產(chǎn)生中斷粮彤,JS 則是通過(guò) yield 關(guān)鍵字
  • Lua 的協(xié)程中斷返回的是一組值根穷,第一個(gè)值表示是否執(zhí)行成功,后續(xù)的值為傳遞的數(shù)據(jù)导坟,JS 函數(shù)只能返回一個(gè)值,所以是通過(guò)對(duì)象來(lái)傳遞狀態(tài)和返回值的
  • Lua 的協(xié)程結(jié)束執(zhí)行后再次調(diào)用圈澈,會(huì)產(chǎn)生異常惫周,JS 不會(huì)

Lua 的 for ... in 也可以對(duì)協(xié)程進(jìn)行迭代,與 JS 類(lèi)似:

co = coroutine.wrap(function ()
    coroutine.yield(1)
    coroutine.yield(2)
    coroutine.yield(3)
  end)

for i in co do
  print(i)
end

不過(guò)區(qū)別在于康栈,由于 Lua 的協(xié)程對(duì)應(yīng)的是 thread 類(lèi)型的值递递,而并非是迭代器,所以這里通過(guò) coroutine.wrap() 將協(xié)程包裝為迭代器返回啥么。

Lua 的協(xié)程也支持進(jìn)行數(shù)據(jù)傳遞登舞,所以在 JS 部分介紹的所謂“同步”的異步模式,在 Lua 中也是可以實(shí)現(xiàn)的悬荣。

不過(guò)還是可以看出菠秒,Lua 的協(xié)程的確是“協(xié)程”,并不是為了實(shí)現(xiàn)“生成器”而設(shè)計(jì)氯迂,只不過(guò)“順便”能夠支持作為生成器來(lái)使用而已践叠。而 JS 生成器則是作為生成器設(shè)計(jì),生成器函數(shù)調(diào)用后返回的就是一個(gè)迭代器嚼蚀,而非像 Lua 那樣的一個(gè)特殊類(lèi)型的值(thread)禁灼。不過(guò)也可以利用 JS 生成器內(nèi)部的“pause-resume”機(jī)制實(shí)現(xiàn)一些編程技巧,這就有點(diǎn)協(xié)程的味道了轿曙。

更深入的分析 Lua 的協(xié)程和 JS 生成器的區(qū)別弄捕,需要對(duì) Lua 的協(xié)程有更深入的理解,不過(guò)我還沒(méi)有這樣的能力导帝。所以守谓,抱歉,目前只能在表面上做些文章舟扎。

總結(jié)

協(xié)程(coroutine)是一個(gè)重要的編程技術(shù)分飞,在許多編程語(yǔ)言中都有體現(xiàn)。

Lua 中的協(xié)程睹限,據(jù)相關(guān)文檔介紹譬猫,是具有較完整的相關(guān)特性的實(shí)現(xiàn)讯檐,而且在 Lua 編程中有廣泛的應(yīng)用,是重要的技術(shù)染服。

JS 的生成器别洪,在 ES6 中才引入,雖然叫做生成器柳刮,不過(guò)的確有些“協(xié)程”的特性挖垛,這也是為什么可以基于這些特性構(gòu)建新的異步編程解決方案,因?yàn)樯善骱瘮?shù)的執(zhí)行具有“暫停-繼續(xù)”的特性秉颗。顯然協(xié)程這一重要的編程技術(shù)被引入到了 JS 中痢毒。

從更高的層面來(lái)講,協(xié)程和多線程是兩種解決“多任務(wù)”編程的技術(shù)蚕甥。多線程使得同一時(shí)刻可以有多個(gè)線程在執(zhí)行哪替,不過(guò)需要在多個(gè)線程間協(xié)調(diào)資源,因?yàn)槎鄠€(gè)線程的執(zhí)行進(jìn)度是“不可控”的菇怀。而協(xié)程則避免了多線程的問(wèn)題凭舶,同一時(shí)刻實(shí)質(zhì)上只有一個(gè)“線程”在執(zhí)行,所以不會(huì)存在資源“搶占”的問(wèn)題爱沟。

不過(guò)在 JS 領(lǐng)域帅霜,貌似不存在技術(shù)選擇的困難,因?yàn)?JS 目前還是“單線程”的呼伸,所以引入?yún)f(xié)程也是很自然的選擇吧身冀。

如有錯(cuò)漏,歡迎指正蜂大。

感謝閱讀闽铐!

更多參考資料:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市奶浦,隨后出現(xiàn)的幾起案子兄墅,更是在濱河造成了極大的恐慌,老刑警劉巖澳叉,帶你破解...
    沈念sama閱讀 211,265評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件隙咸,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡成洗,警方通過(guò)查閱死者的電腦和手機(jī)五督,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)瓶殃,“玉大人充包,你說(shuō)我怎么就攤上這事。” “怎么了基矮?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,852評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵淆储,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我家浇,道長(zhǎng)本砰,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,408評(píng)論 1 283
  • 正文 為了忘掉前任钢悲,我火速辦了婚禮点额,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘莺琳。我一直安慰自己还棱,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布惭等。 她就那樣靜靜地躺著诱贿,像睡著了一般。 火紅的嫁衣襯著肌膚如雪咕缎。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,772評(píng)論 1 290
  • 那天料扰,我揣著相機(jī)與錄音凭豪,去河邊找鬼。 笑死晒杈,一個(gè)胖子當(dāng)著我的面吹牛嫂伞,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播拯钻,決...
    沈念sama閱讀 38,921評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼帖努,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了粪般?” 一聲冷哼從身側(cè)響起拼余,我...
    開(kāi)封第一講書(shū)人閱讀 37,688評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎亩歹,沒(méi)想到半個(gè)月后匙监,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,130評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡小作,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評(píng)論 2 325
  • 正文 我和宋清朗相戀三年亭姥,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片顾稀。...
    茶點(diǎn)故事閱讀 38,617評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡达罗,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出静秆,到底是詐尸還是另有隱情粮揉,我是刑警寧澤巡李,帶...
    沈念sama閱讀 34,276評(píng)論 4 329
  • 正文 年R本政府宣布,位于F島的核電站滔蝉,受9級(jí)特大地震影響击儡,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蝠引,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評(píng)論 3 312
  • 文/蒙蒙 一阳谍、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧螃概,春花似錦矫夯、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,740評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至冒窍,卻和暖如春递沪,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背综液。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,967評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工款慨, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人谬莹。 一個(gè)月前我還...
    沈念sama閱讀 46,315評(píng)論 2 360
  • 正文 我出身青樓檩奠,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親附帽。 傳聞我的和親對(duì)象是個(gè)殘疾皇子埠戳,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容