原文: http://pij.robinqu.me/JavaScript_Core/Functional_JavaScript/Async_Programing_In_JavaScript.htmljavascript
本文從異步風格講起,分析Javascript中異步變成的技巧、問題和解決方案。具體的,從回調形成的問題提及,並談到了利用事件、Promise、Generator等技術來解決這些問題。html
異步,是沒有線程模型的Javascript的救命稻草。說得高大上一些,就是運用了Reactor
設計模式1。java
Javascript的一切都是圍繞着「異步」二子的。不管是瀏覽器環境,仍是node環境,大多數API都是經過「事件」來將請求(或消息、調用)和返回值(或結果)分離。而「事件」,都離不開回調(Callback),例如,node
var fs = require("fs"); fs.readFile(__filename, function(e, data) { console.log("2. in callback"); }); console.log("1. after invoke");
fs模塊封裝了複雜的IO模塊,其調用結果是經過一個簡單的callback告訴調用者的。看起來是十分不錯的,咱們看看Ruby的EventMachine
:jquery
require "em-files" EM::run do EM::File::open(__FILE__, "r") do |io| io.read(1024) do |data| puts data io.close end EM::stop end end
因爲Ruby的標準庫裏面的API全是同步的,異步的只有相似EventMachine這樣的第三方API才能提供支持。實際風格上,二者相似,就咱們這個例子來講,Javascript的版本彷佛更加簡介,並且不須要添加額外的第三方模塊。git
異步模式,相比線程模式,損耗更小,在部分場景性能甚至比Java更好2。而且,non-blocking
的API是node默認的,這使nodejs和它的異步回調大量應用。程序員
例如,咱們想要找到當前目錄中全部文件的尺寸:github
fs.readdir(__dirname, function(e, files) {//callback 1 if(e) { return console.log(e); } dirs.forEach(function(file) {//callback 2 fs.stat(file, function(e, stats) {//callback 3 if(e) { return console.log(e); } if(stats.isFile()) { console.log(stats.size); } }); }); });
很是簡單的一個任務便形成了3層回調。在node應用爆發的初期,大量的應用都是在這樣的風格中誕生的。顯然,這樣的代碼風格有以下風險:web
很多程序員,由於第一條而放棄nodejs,甚至放棄Javascript。而關於第二條,各類隱性bug的排除和性能損耗的優化工做在向程序員招手。
等等,你說我一直再說node,沒有說起瀏覽器中的狀況?咱們來看個例子:
/*glboal $ */ // we have jquery in the `window` $("#sexyButton").on("click", function(data) {//callback 1 $.getJSON("/api/topcis", function(data) {//callback 2 var list = data.topics.map(function(t) { return t.id + ". " + t.title + "\n"; }); var id = confirm("which topcis are you interested in? Select by ID : " + list); $.getJSON("/api/topics/" + id, function(data) {//callback 3 alert("Detail topic: " + data.content); }); }); });
咱們嘗試獲取一個文章列表,而後給予用戶一些交互,讓用戶選擇但願詳細瞭解的一個文章,並繼續獲取文章詳情。這個簡單的例子,產生了3個回調。
事實上,異步的性質是Javascript語言自己的固有風格,跟宿主環境無關。因此,回調漫天飛形成的問題是Javascript語言的共性。
Javascript程序員也許是最有創造力的一羣程序員之一。對於回調問題,最終有了不少解決方案。最天然想到的,即是利用事件機制。
仍是以前加載文章的場景:
var TopicController = new EventEmitter(); TopicController.list = function() {//a simple wrap for ajax request $.getJSON("/api/topics", this.notify("topic:list")); return this; }; TopicController.show = function(id) {//a simple wrap for ajax request $.getJSON("/api/topics/" + id, this.notify("topic:show", id)); return this; }; TopicController.bind = function() {//bind DOM events $("#sexyButton").on("click", this.run.bind(this)); return this; }; TopicController._queryTopic = function(data) { var list = data.topics.map(function(t) { return t.id + ". " + t.title + "\n"; }); var id = confirm("which topcis are you interested in? Select by ID : " + list); this.show(id).listenTo("topic:show", this._showTopic); }; TopicController._showTopic = function(data) { alert(data.content); }; TopicController.listenTo = function(eventName, listener) {//a helper method to `bind` this.on(eventName, listener.bind(this)); }; TopicController.notify = function(eventName) {//generate a notify callback internally var self = this, args; args = Array.prototype.slice(arguments, 1); return function(data) { args.unshift(data); args.unshift(eventName); self.emit.apply(self, args); }; }; TopicController.run = function() { this.list().lisenTo("topic:list", this._queryTopic); }; // kickoff $(function() { TopicController.run(); });
能夠看到,如今這種寫法B格就高了不少。各類封裝、各類解藕。首先,除了萬能的jQuery,咱們還依賴EventEmitter
,這是一個觀察者模式的實現3,好比asyncly/EventEmitter2。簡單的歸納一下這種風格:
若是你硬要挑剔的話,也有缺點;
利用高階函數,能夠順序、併發的將函數遞歸執行。
咱們能夠編寫一個高階函數,讓傳入的函數順序執行:
var runInSeries = function(ops, done) { var i = 0, next; next = function(e) { if(e) { return done(e); } var args = Array.prototype.slice.call(arguments, 1); args.push(next); ops[0].apply(null, args); }; next(); };
仍是咱們以前的例子:
var list = function(next) { $.getJSON("/api/topics", function(data) { next(null, data); }); }; var query = function(data, next) { var list = data.topics.map(function(t) { return t.id + ". " + t.title + "\n"; }); var id = confirm("which topcis are you interested in? Select by ID : " + list); next(null, id); }; var show = function(id, next) { $.getJSON("/api/topics/" + id, function(data) { next(null, data); }); }; $("#sexyButton").on("click", function() { runInSeries([list, query, show], function(e, detail) { alert(detail); }); });
看起來仍是很不錯的,簡潔而且清晰,最終的代碼量也沒有增長。若是你喜歡這種方式,去看一下caolan/async會發現更多精彩。
A promise represents the eventual result of an asynchronous operation. The primary way of interacting with a promise is through its then method, which registers callbacks to receive either a promise’s eventual value or the reason why the promise cannot be fulfilled.
除開文縐縐的解釋,Promise是一種對一個任務的抽象。Promise的相關API提供了一組方法和對象來實現這種抽象。
Promise的實現目前有不少:
雖然標準不少,可是全部的實現基本遵循以下基本規律:
then([fulfill], [reject])
方法,讓使用者分別處理成功失敗done([fn])
、fail([fn])
方法reject
和resolve
方法,來完成一個Promise筆者會在專門的文章內介紹Promise的具體機制和實現。在這裏僅淺嘗輒止,利用基本隨處可得的jQuery來解決以前的那個小場景中的異步問題:
$("#sexyButton").on("click", function(data) { $.getJSON("/api/topcis").done(function(data) { var list = data.topics.map(function(t) { return t.id + ". " + t.title + "\n"; }); var id = confirm("which topcis are you interested in? Select by ID : " + list); $.getJSON("/api/topics/" + id).done(function(done) { alert("Detail topic: " + data.content); }); }); });
很遺憾,使用Promise並無讓回調的問題好多少。在這個場景,Promise的並無體現出它的強大之處。咱們把jQuery官方文檔中的例子拿出來看看:
$.when( $.ajax( "/page1.php" ), $.ajax( "/page2.php" ) ).done(function( a1, a2 ) { // a1 and a2 are arguments resolved for the page1 and page2 ajax requests, respectively. // Each argument is an array with the following structure: [ data, statusText, jqXHR ] var data = a1[ 0 ] + a2[ 0 ]; // a1[ 0 ] = "Whip", a2[ 0 ] = " It" if ( /Whip It/.test( data ) ) { alert( "We got what we came for!" ); } });
這裏,同時發起了兩個AJAX請求,而且將這兩個Promise合併成一個,開發者只用處理這最終的一個Promise。
例如Q.js
或when.js
的第三方庫,能夠支持更多複雜的特性。也會讓你的代碼風格大爲改觀。能夠說,Promise爲處理複雜流程開啓了新的大門,可是也是有成本的。這些複雜的封裝,都有至關大的開銷6。
ES6的Generator引入的yield
表達式,讓流程控制更加多變。node-fiber讓咱們看到了coroutine
在Javascript中的樣子。
var Fiber = require('fibers'); function sleep(ms) { var fiber = Fiber.current; setTimeout(function() { fiber.run(); }, ms); Fiber.yield(); } Fiber(function() { console.log('wait... ' + new Date); sleep(1000); console.log('ok... ' + new Date); }).run(); console.log('back in main');
但想象一下,若是每一個Javascript都有這個功能,那麼一個正常Javascript程序員的各類嘗試就會被挑戰。你的對象會莫名其妙的被另一個fiber中的代碼更改。
也就是說,尚未一種語法設計能讓支持fiber和不支持fiber的Javascript代碼混用而且不形成混淆。node-fiber的這種不可移植性,讓coroutine在Javascript中並不那麼現實7。
可是yield
是一種Shallow coroutines,它只能中止用戶代碼,而且只有在GeneratorFunction
才能夠用yield
。
筆者在另一篇文章中已經詳細介紹瞭如何利用Geneator來解決異步流程的問題。
利用yield
實現的suspend
方法,可讓咱們以前的問題解決的很是簡介:
$("#sexyButton").on("click", function(data) { suspend(function *() { var data = yield $.getJSON("/api/topcis"); var list = data.topics.map(function(t) { return t.id + ". " + t.title + "\n"; }); var id = confirm("which topcis are you interested in? Select by ID : " + list); var detail = yield $.getJSON("/api/topics/"); alert("Detail topic: " + detail.content); })(); });
爲了利用yield
,咱們也是有取捨的:
yield
的一些約束yield
所產生的代碼風格,可能對部分新手形成迷惑yield
所產生堆棧及其難以調試說了這麼多,異步編程這種和線程模型迥然不一樣的併發處理方式,隨着node的流行也讓更多程序員瞭解其不同凡響的魅力。若是下次再有C或者Java程序員說,Javascript的回調太難看,請讓他好好讀一下這篇文章吧!
http://strongloop.com/strongblog/node-js-is-faster-than-java/ ↩
en.wikipedia.org/wiki/Observer_pattern ↩
http://wiki.ecmascript.org/doku.php?id=strawman:concurrency ↩
http://thanpol.as/javascript/promises-a-performance-hits-you-should-be-aware-of/ ↩
http://calculist.org/blog/2011/12/14/why-coroutines-wont-work-on-the-web/ ↩