Promise異步編程整理

一、單線程模型

單線程模型指的是,JavaScript 只在一個線程上運行。也就是說,JavaScript 同時只能執行一個任務,其餘任務都必須在後面排隊等待。

注意,JavaScript 只在一個線程上運行,不表明 JavaScript 引擎只有一個線程。事實上,

JavaScript 引擎有多個線程,單個腳本只能在一個線程上運行(稱爲主線程),其餘線程都是在後臺配合。 JavaScript 之因此採用單線程,而不是多線程,跟歷史有關係。JavaScript 從誕生起就是單線程,緣由是不想讓瀏覽器變得太複雜,

由於多線程須要共享資源、且有可能修改彼此的運行結果,對於一種網頁腳本語言來講,這就太複雜了。若是 JavaScript 同時有兩個線程,

一個線程在網頁 DOM 節點上添加內容,另外一個線程刪除了這個節點,這時瀏覽器應該以哪一個線程爲準?是否是還要有鎖機制?

因此,爲了不復雜性,JavaScript 一開始就是單線程,這已經成了這門語言的核心特徵,未來也不會改變。

二、同步任務和異步任務

程序裏面全部的任務,能夠分紅兩類:同步任務(synchronous)和異步任務(asynchronous)。

同步任務是那些沒有被引擎掛起、在主線程上排隊執行的任務。只有前一個任務執行完畢,才能執行後一個任務。

異步任務是那些被引擎放在一邊,不進入主線程、而進入任務隊列的任務。只有引擎認爲某個異步任務能夠執行了(好比 Ajax 操做從服務器獲得告終果),

該任務(採用回調函數的形式)纔會進入主線程執行。排在異步任務後面的代碼,不用等待異步任務結束會立刻運行,也就是說,異步任務不具備「堵塞」效應。 舉例來講,Ajax 操做能夠看成同步任務處理,也能夠看成異步任務處理,由開發者決定。若是是同步任務,主線程就等着 Ajax 操做返回結果,再往下執行;

若是是異步任務,主線程在發出 Ajax 請求之後,就直接往下執行,等到 Ajax 操做有告終果,主線程再執行對應的回調函數。

三、任務隊列和事件循環

JavaScript 運行時,除了一個正在運行的主線程,引擎還提供一個任務隊列(task queue),裏面是各類須要當前程序處理的異步任務。

(實際上,根據異步任務的類型,存在多個任務隊列。爲了方便理解,這裏假設只存在一個隊列。) 首先,主線程會去執行全部的同步任務。等到同步任務所有執行完,就會去看任務隊列裏面的異步任務。

若是知足條件,那麼異步任務就從新進入主線程開始執行,這時它就變成同步任務了。等到執行完,下一個異步任務再進入主線程開始執行。一旦任務隊列清空,程序就結束執行。 異步任務的寫法一般是回調函數。一旦異步任務從新進入主線程,就會執行對應的回調函數。

若是一個異步任務沒有回調函數,就不會進入任務隊列,也就是說,不會從新進入主線程,由於沒有用回調函數指定下一步的操做。 JavaScript 引擎怎麼知道異步任務有沒有結果,能不能進入主線程呢?答案就是引擎在不停地檢查,一遍又一遍,只要同步任務執行完了,

引擎就會去檢查那些掛起來的異步任務,是否是能夠進入主線程了。這種循環檢查的機制,就叫作事件循環(Event Loop)。

維基百科的定義是:「事件循環是一個程序結構,用於等待和發送消息和事件(a programming construct that waits
for and dispatches events or messages in a program)」。

四、異步操做的模式

      4.1回調函數

      把f2寫成f1的回調函數。javascript

function f1(callback) {
  // ...
  callback();
}

function f2() {
  // ...
}

f1(f2);

  回調函數的優勢是簡單、容易理解和實現,缺點是不利於代碼的閱讀和維護,各個部分之間高度耦合(coupling),使得程序結構混亂、流程難以追蹤(尤爲是多個回調函數嵌套的狀況),並且每一個任務只能指定一個回調函數。java

     4.2 事件監聽

f1.on('done', f2);

function f1() {
  setTimeout(function () {
    // ...
    f1.trigger('done');
  }, 1000);
}

  f1.trigger('done')表示,執行完成後,當即觸發done事件,從而開始執行f2promise

     這種方法的優勢是比較容易理解,能夠綁定多個事件,每一個事件能夠指定多個回調函數,並且能夠「去耦合」(decoupling),有利於實現模塊化。缺點是整個程序都要變成事件驅動型,運行流程會變得很不清晰。閱讀代碼的時候,很難看出主流程。瀏覽器

    4.3 發佈/訂閱

     事件徹底能夠理解成「信號」,若是存在一個「信號中心」,某個任務執行完成,就向信號中心「發佈」(publish)一個信號,其餘任務能夠向信號中心「訂閱」(subscribe)這個信號,從而知道何時本身能夠開始執行。這就叫作「發佈/訂閱模式」(publish-subscribe pattern),又稱「觀察者模式」(observer pattern)。服務器

    f2向信號中心jQuery訂閱done信號。多線程

jQuery.subscribe('done', f2);

function f1() {
  setTimeout(function () {
    // ...
    jQuery.publish('done');
  }, 1000);
}

 上面代碼中,jQuery.publish('done')的意思是,f1執行完成後,向信號中心jQuery發佈done信號,從而引起f2的執行。異步

 f2完成執行後,能夠取消訂閱(unsubscribe)。async

jQuery.unsubscribe('done', f2);

   這種方法的性質與「事件監聽」相似,可是明顯優於後者。由於能夠經過查看「消息中心」,瞭解存在多少信號、每一個信號有多少訂閱者,從而監控程序的運行。模塊化

五、Promise 對象的狀態

Promise 對象經過自身的狀態,來控制異步操做。Promise 實例具備三種狀態。函數

異步操做未完成(pending)
異步操做成功(fulfilled)
異步操做失敗(rejected)

上面三種狀態裏面,fulfilledrejected合在一塊兒稱爲resolved(已定型)。

這三種的狀態的變化途徑只有兩種。

從「未完成」到「成功」
從「未完成」到「失敗」

一旦狀態發生變化,就凝固了,不會再有新的狀態變化。這也是 Promise 這個名字的由來,它的英語意思是「承諾」,一旦承諾成效,就不得再改變了。這也意味着,Promise 實例的狀態變化只可能發生一次。

所以,Promise 的最終結果只有兩種。

異步操做成功,Promise 實例傳回一個值(value),狀態變爲fulfilled。
異步操做失敗,Promise 實例拋出一個錯誤(error),狀態變爲rejected。

六、Promise 構造函數

JavaScript 提供原生的Promise構造函數,用來生成 Promise 實例。

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

  if (/* 異步操做成功 */){
    resolve(value);
  } else { /* 異步操做失敗 */
    reject(new Error());
  }
});

上面代碼中,Promise構造函數接受一個函數做爲參數,該函數的兩個參數分別是resolvereject。它們是兩個函數,由 JavaScript 引擎提供,不用本身實現。

resolve函數的做用是,將Promise實例的狀態從「未完成」變爲「成功」(即從pending變爲fulfilled),在異步操做成功時調用,並將異步操做的結果,做爲參數傳遞出去。reject函數的做用是,將Promise實例的狀態從「未完成」變爲「失敗」(即從pending變爲rejected),在異步操做失敗時調用,並將異步操做報出的錯誤,做爲參數傳遞出去。

下面是一個例子。

function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms, 'done');
  });
}

timeout(100)

上面代碼中,timeout(100)返回一個 Promise 實例。100毫秒之後,該實例的狀態會變爲fulfilled

七、then() 用法辨析

Promise 的用法,簡單說就是一句話:使用then方法添加回調函數。可是,不一樣的寫法有一些細微的差異,請看下面四種寫法,它們的差異在哪裏?

// 寫法一
f1().then(function () {
  return f2();
});

// 寫法二
f1().then(function () {
  f2();
});

// 寫法三
f1().then(f2());

// 寫法四
f1().then(f2);

爲了便於講解,下面這四種寫法都再用then方法接一個回調函數f3。寫法一的f3回調函數的參數,是f2函數的運行結果。

f1().then(function () {
  return f2();
}).then(f3);

寫法二的f3回調函數的參數是undefined

f1().then(function () {
  f2();
  return;
}).then(f3);

寫法三的f3回調函數的參數,是f2函數返回的函數的運行結果。

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

寫法四與寫法一隻有一個差異,那就是f2會接收到f1()返回的結果。

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

八、Promise 優缺點

優勢:讓回調函數變成了規範的鏈式寫法,程序流程能夠看得很清楚。它有一整套接口,能夠實現許多強大的功能,好比同時執行多個異步操做,等到它們的狀態都改變之後,再執行一個回調函數;再好比,爲多個回調函數中拋出的錯誤,統一指定處理方法等等。

並且,Promise 還有一個傳統寫法沒有的好處:它的狀態一旦改變,不管什麼時候查詢,都能獲得這個狀態。這意味着,不管什麼時候爲 Promise 實例添加回調函數,該函數都能正確執行。因此,你不用擔憂是否錯過了某個事件或信號。若是是傳統寫法,經過監聽事件來執行回調函數,一旦錯過了事件,再添加回調函數是不會執行的。

缺點:編寫的難度比傳統寫法高,並且閱讀代碼也不是一眼能夠看懂。你只會看到一堆then,必須本身在then的回調函數裏面理清邏輯。

九、微任務

Promise 的回調函數屬於異步任務,會在同步任務以後執行

new Promise(function (resolve, reject) {
  resolve(1);
}).then(console.log);

console.log(2);
// 2
// 1

上面代碼會先輸出2,再輸出1。由於console.log(2)是同步任務,而then的回調函數屬於異步任務,必定晚於同步任務執行。

可是,Promise 的回調函數不是正常的異步任務,而是微任務(microtask)。它們的區別在於,正常任務追加到下一輪事件循環,微任務追加到本輪事件循環。這意味着,微任務的執行時間必定早於正常任務

setTimeout(function() {
  console.log(1);
}, 0);

new Promise(function (resolve, reject) {
  resolve(2);
}).then(console.log);

console.log(3);
// 3
// 2
// 1

上面代碼的輸出結果是321。這說明then的回調函數的執行時間,早於setTimeout(fn, 0)。由於then是本輪事件循環執行,setTimeout(fn, 0)在下一輪事件循環開始時執行。

相關文章
相關標籤/搜索