JS中的異步與Promise

  我們知道,js的執行環境是“單線程”的。所謂”單線程”,就是指一次只能完成一件任務。如果有多個任務,就必須排隊,前面一個任務完成,再執行後面一個任務,以此類推。
  js被創造出來用於實現操作瀏覽器html中的DOM樹、CSS樣式樹以及實現界面中用戶與html中DOM的動態交互,那麼問題來了,假如js是多線程執行,那麼很可能出現同一時刻有兩個線程在操作同一個UI DOM元素,並且假如一個線程用於給DOM元素添加樣式,而另一個線程用於刪除DOM元素。這時候,這個DOM就成爲了 臨界資源,瀏覽器就無法去裁決究竟以哪一個線程的操作爲準。雖然,我們也可以實現某些 鎖 來規避這種線程操作互斥的情況,但是這一定會給js開發帶來更大的複雜性。所以js最終採用單線程機制。
  這種模式的好處是實現起來比較簡單,執行環境相對單純;壞處是只要有一個任務耗時很長,後面的任務都必須排隊等着,會拖延整個程序的執行。常見的瀏覽器無響應(假死),往往就是因爲某一段Javascript代碼長時間運行(比如死循環),導致整個頁面卡在這個地方,其他任務無法執行。
  爲了解決這個問題,Javascript語言將任務的執行模式分成兩種:同步(Synchronous)和異步(Asynchronous)。
  “同步模式”就是上面所說的,後一個任務等待前一個任務結束,然後再執行,程序的執行順序與任務的排列順序是一致的、同步的;”異步模式”則完全不同,每一個任務有一個或多個回調函數(callback),前一個任務結束後,不是執行後一個任務,而是執行回調函數,後一個任務則是不等前一個任務結束就執行,所以程序的執行順序與任務的排列順序是不一致的、異步的。
“異步模式”非常重要。在瀏覽器端,耗時很長的操作都應該異步執行,避免瀏覽器失去響應,最好的例子就是Ajax操作。在服務器端,”異步模式”甚至是唯一的模式,因爲執行環境是單線程的,如果允許同步執行所有http請求,服務器性能會急劇下降,很快就會失去響應。

四種異步模式的方法

一、回調函數
假定有兩個函數f1和f2,後者等待前者的執行結果(f2依賴f1)。

f1();
f2();

如果f1是一個很耗時的任務,可以考慮改寫f1,把f2寫成f1的回調函數。

 function f1(callback){
    setTimeout(function () {
      // f1的任務代碼
      callback();
    }, 1000);
  }
  f1(f2);

採用這種方式,我們把同步操作變成了異步操作,f1不會堵塞程序運行,相當於先執行程序的主要邏輯,將耗時的操作推遲執行。
回調函數的優點是簡單、容易理解和部署,缺點是不利於代碼的閱讀和維護,各個部分之間高度耦合(Coupling),流程會很混亂,而且每個任務只能指定一個回調函數。

回調函數:就是在不久的將來,也就是一段不確定的時間之後將要執行的事情。例如:setTimeout,Ajax都是異步回調。

回調地獄:

setTimeout(function (name) {
 2   var catList = name + ',';
 3 
 4   setTimeout(function (name) {
 5     catList += name + ',';
 6 
 7     setTimeout(function (name) {
 8       catList += name + ',';
 9 
10       setTimeout(function (name) {
11         catList += name + ',';
12 
13         setTimeout(function (name) {
14           catList += name;
15 
16           console.log(catList);
17 
18         }, 1, 'Lion');
19 
20       }, 1, 'Snow Leopard');
21 
22     }, 1, 'Lynx');
23 
24   }, 1, 'Jaguar');}, 1, 'Panther');

由於回調函數是異步的,在上面的代碼中每一層的回調函數都需要依賴上一層的回調執行完,所以形成了層層嵌套的關係最終形成類似上面的回調地獄,但代碼以此種形式展現時無疑是不利於我們閱讀與維護的。
二、事件監聽

 f1.on('done', f2);
 function f1(){
    setTimeout(function () {
      // f1的任務代碼
      f1.trigger('done');
    }, 1000);
  }

f1.trigger(‘done’)表示,執行完成後,立即觸發done事件,從而開始執行f2。
這種方法的優點是比較容易理解,可以綁定多個事件,每個事件可以指定多個回調函數,而且可以”去耦合”(Decoupling),有利於實現模塊化。缺點是整個程序都要變成事件驅動型,運行流程會變得很不清晰。

三、發佈/訂閱
上一節的”事件”,完全可以理解成”信號”。
我們假定,存在一個”信號中心”,某個任務執行完成,就向信號中心”發佈”(publish)一個信號,其他任務可以向信號中心”訂閱”(subscribe)這個信號,從而知道什麼時候自己可以開始執行。這就叫做”發佈/訂閱模式”(publish-subscribe pattern),又稱”觀察者模式”(observer pattern)。
這個模式有多種實現,下面採用的是Ben Alman的Tiny Pub/Sub,這是jQuery的一個插件。
首先,f2向”信號中心”jQuery訂閱”done”信號。

 jQuery.subscribe("done", f2);

  function f1(){
    setTimeout(function () {
      // f1的任務代碼
      jQuery.publish("done");
    }, 1000);
  }
  jQuery.unsubscribe("done", f2);

