翻譯來源: http://phucnguyen.info/blog/everything-you-need-to-know-about-async-meteor/

Meteor是運行在Node.js之上的。這意味着I/O行為,比如讀文件或者發送網絡請求不會阻塞整個程序。事實上,當異步行為執行結束后,我們可以提供回調。好了解不?下面會有圖畫解釋。
假設我們想讀一個加密過的文件,接着解密出內容:
1
2
3
4
5
6
7
8
9
10
11
|
var aes = Meteor.require('aes-helper')
, fs = Meteor.require('fs');
var getSecretData = function(key) {
fs.readFile('/secret_path/encrypted_secret.txt', 'utf8', function(err, res) {
if (err) console.log(err);
else console.log( 'Secret: ' + aes.decrypt(res, key) );
}
};
getSecretData('my-secret-key');
|
而更通用,多樣的事件序列長成這樣:
事件序列只是等待執行的函數隊列而已。每當調用函數時,就放到事件序列里邊去。
當我們執行函數getSecretData去解密並打印文檔內容時,函數readFile就會被調用,出現在事件序列里邊。
讀文件函數readFile並不關心他后面執行什么,這哥們只是告訴系統發送文件,接着就滾蛋了!
分分秒,readFile結束。‘callback’回調這貨就會跳進事件序列:
很快,收到文件后,英雄歸來,完成后面的所有工作。
很好很有用吧?! 可是如果任務更復雜,需要多層異步該怎么辦?結果就成這吊樣:
真蛋疼!異步流程控制代碼太變態了,無法閱讀和維護!要是getSecretData能同步返回內容就好了,像這樣:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
/* This code looks nicer, but sadly it doesn't work */
getSecretData = function(key) {
var decryptedSecret;
fs.readFile('/secret_path/encrypted_secret.txt', 'utf8', function(err, res) {
if (err) console.log(err);
else decryptedSecret = aes.decrypt(res, key);
}
return decryptedSecret; // undefined <-- oops!
};
// So sweet. We have getSecretData return the value, then print it out, all synchronously.
// If only life were that simple...
var result = getSecretData('my-secret-key'); // undefined
console.log(result); // undefined
|
可惜,這樣的代碼不可行,因為getSecretData會在readFile結束前就執行了,直接返回undefined。解決這問題,非英雄莫屬,那就是Fiber-王者歸來!
接觸Fiber,他是個可以容納多個函數的無敵英雄!
Fiber其實就是特別的容器函數。他可以跟普通函數一樣被扔進事件序列。但他也別有魔力:可以在任意執行點暫停,跳出執行序列,任意時間后再回來,任由程序員調戲!Fiber暫停時,流程控制權就接力到事件序列里邊的下一個函數(普通函數,新Fiber函數都可以)。
你可能已經看到好處了:如果Fiber含有費時的I/O行為,它可以跳出事件序列,等待結果。同時,我們也可以運行序列里的下一個函數。人生苦短,時間珍貴!I/O結束,Fiber可以再轉回來,從上次執行點接着來.下面是用Fiber寫的代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
var Fiber = Npm.require('fibers');
// Our Fiber-powered getSecretData function
getSecretData = function(key) {
var fiber = Fiber.current; // get the currently-running Fiber
fs.readFile('/secret_path/encrypted_secret.txt', 'utf8', function(err, res) {
if (err) console.log(err);
else fiber.run( aes.decrypt(res, key) ); // resume execution of this fiber. The argument passed
// to fiber.run (i.e. the secret data) will become
// the value returned by Fiber.yield below
}
// halt this Fiber for now. When the execution is resumed later, return whatever passed to fiber.run
var result = Fiber.yield();
return result;
};
// We wrap our code in a Fiber, then run it
Fiber(function() {
var result = getSecretData('my-secret-key');
console.log(result); // the decrypted secret
}).run();
|
可能還不好理解是吧?下面的圖標更直觀:
Fiber發現yield時,他會休息一下!
調用run()就回復Fiber的執行,任何傳遞到run()將會變成yield()的返回值。
你還叫?“看起來還行。但是yield run這貨,我感覺有點奇葩”。
我同意!還有比Fiber更猛的大神。那就是Future!
你可以把Future當作Fiber的抽象。這貨提供了更強大的API,像是馴養的Fiber。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
var Future = Npm.require('fibers/future');
// Our Future-powered getSecretData function
getSecretData = function(key) {
var future = new Future; // create a new, bright future
fs.readFile('/secret_path/encrypted_secret.txt', 'utf8', function(err, res) {
if (err) console.log(err);
else future.return( aes.decrypt(res, key) ); // signal that the future has finished (resolved)
// the passed argument (the decrypted secret)
// will become the value returned by wait() below
}
return future; // we return the future instance so other code can wait() for this future
};
// The future method is added to the prototype object of every function
// Calling future() on a function will return a Fiber-wrapped version of it
(function() {
// we wait for the future to finish. While we're waiting, control will be yielded
// when this future finishes, wait() will return the value passed to future.return()
var result = getSecretData('my-secret-key').wait();
console.log(result);
}.future()) ();
|
嘿咻!上面的列子都是可以自由修改getSecretData函數的。可是當異步函數不好修改是怎么辦?比如第三方API。小事一樁,不需要修改,包一下就行了!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
// A native, garden-variety async function
getSecretData = function(key, callback) {
fs.readFile('/secret_path/encrypted_secret.txt', 'utf8', function(err, res) {
if (err) throw new Error(err.message);
else callback && callback( null, aes.decrypt(res, key) );
}
};
// Let's wrap it up
// What returned here is actually a future instance. When the async operation completes,
// this future instance will be marked as finished and the result of the async operation
// will become the value returned by wait()
var getSecretDataSynchronously = Future.wrap(getSecretData);
(function() {
// we call wait() on the returned future instance to, well, wait for this future to finish
// and get the result later when it does
var result = getSecretDataSynchronously ('my-secret-key').wait();
console.log(result);
}.future()) ();
|
嗯,好像每次調用下wait就可以了。還是有點煩!
哈哈,用Meteor.warapAsync,他還可以更簡便!
1
2
3
4
5
6
7
8
9
10
|
getSecretData = function(key, callback) {
fs.readFile('/secret_path/encrypted_secret.txt', 'utf8', function(err, res) {
if (err) throw new Error(err.message);
else callback && callback( null, aes.decrypt(res, key) );
}
};
var getSecretDataSynchronously = Meteor.wrapAsync(getSecretData);
var result = getSecretDataSynchronously(key); // <-- no wait() here!
return result;
|
實際上,除了吸引眼球,我們還有一些異步相關的話題可以聊聊:
– – –
Future.wrap 和 Meteor.wrapAsync不是萬能葯
他們只適合原生的純異步函數。就是有回調,返回error,result那種函數。還有,他們只能在服務器端有用,因為yielding在客戶端不行-沒用Fibers。
– – –
Meteor.wrapAsync會把你純真的函數變成雙半臉!!!
幸運的是,雙面函數也不太壞。有時候他們很有用,他們可以同步調用(上面幾種方案),也可以異步調用(傳一個回調函數)。
服務器端,HTTP.call, collection.insert/update/remove都已經內置了這種包裹方式。比如HTTP,如果直接調用,方法會等到response返回;如果提供回調函數,他就直接跳出,等網絡返回response再條用回調函數。
客戶端,由於不能用阻塞,只能提供回調函數。
– – –
Fiber 瑕疵
默認,客戶端調用是在Fiber里的-----一次一個。這個Fiber可以訪問當前用戶的環境變量(比如Meteor.userId())。這也會產生問題:
1)服務器端,同步調用HTTP.call這類方法會阻塞當前用戶的后續方法。這未必是什么好事情。如果后續方法跟當前方法無關的話,其實可以使用this.unblock(),這樣其他方法調用就會在新的Fiber里進行
1
2
3
4
5
6
|
Meteor.methods({
requestSecret: function() {
this.unblock();
return HTTP.call('GET', 'http://www.nsa.gov/top-secrets');
}
});
|
2) “Meteor代碼必須在Fiber里邊執行”
似曾相識不?錯誤總是不斷當你調用第三方異步API時發生。不能這樣搞,因為回掉函數在Fiber之外執行了,無法訪問環境變量。一種解決方案就是用Meteor.bindEnvironment包一下,他能返回Fiber包過新版函數。方案2就是用Meteor.wrapAsync(實際wrapAsyncn內部就是調用的bindEnvironment ).
希望你對Meteor的異步有所領悟。編碼快樂!