Javascript異步編程 - 函數式編程 - Javascript核心

原文: http://pij.robinqu.me/JavaScript_Core/Functional_JavaScript/Async_Programing_In_JavaScript.htmljavascript

源代碼: https://github.com/RobinQu/Programing-In-JavaScript/blob/master/chapters/JavaScript_Core/Functional_JavaScript/Async_Programing_In_JavaScript.mdphp

  • 本文須要補充更多例子
  • 本文存在批註,但該網站的Markdown編輯器不支持,因此沒法正常展現,請到原文參考。

Async Programing in Javascript

本文從異步風格講起,分析Javascript中異步變成的技巧、問題和解決方案。具體的,從回調形成的問題提及,並談到了利用事件、Promise、Generator等技術來解決這些問題。html

異步之殤

non-blocking無限好?

異步,是沒有線程模型的Javascript的救命稻草。說得高大上一些,就是運用了Reactor設計模式1java

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的EventMachinejquery

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

  1. 代碼難以閱讀、維護:嵌套多層回調以後,做者本身都不清楚函數層次了。
  2. 潛在的調用堆棧消耗:Javascript中,遠比你想像的簡單去超出最大堆棧。很多第三方模塊並無作到異步調用,卻裝做支持回調,堆棧的風險就更大。
  3. 還想更遭麼?前兩條就夠了……

很多程序員,由於第一條而放棄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語言的共性。

解決方案

Evented

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。簡單的歸納一下這種風格:

  1. 杜絕了大部分將匿名函數用做回調的場景,達到零嵌套,代碼簡介明瞭
  2. 每一個狀態(或步驟)之間,利用事件機制進行關聯
  3. 每一個步驟都相互獨立,方便往後維護

若是你硬要挑剔的話,也有缺點;

  1. 因爲過分分離,總體流程模糊
  2. 代碼量激增,又加大了另外一種維護成本

高階函數

利用高階函數,能夠順序、併發的將函數遞歸執行。

咱們能夠編寫一個高階函數,讓傳入的函數順序執行:

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會發現更多精彩。

Promise

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的實現目前有不少:

雖然標準不少,可是全部的實現基本遵循以下基本規律:

  • Promise對象

    • 是一個有限狀態機

      • 完成(fulfilled)
      • 否認(rejected)
      • 等待(pending)
      • 結束(settled)
    • 必定會有一個then([fulfill], [reject])方法,讓使用者分別處理成功失敗
    • 可選的done([fn])fail([fn])方法
    • 支持鏈式API
  • Deffered對象

    • 提供rejectresolve方法,來完成一個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.jswhen.js的第三方庫,能夠支持更多複雜的特性。也會讓你的代碼風格大爲改觀。能夠說,Promise爲處理複雜流程開啓了新的大門,可是也是有成本的。這些複雜的封裝,都有至關大的開銷6

Geneartor

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,咱們也是有取捨的:

  1. Generator的兼容性並很差,僅有新版的node和Chrome支持
  2. 須要大量重寫基礎框架,是接口規範化(thunkify),來支持yield的一些約束
  3. yield所產生的代碼風格,可能對部分新手形成迷惑
  4. 多層yield所產生堆棧及其難以調試

結語

說了這麼多,異步編程這種和線程模型迥然不一樣的併發處理方式,隨着node的流行也讓更多程序員瞭解其不同凡響的魅力。若是下次再有C或者Java程序員說,Javascript的回調太難看,請讓他好好讀一下這篇文章吧!

相關文章
相關標籤/搜索