1. 程式人生 > >深入理解JavaScript程式設計中的同步與非同步機制

深入理解JavaScript程式設計中的同步與非同步機制

JavaScript的優勢之一是其如何處理非同步程式碼。非同步程式碼會被放入一個事件佇列,等到所有其他程式碼執行後才進行,而不會阻塞執行緒。然而,對於初學者來說,書寫非同步程式碼可能會比較困難。而在這篇文章裡,我將會消除你可能會有的任何困惑。
理解非同步程式碼

JavaScript最基礎的非同步函式是setTimeout和setInterval。setTimeout會在一定時間後執行給定的函式。它接受一個回撥函式作為第一引數和一個毫秒時間作為第二引數。以下是用法舉例:

console.log( "a" );
setTimeout(function() {
    console.log( "c" 
) }, 500 ); setTimeout(function() { console.log( "d" ) }, 500 ); setTimeout(function() { console.log( "e" ) }, 500 ); console.log( "b" );

正如預期,控制檯先輸出“a”、“b”,大約500毫秒後,再看到“c”、“d”、“e”。我用“大約”是因為setTimeout事實上是不可預知的。實際上,甚至 HTML5規範都提到了這個問題:

“這個API不能保證計時會如期準確地執行。由於CPU負載、其他任務等所導致的延遲是可以預料到的。”


有趣的是,直到在同一程式段中所有其餘的程式碼執行結束後,超時才會發生。所以如果設定了超時,同時執行了需長時間執行的函式,那麼在該函式執行完成之前,超時甚至都不會啟動。實際上,非同步函式,如setTimeout和setInterval,被壓入了稱之為Event Loop的佇列。

Event Loop是一個回撥函式佇列。當非同步函式執行時,回撥函式會被壓入這個佇列。JavaScript引擎直到非同步函式執行完成後,才會開始處理事件迴圈。這意味著JavaScript程式碼不是多執行緒的,即使表現的行為相似。事件迴圈是一個先進先出(FIFO)佇列,這說明回撥是按照它們被加入佇列的順序執行的。JavaScript被 node選做為開發語言,就是因為寫這樣的程式碼多麼簡單啊。

Ajax

非同步Javascript與XML(AJAX)永久性的改變了Javascript語言的狀況。突然間,瀏覽器不再需要重新載入即可更新web頁面。 在不同的瀏覽器中實現Ajax的程式碼可能漫長並且乏味;但是,幸虧有jQuery(還有其他庫)的幫助,我們能夠以很容易並且優雅的方式實現客戶端-伺服器端通訊。

我們可以使用jQuery跨瀏覽器介面$.ajax很容易地檢索資料,然而卻不能呈現幕後發生了什麼。比如:

var data;
$.ajax({
    url: "some/url/1",
    success: function( data ) {
        // But, this will!
console.log( data );
    }
})
// Oops, this won't work...
console.log( data );
較容易犯的錯誤,是在呼叫$.ajax之後馬上使用data,但是實際上是這樣的:
xmlhttp.open( "GET", "some/ur/1", true );
xmlhttp.onreadystatechange = function( data ) {
    if ( xmlhttp.readyState === 4 ) {
        console.log( data );
    }
};
xmlhttp.send( null );

底層的XmlHttpRequest物件發起請求,設定回撥函式用來處理XHR的readystatechnage事件。然後執行XHR的send方法。在XHR執行中,當其屬性readyState改變時readystatechange事件就會被觸發,只有在XHR從遠端伺服器接收響應結束時回撥函式才會觸發執行。

處理非同步程式碼

非同步程式設計很容易陷入我們常說的“回撥地獄”。因為事實上幾乎JS中的所有非同步函式都用到了回撥,連續執行幾個非同步函式的結果就是層層巢狀的回撥函式以及隨之而來的複雜程式碼。

node.js中的許多函式也是非同步的。因此如下的程式碼基本上很常見:

var fs = require( "fs" );
fs.exists( "index.js", function() {
    fs.readFile( "index.js", "utf8", function( err, contents ) {
        contents = someFunction( contents ); // do something with contents
fs.writeFile( "index.js", "utf8", function() {
            console.log( "whew! Done finally..." );
        });
    });
});
console.log( "executing..." );
下面的客戶端程式碼也很多見:
GMaps.geocode({
    address: fromAddress,
    callback: function( results, status ) {
        if ( status == "OK" ) {
            fromLatLng = results[0].geometry.location;
            GMaps.geocode({
                address: toAddress,
                callback: function( results, status ) {
                    if ( status == "OK" ) {
                        toLatLng = results[0].geometry.location;
                        map.getRoutes({
                            origin: [ fromLatLng.lat(), fromLatLng.lng() ],
                            destination: [ toLatLng.lat(), toLatLng.lng() ],
                            travelMode: "driving",
                            unitSystem: "imperial",
                            callback: function( e ){
                                console.log( "ANNNND FINALLY here's the directions..." );
                                // do something with e
}
                        });
                    }
                }
            });
        }
    }
});

Nested callbacks can get really nasty, but there are several solutions to this style of coding.

巢狀的回撥很容易帶來程式碼中的“壞味道”,不過你可以用以下的幾種風格來嘗試解決這個問題

  •     The problem isn't with the language itself; it's with the way programmers use the language — Async Javascript.

    沒有糟糕的語言,只有糟糕的程式猿 ——非同步JavaSript


命名函式

清除巢狀回撥的一個便捷的解決方案是簡單的避免雙層以上的巢狀。傳遞一個命名函式給作為回撥引數,而不是傳遞匿名函式:

var fromLatLng, toLatLng;
var routeDone = function( e ){
    console.log( "ANNNND FINALLY here's the directions..." );
    // do something with e
};
var toAddressDone = function( results, status ) {
    if ( status == "OK" ) {
        toLatLng = results[0].geometry.location;
        map.getRoutes({
            origin: [ fromLatLng.lat(), fromLatLng.lng() ],
            destination: [ toLatLng.lat(), toLatLng.lng() ],
            travelMode: "driving",
            unitSystem: "imperial",
            callback: routeDone
});
    }
};
var fromAddressDone = function( results, status ) {
    if ( status == "OK" ) {
        fromLatLng = results[0].geometry.location;
        GMaps.geocode({
            address: toAddress,
            callback: toAddressDone
});
    }
};
GMaps.geocode({
    address: fromAddress,
    callback: fromAddressDone
});
此外, async.js 庫可以幫助我們處理多重Ajax requests/responses. 例如:
async.parallel([
    function( done ) {
        GMaps.geocode({
            address: toAddress,
            callback: function( result ) {
                done( null, result );
            }
        });
    },
    function( done ) {
        GMaps.geocode({
            address: fromAddress,
            callback: function( result ) {
                done( null, result );
            }
        });
    }
], function( errors, results ) {
    getRoute( results[0], results[1] );
});

這段程式碼執行兩個非同步函式,每個函式都接收一個名為"done"的回撥函式並在函式結束的時候呼叫它。當兩個"done"回撥函式結束後,parallel函式的回撥函式被呼叫並執行或處理這兩個非同步函式產生的結果或錯誤。

Promises模型
引自 CommonJS/A:

promise表示一個操作獨立完成後返回的最終結果。

有很多庫都包含了promise模型,其中jQuery已經有了一個可使用且很出色的promise API。jQuery在1.5版本引入了Deferred物件,並可以在返回promise的函式中使用jQuery.Deferred的構造結果。而返回promise的函式則用於執行某種非同步操作並解決完成後的延遲。

var geocode = function( address ) {
    var dfd = new $.Deferred();
    GMaps.geocode({
        address: address,
        callback: function( response, status ) {
            return dfd.resolve( response );
        }
    });
    return dfd.promise();
};
var getRoute = function( fromLatLng, toLatLng ) {
    var dfd = new $.Deferred();
    map.getRoutes({
        origin: [ fromLatLng.lat(), fromLatLng.lng() ],
        destination: [ toLatLng.lat(), toLatLng.lng() ],
        travelMode: "driving",
        unitSystem: "imperial",
        callback: function( e ) {
            return dfd.resolve( e );
        }
    });
    return dfd.promise();
};
var doSomethingCoolWithDirections = function( route ) {
    // do something with route
};
$.when( geocode( fromAddress ), geocode( toAddress ) ).
then(function( fromLatLng, toLatLng ) {
    getRoute( fromLatLng, toLatLng ).then( doSomethingCoolWithDirections );
});

這允許你執行兩個非同步函式後,等待它們的結果,之後再用先前兩個呼叫的結果來執行另外一個函式。

  •     promise表示一個操作獨立完成後返回的最終結果。

在這段程式碼裡,geocode方法執行了兩次並返回了一個promise。非同步函式之後執行,並在其回撥裡呼叫了resolve。然後,一旦兩次呼叫resolve完成,then將會執行,其接收了之前兩次呼叫geocode的返回結果。結果之後被傳入getRoute,此方法也返回一個promise。最終,當getRoute的promise解決後,doSomethingCoolWithDirections回撥就執行了。
 
事件
事件是另一種當非同步回撥完成處理後的通訊方式。一個物件可以成為發射器並派發事件,而另外的物件則監聽這些事件。這種型別的事件處理方式稱之為 觀察者模式 。 backbone.js 庫在withBackbone.Events中就建立了這樣的功能模組。

var SomeModel = Backbone.Model.extend({
    url: "/someurl"
});
var SomeView = Backbone.View.extend({
    initialize: function() {
        this.model.on( "reset", this.render, this );
        this.model.fetch();
    },
    render: function( data ) {
        // do something with data
}
});
var view = new SomeView({
    model: new SomeModel()
});

還有其他用於發射事件的混合例子和函式庫,例如 jQuery Event Emitter , EventEmitter , monologue.js ,以及node.js內建的 EventEmitter 模組。

事件迴圈是一個回撥函式的佇列。

一個類似的派發訊息的方式稱為 中介者模式 , postal.js 庫中用的即是這種方式。在中介者模式,有一個用於所有物件監聽和派發事件的中間人。在這種模式下,一個物件不與另外的物件產生直接聯絡,從而使得物件間都互相分離。

絕不要返回promise到一個公用的API。這不僅關係到了API使用者對promises的使用,也使得重構更加困難。不過,內部用途的promises和外部介面的事件的結合,卻可以讓應用更低耦合且便於測試。

在先前的例子裡面,doSomethingCoolWithDirections回撥函式在兩個geocode函式完成後執行。然後,doSomethingCoolWithDirections才會獲得從getRoute接收到的響應,再將其作為訊息傳送出去。

var doSomethingCoolWithDirections = function( route ) {
    postal.channel( "ui" ).publish( "directions.done", {
        route: route
    });
};
這允許了應用的其他部分不需要直接引用產生請求的物件,就可以響應非同步回撥。而在取得命令時,很可能頁面的好多區域都需要更新。在一個典型的jQuery Ajax過程中,當接收到的命令變化時,要順利的回撥可能就得做相應的調整了。這可能會使得程式碼難以維護,但通過使用訊息,處理UI多個區域的更新就會簡單得多了。
var UI = function() {
    this.channel = postal.channel( "ui" );
    this.channel.subscribe( "directions.done", this.updateDirections ).withContext( this );
};
UI.prototype.updateDirections = function( data ) {
    // The route is available on data.route, now just update the UI
};
app.ui = new UI();

另外一些基於中介者模式傳送訊息的庫有 amplify, PubSubJS, and radio.js。

結論

