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.value
在d1.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
的簡單實現。