JavaScript 異步編程–await實現原理
Generator(生成器)是ES6標準引入的新的數據類型,其最大的特點就是可以交出函數的執行的控制權,即:通過yield關鍵字標明需要暫停的語句,執行時遇到yield語句則返回該語句執行結果,等到調用next函數時(也就是說可以通過控制調用next函數的時機達到控制generator執行的目的)重新回到暫停的地方往下執行,直至generator執行結束。
基本結構
以下是一個典型的generator函數的示例,以"*"標明爲generator。
function* gen(x){
var y = yield x + 2;
console.log(y); // undefine
var yy = yield x + 3;
console.log(yy); // 6
return y; // 沒啥用
}
var g = gen(1);
var r1 = g.next();
console.log(r1); // { value: 3, done: false }
var r2 = g.next();
console.log(r2); // { value: 4, done: false }
var r3 = g.next(6);
console.log(r3); // { value: undefined, done: true }
上述代碼中,調用gen函數,會返回一個內部指針(即遍歷器)g,這是Generator函數和一般函數不同的地方,調用它不會返回結果,而是一個指針對象。調用指針g的next方法,會移動內部指針,指向第一個遇到的yield語句,上例就是執行到x+2爲止。換言之,next方法的作用是分階段執行Generator函數。每次調用next方法,會返回一個對象{value: any, done: boolean},表示當前階段的信息,其中value屬性是yield語句後面表達式的值;done屬性是一個布爾值,表示Generator函數是否執行完畢,即是否還有下一個階段。next方法輸入參數即爲yield語句的值,因此生成器gen中y爲第二次調用next的輸入參數"undefine",yy爲第三次調用next的輸入參數6。
總結:
- generator返回遍歷器,可遍歷所有yield
- yield將生成器內部代碼分割成n段,通過調用next方法一段一段執行
- next方法返回的value屬性向外輸出數據,next方法通過實參向生成器內部輸入數據
##思考
如果yield標記的語句是個異步執行的函數func,然後在func回調中調用next,則實現了等待func異步執行的效果-----“func要做的事做完了,纔會往下走”,這樣就避免了多重回調嵌套(callback hell,回調地獄, 如下所示)
func1(function (res) {
// do something
func2(function (res2) {
// do something
func3(function (res3) {
// do something
})
})
})
##Thunk函數
什麼是thunk函數?詳見Thunk 函數的含義和用法
簡單理解:thunk函數利用閉包可以緩存狀態的特性,先傳參數,再執行函數,這樣就將函數調用過程分成了兩步。以下thunkify函數可將普通異步函數轉化爲thunk函數。
function thunkify(fn) {
assert('function' == typeof fn, 'function required');
return function () {
// arguments爲異步函數的參數(不包含回調函數參數)
var args = new Array(arguments.length);
var ctx = this;
for (var i = 0; i < args.length; ++i) {
args[i] = arguments[i];
}
// done爲異步函數的回調函數(callback)
return function (done) {
var called;
args.push(function () {
if (called) return;
called = true;
done.apply(null, arguments);
});
try {
// 到這裏,異步函數才真正被調用
fn.apply(ctx, args);
} catch (err) {
done(err);
}
}
}
};
##Generator執行控制
thunk函數有什麼用呢?其一個典型應用就是用於控制generator的執行,見如下示例是爲了實現多個文件的順序讀取,實現了同步寫法,避免回調嵌套。
const fs = require('fs');
const readFileThunk = thunkify(fs.readFile);
var generator = function* () {
for (var i = 0; i < arguments.length; i++) {
console.log('file: %s', arguments[i]);
// yield 返回thunkify最內部的 function (done){} 函數,此處傳入了readFile函數參數,但並沒有執行
var r1 = yield readFileThunk(arguments[i], 'utf8');
console.log('r1: %s', r1);
}
}
function rungenerator(generator) {
//文件名稱
var args = [];
for (var i = 1; i < arguments.length; i++) {
args.push(arguments[i]);
}
//生成generator實例
var gen = generator.apply(null, args);
function done(err, data) {
//執行跳到 generator中去
var result = gen.next(data);
if (result.done) { return; }
// 此處纔是真正的調用readFile函數開始讀取文件內容,done作爲回調, 文件讀取完成後,執行gen.next(),
// 告訴generator繼續執行,並通過yield返回下一個thunk函數,開始讀取下一個文件,從而達到順序執行的效果
result.value(done);
}
done();
}
rungenerator(generator, '123.txt', '1234.txt', 'he.txt')
上述代碼中,rungenerator是一個執行generator的函數,具有通用性,封裝下就成了co庫----generator函數自動執行的解決方案。
var fs = require('fs');
var co = require('co');
var thunkify = require('thunkify');
var readFile = thunkify(fs.readFile);
co(function*(){
var files=['./text1.txt', './text2.txt', './text3.txt'];
var p1 = yield readFile(files[0]);
console.log(files[0] + ' ->' + p1);
var p2 = yield readFile(files[1]);
console.log(files[1] + ' ->' + p2);
var p3 = yield readFile(files[2]);
console.log(files[2] + ' ->' + p3);
return 'done';
});
看起來舒服多了。。。
##async和await
async和await是ES7中的新語法,實際上是generator函數的語法糖
Nodejs最新版已經支持了,瀏覽器不支持的,可以用Babel轉下。
var fs = require('fs');
var readFile = function (fileName){
return new Promise(function (resolve, reject){
fs.readFile(fileName, function(error, data){
if (error){
reject(error);
}
else {
resolve(data);
}
});
});
};
var asyncReadFile = async function (){
var f1 = await readFile('./text1.txt');
var f2 = await readFile('./text2.txt');
console.log(f1.toString());
console.log(f2.toString());
};
asyncReadFile();
##致謝
主要學習了nullcc的博客《深入解析Javascript異步編程》、無腦的博客《node的 thunkify模塊說明》、阮一峯老師的博客《Thunk 函數的含義和用法》,由衷地感謝以上作者!!!!!