JavaScript 使得編寫非同步程式碼很容易. 使用 promises, 事件, 或者命名函式來避免“callback hell”. 為獲取更多javascript非同步程式設計資訊,請點選Async JavaScript: Build More Responsive Apps with Less . 更多的例項託管在github上,地址NetTutsAsyncJS,趕快Clone吧 ! 


相關推薦

深入理解JavaScript程式設計同步非同步機制

JavaScript的優勢之一是其如何處理非同步程式碼。非同步程式碼會被放入一個事件佇列,等到所有其他程式碼執行後才進行,而不會阻塞執行緒。然而,對於初學者來說,書寫非同步程式碼可能會比較困難。而在這篇文章裡,我將會消除你可能會有的任何困惑。理解非同步程式碼 JavaS

網路程式設計阻塞非阻塞、同步非同步、I/O模型的理解

1. 概念理解      在進行網路程式設計時,我們常常見到同步(Sync)/非同步(Async),阻塞(Block)/非阻塞(Unblock)四種呼叫方式:同步:所謂同步,就是在發出一個功能呼叫時,在沒有得到結果之前,該呼叫就不返回。也就是必須一件一件事做,等前一件做完了才能做下一件事。 例如

深入理解Javascript的valueOftoString

基本上,javascript中所有資料型別都擁有valueOf和toString這兩個方法,null除外。它們倆解決javascript值運算與顯示的問題,本文將詳細介紹,有需要的朋友可以參考下。 toString() toString()函式的作用是返回object的字串表示,JavaScript中ob

深入理解Javascript的堆棧、淺拷貝深拷貝

Javascript中的淺拷貝與深拷貝 先從JavaScript的資料型別存放的位置堆疊開始說吧 什麼是堆疊? 我們知道計算機領域中堆疊是兩種資料結構,它們只能再一端(稱為棧頂(top))對資料項進行插入和刪除。 堆:佇列優先,先進先出,由作業系統自動分配釋放,存放函式的引數值,區域性變數的

深入理解JavaScript的執行機制同步非同步

不論是面試求職,還是日常開發工作,我們經常會遇到這樣的情況:給定的幾行程式碼,我們需要知道其輸出內容和順序。因為JavaScript是一門單執行緒語言,所以我們可以得出結論: JavaScript是按照語句出現的順序執行的 所以我們以為JS都是這樣的:

深入理解javascript非同步程式設計障眼法&&h5 web worker實現多執行緒

0.從一道題說起 var t = true; setTimeout(function(){ t = false; }, 1000); while(t){ } alert('end'); 問,以上程式碼何時alert“end”呢? 測試一下:答案是:

深入理解python3.4Asyncio庫Node.js的非同步IO機制

譯者前言 如何用yield以及多路複用機制實現一個基於協程的非同步事件框架? 現有的元件中yield from是如何工作的,值又是如何被傳入yield from表示式的? 在這個yield from之上,是如何在一個執行緒內實現一個排程機制去排程協程的? 協程中呼叫協程的呼叫

通過例子深入理解javascript的new操作符

not 而是 efi undefine new blog div 函數功能 成功 1.首先看一道題目 1 function Cat(name,age){ 2 this.name=name; 3 this.age=age; 4 } 5 console.l

深入理解JavaScript的函數操作

要求 使用情況 並不是 回文 工作 alert http load 函數 匿名函數 對於什麽是匿名函數,這裏就不做過多介紹了。我們需要知道的是,對於JavaScript而言,匿名函數是一個很重要且具有邏輯性的特性。通常,匿名函數的使用情況是:創建一個供以後使用的函數。 簡單

深入理解JavaScript的閉包特性如何給循環的對象添加事件

彈出 所有 了解 ext catch 形參 efi 運行期 -- 初學者經常碰到的,即獲取HTML元素集合,循環給元素添加事件。在事件響應函數中(event handler)獲取對應的索引。但每次獲取的都是最後一次循環的索引。原因是初學者並未理解JavaScri

圖說js的this——深入理解javascriptthis指針

