javascript非同步編程,javascript非同步

來源:互聯網
上載者:User

javascript非同步編程,javascript非同步
非同步機制

JavaScript的執行環境是單線程的,單線程的好處是執行環境簡單,不用去考慮諸如資源同步,死結等多線程阻塞式編程等所需要面對的惱人的問題。但帶來的壞處是當一個任務執行時間較長時,後面的任務會等待很長時間。在瀏覽器端就會出現瀏覽器假死,滑鼠無法響應等情況。所以在瀏覽器端,耗時很長的操作都應該非同步執行,避免瀏覽器失去響應。所謂非同步執行,不同於同步執行(程式的執行順序與任務的排列順序是一致的、同步的),每一個任務有一個或多個回呼函數(callback),前一個任務結束後,不是執行後一個任務,而是執行回呼函數,後一個任務則是不等前一個任務結束就執行,所以程式的執行順序與任務的排列順序是不一致的、非同步。既然Javascript是單線程的,那它又如何能夠非同步執行呢?

Javascript執行緒模式和事件驅動

JavaScript有一個基於事件迴圈的併發模式。這個模式與C語言和java有很大不同。

運行時的概念

  • 棧
    函數調用形成堆疊框架。

    function f(b){    var a = 12;    return a+b+35;}function g(x){    var m = 4;    return f(m*x);}g(21);

    當調用函數g時,建立第一個包含g參數和局部變數的幀。當g函數調用f函數時,建立包含f參數和局部變數第二個堆疊框架並推到第一個堆疊框架的頂部。當f返回時,頂部的堆疊框架元素被彈出(只留下g調用)。當g函數返回時,堆棧為空白。

  • 堆
    堆是一個大型的非結構化地區,對象被分配到堆中。

  • 隊列
    一個javascript運行環境包含一個資訊隊列,這個隊列是一系列將被執行的資訊列表。每一個訊息被關聯到一個函數上。當堆棧為空白時,從訊息佇列中取出一個訊息並進行處理。該處理包含調用相關的函數(以及因此產生一個初始化的堆疊框架)。當堆棧再次為空白時,訊息處理結束。

事件迴圈

事件迴圈的名字源於它的實現,經常像下面這樣:

while(queue.waitForMessage()){  queue.processNextMessage();}

queue.waitForMessage同步等待一個訊息。

  • 運行到完成
    每個訊息完全處理之後,其它訊息才會被處理。這樣的好處就是當一個函數不能被提前,只能等其他函數執行完畢(並且可以修改資料的函數操作)。這不同於C,例如,如果一個函數在一個線程運行時,它可以停在任何點運行在另一個線程一些其他的代碼。這種模式的缺點是,如果一個訊息時間過長完成,Web應用程式無法處理像點擊或滾動的使用者互動。該瀏覽器可緩解此與“指令碼花費的時間太長運行”對話方塊。一個很好的做法,遵循的是使資訊處理短,如果可能削減一個訊息到幾條訊息。

  • 添加訊息
    在網頁瀏覽器中,事件可以在任何時候添加,一個事件發生並伴隨事件監聽綁定到事件上。如果沒有事件監聽,則事件丟失。就像點擊一個元素,元素上綁定點擊事件。調用setTimeout時,當函數的第二個參數時間被傳遞進去,將添加一個訊息到隊列中。如果在隊列中沒有其他訊息,該訊息被立即處理;然而,如果有訊息,則setTimeout的資訊將必須等待其它訊息以進行處理。由於這個原因,第二個參數是最小的時間,而不是一個保證時間。

  • 幾個運行環境之間的通訊
    一個web worker或跨域iframe都有自己的堆棧,堆,和訊息佇列。兩個不同的運行環境只能通過postMessage的方法發送訊息進行通訊。這種方法增加了一個訊息到其他運行時,如果後者監聽訊息事件。

從不阻塞

事件迴圈模型是javascript的一個很有意思的屬性,不像其它語言,它從不阻塞。假定瀏覽器中有一個專門用於事件調度的執行個體(該執行個體可以是一個線程,我們可以稱之為事件分發線程event dispatch thread),該執行個體的工作就是一個不結束的迴圈,從事件隊列中取出事件,處理所有很事件關聯的回呼函數(event handler)。注意回呼函數是在Javascript的主線程中啟動並執行,而非事件分發線程中,以保證事件處理不會發生阻塞。通過事件和回調的I/O操作是一個典型的表現,所以當應用等待索引型資料庫查詢返回或XHR請求返回時,它仍然可以處理其他事情比如使用者輸入。

回調

回調是javascript的基礎,函數被作為參數進行傳遞。像下面:

f1();f2();f3();

如果f1中執行了大量的耗時操作,而且f2需要在f1之後執行。則程式可以改為回調的形式。如下:

