原文:JavaScript中的異步編程和CPS
在這篇博文中挠唆,我們將基于回調(diào)的異步編程風(fēng)格的稱為:延續(xù)傳遞風(fēng)格(CPS)。 我們將解釋CPS的工作原理并提供一些使用的小提示造虏。
1、異步編程與回調(diào)函數(shù)
如果你曾經(jīng)用javascript編寫異步程序,你肯定注意到與編寫同步程序有很大的差別:調(diào)用一個(gè)異步函數(shù)的時(shí)候缓屠,我們不再等待函數(shù)的返回值,取而代之的是將一個(gè)回調(diào)函數(shù)作為參數(shù)傳遞給它护侮。例如請(qǐng)看下面同步程序片段:
function loadAvatarImage(id) {
var profile = loadProfile(id);
return loadImage(profile.avatarUrl);
}
在這里加載profile的操作可能非常耗時(shí)敌完,最好是將其異步化。例如給 loadProfile 傳遞一個(gè)額外的回調(diào)函數(shù)羊初。loadProfile立即返回滨溉,然后你就可以去處理其它事情了什湘。等到profile加載完成,用它作參數(shù)調(diào)用你傳遞給 loadProfile 的回調(diào)函數(shù)晦攒,然后你就可以在回調(diào)函數(shù)中執(zhí)行后續(xù)的處理闽撤,加載image。這就產(chǎn)生了一種基于回調(diào)的異步編程風(fēng)格:
function loadAvatarImage(id, callback) {
loadProfile(id, function (profile) {
loadImage(profile.avatarUrl, callback);
});
}
這種異步編程風(fēng)格被稱為continuation-passing style(CPS)脯颜,同步編程被成為直接風(fēng)格哟旗。CPS得名于你總是用一個(gè)回調(diào)函數(shù)作為參數(shù)去調(diào)用目標(biāo)函數(shù)(譯注,回調(diào)函數(shù)可以看成是控制流栋操,CPS即將控制流顯式作為參數(shù)傳遞的編程風(fēng)格)闸餐。回調(diào)函數(shù)延續(xù)了控制流程矾芙。正因如此回調(diào)函數(shù)通常被稱之為 continuation,尤其是在函數(shù)式程序語言中舍沙。CPS的主要問題是其具有傳染性,要么完全不用要么全都使用這種風(fēng)格:loadAvatarImage在內(nèi)部使用了CPS,但它無法將這個(gè)事實(shí)隱藏在外部蠕啄,loadAvatarImage的調(diào)用者也必須使用CPS场勤。
2、CPS轉(zhuǎn)化
本節(jié)將展示一些用于將普通風(fēng)格的代碼轉(zhuǎn)化成continuation-passing style風(fēng)格的技術(shù)歼跟。
2.1. 函數(shù)調(diào)用序列
通常和媳,函數(shù)的調(diào)用鏈很容易會(huì)形成一個(gè)序列。如先前的示例哈街,十分復(fù)雜的函數(shù)嵌套留瞳,這可以通過函數(shù)聲明來避免:
function loadAvatarImage(id, callback) {
loadProfile(id, loadProfileAvatarImage); // (*)
function loadProfileAvatarImage(profile) {
loadImage(profile.avatarUrl, callback);
}
}
JavaScript提升函數(shù)loadProfileAvatar(將其移動(dòng)到函數(shù)的開頭)。 因此骚秦,它可以在(*)處調(diào)用她倘。我們?cè)?loadAvatarImage內(nèi)部定義loadProfileAvatarImage,因?yàn)閘oadProfileAvatarImage需要使用參數(shù)callback(譯注,loadAvatarImage與其可訪問的外部變量一起形成閉包)作箍。 只要函數(shù)調(diào)用之間有共享狀態(tài)硬梁,您就會(huì)看到這種嵌套。 另一種方法是使用立即調(diào)用的函數(shù)表達(dá)式:
var loadAvatarImage = function () {
var cb;
function loadAvatarImage(id, callback) {
cb = callback;
loadProfile(id, loadProfileAvatarImage);
}
function loadProfileAvatarImage(profile) {
loadImage(profile.avatarUrl, cb);
}
return loadAvatarImage;
}();
2.2. 數(shù)組的遍歷
以下代碼含有一個(gè)簡(jiǎn)單的循環(huán):
function logArray(arr) {
for(var i=0; i < arr.length; i++) {
console.log(arr[i]);
}
console.log("### Done");
}
通過兩個(gè)步驟將上面的代碼轉(zhuǎn)化成CPS風(fēng)格.
首先將迭代轉(zhuǎn)化成遞歸.在函數(shù)式程序設(shè)計(jì)語言中是一種普遍的技術(shù).
function logArray(arr) {
logArrayRec(0, arr);
console.log("### Done");
}
function logArrayRec(index, arr) {
if (index < arr.length) {
console.log(arr[index]);
logArrayRec(index+1, arr);
}
// else: done
}
現(xiàn)在很容易就能把它轉(zhuǎn)化成CPS風(fēng)格.我們通過引入一個(gè)helper函數(shù)functionforEachCps來實(shí)現(xiàn).
function logArray(arr) {
forEachCps(arr, function (elem, index, next) { // (*)
console.log(elem);
next();
}, function () {
console.log("### Done");
});
}
function forEachCps(arr, visitor, done) { // (**)
forEachCpsRec(0, arr, visitor, done)
}
function forEachCpsRec(index, arr, visitor, done) {
if (index < arr.length) {
visitor(arr[index], index, function () {
forEachCpsRec(index+1, arr, visitor, done);
});
} else {
done();
}
}
這里有兩個(gè)有趣的改變:我們向訪問者(在(*)位置)傳遞了一個(gè)continuation函數(shù)next作為參數(shù).這個(gè)函數(shù)用于在forEachCpsRec里面觸發(fā)后續(xù)處理.這讓我們可以在訪問者函數(shù)內(nèi)部執(zhí)行CPS調(diào)用.例如,執(zhí)行一個(gè)異步請(qǐng)求.我們還需要給 forEachCps 提供一個(gè) continuation 參數(shù) done,用于指定循環(huán)結(jié)束之后該做什么.
2.3. 對(duì)數(shù)組映射
我們對(duì)forEachCps稍做調(diào)整就可以獲得一個(gè)Array.Map函數(shù):
function mapCps(arr, func, done) {
mapCpsRec(0, [], arr, func, done)
}
function mapCpsRec(index, outArr, inArr, func, done) {
if (index < inArr.length) {
func(inArr[index], index, function (result) {
mapCpsRec(index+1, outArr.concat(result),
inArr, func, done);
});
} else {
done(outArr);
}
}
mapCps接受一個(gè)數(shù)組作為參數(shù),輸出一個(gè)新的數(shù)組,數(shù)組中每個(gè)元素都用func執(zhí)行了映射操作.上面的mapCps是一個(gè)無副作用版本,每次遞歸都會(huì)創(chuàng)建一個(gè)新的outArr數(shù)組,下面是一個(gè)有副作用(譯注,副作用意味著會(huì)改變一些外部狀態(tài),例如在下面的版本中,每次遞歸調(diào)用都會(huì)改變r(jià)esults和index,而在上面的無副作用版本中則沒有任何狀態(tài)被改變)變體版本:
function mapCps(arrayLike, func, done) {
var index = 0;
var results = [];
mapOne();
function mapOne() {
if (index < arrayLike.length) {
func(arrayLike[index], index, function (result) {
results.push(result);
index++;
mapOne();
});
} else {
done(results);
}
}
}
mapCps可以這樣使用:
function done(result) {
console.log("RESULT: "+result); // RESULT: ONE,TWO,THREE
}
mapCps(["one", "two", "three"],
function (elem, i, callback) {
callback(elem.toUpperCase());
},
done);
變體:并行映射. 順序版本的mapCps在某些情形下不是效率最好的.例如,如果每一次的映射操作都涉及一次向服務(wù)器的請(qǐng)求,發(fā)送一個(gè)請(qǐng)求,等待結(jié)果,發(fā)送另一個(gè)請(qǐng)求,等等.更可取的方案應(yīng)該是發(fā)送所有的請(qǐng)求然后再等待結(jié)果.這種方案需要注意的是,要確保結(jié)果以正確的順序添加到輸出數(shù)組中.以下代碼實(shí)現(xiàn)了并行映射.
function parMapCps(arrayLike, func, done) {
var resultCount = 0;
var resultArray = new Array(arrayLike.length);
for (var i=0; i < arrayLike.length; i++) {
func(arrayLike[i], i, maybeDone.bind(null, i)); // (*)
}
function maybeDone(index, result) {
resultArray[index] = result;
resultCount++;
if (resultCount === arrayLike.length) {
done(resultArray);
}
}
}
在(*)胞得,我們必須復(fù)制循環(huán)變量i的當(dāng)前值荧止。 如果我們不復(fù)制,我們將始終在延續(xù)中獲得i的當(dāng)前值阶剑。 例如跃巡,arrayLike.length,如果在循環(huán)結(jié)束后調(diào)用continuation牧愁。 復(fù)制也可以通過IIFE或使用Array.prototype.forEach而不是for循環(huán)來完成素邪。
2.4. 樹的遍歷
下面的代碼用普通模式遞歸遍歷一棵用嵌套數(shù)組表示的樹
function visitTree(tree, visitor) {
if (Array.isArray(tree)) {
for(var i=0; i < tree.length; i++) {
visitTree(tree[i], visitor);
}
} else {
visitor(tree);
}
}
可以像這樣調(diào)用:
> visitTree([[1,2],[3,4], 5], function (x) { console.log(x) })
1
2
3
4
5
如果需要在visitor中執(zhí)行異步請(qǐng)求,那么必須將visitTree改寫成CPS風(fēng)格:
function visitTree(tree, visitor, done) {
if (Array.isArray(tree)) {
visitNodes(tree, 0, visitor, done);
} else {
visitor(tree, done);
}
}
function visitNodes(nodes, index, visitor, done) {
if (index < nodes.length) {
visitTree(nodes[index], visitor, function () {
visitNodes(nodes, index+1, visitor, done);
});
} else {
done();
}
}
當(dāng)然我們也可以選擇使用forEachCps實(shí)現(xiàn):
function visitTree(tree, visitor, done) {
if (Array.isArray(tree)) {
forEachCps(
tree,
function (subTree, index, next) {
visitTree(subTree, visitor, next);
},
done);
} else {
visitor(tree, done);
}
}
2.5. 陷阱:獲得結(jié)果之后繼續(xù)執(zhí)行
在普通模式下,函數(shù)返回一個(gè)值將使得函數(shù)立即終止:
function abs(n) {
if (n < 0) return -n;
return n; // (*)
}
因此,如果n小于0,注釋()的部分將不會(huì)被執(zhí)行.但是,下面的代碼中,在注釋(*)處CPS風(fēng)格的處理返回值不會(huì)導(dǎo)致函數(shù)的終止:
// Wrong!
function abs(n, success) {
if (n < 0) success(-n); // (**)
success(n);
}
因此,如果 n > 0,那么success(-n)和success(n)都會(huì)執(zhí)行.要修復(fù)這個(gè)問題我們只使用完整的if判斷語句.
function abs(n, success) {
if (n < 0) {
success(-n);
} else {
success(n);
}
}
上面代碼做了適當(dāng)調(diào)整以適應(yīng)CPS風(fēng)格,邏輯控制流經(jīng)由continuation得以繼續(xù)執(zhí)行,而物理控制流沒有(這里的物理控制流指代碼的實(shí)際執(zhí)行路徑,而邏輯控制流是指編程邏輯上的控制流).
3、CPS和控制流
CPS將后續(xù)步驟具體化--將它轉(zhuǎn)化成可由你操控的對(duì)象猪半。在普通風(fēng)格下,一個(gè)函數(shù)無法決定自己調(diào)用返回之后做什么(譯注:因?yàn)榭刂茩?quán)必須返回給調(diào)用者,只有調(diào)用者可以決定之后干什么)兔朦,而對(duì)于CPS風(fēng)格函數(shù),它擁有完全自主權(quán),即發(fā)生了控制反轉(zhuǎn).讓我們更細(xì)致的分析兩種風(fēng)格下控制流的差別偷线。
普通風(fēng)格:調(diào)用函數(shù),函數(shù)返回后將控制交還給調(diào)用者烘绽,一個(gè)函數(shù)無法從已經(jīng)發(fā)生的嵌套調(diào)用中跳出(譯注淋昭,即無法通過非本地跳轉(zhuǎn)從內(nèi)層調(diào)用返回,例如安接,一個(gè)遞歸求積函數(shù),一旦在遞歸調(diào)用的過程中發(fā)現(xiàn)一個(gè)乘數(shù)是0,應(yīng)該可以立即停止遞歸過程返回0,但普通模式的函數(shù)調(diào)用無法實(shí)現(xiàn)這個(gè)目標(biāo)).以下代碼含有兩個(gè)這種類型的函數(shù)調(diào)用:f調(diào)用g翔忽,g調(diào)用h。
function f() {
console.log(g());
}
function g() {
return h();
}
function h() {
return 123;
}
控制流圖:
CPS風(fēng)格:函數(shù)自己決定接下來做什么盏檐。它可以按預(yù)定的控制流執(zhí)行也可以選擇完全不同的執(zhí)行次序(譯注:按傳統(tǒng)模式,函數(shù) a 返回之后是調(diào)用函數(shù) b 還是函數(shù) c 是由 a的調(diào)用者決定的,但在CPS風(fēng)格下,這個(gè)選擇權(quán)被交給了函數(shù) a)歇式。以下代碼是之前示例代碼的CPS版本.
function f() {
g(function (result) {
console.log(result);
});
}
function g(success) {
h(success);
}
function h(success) {
success(123);
}
現(xiàn)在控制流完全變樣了。f 調(diào)用 g胡野,g 調(diào)用 h材失,h調(diào)用g的continuation然后再調(diào)用console.log ,控制流圖如下:
3.1 Return
作為第一個(gè)展示操控控制流威力的例子,請(qǐng)看以下代碼硫豆。之后我會(huì)對(duì)這段代碼做適當(dāng)?shù)恼{(diào)整并添加一個(gè)helper函數(shù)以提供與return等價(jià)的能力龙巨。
function searchArray(arr, searchFor, success, failure) {
forEachCps(arr, function (elem, index, next) {
if (compare(elem, searchFor)) {
success(elem); // (*)
} else {
next();
}
}, failure);
}
function compare(elem, searchFor) {
return (elem.localeCompare(searchFor) === 0);
}
CPS讓我們可以在注釋(*)的地方退出循環(huán).而javascript中的Array.prototype.forEach方法則不行,它需要我們等待循環(huán)的結(jié)束(譯注,這里的循環(huán)是遞歸函數(shù)實(shí)現(xiàn)的).我們也可以將campare改成CPS的形式使得在campare內(nèi)部跳出循環(huán).
function searchArray(arr, searchFor, success, failure) {
forEachCps(arr, function (elem, index, next) {
compareCps(elem, searchFor, success, next);
}, failure);
}
function compareCps(elem, searchFor, success, failure) {
if (elem.localeCompare(searchFor) === 0) {
success(elem);
} else {
failure();
}
}
這令人驚訝,在普通模式下如果要實(shí)現(xiàn)這樣的效果我們只能動(dòng)用異常機(jī)制了.
3.2 try-catch
利用CPS你還可以為語言實(shí)現(xiàn)異常處理機(jī)制.在下面的示例中,我用CPS形式實(shí)現(xiàn)了一個(gè)函數(shù)printDiv.它在內(nèi)部調(diào)用另一個(gè)CPS函數(shù)div,div可以拋出異常.因此,它必須被包裹在tryIt一種我們自己實(shí)現(xiàn)的try-catch機(jī)制的內(nèi)部。
function printDiv(a, b, success, failure) {
tryIt(
function (succ, fail) { // try
div(a, b, function (result) { // might throw
console.log(result);
succ();
}, fail);
},
function (errorMsg, succ, fail) { // catch
handleError(succ, fail); // might throw again
},
success,
failure
);
}
為了使得異常處理得以正常工作,需要為每個(gè)函數(shù)提供額外兩個(gè)continuation作為參數(shù);一個(gè)用以處理正常終止另一個(gè)處理失敗情況.注釋函數(shù)tryIt實(shí)現(xiàn)了try-catch語句的功能.它的第一個(gè)參數(shù)相當(dāng)于try塊,第二個(gè)參數(shù)相當(dāng)于catch塊.而最后兩個(gè)參數(shù)實(shí)際上是用于傳遞給try和catch的.div函數(shù)會(huì)在除數(shù)為0的時(shí)候拋出異常.
function div(dividend, divisor, success, failure) {
if (divisor === 0) {
throwIt("Division by zero", success, failure);
} else {
success(dividend / divisor);
}
}
以下是異常處理的實(shí)現(xiàn).
function tryIt(tryBlock, catchBlock, success, failure) {
tryBlock(
success,
function (errorMsg) {
catchBlock(errorMsg, success, failure);
});
}
function throwIt(errorMsg, success, failure) {
failure(errorMsg);
}
請(qǐng)注意熊响,catch塊的延續(xù)是靜態(tài)確定的旨别,當(dāng)調(diào)用失敗繼續(xù)時(shí),它們不會(huì)傳遞給它汗茄。 它們與完整的tryIt函數(shù)具有相同的延續(xù)秸弛。
3.3 Generator
Generators與ECMAScript.next特性類似,你可能已經(jīng)在Firefox瀏覽器上嘗試過[2].generator是一個(gè)包裝了函數(shù)的對(duì)象.每當(dāng)你調(diào)用一個(gè)generator對(duì)象的next方法,內(nèi)部函數(shù)的執(zhí)行就得以繼續(xù).每當(dāng)在內(nèi)部函數(shù)中執(zhí)行yield value,函數(shù)的執(zhí)行就被掛起,并從generator的next調(diào)用中返回,返回值是value.下面的generator將會(huì)產(chǎn)生一個(gè)無限數(shù)序列0,1,2,...
function* countUp() {
for(let i=0;; i++) {
yield i;
}
}
注意這個(gè)包含無限循環(huán)的函數(shù)外部被generator對(duì)象包裹.循環(huán)的執(zhí)行由next調(diào)用觸發(fā),并且每當(dāng)調(diào)用yield的時(shí)候都會(huì)掛起.以下是在交互式環(huán)境下的輸出.
> let g = countUp();
> g.next()
0
> g.next()
1
如果我們通過CPS來實(shí)現(xiàn)generators,我們會(huì)發(fā)現(xiàn)generator函數(shù)和generator對(duì)象之間的差別越發(fā)明顯.下面我們編寫一個(gè)generator函數(shù)countUpCps.
function countUpCps() {
var i=0;
function nextStep(yieldIt) {
yieldIt(i++, nextStep);
}
return new Generator(nextStep);
}
countUpCps返回一個(gè)generator對(duì)象,這個(gè)對(duì)象的generator函數(shù)以CPS風(fēng)格編寫.下面是使用方式:
var g = countUpCps();
g.next(function (result) {
console.log(result);
g.next(function (result) {
console.log(result);
// etc.
});
});
下面是generator對(duì)象構(gòu)造函數(shù)的實(shí)現(xiàn).
function Generator(genFunc) {
this._genFunc = genFunc;
}
Generator.prototype.next = function (success) {
this._genFunc(function (result, nextGenFunc) {
this._genFunc = nextGenFunc;
success(result);
});
};
注意我是如何將generator函數(shù)的當(dāng)前continuation保存在generator對(duì)象內(nèi)(譯注,nextGenFunc).這樣下次調(diào)用next的時(shí)候就不需要再顯式傳入.
4. CPS和棧
cps另一個(gè)讓你感興趣的方面是,不使用棧,你每次都是調(diào)用另一個(gè)函數(shù)繼續(xù)執(zhí)行從沒使用過return.這意味著如果你的整個(gè)程序完全以CPS風(fēng)格編寫,那么你需要的機(jī)制只是從一個(gè)函數(shù)跳轉(zhuǎn)到另一個(gè)函數(shù),以及創(chuàng)建環(huán)境(用于保存參數(shù)和局部變量).也就是說,CPS風(fēng)格的函數(shù)調(diào)用更像goto語句.讓我們通過一個(gè)示例來展示這點(diǎn),下面的函數(shù)含有一個(gè)for循環(huán):
function f(n) {
var i=0;
for(; i < n; i++) {
if (isFinished(i)) {
break;
}
}
console.log("Stopped at "+i);
}
同樣的函數(shù)用goto實(shí)現(xiàn)如下:
function f(n) {
var i=0;
L0: if (i >= n) goto L1;
if (isFinished(i)) goto L1;
i++;
goto L0;
L1: console.log("Stopped at "+i);
}
CPS版本看上去就相當(dāng)不一樣了:
function f(n) {
var i=0;
L0();
function L0() {
if (i >= n) {
L1();
} else if (isFinished(i)) {
L1();
} else {
i++;
L0();
}
}
function L1() {
console.log("Stopped at "+i);
}
}
4.1 尾調(diào)用
以下代碼使用普通遞歸函數(shù)實(shí)現(xiàn)對(duì)數(shù)組的遍歷:
function logArrayRec(index, arr) {
if (index < arr.length) {
console.log(arr[index]);
logArrayRec(index+1, arr); // (*)
}
// else: done
}
對(duì)一于些編程語言,遞歸會(huì)導(dǎo)致棧增長(zhǎng).但像上面的示例中,我們注意到在注釋(*)的位置,是在對(duì)自身做遞歸調(diào)用,并且這個(gè)調(diào)用是函數(shù)中最后一條語句.因此實(shí)際上我們無需保留棧空間,因?yàn)槲覀儫o論如何都不會(huì)返回到上層函數(shù)去.這種在函數(shù)中最后一條語句執(zhí)行函數(shù)調(diào)用的情況被稱為尾部調(diào)用.幾乎所有的函數(shù)式程序設(shè)計(jì)語言都對(duì)尾部調(diào)用作了優(yōu)化,因此在這樣的語言中通過函數(shù)遞歸實(shí)現(xiàn)迭代結(jié)構(gòu)具有很高的效率.所有真正的CPS函數(shù)調(diào)用都應(yīng)該是尾部調(diào)用的,因此都可以被優(yōu)化.我在前文中提到過這種調(diào)用方式與goto語句類似也暗示了這點(diǎn).
4.2 trampolining
很多函數(shù)式程序語言編譯的中間代碼都具有CPS風(fēng)格,因?yàn)榇蠖鄶?shù)控制結(jié)構(gòu)都可以通過函數(shù)遞歸優(yōu)雅的表達(dá)出來,并且對(duì)尾調(diào)用優(yōu)化也非常簡(jiǎn)單.即便某些語言無法優(yōu)化尾部調(diào)用,還是可以使用一種被稱為trampolining的技術(shù)來避免棧增長(zhǎng).其主要概念就是在函數(shù)中不直接調(diào)用最后一個(gè)continuation(一個(gè)函數(shù)),而是終止當(dāng)前函數(shù),并將continuation返回給trampolining.trampolining只是一段循環(huán)代碼,不斷調(diào)用接收到的continuation(類似遞歸轉(zhuǎn)循環(huán)的方法).這樣就不存在嵌套函數(shù)調(diào)用,因此也不會(huì)導(dǎo)致棧增長(zhǎng).例如,上面的CPS代碼可以被重寫如下以支持被trampolining處理.
function f(n) {
var i=0;
return [L0];
function L0() {
if (i >= n) {
return [L1];
} else if (isFinished(i)) {
return [L1];
} else {
i++;
return [L0];
}
}
function L1() {
console.log("Stopped at "+i);
}
}
在CPS風(fēng)格中所有的函數(shù)調(diào)用都是尾部調(diào)用,我們可以將每條函數(shù)調(diào)用語句
func(arg1, arg2, arg3)
轉(zhuǎn)化成一條return語句
return {func, {arg1, arg2, arg3}}
由trampolining收集返回值然后執(zhí)行正確的函數(shù)調(diào)用
function trampoline(result) {
while(Array.isArray(result)) {
var func = result[0];
var args = (result.length >= 2 ? result[1] : []);
result = func.apply(null, args);
}
}
而我們對(duì)函數(shù)f的要?jiǎng)t需要改下成如下的樣子:
trampoline(f(14))
4.3事件隊(duì)列和trampolining
在瀏覽器和Node.js中,trampolining配合事件隊(duì)列一同使用.如果你有一些連續(xù)的CPS調(diào)用(無需通過事件隊(duì)列獲取異步調(diào)用結(jié)果的調(diào)用),那么你可以將continuation push到事件隊(duì)列中,這樣可以避免棧溢出.因此,以下代碼:
continuation(result);
應(yīng)該改寫成這樣
setTimeout(function () { continuation(result) }, 0);
Node.js中甚至提供了一個(gè)特殊的方法process.nextTick()來完成類似的任務(wù).
process.nextTick(function () { continuation(result) });
5. 結(jié)論
JavaScript的異步編程風(fēng)格非常高效.并且相當(dāng)容易理解.但它正在變得越來越笨重.因此你需要了解更多的背景知識(shí),正因如此我寫了本文介紹continuation-passing style.還有一些技術(shù)手段可以使得CPS風(fēng)格的代碼更容接受,但這超過了本文介紹的內(nèi)容.我將會(huì)在另一篇博客(quick teaser:promises)中介紹.