翻译来源: http://phucnguyen.info/blog/everything-you-need-to-know-about-async-meteor/
posted in Web Development on October 26, 2014 by Phuc Nguyen
Meteor是运行在Node.js之上的。这意味着I/O行为,比如读文件或者发送网络请求不会阻塞整个程序。事实上,当异步行为执行结束后,我们可以提供回调。好了解不?下面会有图画解释。
假设我们想读一个加密过的文件,接着解密出内容:
JavaScript
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能同步返回内容就好了,像这样:
JavaScript
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写的代码:
JavaScript
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。
JavaScript
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。小事一桩,不需要修改,包一下就行了!
JavaScript
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,他还可以更简便!
JavaScript
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里进行
JavaScript
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的异步有所领悟。编码快乐!