jQuery.publish(“done”)的意思是,f1執行完成後,向”信號中心”jQuery發佈”done”信號,從而引發f2的執行。
此外,f2完成執行後,也可以取消訂閱(unsubscribe)。
這種方法的性質與”事件監聽”類似,但是明顯優於後者。因爲我們可以通過查看”消息中心”,瞭解存在多少信號、每個信號有多少訂閱者,從而監控程序的運行。

四、Promises對象
ES6 原生提供了 Promise 對象。

所謂 Promise,就是一個對象,用來傳遞異步操作的消息。它代表了某個未來纔會知道結果的事件(通常是一個異步操作),並且這個事件提供統一的 API,可供進一步處理。
簡單說,它的思想是,每一個異步任務返回一個Promise對象,該對象有一個then方法,允許指定回調函數。比如,f1的回調函數f2,可以寫成:

 f1().then(f2);

  function f1(){
    var dfd = $.Deferred();
    setTimeout(function () {
      // f1的任務代碼
      dfd.resolve();
    }, 500);
    return dfd.promise;
  }

這樣寫的優點在於,回調函數變成了鏈式寫法,程序的流程可以看得很清楚,而且有一整套的配套方法,可以實現許多強大的功能。
比如,指定多個回調函數:

 f1().then(f2).then(f3);

再比如,指定發生錯誤時的回調函數:

f1().then(f2).fail(f3);

這裏再對promise進行一下說明,這是es6中的定義的對象。所謂Promise,簡單說就是一個容器,裏面保存着某個未來纔會結束的事件(通常是一個異步操作)的結果。
Promise對象有以下兩個特點。

(1)對象的狀態不受外界影響。Promise對象代表一個異步操作,有三種狀態:Pending(進行中)、Resolved(已完成,又稱 Fulfilled)和Rejected(已失敗)。只有異步操作的結果,可以決定當前是哪一種狀態,任何其他操作都無法改變這個狀態。這也是Promise這個名字的由來,它的英語意思就是“承諾”,表示其他手段無法改變。

(2)一旦狀態改變,就不會再變,任何時候都可以得到這個結果。Promise對象的狀態改變,只有兩種可能:從Pending變爲Resolved和從Pending變爲Rejected。只要這兩種情況發生,狀態就凝固了,不會再變了,會一直保持這個結果。就算改變已經發生了,你再對Promise對象添加回調函數,也會立即得到這個結果。這與事件(Event)完全不同,事件的特點是,如果你錯過了它,再去監聽,是得不到結果的。

有了Promise對象,就可以將異步操作以同步操作的流程表達出來,避免了層層嵌套的回調函數。此外,Promise對象提供統一的接口,使得控制異步操作更加容易。

var promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* 異步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

上面代碼創造了一個Promise實例。
Promise構造函數接受一個函數作爲參數,該函數的兩個參數分別是resolve和reject。它們是兩個函數,由JavaScript引擎提供,不用自己部署。

resolve函數的作用是,將Promise對象的狀態從“未完成”變爲“成功”(即從Pending變爲Resolved),在異步操作成功時調用,並將異步操作的結果,作爲參數傳遞出去;reject函數的作用是,將Promise對象的狀態從“未完成”變爲“失敗”(即從Pending變爲Rejected),在異步操作失敗時調用,並將異步操作報出的錯誤,作爲參數傳遞出去。

Promise實例生成以後,可以用then方法分別指定Resolved狀態和Reject狀態的回調函數。

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

then方法可以接受兩個回調函數作爲參數。第一個回調函數是Promise對象的狀態變爲Resolved時調用,第二個回調函數是Promise對象的狀態變爲Reject時調用。其中,第二個函數是可選的,不一定要提供。這兩個函數都接受Promise對象傳出的值作爲參數。
這裏有一個promise異步執行的經典例題:

console.log(0);
setTimeout(function() {
  console.log(1)}, 0);
new Promise(function executor(resolve) {
 console.log(2);
 for( var i=0 ; i<10000 ; i++ ) {
    i == 9999 && resolve();
 }
  console.log(3);
}).then(function() {
  console.log(4);}
);
  console.log(5);

//0,2,3,5,4,1
/*先輸出0
然後碰到一個 setTimeout,於是會先設置一個定時,在定時結束後將傳遞這個函數放到任務隊列裏面,因此開始肯定不會輸出 1 。·

然後是一個 Promise,裏面的函數是直接執行的,因此應該直接輸出 2 3 。

然後,Promise 的 then 應當會放到當前 tick 的最後,但是還是在當前 tick 中。

因此,應當先輸出 5,然後再輸出 4 。

最後在到下一個 tick,就是 1 。

“2 3 5 4 1”*/

  Promise 也有一些缺點。首先,無法取消 Promise,一旦新建它就會立即執行,無法中途取消。其次,如果不設置回調函數,Promise 內部拋出的錯誤,不會反應到外部。第三,當處於 Pending 狀態時,無法得知目前進展到哪一個階段(剛剛開始還是即將完成)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章