摘要:不少第三方模塊并沒有做到異步調用,卻裝作支持回調,堆棧的風險就更大。我們可以編寫一個高階函數,讓傳入的函數順序執行還是我們之前的例子看起來還是很不錯的,簡潔并且清晰,最終的代碼量也沒有增加。
原文: http://pij.robinqu.me/JavaScript_Core/Functional_JavaScript/Async_Programing_In_JavaScript.html
源代碼: https://github.com/RobinQu/Programing-In-JavaScript/blob/master/chapters/JavaScript_Core/Functional_JavaScript/Async_Programing_In_JavaScript.md
本文需要補充更多例子
本文存在批注,但該網站的Markdown編輯器不支持,所以無法正常展示,請到原文參考。
Async Programing in Javascript本文從異步風格講起,分析Javascript中異步變成的技巧、問題和解決方案。具體的,從回調造成的問題說起,并談到了利用事件、Promise、Generator等技術來解決這些問題。
異步之殤 non-blocking無限好?異步,是沒有線程模型的Javascript的救命稻草。說得高大上一些,就是運用了Reactor設計模式1。
Javascript的一切都是圍繞著“異步”二子的。無論是瀏覽器環境,還是node環境,大多數API都是通過“事件”來將請求(或消息、調用)和返回值(或結果)分離。而“事件”,都離不開回調(Callback),例如,
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:
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的版本似乎更加簡介,而且不需要添加額外的第三方模塊。
異步模式,相比線程模式,損耗更小,在部分場景性能甚至比Java更好2。并且,non-blocking的API是node默認的,這使nodejs和它的異步回調大量應用。
例如,我們想要找到當前目錄中所有文件的尺寸:
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應用爆發的初期,大量的應用都是在這樣的風格中誕生的。顯然,這樣的代碼風格有如下風險:
代碼難以閱讀、維護:嵌套多層回調之后,作者自己都不清楚函數層次了。
潛在的調用堆棧消耗:Javascript中,遠比你想像的簡單去超出最大堆棧。不少第三方模塊并沒有做到異步調用,卻裝作支持回調,堆棧的風險就更大。
還想更遭么?前兩條就夠了……
不少程序員,因為第一條而放棄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 + " "; }); 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語言的共性。
解決方案 EventedJavascript程序員也許是最有創造力的一群程序員之一。對于回調問題,最終有了很多解決方案。最自然想到的,便是利用事件機制。
還是之前加載文章的場景:
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 + " "; }); 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 + " "; }); 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會發現更多精彩。
PromiseA 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的實現目前有很多:
ECMAScript Promise4
即原生的Promise對象, Chrome32+以上支持
Promise/A+5標準
kriskowal/q
cujojs/when
tildeio/rsvp.js
其他廠商標準
jQuery.Deferred
WinJS
雖然標準很多,但是所有的實現基本遵循如下基本規律:
Promise對象
是一個有限狀態機
完成(fulfilled)
否定(rejected)
等待(pending)
結束(settled)
一定會有一個then([fulfill], [reject])方法,讓使用者分別處理成功失敗
可選的done([fn])、fail([fn])方法
支持鏈式API
Deffered對象
提供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 + " "; }); 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。
GeneartorES6的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 + " "; }); 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,我們也是有取舍的:
Generator的兼容性并不好,僅有新版的node和Chrome支持
需要大量重寫基礎框架,是接口規范化(thunkify),來支持yield的一些約束
yield所產生的代碼風格,可能對部分新手造成迷惑
多層yield所產生堆棧及其難以調試
結語說了這么多,異步編程這種和線程模型迥然不同的并發處理方式,隨著node的流行也讓更多程序員了解其與眾不同的魅力。如果下次再有C或者Java程序員說,Javascript的回調太難看,請讓他好好讀一下這篇文章吧!
http://en.wikipedia.org/wiki/Reactor_pattern??
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://promises-aplus.github.io/promises-spec/??
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/??
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/78132.html
摘要:補充說明響應式編程采用了訂閱觀察者設計模式,使訂閱者可以將通知主動發送給各訂閱者。一個響應式編程的實現庫是一個庫,它通過使用序列來編寫異步和基于事件的程序。 或許響應式布局這個名單大家都聽過或者都自己實現過,那么響應式編程是什么呢?下面我們來具體聊一聊。 我的理解 從字面意思上我們可以大致理解為:所有的事件存在于一條事件總線上,所有的事件都可以看作未來某個時間將要發生的事件流(stre...
摘要:本文最早為雙十一而作,原標題雙大前端工程師讀書清單,以付費的形式發布在上。發布完本次預告后,捕捉到了一個友善的吐槽讀書清單也要收費。這本書便從的異步編程講起,幫助我們設計快速響應的網絡應用,而非簡單的頁面。 本文最早為雙十一而作,原標題雙 11 大前端工程師讀書清單,以付費的形式發布在 GitChat 上。發布之后在讀者圈群聊中和讀者進行了深入的交流,現免費分享到這里,不足之處歡迎指教...
摘要:本文最早為雙十一而作,原標題雙大前端工程師讀書清單,以付費的形式發布在上。發布完本次預告后,捕捉到了一個友善的吐槽讀書清單也要收費。這本書便從的異步編程講起,幫助我們設計快速響應的網絡應用,而非簡單的頁面。 本文最早為雙十一而作,原標題雙 11 大前端工程師讀書清單,以付費的形式發布在 GitChat 上。發布之后在讀者圈群聊中和讀者進行了深入的交流,現免費分享到這里,不足之處歡迎指教...
摘要:本文最早為雙十一而作,原標題雙大前端工程師讀書清單,以付費的形式發布在上。發布完本次預告后,捕捉到了一個友善的吐槽讀書清單也要收費。這本書便從的異步編程講起,幫助我們設計快速響應的網絡應用,而非簡單的頁面。 本文最早為雙十一而作,原標題雙 11 大前端工程師讀書清單,以付費的形式發布在 GitChat 上。發布之后在讀者圈群聊中和讀者進行了深入的交流,現免費分享到這里,不足之處歡迎指教...
閱讀 3021·2023-04-25 18:00
閱讀 2222·2021-11-23 10:07
閱讀 4060·2021-11-22 09:34
閱讀 1249·2021-10-08 10:05
閱讀 1572·2019-08-30 15:55
閱讀 3435·2019-08-30 11:21
閱讀 3339·2019-08-29 13:01
閱讀 1378·2019-08-26 18:26