“協(xié)程(coroutine)”于我而言還是比較新的概念,Lua 也是剛接觸不久怔鳖。不過(guò)碰巧這段時(shí)間我又在看 ES6 生成器的文章:
- 【譯】ES6 生成器 - 1. ES6 生成器基礎(chǔ)
- 【譯】ES6 生成器 - 2. 深入理解 ES6 生成器
- 【譯】ES6 生成器 - 3. ES6 生成器異步編程
- 【譯】ES6 生成器 - 4. ES6 生成器與并發(fā)
然后很自然地發(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ì)象包含 value
和 done
兩個(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ò)漏,歡迎指正蜂大。
感謝閱讀闽铐!
更多參考資料: