函數(shù)式編程Continuation總結(jié)

原文: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;
    }

控制流圖:

image

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 ,控制流圖如下:


image.png

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)中介紹.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末洪碳,一起剝皮案震驚了整個(gè)濱河市递览,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌瞳腌,老刑警劉巖绞铃,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異嫂侍,居然都是意外死亡憎兽,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門吵冒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人西剥,你說我怎么就攤上這事痹栖。” “怎么了瞭空?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵揪阿,是天一觀的道長(zhǎng)疗我。 經(jīng)常有香客問我,道長(zhǎng)南捂,這世上最難降的妖魔是什么吴裤? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮溺健,結(jié)果婚禮上麦牺,老公的妹妹穿的比我還像新娘。我一直安慰自己鞭缭,他們只是感情好剖膳,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著岭辣,像睡著了一般吱晒。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上沦童,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天仑濒,我揣著相機(jī)與錄音,去河邊找鬼偷遗。 笑死墩瞳,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的鹦肿。 我是一名探鬼主播矗烛,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼箩溃!你這毒婦竟也來了瞭吃?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤涣旨,失蹤者是張志新(化名)和其女友劉穎歪架,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體霹陡,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡和蚪,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了烹棉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片攒霹。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖浆洗,靈堂內(nèi)的尸體忽然破棺而出催束,到底是詐尸還是另有隱情,我是刑警寧澤伏社,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布抠刺,位于F島的核電站塔淤,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏速妖。R本人自食惡果不足惜高蜂,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望罕容。 院中可真熱鬧备恤,春花似錦、人聲如沸杀赢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽脂崔。三九已至滤淳,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間砌左,已是汗流浹背脖咐。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留汇歹,地道東北人屁擅。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像产弹,于是被迫代替她去往敵國(guó)和親派歌。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

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

  • Lua 5.1 參考手冊(cè) by Roberto Ierusalimschy, Luiz Henrique de F...
    蘇黎九歌閱讀 13,740評(píng)論 0 38
  • rljs by sennchi Timeline of History Part One The Cognitiv...
    sennchi閱讀 7,292評(píng)論 0 10
  • 早上起床痰哨,洗漱完畢胶果,準(zhǔn)備上班。發(fā)現(xiàn)電腦不見了斤斧,左找早抠,右找,才想起昨天晚上開門時(shí)撬讽,找鑰匙順手把電腦放在小區(qū)的健身器...
    滟之霞閱讀 308評(píng)論 0 1
  • 昨晚蕊连,上了一段火,這火游昼,不是小火甘苍,歷史悠久。 雖然比較羞于重復(fù)烘豌,但是為了說明問題羊赵,還是鼓足勇氣聊一聊吧。...
    茶朵張涵閱讀 718評(píng)論 0 1
  • 今天有點(diǎn)想吃餃子,就跟媽媽說昧捷,媽媽我想吃餃子,媽媽去買回來餡罐寨,把調(diào)料都準(zhǔn)備好了靡挥,就開始包餃子,我也跟媽媽學(xué)著先把餃...
    一世諾言閱讀 583評(píng)論 0 1