前端 javascript this沒搞錯吧!js寫了那麽多年,this還是會搞錯!沒搞錯,javascript就是回搞錯!…………在寫java的時候,this用錯了,idea都會直接報錯!比如……但是,js,……idea,愛莫能助了……在面向對象編程裏有兩個重要的概念:一個是類,一個是實例化的對象,類是一個

深入理解Javascript箭頭函式的this

今日頭條:https://m.jb51.net/article/105340.htm 新增連結描述 ES6標準新增了一種新的函式:Arrow Function(箭頭函式)。那麼下面這篇文章主要給大家介紹了箭頭函式中this的相關資料,有需要的朋友可以參考借鑑,下面來一起看看吧。 首先我們先看

深入理解Java同步靜態方法和synchronized(class)程式碼塊的類鎖 深入理解Java併發synchronized同步化的程式碼塊不是this物件時的操作

一.回顧學習內容  在前面幾篇部落格中我我們已經理解了synchronized物件鎖、物件鎖的重入、synchronized方法塊、synchronized非本物件的程式碼塊,  連結:https://www.cnblogs.com/SAM-CJM/category/1314992.h

JavaScript進階】深入理解JavaScriptES6的Promise的作用並實現一個自己的Promise

  1.Promise的基本使用 1 // 需求分析: 封裝一個方法用於讀取檔案路徑,返回檔案內容 2 3 const fs = require('fs'); 4 const path = require('path'); 5 6 7 /** 8 * 把一個回

深入理解JavaScript原型鏈繼承

原型鏈 原型鏈一直都是一個在JS中比較讓人費解的知識點,但是在面試中經常會被問到,這裡我來做一個總結吧,首先引入一個關係圖: 一.要理解原型鏈,首先可以從上圖開始入手,圖中有三個概念: 1.建構函式: JS中所有函式都可以作為建構函式,前提是被new操作符操作; function P

深入理解 JavaScript的變數、值、傳參

1. demo 如果你對下面的程式碼沒有任何疑問就能自信的回答出輸出的內容,那麼本篇文章就不值得你浪費時間了。 var var1 = 1 var var2 = true var var3 = [1,2,3] var var4 = var3 function test (var1, var3) {

深入理解並行程式設計-分割和同步設計(五)

原文連結    作者:paul    譯者:謝寶友,魯陽,陳渝 並行快速路徑 細粒度的設計一般要比粗粒度的設計複雜。在許多情況,絕大部分開銷只由一小部分程式碼產生[Knu73]。所以為什麼不把精力放在這一小塊程式碼上。 這就是並行快速路徑設計模式背後的想法,儘可能地並行化常見情況下的程式碼路徑

深入理解並行程式設計-分割和同步設計(三)

原文連結    作者:paul    譯者:謝寶友,魯陽,陳渝 設計準則 上面的章節中給出了三個並行程式設計的目標:效能、生產率和通用性。但是還需要更詳細的設計準則來真正的指導真實世界中的設計,這就是本節將解決的任務。在真實世界中,這些準則經常在某種程度上衝突,這需要設計者小心的權衡得失。 這

深入理解並行程式設計-分割和同步設計(四)

原文連結    作者:paul    譯者:謝寶友,魯陽,陳渝 圖1.1:設計模式與鎖粒度 圖1.1是不同程度同步粒度的圖形表示。每一種同步粒度都用一節內容來描述。下面幾節主要關注鎖,不過其他幾種同步方式也有類似的粒度問題。 1.1. 序列程式 圖1.2:Intel處理器的MIPS/時鐘

深入理解並行程式設計-分割和同步設計(二)

原文連結    作者:paul    譯者:謝寶友,魯陽,陳渝 雙端佇列是一種元素可以從兩端插入或刪除的資料結構[Knu73]。據說實現一種基於鎖的允許在雙端佇列的兩端進行併發操作的方法非常困難[Gro07]。本節將展示一種分割設計策略,能實現合理且簡單的解決方案,請看下面的小節中的三種通用方