摘要:不少第三方模塊并沒(méi)有做到異步調(diào)用,卻裝作支持回調(diào),堆棧的風(fēng)險(xiǎn)就更大。我們可以編寫(xiě)一個(gè)高階函數(shù),讓傳入的函數(shù)順序執(zhí)行還是我們之前的例子看起來(lái)還是很不錯(cuò)的,簡(jiǎn)潔并且清晰,最終的代碼量也沒(méi)有增加。
原文: 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
本文需要補(bǔ)充更多例子
本文存在批注,但該網(wǎng)站的Markdown編輯器不支持,所以無(wú)法正常展示,請(qǐng)到原文參考。
Async Programing in Javascript本文從異步風(fēng)格講起,分析Javascript中異步變成的技巧、問(wèn)題和解決方案。具體的,從回調(diào)造成的問(wèn)題說(shuō)起,并談到了利用事件、Promise、Generator等技術(shù)來(lái)解決這些問(wèn)題。
異步之殤 non-blocking無(wú)限好?異步,是沒(méi)有線程模型的Javascript的救命稻草。說(shuō)得高大上一些,就是運(yùn)用了Reactor設(shè)計(jì)模式1。
Javascript的一切都是圍繞著“異步”二子的。無(wú)論是瀏覽器環(huán)境,還是node環(huán)境,大多數(shù)API都是通過(guò)“事件”來(lái)將請(qǐng)求(或消息、調(diào)用)和返回值(或結(jié)果)分離。而“事件”,都離不開(kāi)回調(diào)(Callback),例如,
var fs = require("fs"); fs.readFile(__filename, function(e, data) { console.log("2. in callback"); }); console.log("1. after invoke");
fs模塊封裝了復(fù)雜的IO模塊,其調(diào)用結(jié)果是通過(guò)一個(gè)簡(jiǎn)單的callback告訴調(diào)用者的。看起來(lái)是十分不錯(cuò)的,我們看看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的標(biāo)準(zhǔn)庫(kù)里面的API全是同步的,異步的只有類(lèi)似EventMachine這樣的第三方API才能提供支持。實(shí)際風(fēng)格上,兩者類(lèi)似,就我們這個(gè)例子來(lái)說(shuō),Javascript的版本似乎更加簡(jiǎn)介,而且不需要添加額外的第三方模塊。
異步模式,相比線程模式,損耗更小,在部分場(chǎng)景性能甚至比Java更好2。并且,non-blocking的API是node默認(rèn)的,這使nodejs和它的異步回調(diào)大量應(yīng)用。
例如,我們想要找到當(dāng)前目錄中所有文件的尺寸:
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); } }); }); });
非常簡(jiǎn)單的一個(gè)任務(wù)便造成了3層回調(diào)。在node應(yīng)用爆發(fā)的初期,大量的應(yīng)用都是在這樣的風(fēng)格中誕生的。顯然,這樣的代碼風(fēng)格有如下風(fēng)險(xiǎn):
代碼難以閱讀、維護(hù):嵌套多層回調(diào)之后,作者自己都不清楚函數(shù)層次了。
潛在的調(diào)用堆棧消耗:Javascript中,遠(yuǎn)比你想像的簡(jiǎn)單去超出最大堆棧。不少第三方模塊并沒(méi)有做到異步調(diào)用,卻裝作支持回調(diào),堆棧的風(fēng)險(xiǎn)就更大。
還想更遭么?前兩條就夠了……
不少程序員,因?yàn)榈谝粭l而放棄nodejs,甚至放棄Javascript。而關(guān)于第二條,各種隱性bug的排除和性能損耗的優(yōu)化工作在向程序員招手。
等等,你說(shuō)我一直再說(shuō)node,沒(méi)有提及瀏覽器中的情況?我們來(lái)看個(gè)例子:
/*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); }); }); });
我們嘗試獲取一個(gè)文章列表,然后給予用戶一些交互,讓用戶選擇希望詳細(xì)了解的一個(gè)文章,并繼續(xù)獲取文章詳情。這個(gè)簡(jiǎn)單的例子,產(chǎn)生了3個(gè)回調(diào)。
事實(shí)上,異步的性質(zhì)是Javascript語(yǔ)言本身的固有風(fēng)格,跟宿主環(huán)境無(wú)關(guān)。所以,回調(diào)漫天飛造成的問(wèn)題是Javascript語(yǔ)言的共性。
解決方案 EventedJavascript程序員也許是最有創(chuàng)造力的一群程序員之一。對(duì)于回調(diào)問(wèn)題,最終有了很多解決方案。最自然想到的,便是利用事件機(jī)制。
還是之前加載文章的場(chǎng)景:
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(); });
可以看到,現(xiàn)在這種寫(xiě)法B格就高了很多。各種封裝、各種解藕。首先,除了萬(wàn)能的jQuery,我們還依賴EventEmitter,這是一個(gè)觀察者模式的實(shí)現(xiàn)3,比如asyncly/EventEmitter2。簡(jiǎn)單的概括一下這種風(fēng)格:
杜絕了大部分將匿名函數(shù)用作回調(diào)的場(chǎng)景,達(dá)到零嵌套,代碼簡(jiǎn)介明了
每個(gè)狀態(tài)(或步驟)之間,利用事件機(jī)制進(jìn)行關(guān)聯(lián)
每個(gè)步驟都相互獨(dú)立,方便日后維護(hù)
如果你硬要挑剔的話,也有缺點(diǎn);
由于過(guò)度分離,整體流程模糊
代碼量激增,又加大了另一種維護(hù)成本
高階函數(shù)利用高階函數(shù),可以順序、并發(fā)的將函數(shù)遞歸執(zhí)行。
我們可以編寫(xiě)一個(gè)高階函數(shù),讓傳入的函數(shù)順序執(zhí)行:
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); }); });
看起來(lái)還是很不錯(cuò)的,簡(jiǎn)潔并且清晰,最終的代碼量也沒(méi)有增加。如果你喜歡這種方式,去看一下caolan/async會(huì)發(fā)現(xiàn)更多精彩。
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.
除開(kāi)文縐縐的解釋?zhuān)琍romise是一種對(duì)一個(gè)任務(wù)的抽象。Promise的相關(guān)API提供了一組方法和對(duì)象來(lái)實(shí)現(xiàn)這種抽象。
Promise的實(shí)現(xiàn)目前有很多:
ECMAScript Promise4
即原生的Promise對(duì)象, Chrome32+以上支持
Promise/A+5標(biāo)準(zhǔn)
kriskowal/q
cujojs/when
tildeio/rsvp.js
其他廠商標(biāo)準(zhǔn)
jQuery.Deferred
WinJS
雖然標(biāo)準(zhǔn)很多,但是所有的實(shí)現(xiàn)基本遵循如下基本規(guī)律:
Promise對(duì)象
是一個(gè)有限狀態(tài)機(jī)
完成(fulfilled)
否定(rejected)
等待(pending)
結(jié)束(settled)
一定會(huì)有一個(gè)then([fulfill], [reject])方法,讓使用者分別處理成功失敗
可選的done([fn])、fail([fn])方法
支持鏈?zhǔn)紸PI
Deffered對(duì)象
提供reject和resolve方法,來(lái)完成一個(gè)Promise
筆者會(huì)在專(zhuān)門(mén)的文章內(nèi)介紹Promise的具體機(jī)制和實(shí)現(xiàn)。在這里僅淺嘗輒止,利用基本隨處可得的jQuery來(lái)解決之前的那個(gè)小場(chǎng)景中的異步問(wèn)題:
$("#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并沒(méi)有讓回調(diào)的問(wèn)題好多少。在這個(gè)場(chǎng)景,Promise的并沒(méi)有體現(xiàn)出它的強(qiáng)大之處。我們把jQuery官方文檔中的例子拿出來(lái)看看:
$.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!" ); } });
這里,同時(shí)發(fā)起了兩個(gè)AJAX請(qǐng)求,并且將這兩個(gè)Promise合并成一個(gè),開(kāi)發(fā)者只用處理這最終的一個(gè)Promise。
例如Q.js或when.js的第三方庫(kù),可以支持更多復(fù)雜的特性。也會(huì)讓你的代碼風(fēng)格大為改觀。可以說(shuō),Promise為處理復(fù)雜流程開(kāi)啟了新的大門(mén),但是也是有成本的。這些復(fù)雜的封裝,都有相當(dāng)大的開(kāi)銷(xiāo)6。
GeneartorES6的Generator引入的yield表達(dá)式,讓流程控制更加多變。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");
但想象一下,如果每個(gè)Javascript都有這個(gè)功能,那么一個(gè)正常Javascript程序員的各種嘗試就會(huì)被挑戰(zhàn)。你的對(duì)象會(huì)莫名其妙的被另外一個(gè)fiber中的代碼更改。
也就是說(shuō),還沒(méi)有一種語(yǔ)法設(shè)計(jì)能讓支持fiber和不支持fiber的Javascript代碼混用并且不造成混淆。node-fiber的這種不可移植性,讓coroutine在Javascript中并不那么現(xiàn)實(shí)7。
但是yield是一種Shallow coroutines,它只能停止用戶代碼,并且只有在GeneratorFunction才可以用yield。
筆者在另外一篇文章中已經(jīng)詳細(xì)介紹了如何利用Geneator來(lái)解決異步流程的問(wèn)題。
利用yield實(shí)現(xiàn)的suspend方法,可以讓我們之前的問(wèn)題解決的非常簡(jiǎn)介:
$("#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支持
需要大量重寫(xiě)基礎(chǔ)框架,是接口規(guī)范化(thunkify),來(lái)支持yield的一些約束
yield所產(chǎn)生的代碼風(fēng)格,可能對(duì)部分新手造成迷惑
多層yield所產(chǎn)生堆棧及其難以調(diào)試
結(jié)語(yǔ)說(shuō)了這么多,異步編程這種和線程模型迥然不同的并發(fā)處理方式,隨著node的流行也讓更多程序員了解其與眾不同的魅力。如果下次再有C或者Java程序員說(shuō),Javascript的回調(diào)太難看,請(qǐng)讓他好好讀一下這篇文章吧!
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/??
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/78132.html
摘要:補(bǔ)充說(shuō)明響應(yīng)式編程采用了訂閱觀察者設(shè)計(jì)模式,使訂閱者可以將通知主動(dòng)發(fā)送給各訂閱者。一個(gè)響應(yīng)式編程的實(shí)現(xiàn)庫(kù)是一個(gè)庫(kù),它通過(guò)使用序列來(lái)編寫(xiě)異步和基于事件的程序。 或許響應(yīng)式布局這個(gè)名單大家都聽(tīng)過(guò)或者都自己實(shí)現(xiàn)過(guò),那么響應(yīng)式編程是什么呢?下面我們來(lái)具體聊一聊。 我的理解 從字面意思上我們可以大致理解為:所有的事件存在于一條事件總線上,所有的事件都可以看作未來(lái)某個(gè)時(shí)間將要發(fā)生的事件流(stre...
摘要:本文最早為雙十一而作,原標(biāo)題雙大前端工程師讀書(shū)清單,以付費(fèi)的形式發(fā)布在上。發(fā)布完本次預(yù)告后,捕捉到了一個(gè)友善的吐槽讀書(shū)清單也要收費(fèi)。這本書(shū)便從的異步編程講起,幫助我們?cè)O(shè)計(jì)快速響應(yīng)的網(wǎng)絡(luò)應(yīng)用,而非簡(jiǎn)單的頁(yè)面。 本文最早為雙十一而作,原標(biāo)題雙 11 大前端工程師讀書(shū)清單,以付費(fèi)的形式發(fā)布在 GitChat 上。發(fā)布之后在讀者圈群聊中和讀者進(jìn)行了深入的交流,現(xiàn)免費(fèi)分享到這里,不足之處歡迎指教...
摘要:本文最早為雙十一而作,原標(biāo)題雙大前端工程師讀書(shū)清單,以付費(fèi)的形式發(fā)布在上。發(fā)布完本次預(yù)告后,捕捉到了一個(gè)友善的吐槽讀書(shū)清單也要收費(fèi)。這本書(shū)便從的異步編程講起,幫助我們?cè)O(shè)計(jì)快速響應(yīng)的網(wǎng)絡(luò)應(yīng)用,而非簡(jiǎn)單的頁(yè)面。 本文最早為雙十一而作,原標(biāo)題雙 11 大前端工程師讀書(shū)清單,以付費(fèi)的形式發(fā)布在 GitChat 上。發(fā)布之后在讀者圈群聊中和讀者進(jìn)行了深入的交流,現(xiàn)免費(fèi)分享到這里,不足之處歡迎指教...
摘要:本文最早為雙十一而作,原標(biāo)題雙大前端工程師讀書(shū)清單,以付費(fèi)的形式發(fā)布在上。發(fā)布完本次預(yù)告后,捕捉到了一個(gè)友善的吐槽讀書(shū)清單也要收費(fèi)。這本書(shū)便從的異步編程講起,幫助我們?cè)O(shè)計(jì)快速響應(yīng)的網(wǎng)絡(luò)應(yīng)用,而非簡(jiǎn)單的頁(yè)面。 本文最早為雙十一而作,原標(biāo)題雙 11 大前端工程師讀書(shū)清單,以付費(fèi)的形式發(fā)布在 GitChat 上。發(fā)布之后在讀者圈群聊中和讀者進(jìn)行了深入的交流,現(xiàn)免費(fèi)分享到這里,不足之處歡迎指教...
閱讀 3027·2023-04-25 18:00
閱讀 2234·2021-11-23 10:07
閱讀 4078·2021-11-22 09:34
閱讀 1256·2021-10-08 10:05
閱讀 1577·2019-08-30 15:55
閱讀 3447·2019-08-30 11:21
閱讀 3350·2019-08-29 13:01
閱讀 1387·2019-08-26 18:26