function f1(callback){    setTimeout(function () {      // f1的大量耗時任務代碼並的到三個結果i,l,you.      console.log("this is function1");      var i = "i", l = "love", y = "you";        if (callback && typeof(callback) === "function") {            callback(i,l,y);        }    }, 50);}function f2(a, b, c) {    alert(a + " " + b + " " + c);    console.log("this is function2");}function f3(){console.log("this is function3");}f1(f2);f3();

運行結果:

this is function3
this is function1
i love you
this is function2

採用這種方式,我們把同步操作變成了非同步作業,f1不會堵塞程式運行,相當於先執行程式的主要邏輯,將耗時的操作延遲執行。
回呼函數的優點是簡單,輕量級(不需要額外的庫)。缺點是各個部分之間高度耦合(Coupling),流程會很混亂,而且每個任務只能指定一個回呼函數。某個操作需要經過多個非阻塞的IO操作,每一個結果都是通過回調,產生意大利麵條式(spaghetti)的代碼。

operation1(function(err, result) {    operation2(function(err, result) {        operation3(function(err, result) {            operation4(function(err, result) {                operation5(function(err, result) {                    // do something useful                })            })        })    })})
事件監聽

另一種思路是採用事件驅動模式。任務的執行不取決於代碼的順序,而取決於某個事件是否發生。

// plain, non-jQuery version of hooking up an event handlervar clickity = document.getElementById("clickity");clickity.addEventListener("click", function (e) {    //console log, since it's like ALL real world scenarios, amirite?    console.log("Alas, someone is pressing my buttons…");});// the obligatory jQuery version$("#clickity").on("click", function (e) {    console.log("Alas, someone is pressing my buttons…");});

也可以自訂事件進行監聽,關於自訂事件,屬於另外一部分的內容。這種方法的優點是比較容易理解,可以綁定多個事件,每個事件可以指定多個回呼函數,而且可以”去耦合”(Decoupling),有利於實現模組化。缺點是整個程式都要變成事件驅動型,運行流程會變得很不清晰。

觀察者模式

我們假定,存在一個”訊號中心”,某個任務執行完成,就向訊號中心”發布”(publish)一個訊號,其他任務可以向訊號中心”訂閱”(subscribe)這個訊號,從而知道什麼時候自己可以開始執行。這就叫做”發布/訂閱模式”(publish-subscribe pattern),又稱”觀察者模式”(observer pattern)。

var pubsub  = (function(){    var q = {}        topics = {},        subUid = -1;    //發布訊息    q.publish = function(topic, args) {        if(!topics[topic]) {return;}        var subs = topics[topic],            len = subs.length;        while(len--) {            subs[len].func(topic, args);        }        return this;    };    //訂閱事件    q.subscribe = function(topic, func) {        topics[topic] = topics[topic] ? topics[topic] : [];        var token = (++subUid).toString();        topics[topic].push({            token : token,            func : func        });        return token;    };    return q;    //取消訂閱就不寫了,遍曆topics,然後通過儲存前面返回token,刪除指定元素})();//觸發的事件var f2 = function(topics, data) {    console.log("logging:" + topics + ":" + data);    console.log("this is function2");}function f1(){    setTimeout(function () {      // f1的任務代碼      console.log("this is function1");       //發布訊息'done'        pubsub .publish('done', 'hello world');    }, 1000);}pubsub.subscribe('done', f2);f1();

上面代碼的運行結果為:

this is function1
logging:done:hello world
this is function2

觀察者模式的實現方法有很多種,也可以直接借用第三方庫。這種方法的性質與”事件監聽”類似(觀察者模式和自訂事件非常相似),但是明顯優於後者。觀察者模式和事件監聽一樣具有良好的去耦性,並且有一個訊息中心,通過對訊息中心的處理,可以良好地監控程式運行。

Promises對象

Promises的概念是由CommonJS小組的成員在 Promises/A規範 中提出來的。Promises被逐漸用作一種管理非同步作業回調的方法,但出於它們的設計,它們遠比那個有用得多。Promise允許我們以同步的方式寫代碼,同時給予我們代碼的非同步執行。

function f1(){    var def = $.Deferred();    setTimeout(function () {       // f1的任務代碼        console.log("this is f1");        def.resolve();      }, 500);    return def.promise();}function f2(){    console.log("this is f2");}f1().then(f2);

上面代碼的運行結果為:

this is f1
this is f2

上面引用的是jquery對Promises/A的實現,jquery中還有一系列方法,具體可參考:Deferred Object.關於Promises,強烈建議讀一下You’re Missing the Point of Promises.還有很多第三方庫實現了Promises,如:Q、Bluebird、 mmDeferred 等。Promise(中文:承諾)其實為一個有限狀態機器,共有三種狀態:pending(執行中)、fulfilled(執行成功)和rejected(執行失敗)。其中pending為初始狀態,fulfilled和rejected為結束狀態(結束狀態表示promise的生命週期已結束)。狀態轉換關係為:pending->fulfilled,pending->rejected。隨著狀態的轉換將觸發各種事件(如執行成功事件、執行失敗事件等)。 下節具體講述狀態機器實現js非同步編程。

狀態機器

Promises的本質實際就是通過狀態機器來實現的,把非同步作業與對象的狀態改變掛鈎,當非同步作業結束的時候,發生相應的狀態改變,由此再觸發其他動作。這要比回呼函數、事件監聽、發布/訂閱等解決方案,在邏輯上更合理,更易於降低代碼的複雜度。關於Promises可參考:JS魔法堂:剖析源碼理解Promises/A規範 。

ES6對非同步支援

這是一個新的技術,成為2015年的ECMAScript(ES6)標準的一部分。該技術的規範已經完成,但實施情況在不同的瀏覽器不同,在瀏覽器中的支援情況如下。
案頭端:

手機端:

var f1 = new Promise(function(resolve, reject) {    setTimeout(function () {       // f1的任務代碼       console.log("this is f1");       resolve("Success");    }, 500);  });function f2(val){    console.log(val + ":" + "this is f2");}function f3(){    console.log("this is f3")}f1.then(f2);f3();

以上代碼在Chrome 版本43中的運行結果為:

this is f3
this is f1
Success:this is f2

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.