理解thunk函式的作用及co的實現

Horve大叔發表於2019-02-16

thunk

thunk 從形式上將函式的執行部分回撥部分分開,這樣我們就可以在一個地方執行執行函式,在另一個地方執行回撥函式。這樣做的價值就在於,在做非同步操作的時候,我們只需要知道回撥函式執行的順序和巢狀關係,就能按順序取得執行函式的結果。

以下是 thunk 的簡單實現:

function thunkify (fn) {
    return function () {
        var args = Array.prototype.slice(arguments);
        var ctx = this;

        return function (done) {
            var called = false;
            args.push(function() {
                if (called) return;
                called = true;
                done.apply(null, arguments);
            });
            try {
                fn.apply(ctx, args);
            } catch (err) {
                done(err);
            }
        }
    }
}

上面的實現將函式原有的執行變為按“執行部分”和“回撥部分”分別執行的方式:

fn(a, callback) => thunkify(fn)(a)(callback)

例如:

var fs = require(`fs`);
var readFile = thunkify(fs.readFile); // 將readFile函式包進thunkify,變為thunkify函式

//**這是執行函式集合**//
var f1 = readFile(`./a.js`);
var f2 = readFile(`./b.js`);
var f3 = readFile(`./c.js`);

//**這是回撥函式集合**//
//利用巢狀控制f1 f2執行的順序
f1(function(err, data1) {
    // doSomething
    f2(function(err, data2) {
        // doSomething
        f3(function (err, data3) {
            // doSomething
        })
    })
})

而傳統的寫法為:

//傳統寫法
fs.readFile(`./a.js`, function(err, data1) {
    // doSomething
    fs.readFile(`./b.js`, function(err, data2) {
        // doSomething
        fs.readFile(`./c.js`, function(err, data3) {
            // doSomething
        })
    })
})

在執行部分和回撥部分分開之後,就可以使用generator等非同步控制技術方便地進行流程控制,避免回撥黑洞。上述的檔案讀取流程就可以用generator進行改造:

var fs = require(`fs`);
var readFile = thunkify(fs.readFile);

//**函式的‘執行部分’放在一起執行**//
var gen = function* () {
    var data1 = yield readFile(`./a.js`);
    // 使用者獲取資料後自定義寫在這裡
    console.log(data1.toString());
    
    var data2 = yield readFile(`./b.js`);
    // 使用者獲取資料後自定義寫在這裡
    console.log(data2);
    ····
}

// 函式的‘回撥部分’在另一個地方執行,且呼叫的形式都一樣
var g = gen();
var d1 = g.next(); // 返回的結果為{value: func, done: boolean}

// 執行value,實際為執行`d1.value(callback)`
// 也即`thunkify(fs.readFile)(`./a.js`)(callback)`
d1.value(function(err, data) {
    if (err) throw err;
    // g.next(data) 可以將引數data傳回generator函式體,作為上一個階段非同步任務的執行結果
    // 例子中,data被傳回了gen函式體,作為data1的值
    var d2 = g.next(data);
    d2.value(function(err, data2) {
        if (err) throw err;
        g.next(data2);
    });
});

co

在上述的改造中發現,執行回撥部分的時候,依舊存在回撥巢狀:d2.valued1.value的回撥中執行。觀察後發現,其實在執行回撥的時候,也就是g在執行next()的時候,執行的形式基本相同,都是:

d.value(function(err, data) {
    if (err) throw err;
    g.next(data);
});

這種形式,所以可以通過編寫一個遞迴函式來整理流程。

function run(fn) {
    var g = fn();
    
    // 下一步控制函式,實際就是d.value的回撥函式
    function next(err, data) {
        // 把前面一個資料給傳遞到gen()函式裡面
        var result = g.next(data);
        // 判斷是否結束
        if (result.done) return;
        // 下一句執行回撥next的時候 不斷的遞迴
        result.value(next);
    }
    // 執行第一步
    next();
}

// 使用
run(gen);

上面程式碼中的過程很好理解,就是把gen放到一個遞迴器中去執行,在這個遞迴器中有一個核心的函式next,這個函式就是遞迴函式。當函式中的g.next(data)返回的done屬性值為true,就表示當前生成器函式中的yield已經執行完畢,退出就OK。當不為true,表示當前生成器函式還有未執行的yield,於是繼續呼叫next函式繼續執行同樣的流程。

而上述的流程就是非同步流控制庫co的簡單實現。

相關文章